<?php
// This file is part of the Studyplan plugin for Moodle
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <https://www.gnu.org/licenses/>.

/**
 * Scan gradables for a pending grading action
 * @package    local_treestudyplan
 * @copyright  2023 P.M. Kuipers
 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace local_treestudyplan;
defined('MOODLE_INTERNAL') || die();

require_once($CFG->libdir . '/externallib.php');

use grade_item;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_description;
use core_external\external_value;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core\context;
use core\context\system as context_system;
use core\context\course as context_course;
use core\context\coursecat as context_coursecat;

/**
 * Scan gradables for a pending grading action
 */
class gradingscanner {
    /**
     * Cache of supported mods
     * @var array
     */
    private static $modsupported = [];
    /**
     * Cache of enrolled students in a particular course
     * @var array
     */
    private static $coursestudents = [];
    /** The internally used grading scanner
     * @var local\ungradedscanners\scanner_base
     */
    private $scanner = null;
    /** @var grade_item */
    private $gi = null;
    /**
     * Cache of pending ungraded results per user
     * @var array
     */
    private $pendingcache = [];

    /**
     * Check if a certain activity type is supported for scanning pending results
     * @param string $mod name of activity module
     */
    public static function supported($mod): bool {
        if (!array_key_exists($mod, self::$modsupported)) {
            self::$modsupported[$mod] = class_exists("\local_treestudyplan\\local\\ungradedscanners\\{$mod}_scanner");
        }
        return self::$modsupported[$mod];
    }

    /**
     * List all users enrolled in a course as student by userid
     * @param int $courseid Course id of the course to check
     * @return int[] Array if user ids
     */
    public static function get_course_students($courseid): array {
        global $CFG;
        if (!array_key_exists($courseid, self::$coursestudents)) {
            $students = [];
            $context = context_course::instance($courseid);
            foreach (explode(',', $CFG->gradebookroles) as $roleid) {
                $roleid = trim($roleid);
                $students = array_keys(get_role_users($roleid, $context, false, 'u.id', 'u.id ASC'));
            }
            self::$coursestudents[$courseid] = $students;
        }
        return self::$coursestudents[$courseid];
    }

    /**
     * Construct new scanner based on grade item
     * @param grade_item $gi Grade item
     */
    public function __construct(grade_item $gi) {
        $this->courseid = $gi->courseid;
        $this->gi = $gi;
        if (self::supported($gi->itemmodule)) {
            $scannerclass = "\local_treestudyplan\\local\ungradedscanners\\{$gi->itemmodule}_scanner";
            $this->scanner = new $scannerclass($gi);
        }
    }

    /**
     * Check if this scanner is usable (has an internal activity specific scanner)
     */
    public function is_available(): bool {
        return $this->scanner !== null;
    }

    /**
     * Check if the gradable item this scanner scans has pending submissions for a specific user
     * @param int $userid ID of the user to check for
     */
    public function pending($userid): bool {
        if (!array_key_exists($userid, $this->pendingcache)) {
            if ($this->scanner === null) {
                $this->pendingcache[$userid] = false;
            } else {
                $this->pendingcache[$userid] = $this->scanner->has_ungraded_submission($userid);
            }
        }
        return $this->pendingcache[$userid];
    }

    /**
     * Webservice structure for basic info
     * @param int $value Webservice requirement constant
     */
    public static function structure($value = VALUE_OPTIONAL): external_description {
        return new external_single_structure([
            "ungraded" => new external_value(PARAM_INT, 'number of ungraded submissions'),
            "completed" => new external_value(PARAM_INT, 'number of completed students'),
            "completed_pass" => new external_value(PARAM_INT, 'number of completed-pass students'),
            "completed_fail" => new external_value(PARAM_INT, 'number of completed-fail students'),
            "students" => new external_value(PARAM_INT, 'number of students that should submit'),
        ], "details about gradable submissions", $value);
    }

    /**
     * Webservice model for basic info
     */
    public function model(): array {
        // Upda.
        $students = self::get_course_students($this->courseid);
        $completed = 0;
        $ungraded = 0;
        $completedpass = 0;
        $completedfail = 0;
        foreach ($students as $userid) {
            if ($this->pending($userid)) {
                // First check if the completion needs grading.
                $ungraded++;
            } else {
                $grade = preloader::find_grade_item_final($this->gi, $userid);
                if (!empty($grade) && !empty($grade->finalgrade) && is_numeric($grade->finalgrade)) {
                    // Compare grade to minimum grade.
                    if ($this->grade_passed($grade)) {
                        $completedpass++;
                    } else {
                        $completedfail++;
                    }
                }
            }
        }

        return [
            'ungraded' => $ungraded,
            'completed' => $completed,
            'completed_pass' => $completedpass,
            'completed_fail' => $completedfail,
            'students' => count($students),
        ];
    }

    /**
     * Check if a grade is considered passed according to the rules
     * @param grade_grade $grade
     */
    private function grade_passed($grade): bool {
        // Function copied from bistate aggregator to avoid reference mazes.
        global $DB;
        $table = "local_treestudyplan_gradecfg";
        // First determine if we have a grade_config for this scale or this maximum grade.
        $finalgrade = $grade->finalgrade;
        $scale = $this->gi->load_scale();
        if (isset($scale)) {
            $gradecfg = $DB->get_record($table, ["scale_id" => $scale->id]);
        } else if ($this->gi->grademin == 0) {
            $gradecfg = $DB->get_record($table, ["grade_points" => $this->gi->grademax]);
        } else {
            $gradecfg = null;
        }

        // For point grades, a provided grade pass overrides the defaults in the gradeconfig.
        // For scales, the configuration in the gradeconfig is leading.

        if ($gradecfg && (isset($scale) || $this->gi->gradepass == 0)) {
            // If so, we need to know if the grade is .
            if ($finalgrade >= $gradecfg->min_completed) {
                return true;
            } else {
                return false;
            }
        } else if ($this->gi->gradepass > 0) {
            $range = floatval($this->gi->grademax - $this->gi->grademin);
            // If no gradeconfig and gradepass is set, use that one to determine config.
            if ($finalgrade >= $this->gi->gradepass) {
                return true;
            } else {
                return false;
            }
        } else {
            // Blind assumptions if nothing is provided.
            // Over 55% of range is completed.
            // If range >= 3 and failed is enabled, assume that this means failed.
            $g = floatval($finalgrade - $this->gi->grademin);
            $range = floatval($this->gi->grademax - $this->gi->grademin);
            $score = $g / $range;

            if ($score > 0.55) {
                return true;
            } else {
                return false;
            }
        }
    }
}
