<?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/>.

/**
 * Class to collect course completion info for a given course
 * @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;
use core\context\course as context_course;

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

require_once($CFG->libdir . '/externallib.php');
require_once($CFG->libdir . '/gradelib.php');
require_once($CFG->dirroot . '/course/lib.php');

use dml_missing_record_exception;
use grade_item;
use grade_scale;
use grade_outcome;

/**
 * Class to collect course completion info for a given course
 */
class corecompletioninfo {
    /** @var \stdClass */
    private $course;
    /** @var \completion_info */
    private $completion;

    /**
     * Cached dict of completion:: constants to equivalent webservice strings
     * @var array */
    private static $completionhandles = null;
    /**
     * Cached dict of all completion type constants to equivalent strings
     * @var array */
    private static $completiontypes = null;

    /**
     * Course id of relevant course
     */
    public function id() {
        return $this->course->id;
    }
    /**
     * Construct new object for a given course
     * @param \stdClass $course Course database record
     */
    public function __construct($course) {
        $this->course = $course;
        $this->completion = new \completion_info($this->course);
    }

    /**
     * Get dictionary of all completion types as CONST => 'string'
     * @return array
     */
    public static function completiontypes() {
        /*  While it is tempting to use the global array COMPLETION_CRITERIA_TYPES,
            so we don't have to manually add any completion types if moodle decides to add a few.
            We can just as easily add the list here manually, since adding a completion type
            requires adding code to this page anyway.
            And this way we can keep the moodle code style checker happy.
            (Moodle will probably refator that part of the code anyway in the future, without
            taking effects of this plugin into account :)

            Array declaration based completion/criteria/completion_criteria.php:85 where the global
            COMPLETION_CRITERIA_TYPES iw/was defined.
        */
        if (!isset(self::$completiontypes)) {
            self::$completiontypes = [
                COMPLETION_CRITERIA_TYPE_SELF       => 'self',
                COMPLETION_CRITERIA_TYPE_DATE       => 'date',
                COMPLETION_CRITERIA_TYPE_UNENROL    => 'unenrol',
                COMPLETION_CRITERIA_TYPE_ACTIVITY   => 'activity',
                COMPLETION_CRITERIA_TYPE_DURATION   => 'duration',
                COMPLETION_CRITERIA_TYPE_GRADE      => 'grade',
                COMPLETION_CRITERIA_TYPE_ROLE       => 'role',
                COMPLETION_CRITERIA_TYPE_COURSE     => 'course',
            ];
        }
        return self::$completiontypes;
    }

    /**
     * Translate a numeric completion constant to a text string
     * @param int $completion The completion code as defined in completionlib.php to translate to a text handle
     */
    public static function completion_handle($completion) {
        if (empty(self::$completionhandles)) {
            // Cache the translation table, to avoid overhead.
            self::$completionhandles = [
                COMPLETION_INCOMPLETE => "incomplete",
                COMPLETION_COMPLETE => "complete",
                COMPLETION_COMPLETE_PASS => "complete-pass",
                COMPLETION_COMPLETE_FAIL => "complete-fail",
                COMPLETION_COMPLETE_FAIL_HIDDEN => "complete-fail"]; // The front end won't differentiate between hidden or not.
        }
        return self::$completionhandles[$completion] ?? "undefined";
    }

    /**
     * Webservice editor structure for completion_item
     * @param bool $teachermode Teachermode? (Includes progress info)
     * @param int $value Webservice requirement constant
     */
    public static function completion_item_editor_structure($teachermode, $value = VALUE_REQUIRED): external_description {
        $parts = [
            "id" => new external_value(PARAM_INT, 'criteria id', VALUE_OPTIONAL),
            "title" => new external_value(PARAM_RAW, 'name of subitem', VALUE_OPTIONAL),
            "link" => new external_value(PARAM_RAW, 'optional link to more details', VALUE_OPTIONAL),
            "details" => new external_single_structure([
                "type" => new external_value(PARAM_RAW, 'type', VALUE_OPTIONAL),
                "criteria" => new external_value(PARAM_RAW, 'criteria', VALUE_OPTIONAL),
                "requirement" => new external_value(PARAM_RAW, 'requirement', VALUE_OPTIONAL),
                "status" => new external_value(PARAM_RAW, 'status', VALUE_OPTIONAL),
            ]),
        ];
        if ($teachermode) {
            $parts["progress"] = completionscanner::structure();
        }
        return new external_single_structure($parts, 'completion type', $value);
    }

