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

/**
 * Collect, process and display information about gradable items
 * @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;
use core\context\system as context_system;
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 core_course\local\repository\content_item_readonly_repository;
use core_course\local\entity\content_item;
use grade_item;
use grade_scale;
use grade_outcome;

/**
 * Collect, process and display information about gradable items
 */
class gradeinfo {
    /** @var studyitem */
    private $studyitem = null;
    /** @var int */
    private $id;
    /** @var grade_item */
    private $gradeitem;

    /** @var string */
    private $icon;
    /** @var string */
    private $link;
    /** @var string */
    private $gradinglink;
    /** @var grade_scale*/
    private $scale;
    /** @var grade_outcome*/
    private $outcome;
    /** @var bool*/
    private $hidden = false;
    /** @var string */
    private $name;
    /** @var string */
    private $typename;
    /** @var int */
    private $section;
    /** @var int */
    private $sectionorder;
    /** @var int */
    private $cmid;
    /** @var int */
    private $coursesort;

    /** @var array */
    private static $gradablecache = [];

    /** @var array */
    private static $contentitems = null;
    /** @var gradingscanner */
    private $gradingscanner;

    /** @var array */
    private static $sections = [];

    /**
     * Get the sequence of activities for a given section id
     * @param mixed $sectionid Id of section
     * @return int[] Sequence of cms in a section
     */
    protected static function get_sectionsequence($sectionid) {
        global $DB;
        if (!array_key_exists($sectionid, self::$sections)) {
            self::$sections[$sectionid] = explode(",", $DB->get_field("course_sections", "sequence", ["id" => $sectionid]));
        }
        return self::$sections[$sectionid];
    }

    /**
     * Get the grade_item
     * @return grade_item
     */
    public function get_gradeitem(): grade_item {
        return $this->gradeitem;
    }
    /**
     * Get the gradingscanner
     * @return gradingscanner
     */
    public function get_gradingscanner(): gradingscanner {
        return $this->gradingscanner;
    }

    /**
     * Get the grade's scale if applicable
     * @return grade_scale|null
     */
    public function get_scale(): ?grade_scale {
        return $this->scale;
    }

    /**
     * Get content items (activity icons) from the repository
     * @return content_item[]
     */
    protected static function get_contentitems(): array {
        global $PAGE;
        if (empty(static::$contentitems)) {
            if (empty($PAGE->context)) {
                $PAGE->set_context(context_system::instance());
            }
            static::$contentitems = (new content_item_readonly_repository())->find_all();
        }
        return static::$contentitems;
    }

    /**
     * Get specific contentitem (activity icons) by name
     * @param mixed $name Name of content item
     * @return content_item|null
     */
    public static function get_contentitem($name): ?content_item {
        $contentitems = static::get_contentitems();
        foreach ($contentitems as $item) {
            if ($item->get_name() == $name) {
                return $item;
            }
        }
        return null;
    }

    /**
     * Get a specific course context from grade item id
     * @param int $id Grade item id
     * @return context_course
     * @throws InvalidArgumentException if grade id is not found
     */
    public static function get_coursecontext_by_id($id): context_course {
        $gi = grade_item::fetch(["id" => $id]);
        if (!is_object($gi) || course_module_instance_pending_deletion($gi->courseid, $gi->itemmodule, $gi->iteminstance)) {
            throw new \InvalidArgumentException("Grade {$id} not found in database");
        }
        return context_course::instance($gi->courseid);
    }

