<?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 course completion criteria for pending grading actions
 * @package    local_treestudyplan
 * @copyright  2023 P.M. Kuipers
 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace local_treestudyplan;

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;

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

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

use grade_item;
use completion_criteria;
use dml_missing_record_exception;

/**
 * Scan course completion criteria for pending grading actions
 */
class completionscanner {
    /**
     * Cache of supported mods
     * @var array
     */
    private static $modsupported = [];

    /** The internally used grading scanner
     * @var local\ungradedscanners\scanner_base
     */
    private $scanner = null;

    /** @var int */
    private $courseid;
    /** @var stdClass */
    private $course;
    /** @var \course_modinfo */
    private $modinfo;
    /** @var completion_criteria */
    private $crit;

    /**
     * Course module
     * @var \cm_info
     */
    private $cm = 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];
    }

    /**
     * Filter a list of students to return only the students enrolled as student
     * in this scanner's course
     * @param int[] $studentlist Array of student id's
     * @return int[]
     */
    private function filter_studentlist(array $studentlist): array {
        $coursestudents = courseinfo::get_course_students($this->courseid);
        return array_intersect($studentlist, $coursestudents);
    }

    /**
     * Construct new scanner based on completion criteria and course
     * @param completion_criteria $crit Criteria to check for
     * @param stdClass $course Course DB record
     */
    public function __construct($crit, $course) {
        $this->courseid = $course->id;
        $this->course = $course;
        $this->crit = $crit;

        $this->completioninfo = new \completion_info($course);

        // Find a related scanner if the type is an activity type.
        if ($crit->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
            // Get the cm_info object.
            $this->cm = preloader_core::get_cm($crit->moduleinstance);
            // Then attempt to find a grade.
            try {
                $gi = preloader_core::find_cm_grade_item($this->cm->id);

                /* Grade none items should not be relevant.
                    Note that the grade status is probably only relevant if the item
                    has not yet received a completion, but has been submitted.
                */
                if ($gi->gradetype == GRADE_TYPE_VALUE || $gi->gradetype == GRADE_TYPE_SCALE) {
                    // If it's a relevant grade type, initialize a scanner if possible.
                    $this->gi = $gi;
                    if (self::supported($gi->itemmodule)) {
                        $scannerclass = "\local_treestudyplan\\local\ungradedscanners\\{$gi->itemmodule}_scanner";
                        $this->scanner = new $scannerclass($gi);
                    }
                }
            } catch (dml_missing_record_exception $x) {
                // Record not found, so not a gradable activity.
                $this->gi = null;
            }
        }
    }

    /**
     * Check if the criteria 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
     * @param int[]|null $studentlist Array of student userids to use for checking. Leave empty to use all course students
     */
    public function model(?array $studentlist = null): array {
        /*
            If an array of students is provided (usually the case if called from a courecompletioninfo object),
            make sure to filter it out, so only the students enrolled in the course are included.. Otherwise
            statistics are marred.
         */
        // Get completion info.
        $students = isset($studentlist) ?
                    $this->filter_studentlist($studentlist) :
                    courseinfo::get_course_students($this->courseid);
        $completed = 0;
        $ungraded = 0;
        $completedpass = 0;
        $completedfail = 0;
        foreach ($students as $userid) {
            $completion = preloader_core::find_criteria_completion($this->courseid, $this->crit->id, $userid);

            if ($this->crit->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
                // If it's an activity completion, check for detailed type of completion.
                // Retrieve data for this object.
                $data = preloader_core::find_cm_completion($this->cm->id, $userid);
                // If it's an activity completion, add all the relevant activities as sub-items.
                $completionstatus = $data->completionstate;

                if ($completionstatus == COMPLETION_COMPLETE_PASS) {
                    $completedpass++;
                } else if ($completionstatus == COMPLETION_COMPLETE_FAIL) {
                    $completedfail++;
                } else if ($completionstatus == COMPLETION_COMPLETE) {
                    $completed++;
                } else if ($this->pending($userid)) {
                    // Check if the completion needs grading.
                    $ungraded++;
                }
            } else {
                if ($completion->is_complete()) {
                    $completed++;
                }
            }
        }

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