    /**
     * Webservice editor structure for completion_type
     * @param bool $teachermode Teachermode? (Includes progress info)
     * @param int $value Webservice requirement constant
     */
    public static function completion_type_editor_structure($teachermode = false, $value = VALUE_REQUIRED): external_description {
        return new external_single_structure([
            "items" => new external_multiple_structure(
                self::completion_item_editor_structure($teachermode),
                'subitems',
                VALUE_OPTIONAL
            ),
            "title" => new external_value(PARAM_RAW, 'optional title', VALUE_OPTIONAL),
            "desc" => new external_value(PARAM_RAW, 'optional description', VALUE_OPTIONAL),
            "type" => new external_value(PARAM_TEXT, 'completion type name'),
            "aggregation" => new external_value(PARAM_TEXT, 'completion aggregation for this type ["all", "any"]'),
         ], 'completion type', $value);
    }

    /**
     * Webservice editor structure for course completion
     * @param int $value Webservice requirement constant
     */
    public static function editor_structure($value = VALUE_REQUIRED): external_description {
        return new external_single_structure([
           "conditions" => new external_multiple_structure(self::completion_type_editor_structure(false), 'completion conditions'),
           "aggregation" => new external_value(PARAM_TEXT, 'completion aggregation ["all", "any"]'),
           "enabled" => new external_value(PARAM_BOOL, "whether completion is enabled here"),
        ], 'course completion info', $value);
    }

    /**
     * Webservice teacher structure for course completion
     * @param int $value Webservice requirement constant
     */
    public static function teacher_structure($value = VALUE_REQUIRED): external_description {
        return new external_single_structure([
           "conditions" => new external_multiple_structure(self::completion_type_editor_structure(true), 'completion conditions'),
           "aggregation" => new external_value(PARAM_TEXT, 'completion aggregation ["all", "any"]'),
           "enabled" => new external_value(PARAM_BOOL, "whether completion is enabled here"),
        ], 'course completion info', $value);
    }

    /**
     * Webservice user view structure for completion_item
     * @param int $value Webservice requirement constant
     */
    public static function completion_item_user_structure($value = VALUE_REQUIRED): external_description {
        return new external_single_structure([
            "id" => new external_value(PARAM_INT, 'id of completion', VALUE_OPTIONAL),
            "title" => new external_value(PARAM_RAW, 'name of subitem', VALUE_OPTIONAL),
            "details" => new external_single_structure([
                "type" => new external_value(PARAM_RAW, 'type', VALUE_OPTIONAL),
                "criteria" => new external_value(PARAM_RAW, 'criteria', VALUE_OPTIONAL),
                "requirement" => new external_value(PARAM_RAW, 'requirement', VALUE_OPTIONAL),
                "status" => new external_value(PARAM_RAW, 'status', VALUE_OPTIONAL),
            ]),
            "link" => new external_value(PARAM_RAW, 'optional link to more details', VALUE_OPTIONAL),
            "completed" => new external_value(PARAM_BOOL, 'simple completed or not'),
            "status" => new external_value(
                PARAM_TEXT,
                'extended completion status ["incomplete", "progress", "complete", "complete-pass", "complete-fail"]'
            ),
            "pending" => new external_value(
                PARAM_BOOL,
                'optional pending state, for submitted but not yet reviewed activities',
                VALUE_OPTIONAL
            ),
            "grade" => new external_value(PARAM_TEXT, 'optional grade result for this subitem', VALUE_OPTIONAL),
            "feedback" => new external_value(PARAM_RAW, 'optional feedback for this subitem ', VALUE_OPTIONAL),
            "warning" => new external_value(PARAM_TEXT, 'optional warning text', VALUE_OPTIONAL),
         ], 'completion type', $value);
    }