    /**
     * Create new object around a grade_item
     * @param int|grade_item $gi grade_item object or id of the grade item to use as base of
     * @param studyitem|null $studyitem Studyitem containg the course that references this grade
     */
    public function __construct($gi, ?studyitem $studyitem = null) {
        $this->studyitem = $studyitem;

        // Retrieve grade item if $gi is integer.
        if (is_numeric($gi)) {
            $id = $gi;
            $gi = preloader::get_grade_item($id);
            if (!is_object($gi) || course_module_instance_pending_deletion($gi->courseid, $gi->itemmodule, $gi->iteminstance)) {
                throw new \InvalidArgumentException("Grade {$id} not found in database");
            }
        }

        $this->id = $gi->id;
        $this->gradeitem = $gi;

        // Determine the icon for the associated activity.
        $contentitem = static::get_contentitem($gi->itemmodule);
        $this->icon = empty($contentitem) ? "" : $contentitem->get_icon();

        // To avoid a high number of database calls, scales and outcomes are preloaded if needed.
        // Unfortunately, that means we need to avoid using the moodle grade_item's functions.
        $this->scale = (!empty($gi->scaleid)) ? preloader::get_grade_scale($gi->scaleid) : null;
        $this->outcome = (!empty($gi->outcomeid)) ? preloader::get_grade_outcome($gi->outcomeid) : null;

        $this->hidden = ($gi->hidden || (!empty($outcome) && $outcome->hidden)) ? true : false;

        $this->name = empty($outcome) ? $gi->itemname : $outcome->name;

        // Determine a link to the associated activity.
        if ($gi->itemtype != "mod" || empty($gi->itemmodule) || empty($gi->iteminstance)) {
            $this->link = "";
            $this->cmid = 0;
            $this->section = 0;
            $this->sectionorder = 0;
        } else {
            $ccm = get_course_and_cm_from_instance($gi->iteminstance, $gi->itemmodule);
            $cminfo = $ccm[1]; // Second item in return array.

            $this->cmid = $cminfo->id;
            // Sort by position in course.
            // .
            $this->section = $cminfo->sectionnum;
            $ssequence = self::get_sectionsequence($cminfo->section);
            $this->sectionorder = array_search($cminfo->id, $ssequence);

            $this->name = $cminfo->get_formatted_name();

            $this->link = $cminfo->url->out(true);
            if ($gi->itemmodule == 'quiz') {
                $this->gradinglink = "/mod/{$gi->itemmodule}/report.php?id={$cminfo->id}&mode=grading";
            } else if ($gi->itemmodule == "assign") {
                $this->gradinglink = $this->link . "&action=grading";
            } else {
                $this->gradinglink = $this->link;
            }
        }

        $this->typename = empty($contentitem) ? $gi->itemmodule : $contentitem->get_title()->get_value();
        $this->gradingscanner = new gradingscanner($gi);

        $this->coursesort = $this->section * 1000 + $this->sectionorder;
    }

    /**
     * Check if this gradable item is selected in the studyitem
     * @return bool
     */
    public function is_selected(): bool {
        global $DB;
        if ($this->studyitem) {
            // Check if selected for this studyitem.
            $r = $DB->get_record(
                'local_treestudyplan_gradeinc',
                [
                    'studyitem_id' => $this->studyitem->id(),
                    'grade_item_id' => $this->gradeitem->id,
                ]
            );
            if ($r && $r->include) {
                return(true);
            }
        }
        return(false);
    }

    /**
     * Check if this gradable item is marked required in the studyitem
     * @return bool
     */
    public function is_required() {
        global $DB;
        if ($this->studyitem) {
            // Check if selected for this studyitem.
            $r = $DB->get_record(
                'local_treestudyplan_gradeinc',
                [
                    'studyitem_id' => $this->studyitem->id(),
                    'grade_item_id' => $this->gradeitem->id,
                ]
            );
            if ($r && $r->include && $r->required) {
                return true;
            }
        }
        return false;
    }

    /**
     * Webservice structure for editor info
     * @param int $value Webservice requirement constant
     */
    public static function editor_structure($value = VALUE_REQUIRED): external_description {
        return new external_single_structure([
            "id" => new external_value(PARAM_INT, 'grade_item id'),
            "cmid" => new external_value(PARAM_INT, 'course module id'),
            "name" => new external_value(PARAM_RAW, 'grade item name'),
            "typename" => new external_value(PARAM_TEXT, 'grade item type name'),
            "outcome" => new external_value(PARAM_BOOL, 'is outcome'),
            "selected" => new external_value(PARAM_BOOL, 'is selected for current studyitem'),
            "icon" => new external_value(PARAM_RAW, 'html for icon of related activity'),
            "link" => new external_value(PARAM_RAW, 'link to related activity'),
            "gradinglink" => new external_value(PARAM_RAW, 'link to related activity'),
            "required" => new external_value(PARAM_BOOL, 'is required for current studyitem'),
        ], 'referenced course information', $value);
    }

    /**
     * Webservice model for editor info
     * @return array Webservice data model
     */
    public function editor_model() {
        $model = [
            "id" => $this->id,
            "cmid" => $this->cmid,
            "name" => $this->name,
            "typename" => $this->typename,
            "outcome" => isset($this->outcome),
            "selected" => $this->is_selected(),
            "icon" => $this->icon,
            "link" => $this->link,
            "gradinglink" => $this->gradinglink,
            "required" => $this->is_required(),
        ];
        return $model;
    }

    /**
     * Webservice structure for teacher info
     * @param int $value Webservice requirement constant
     */
    public static function teacher_structure($value = VALUE_REQUIRED): external_description {
        return new external_single_structure([
            "id" => new external_value(PARAM_INT, 'grade_item id'),
            "cmid" => new external_value(PARAM_INT, 'course module id'),
            "name" => new external_value(PARAM_RAW, 'grade item name'),
            "typename" => new external_value(PARAM_TEXT, 'grade item type name'),
            "outcome" => new external_value(PARAM_BOOL, 'is outcome'),
            "selected" => new external_value(PARAM_BOOL, 'is selected for current studyitem'),
            "icon" => new external_value(PARAM_RAW, 'html for icon of related activity'),
            "link" => new external_value(PARAM_RAW, 'link to related activity'),
            "gradinglink" => new external_value(PARAM_RAW, 'link to related activity'),
            "grading" => gradingscanner::structure(),
            "required" => new external_value(PARAM_BOOL, 'is required for current studyitem'),
        ], 'referenced course information', $value);
    }