    /**
     * Webservice user view structure for completion_type
     * @param int $value Webservice requirement constant
     */
    public static function completion_type_user_structure($value = VALUE_REQUIRED): external_description {
        return new external_single_structure([
            "items" => new external_multiple_structure(self::completion_item_user_structure(), 'subitems', VALUE_OPTIONAL),
            "title" => new external_value(PARAM_RAW, 'optional title', VALUE_OPTIONAL),
            "desc" => new external_value(PARAM_RAW, 'optional description', VALUE_OPTIONAL),
            "type" => new external_value(PARAM_TEXT, 'completion type name'),
            "aggregation" => new external_value(PARAM_TEXT, 'completion aggregation for this type ["all", "any"]'),
            "completed" => new external_value(PARAM_BOOL, 'current completion value for this type'),
            "status" => new external_value(
                PARAM_TEXT,
                'extended completion status ["incomplete", "progress", "complete", "complete-pass", "complete-fail"]'
            ),
            "progress" => new external_value(PARAM_INT, 'completed sub-conditions'),
            "count" => new external_value(PARAM_INT, 'total number of sub-conditions'),
         ], 'completion type', $value);
    }

    /**
     * Webservice structure for userinfo
     * @param int $value Webservice requirement constant
     */
    public static function user_structure($value = VALUE_REQUIRED): external_description {
        return new external_single_structure([
            "progress" => new external_value(PARAM_INT, 'completed sub-conditions'),
            "enabled" => new external_value(PARAM_BOOL, "whether completion is enabled here"),
            "tracked" => new external_value(PARAM_BOOL, "whether completion is tracked for the user", VALUE_OPTIONAL),
            "count" => new external_value(PARAM_INT, 'total number of sub-conditions'),
            "conditions" => new external_multiple_structure(self::completion_type_user_structure(), 'completion conditions'),
            "completed" => new external_value(PARAM_BOOL, 'current completion value'),
            "aggregation" => new external_value(PARAM_TEXT, 'completion aggregation ["all", "any"]'),
            "pending" => new external_value(PARAM_BOOL, "true if the user has any assignments pending grading", VALUE_OPTIONAL),
        ], 'course completion info', $value);
    }

    /**
     * Convert agregation method constant to equivalent string for webservice
     * @param int $method COMPLETION_AGGREGATION_ALL || COMPLETION_AGGREGATION_ANY
     * @return string 'all' or 'any'
     */
    private static function aggregation_handle($method) {
        return ($method == COMPLETION_AGGREGATION_ALL) ? "all" : "any";
    }

    /**
     * Webservice model for editor info
     * @return array Webservice data model
     */
    public function editor_model() {
        return $this->teacher_model(null);
    }

    /**
     * Webservice model for teacher info
     * @param int[]|null $studentlist List of user id's to use for checking issueing progress within a study plan
     * @return array Webservice data model
     */
    public function teacher_model(?array $studentlist) {
        global $CFG;
        $conditions = [];
        $info = [
            "conditions" => $conditions,
            "aggregation" => self::aggregation_handle($this->completion->get_aggregation_method()),
            "enabled" => $this->completion->is_enabled(),
        ];

        // Check if completion tracking is enabled for this course - otherwise, revert to defaults .
        if ($this->completion->is_enabled()) {
            // Loop through all condition types to see if they are applicable.
            foreach (self::completiontypes() as $type => $handle) {
                $criterias = preloader_core::find_criteria($this->course->id, $type);
                if (count($criterias) > 0) {
                    // Only take it into account if the criteria count is > 0.
                    $aggr = preloader_core::find_aggregation($this->course->id, $type);
                    $cinfo = [
                        "type" => $handle,
                        "aggregation" => self::aggregation_handle($aggr->method),
                        "title" => ((object)reset($criterias))->get_type_title(),
                        "items" => [],
                    ];

                    foreach ($criterias as $criteria) {
                        /*  Unfortunately, we cannot easily get the criteria details with get_details() without having a
                            user completion object involved, so'we'll have to retrieve the details per completion type.
                            See moodle/completion/criteria/completion_criteria_*.php::get_details() for the code that
                            the code below is based on.
                        */
                        $link = "";
                        unset($title); // Clear title from previous iteration if it was set.
                        if ($type == COMPLETION_CRITERIA_TYPE_SELF) {
                            $details = [
                                "type" => $criteria->get_title(),
                                "criteria" => $criteria->get_title(),
                                "requirement" => get_string('markingyourselfcomplete', 'completion'),
                                "status" => "",
                            ];
                        } else if ($type == COMPLETION_CRITERIA_TYPE_DATE) {
                            $details = [
                                "type" => get_string('datepassed', 'completion'),
                                "criteria" => get_string('remainingenroleduntildate', 'completion'),
                                "requirement" => date("Y-m-d", $criteria->timeend),
                                "status" => "",
                            ];
                        } else if ($type == COMPLETION_CRITERIA_TYPE_UNENROL) {
                            $details = [
                                "type" => get_string('unenrolment', 'completion'),
                                "criteria" => get_string('unenrolment', 'completion'),
                                "requirement" => get_string('unenrolingfromcourse', 'completion'),
                                "status" => "",
                            ];
                        } else if ($type == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
                            // Get the cm_info object.
                            $cm = preloader_core::get_cm($criteria->moduleinstance);

                            /* Criteria and requirements will be built in a moment by code copied
                               from completion_criteria_activity.php.
                            */
                            $details = [
                                "type" => $criteria->get_title(),
                                "criteria" => "",
                                "requirement" => "",
                                "status" => "",
                            ];
                            // Add CM->url link as $link.
                            $link = $cm->url->out(true);
                            if ($cm->has_view()) {
                                $details['criteria'] = \html_writer::link($cm->url, $cm->get_formatted_name());
                            } else {
                                $details['criteria'] = $cm->get_formatted_name();
                            }
                            // Set title based on cm formatted name.
                            $title = $cm->get_formatted_name();
                            // Build requirements.
                            $details['requirement'] = [];

                            if ($cm->completion == COMPLETION_TRACKING_MANUAL) {
                                $details['requirement'][] = get_string('markingyourselfcomplete', 'completion');
                            } else if ($cm->completion == COMPLETION_TRACKING_AUTOMATIC) {
                                if ($cm->completionview) {
                                    $modulename = \core_text::strtolower(get_string('modulename', $criteria->module));
                                    $details['requirement'][] = get_string('viewingactivity', 'completion', $modulename);
                                }

                                if (!is_null($cm->completiongradeitemnumber)) {
                                    $details['requirement'][] = get_string('achievinggrade', 'completion');
                                }

                                if ($cm->completionpassgrade) {
                                    $details['requirement'][] = get_string('achievingpassinggrade', 'completion');
                                }
                            }

                            $details['requirement'] = implode(', ', $details['requirement']);
                        } else if ($type == COMPLETION_CRITERIA_TYPE_DURATION) {
                            $details = [
                                "type" => get_string('periodpostenrolment', 'completion'),
                                "criteria" => get_string('remainingenroledfortime', 'completion'),
                                "requirement" => get_string('xdays', 'completion', ceil($criteria->enrolperiod / (60 * 60 * 24))),
                                "status" => "",
                            ];
                        } else if ($type == COMPLETION_CRITERIA_TYPE_GRADE) {
                            $displaytype = \grade_get_setting($this->course->id, 'displaytype', $CFG->grade_displaytype);
                            $gradepass = $criteria->gradepass;
                            // Find grade item for course result.
                            $gi = preloader::find_course_grade_items($this->course->id, ['itemtype' => 'course'])[0];
                            $displaygrade = \grade_format_gradevalue($gradepass, $gi, true, $displaytype, 1);
                            $details = [
                                "type" => get_string('coursegrade', 'completion'),
                                "criteria" => get_string('graderequired', 'completion'),
                                "requirement" => get_string('graderequired', 'completion') . ": " . $displaygrade,
                                "status" => "",
                            ];
                            $title = get_string('graderequired', 'completion') . ': ' . $displaygrade;
                        } else if ($type == COMPLETION_CRITERIA_TYPE_ROLE) {
                            $details = [
                                "type" => get_string('manualcompletionby', 'completion'),
                                "criteria" => $criteria->get_title(),
                                "requirement" => get_string('markedcompleteby', 'completion', $criteria->get_title()),
                                "status" => "",
                            ];
                        } else if ($type == COMPLETION_CRITERIA_TYPE_COURSE) {
                            $prereq = preloader::get_course($criteria->courseinstance);
                            $coursecontext = context_course::instance($prereq->id, MUST_EXIST);
                            $fullname = format_string($prereq->fullname, true, ['context' => $coursecontext]);
                            $details = [
                                "type" => $criteria->get_title(),
                                "criteria" => '<a href="' . $CFG->wwwroot . '/course/view.php?id='
                                    . $criteria->courseinstance . '">' . s($fullname) . '</a>',
                                "requirement" => get_string('coursecompleted', 'completion'),
                                "status" => "",
                            ];
                        } else {
                            // Moodle added a criteria type.
                            $details = [
                                "type" => "",
                                "criteria" => "",
                                "requirement" => "",
                                "status" => "",
                            ];
                        }

                        if (isset($studentlist)) {
                            $scanner = new completionscanner($criteria, $this->course);

                            // Only add the items list if we actually have items...
                            $cinfo["items"][] = [
                                "id" => $criteria->id,
                                "title" => isset($title) ? $title : $criteria->get_title_detailed(),
                                "details" => $details,
                                "progress" => $scanner->model($studentlist),
                                "link" => isset($link) ? $link : "",
                            ];
                        } else {
                            // Skip progress scanning unless teachermod is active.
                            $cinfo["items"][] = [
                                "id" => $criteria->id,
                                "title" => isset($title) ? $title : $criteria->get_title_detailed(),
                                "details" => $details,
                                "link" => isset($link) ? $link : "",
                            ];
                        }
                    }

                    $info['conditions'][] = $cinfo;
                }
            }
        }

        return $info;
    }