    /**
     * Webservice model for teacher info
     * @param studyitem|null $studyitem Related studyitem to check for
     * @return array Webservice data model
     */
    public function teacher_model(?studyitem $studyitem = null) {
        $model = [
            "id" => $this->id,
            "cmid" => $this->cmid,
            "name" => $this->name,
            "typename" => $this->typename,
            "outcome" => isset($this->outcome),
            "selected" => $this->is_selected(),
            "icon" => $this->icon,
            "link" => $this->link,
            "gradinglink" => $this->gradinglink,
            "required" => $this->is_required(),
        ];
        // Unfortunately, lazy loading of the completion data is off, since we need the data to show study item completion...
        if (
            $studyitem !== null
            && $this->is_selected()
            && has_capability('local/treestudyplan:viewuserreports', $studyitem->studyline()->studyplan()->context())
            && $this->gradingscanner->is_available()
        ) {
            $model['grading'] = $this->gradingscanner->model();
        }
        return $model;
    }

    /**
     * Webservice structure for user info
     * @param int $value Webservice requirement constant
     */
    /**
     * 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([
            "id" => new external_value(PARAM_INT, 'grade_item id'),
            "cmid" => new external_value(PARAM_INT, 'course module id'),
            "name" => new external_value(PARAM_RAW, 'grade item name'),
            "typename" => new external_value(PARAM_TEXT, 'grade item type name'),
            "grade" => new external_value(PARAM_TEXT, 'grade value'),
            "gradetype" => new external_value(PARAM_TEXT, 'grade type (completion|grade)'),
            "feedback" => new external_value(PARAM_RAW, 'html for feedback'),
            "completion" => new external_value(PARAM_TEXT, 'completion state (incomplete|progress|completed|excellent)'),
            "icon" => new external_value(PARAM_RAW, 'html for icon of related activity'),
            "link" => new external_value(PARAM_RAW, 'link to related activity'),
            "pendingsubmission" => new external_value(PARAM_BOOL, 'is selected for current studyitem', VALUE_OPTIONAL),
            "required" => new external_value(PARAM_BOOL, 'is required for current studyitem'),
            "selected" => new external_value(PARAM_BOOL, 'is selected for current studyitem'),
        ], 'referenced course information', $value);
    }

    /**
     * Webservice model for user info
     * @param int $userid ID of user to check specific info for
     * @return array Webservice data model
     */
    public function user_model($userid) {
        $grade = preloader::find_grade_item_final($this->gradeitem, $userid);
        // Format grade for proper display.
        $finalgrade = \grade_format_gradevalue(empty($grade) ? null : $grade->finalgrade, $this->gradeitem, true, null, 1);

        // Retrieve the aggregator and determine completion.
        if (!isset($this->studyitem)) {
            throw new \UnexpectedValueException("Study item not set (null) for gradeinfo in report mode");
        }
        $aggregator = $this->studyitem->studyline()->studyplan()->aggregator();
        $completion = $aggregator->grade_completion($this, $userid);

        $model = [
            "id" => $this->id,
            "cmid" => $this->cmid,
            "name" => $this->name,
            "typename" => $this->typename,
            "grade" => $finalgrade,
            "gradetype" => isset($this->scale) ? "completion" : "grade",
            "feedback" => empty($grade) ? null : $grade->feedback,
            "completion" => completion::label($completion),
            "icon" => $this->icon,
            "link" => $this->link,
            "pendingsubmission" => $this->gradingscanner->pending($userid),
            "required" => $this->is_required(),
            "selected" => $this->is_selected(),
        ];

        return $model;
    }

    /**
     * Export essential information for export
     * @return array information model
     */
    public function export_model(): array {
        return [
            "name" => $this->name,
            "type" => $this->gradeitem->itemmodule,
            "selected" => $this->is_selected(),
            "required" => $this->is_required(),
        ];
    }

    /**
     * Import data from exported model into database
     * @param studyitem $item Studyitem related to this gradable
     * @param array $model Model data previously exported
     */
    public static function import(studyitem $item, array $model) {
        if ($item->type() == studyitem::COURSE) {
            $courseid = $item->courseid();
            $gradeitems = grade_item::fetch_all(['itemtype' => 'mod', 'courseid' => $courseid]);
            foreach ($gradeitems as $gi) {
                $giname = empty($outcome) ? $gi->itemname : $outcome->name;
                $gitype = $gi->itemmodule;

                if ($giname == $model["name"] && $gitype == $model["type"]) {
                    // We have a match.
                    if (!isset($model["selected"])) {
                        $model["selected"] = true;
                    }
                    if (!isset($model["required"])) {
                        $model["required"] = false;
                    }
                    if ($model["selected"] || $model["required"]) {
                        static::include_grade($gi->id, $item->id(), $model["selected"], $model["required"]);
                    }
                }
            }
        }
    }