    /**
     * Determine overall completion for a given type
     * @param int $typeaggregation COMPLETION_AGGREGATION_ALL or COMPLETION_AGGREGATION_ANY
     * @param \completion_criteria[] $criteria List of criteria to aggregate for
     * @param int $userid Id of user to check completion for
     * @return bool Completed or not
     */
    private function aggregate_completions($typeaggregation, $criteria, $userid) {
        $completed = 0;
        $count = count($criteria);
        foreach ($criteria as $crit) {
            $c = preloader_core::find_criteria_completion($this->course->id, $crit->id, $userid);
            if ($c->is_complete()) {
                $completed++;
            }
        }
        if ($typeaggregation == COMPLETION_AGGREGATION_ALL) {
            return $completed >= $count;
        } else { // COMPLETION_AGGREGATION_ANY.
            return $completed > 1;
        }
    }

    /**
     * Webservice model for user course completion info
     * @param int $userid ID of user to check specific info for
     * @return array Webservice data model
     */
    public function user_model($userid) {
        $progress = $this->get_advanced_progress_percentage($userid);
        $coursecompletion = preloader_core::find_course_completion($this->course->id, $userid);
        $info = [
            'progress' => $progress->completed,
            "count" => $progress->count,
            "conditions" => [],
            "completed" => $coursecompletion->is_complete(),
            "aggregation" => self::aggregation_handle($this->completion->get_aggregation_method()),
            "enabled" => $this->completion->is_enabled(),
            "tracked" => $this->completion->is_tracked_user($userid),
        ];

        // Check if completion tracking is enabled for this course - otherwise, revert to defaults .
        if ($this->completion->is_enabled() && $this->completion->is_tracked_user($userid)) {
            $anypending = false;
            // Loop through all conditions to see if they are applicable.
            foreach (self::completiontypes() as $type => $handle) {
                // Get the main completion for this type.
                $criteria = preloader_core::find_criteria($this->course->id, $type);
                if (count($criteria) > 0) {
                    $agg = preloader_core::find_aggregation($this->course->id, $type);
                    $typeaggregation = $agg->method;
                    $completed = $this->aggregate_completions($typeaggregation, $criteria, $userid);
                    $cinfo = [
                        "type" => $handle,
                        "aggregation" => self::aggregation_handle($typeaggregation),
                        "completed" => $completed,
                        "status" => $completed ? "complete" : "incomplete",
                        "title" => ((object)reset($criteria))->get_type_title(),
                        "items" => [],
                    ];

                    $progress = 0;
                    foreach ($criteria as $criterion) {
                        $completion = preloader_core::find_criteria_completion($this->course->id, $criterion->id, $userid);

                        $iinfo = [
                            "id" => $criterion->id,
                            "title" => $criterion->get_title_detailed(),
                            "details" => $criterion->get_details($completion),
                            "completed" => $completion->is_complete(), // Make sure to override for activi.
                            "status" => self::completion_handle(
                                $completion->is_complete() ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE
                            ),
                        ];

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

                            /* To comply with the moodle completion report, only count COMPLETED_PASS as completed if
                               the completion is marked as complete by the system. Occasinally those don't match
                               and we want to show similar behaviour. This happens when completion data is reset
                               in a module
                            */
                            if (
                                !$completion->is_complete()
                                && in_array($completionstatus, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS])
                            ) {
                                /* If a passing grade was provided, but the activity was not completed,
                                 * most likely the completion data was erased.
                                 */

                                if (!is_null($cm->completiongradeitemnumber) || ($cm->completionpassgrade)) {
                                    // Show a warning if this activity has grade completions to help make sense of the completion.
                                    $iinfo["warning"] = get_string("warning_incomplete_pass", "local_treestudyplan");
                                } else {
                                    // Show a warning if this activity has no grade requirment for completion.
                                    $iinfo["warning"] = get_string("warning_incomplete_nograderq", "local_treestudyplan");
                                }
                            }

                            $iinfo['status'] = self::completion_handle($data->completionstate);
                            // Re-evaluate the completed value, to make sure COMPLETE_FAIL doesn't creep in as completed.
                            if (
                                $data->completionstate == COMPLETION_INCOMPLETE
                                || $data->completionstate == COMPLETION_COMPLETE_FAIL
                            ) {
                                $iinfo['completed'] = false;
                            } else {
                                $iinfo['completed'] = true;
                                $progress += 1; // Add a point to the progress counter.
                            }

                            // Determine the grade (retrieve from grade item, not from completion).
                            $grade = $this->get_grade($cm, $userid);
                            $iinfo['grade'] = $grade->grade;
                            $iinfo['feedback'] = $grade->feedback;
                            $iinfo['pending'] = $grade->pending;

                            $anypending = $anypending || $grade->pending;
                            // Overwrite the status with progress if something has been graded, or is pending.
                            if ($completionstatus != COMPLETION_INCOMPLETE || $anypending) {
                                if ($cinfo["status"] == "incomplete") {
                                    $cinfo["status"] = "progress";
                                }
                            }
                        } else if ($type == COMPLETION_CRITERIA_TYPE_GRADE) {
                            // Make sure we provide the current course grade.
                            $rawgrade = floatval($iinfo['details']['status']);
                            $iinfo['grade'] = $this->format_course_grade($rawgrade);
                            $rq = floatval($iinfo['details']['requirement']);
                            $iinfo['details']['requirement'] = $this->format_course_grade($rq);
                                               ;
                            $iinfo["status"] = $completion->is_complete() ? "complete-pass" : "complete-fail";
                            if ($cinfo["status"] == "incomplete") {
                                $cinfo["status"] = "progress";
                            }

                            if ($completion->is_complete()) {
                                $progress += 1; // Add a point to the progress counter.
                            }
                        } else {
                            if ($completion->is_complete()) {
                                $progress += 1; // Add a point to the progress counter.
                            }
                        }
                        // Finally add the item to the items list.
                        $cinfo["items"][] = $iinfo;
                    }

                    // Set the count and progress stats based on the Type's aggregation style.
                    if ($typeaggregation == COMPLETION_AGGREGATION_ALL) {
                        // Count and Progress amount to the sum of items.
                        $cinfo["count"] = count($cinfo["items"]);
                        $cinfo["progress"] = $progress;
                    } else { // Typeaggregation == COMPLETION_AGGREGATION_ANY.
                        // Count and progress are either 1 or 0, since any of the items.
                        // Complete's the type.
                        $cinfo["count"] = (count($cinfo["items"]) > 0) ? 1 : 0;
                        $cinfo["progress"] = ($progress > 0) ? 1 : 0;
                    }

                    $info['conditions'][] = $cinfo;
                    $info['pending'] = $anypending;
                }
            }
        }

        return $info;
    }

    /**
     * Get the grade for a certain course module
     * @param \cm_info $cm Course module
     * @param int $userid ID of user to retrieve grade for
     * @return object object containing 'grade' and optional 'feedback' attribute
     */
    private function get_grade($cm, $userid): object {
        $result = new \stdClass();
        $result->grade = ""; // Fallback code if activity cannot be graded.
        $result->feedback = null;
        $result->pending = false;

        try {
            $gi = preloader_core::find_cm_grade_item($cm->id);

            // Only the following types of grade yield a result.
            if (($gi->gradetype == GRADE_TYPE_VALUE || $gi->gradetype == GRADE_TYPE_SCALE)) {
                $grade = preloader_core::find_grade_item_final($gi, $userid); // Get the grade for the specified user.
                // Check if the final grade is available and numeric (safety check).
                if (!empty($grade) && !empty($grade->finalgrade) && is_numeric($grade->finalgrade)) {
                    $result->grade = \grade_format_gradevalue($grade->finalgrade, $gi, true, null, 1);
                    $result->feedback = \trim($grade->feedback);
                    $result->pending = (new gradingscanner($gi))->pending($userid);
                } else {
                    $result->grade = "-"; // Activity is gradable, but user did not receive a grade yet.
                    $result->feedback = null;
                    $result->pending = false;
                }
            }
            return $result;
        } catch (dml_missing_record_exception $x) {
            // Grade item not found, not a gradable activity.
            return $result;
        }
    }

    /**
     * Get the overall grade for this course
     * @param int $grade The grade object to format
     * @return string Formatted string of grade value
     */
    private function format_course_grade($grade) {
        $gi = preloader::find_course_grade_items($this->course->id, ['itemtype' => 'course'])[0];
        if ($gi) {
            return \grade_format_gradevalue($grade, $gi, true, null, 1);
        }

        return "x"; // Course cannot be graded (Shouldn't be happening, but still....).
    }

    /**
     * Returns the percentage completed by a certain user, returns null if no completion data is available.
     *
     * @param int $userid The id of the user, 0 for the current user
     * @return object The percentage info, left all 0 if  completion is not supported in the course,
     *         or if there are no activities that support completion.
     */
    public function get_advanced_progress_percentage($userid): object {

        // First, let's make sure completion is enabled.
        if (!$this->completion->is_enabled()) {
            debugging("Completion is not enabled for {$this->course->shortname}", DEBUG_NORMAL);
            return (object)[
                'count' => 0,
                'completed' => 0,
                'percentage' => 0,
            ];
        }

        if (!$this->completion->is_tracked_user($userid)) {
            debugging("$userid is not tracked in {$this->course->shortname}");
            return (object)[
                'count' => 0,
                'completed' => 0,
                'percentage' => 0,
            ];
        }

        $criteria = preloader_core::find_criteria($this->course->id);
        $aggregation = (preloader_core::find_aggregation($this->course->id))->method;
        $critcount = [];

        // Before we check how many modules have been completed see if the course has completed.
        $coursecompletion = preloader_core::find_course_completion($this->course->id, $userid);
        if ($coursecompletion->is_complete()) {
            // Simplify the progress bar to all criteria met if the course is completed.
            $count = count($criteria);
            $completed = $count;
        } else {
            // Count all completions, and sort by type.
            foreach ($criteria as $criterion) {
                $completion = preloader_core::find_criteria_completion($this->course->id, $criterion->id, $userid);

                // Make a new object for the type if it's not already there.
                $type = $criterion->criteriatype;
                if (!array_key_exists($type, $critcount)) {
                    $critcount[$type] = new \stdClass();
                    $critcount[$type]->count = 0;
                    $critcount[$type]->completed = 0;
                    $critcount[$type]->aggregation = (preloader_core::find_aggregation($this->course->id, $type))->method;
                }
                // Get a reference to the counter object for this type.
                $typecount = $critcount[$type];

                $typecount->count += 1;
                if ($criterion->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
                    // Get the cm_info object.
                    $cm = preloader_core::get_cm($criterion->moduleinstance);
                    // Retrieve data for this object.
                    $data = preloader_core::find_cm_completion($cm->id, $userid);
                    // Count complete, but failed as incomplete too...
                    if (($data->completionstate == COMPLETION_INCOMPLETE) || ($data->completionstate == COMPLETION_COMPLETE_FAIL)) {
                        $typecount->completed += 0;
                    } else {
                        $typecount->completed += 1;
                    }
                } else {
                    if ($completion->is_complete()) {
                        $typecount->completed += 1;
                    }
                }
            }

            // Now that we have all completions sorted by type, we can be smart about how to do the count.
            $count = 0;
            $completed = 0;
            $completionpercentage = 0;
            foreach ($critcount as $c) {
                // Take only types that are actually present into account.
                if ($c->count > 0) {
                    // If the aggregation for the type is ANY, reduce the count to 1 for this type.
                    // And adjust the progress accordingly (check if any have been completed or not).
                    if ($c->aggregation == COMPLETION_AGGREGATION_ALL) {
                        $ct = $c->count;
                        $cmpl = $c->completed;
                    } else {
                        $ct = 1;
                        $cmpl = ($c->completed > 0) ? 1 : 0;
                    }
                    // If ANY completion for the types, count only the criteria type with the highest completion percentage -.
                    // Overwrite data if current type is more complete.
                    if ($aggregation == COMPLETION_AGGREGATION_ANY) {
                        $pct = $cmpl / $ct;
                        if ($pct > $completionpercentage) {
                            $count = $ct;
                            $completed = $cmpl;
                            $completionpercentage = $pct;
                        }
                    } else {
                        // If ALL completion for the types, add the count for this type to that of the others.
                        $count += $ct;
                        $completed += $cmpl;
                        // Don't really care about recalculating completion percentage every round in this case.
                    }
                }
            }
        }

        $result = new \stdClass();
        $result->count = $count;
        $result->completed = $completed;
        $result->percentage = ($count > 0) ? (($completed / $count) * 100) : 0;
        return $result;
    }
}