    /**
     * Get a list if all gradable activities in a given course
     * @param \stdClass $course Course database record
     * @param studyitem|null $studyitem Studyitem linked to the course which can be linked to created gradeinfo objects
     * @return gradeinfo[] Array of gradeinfo
     */
    public static function list_course_gradables($course, ?studyitem $studyitem = null): array {
        $list = [];

        $activities = \course_modinfo::get_array_of_activities($course);
        $gradeitems = preloader::find_course_grade_items($course->id, ['itemtype' => 'mod']);

        // Identify the visible activities.
        $visibleactivities = [];
        foreach ($activities as $act) {
            if (!array_key_exists($act->mod, $visibleactivities)) {
                $visibleactivities[$act->mod] = [];
            }
            if ($act->visible) {
                $visibleactivities[$act->mod][] = $act->id;
            }
        }

        foreach ($gradeitems as $gi) {
            if (
                array_key_exists($gi->itemmodule, $visibleactivities)
                && in_array($gi->iteminstance, $visibleactivities[$gi->itemmodule])
            ) {
                if (($gi->gradetype == GRADE_TYPE_VALUE || $gi->gradetype == GRADE_TYPE_SCALE)) {
                    try {
                        // Make new self based on fetched grade item. No point in redoing databse calls here.
                        $gradable = new static($gi, $studyitem);
                        $list[] = $gradable;
                    } catch (\InvalidArgumentException $x) {
                        // Pass and do not add to the list if an error occurs.
                        $gradable = null; // Clean up gradable variable to avoid empty catch statement.
                    }
                }
            }
        }

        usort($list, function ($a, $b) {
            $course = $a->coursesort <=> $b->coursesort;
            return ($course != 0) ? $course : $a->gradeitem->sortorder <=> $b->gradeitem->sortorder;
        });
        return $list;
    }

    /**
     * List all gradables enabled for a given study item
     * @param studyitem $studyitem The studyitem to search for
     * @return gradeinfo[] Array of gradeinfo
     */
    public static function list_studyitem_gradables(studyitem $studyitem): array {
        global $DB;
        if (array_key_exists($studyitem->id(), self::$gradablecache)) {
            return self::$gradablecache[$studyitem->id()];
        } else {
            $list = [];
            $links = preloader::find_studyitem_links($studyitem->id());
            foreach ($links as $r) {
                if ($r->include || $r->required) {
                    // Create new grade_item from record and new gradeinfo object.
                    try {
                        $gi = preloader::get_grade_item($r->grade_item_id);
                        $list[] = new static($gi, $studyitem);
                    } catch (\dml_exception $x) {
                        // Grade item not found, delete link.
                        $DB->delete_records("local_treestudyplan_gradeinc", ["id" => $r->id]);
                    }
                }
            }

            usort($list, function ($a, $b) {
                $course = $a->coursesort <=> $b->coursesort;
                return ($course != 0) ? $course : $a->gradeitem->sortorder <=> $b->gradeitem->sortorder;
            });

            self::$gradablecache[$studyitem->id()] = $list; // Cache the results.
            return $list;
        }
    }

    /**
     * Webservice executor to include grade with studyitem or not.
     * if both $inclue and $required are false, any existing DB record will be removed
     * @param int $gradeid ID of the grade_item
     * @param int $itemid ID of the study item
     * @param bool $include Select grade_item for studyitem or not
     * @param bool $required Mark grade_item as required or not
     * @return success Always returns successful success object
     */
    public static function include_grade(int $gradeid, int $itemid, bool $include, bool $required = false) {
        global $DB;
        $table = 'local_treestudyplan_gradeinc';
        if ($include) {
            // Make sure a record exits.
            $r = $DB->get_record($table, ['studyitem_id' => $itemid, 'grade_item_id' => $gradeid]);
            if ($r) {
                $r->include = 1;
                $r->required = boolval($required) ? 1 : 0;
                $DB->update_record($table, $r);
            } else {
                $DB->insert_record(
                    $table,
                    [
                        'studyitem_id' => $itemid,
                        'grade_item_id' => $gradeid,
                        'include' => 1,
                        'required' => boolval($required) ? 1 : 0,
                    ]
                );
            }
        } else {
            // Remove if it should not be included.
            $r = $DB->get_record($table, ['studyitem_id' => $itemid, 'grade_item_id' => $gradeid]);
            if ($r) {
                $DB->delete_records($table, ['id' => $r->id]);
            }
        }

        return success::success();
    }
}
