<?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;
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_competency\competency;
use core_competency\course_competency;
use core_competency\user_competency;
use core_competency\user_competency_course;
use dml_exception;
use stdClass;

/**
 * Preload, cache and return competency objects.
 */
class preloader_competency {
    /**
     * @var stdClass[]
     */
    private static $coursecollection = [];
    /**
     * @var competency[]
     */
    private static $competencies = [];
    /**
     * @var user_competency[]
     */
    private static $usercompetencies = [];

    /**
     * @var user_competency_course[]
     */
    private static $usercompetenciescourse = [];

    /**
     * @var course_competency[]
     */
    private static $coursecompetencies = [];

    /**
     * @var int[][]
     */
    private static $usercompetencymap = [];

    /**
     * Return an object containing id's for a specific course
     *
     * @param int $courseid Id of the course to get lists of collected data for.
     * @return object
     */
    public static function coursecollection($courseid) {
        if (!array_key_exists($courseid, self::$coursecollection)) {
            self::$coursecollection[$courseid] = new stdClass();
            self::$coursecollection[$courseid]->competencies = [];
            self::$coursecollection[$courseid]->usercompetencies = [];
            self::$coursecollection[$courseid]->usercompetenciescourse = [];
            self::$coursecollection[$courseid]->coursecompetencies = [];
        }
        return self::$coursecollection[$courseid];
    }

    /**
     * Preload grades and grade items for all grades and users in the plan.
     *
     * @param studyplan $plan The studyplan to preload for
     * @param int[] $userids Ids of users in plan
     * @return void
     */
    public static function preload_all(studyplan $plan, $userids = []) {
        if (empty($userids)) {
            $userids = $plan->find_linked_userids();
        }
        self::preload_competencies($plan);
        self::preload_user_competencies($plan, $userids);
    }

    /**
     * Preload grades and grade items for all grades and users in the plan.
     *
     * @param studyplan $plan The studyplan to preload for
     * @return void
     */
    public static function preload_map(studyplan $plan) {
        self::preload_competencies($plan);
    }

    /**
     * Preload grades and grade items for all grades and users in the plan.
     *
     * @param studyplan $plan The studyplan to preload for
     * @param int[]|int $userids Userid or Array of userids to preload for
     * @return void
     */
    public static function preload_for_users(studyplan $plan, $userids) {
        if (!is_array($userids)) {
            $userids = [$userids];
        }
        self::preload_competencies($plan);
        self::preload_user_competencies($plan, $userids);
    }


    /**
     * Retrieve competency from cache or db
     *
     * @param int $id Record ID
     * @return competency
     */
    public static function get_competency(int $id): competency {
        if (array_key_exists($id, self::$competencies)) {
            // Return from cache.
            return self::$competencies[$id];
        } else {
            // Retrieve from database and cache.
            $o = new competency($id);
            self::$competencies[$id] = $o;
            return $o;
        }
    }

    /**
     * Retrieve competency from cache or db
     *
     * @param int $id Record ID
     * @return course_competency
     */
    public static function get_course_competency(int $id): course_competency {
        if (array_key_exists($id, self::$competencies)) {
            // Return from cache.
            return self::$coursecompetencies[$id];
        } else {
            // Retrieve from database and cache.
            $o = new course_competency($id);
            self::$coursecompetencies[$id] = $o;
            return $o;
        }
    }

    /**
     * Retrieve all course_competencies and competencies for a given course
     *
     * @param int $courseid
     * @return stdClass
     */
    public static function get_course_coursecompetencies($courseid) {
        $cc = self::coursecollection($courseid);
        // Retrieve from database if needed.
        if (count($cc->coursecompetencies) == 0) {
            // Retrieve directly.
            $coursecompetencies = course_competency::list_course_competencies($courseid);
            foreach ($coursecompetencies as $item) {
                // Append to cache.
                $cc->coursecompetencies[] = $item->id;
                self::$coursecompetencies[$item->id] = $item;
            }
            $competencies = course_competency::list_competencies($courseid);
            foreach ($competencies as $item) {
                // Append to cache.
                $cc->competencies[] = $item->id;
                self::$competencies[$item->id] = $item;
            }
        }
        $list = [];
        foreach ($cc->coursecompetencies as $id) {
            $list[$id] = self::$coursecompetencies[$id];
        }

        return $list;
    }

    /**
     * Retrieve usercompetency from cache or db
     *
     * @param int $id Record ID
     * @return user_competency
     */
    public static function get_user_competency(int $id): user_competency {
        if (array_key_exists($id, self::$usercompetencies)) {
            // Return from cache.
            return self::$usercompetencies[$id];
        } else {
            // Retrieve from database and cache.
            $o = new user_competency($id);
            self::$usercompetencies[$id] = $o;
            return $o;
        }
    }

    /**
     * Retrieve usercompetency from cache or db
     *
     * @param int $userid User id
     * @param int $competencyid Competency ID
     * @return user_competency
     */
    public static function find_user_competency(int $userid, int $competencyid): user_competency {
        if (
            array_key_exists($competencyid, self::$usercompetencymap)
            && array_key_exists($userid, self::$usercompetencymap[$competencyid])
        ) {
            $ucid = self::$usercompetencymap[$competencyid][$userid];
            $uc = ($ucid) ? self::get_user_competency($ucid) : null;
        } else {
            $existing = user_competency::get_multiple($userid, [$competencyid]);
            $uc = array_pop($existing);
            if (!array_key_exists($competencyid, self::$usercompetencymap)) {
                self::$usercompetencymap[$competencyid] = [];
            }
            if ($uc) {
                $ucid = $uc->get('id');
                self::$usercompetencymap[$competencyid][$userid] = $ucid;
                self::$usercompetencies[$ucid];
            } else {
                self::$usercompetencymap[$competencyid][$userid] = null;
            }
        }

        // Create empty user_competency if none found.
        if (!$uc) {
            $uc = user_competency::create_relation($userid, $competencyid);
        }

        return $uc;
    }

    /**
     * Retrieve usercompetency from cache or db
     *
     * @param int $id Record ID
     * @return user_competency_course
     */
    public static function get_user_competency_course(int $id): user_competency_course {
        if (array_key_exists($id, self::$usercompetenciescourse)) {
            // Return from cache.
            return self::$usercompetenciescourse[$id];
        } else {
            // Retrieve from database and cache.
            $o = new user_competency_course($id);
            self::$usercompetenciescourse[$id] = $o;
            return $o;
        }
    }

    /**
     * Retrieve usercompetency from cache or db
     *
     * @param int $courseid Course id
     * @param int $userid User id
     * @param int $competencyid Competency ID
     * @return user_competency_course
     */
    public static function find_user_competency_in_course(int $courseid, int $userid, int $competencyid): user_competency_course {
        $cc = self::coursecollection($courseid);

        if (
            array_key_exists($competencyid, $cc->usercompetenciescourse)
            && array_key_exists($userid, $cc->usercompetenciescourse[$competencyid])
        ) {
            $uccid = $cc->usercompetenciescourse[$competencyid][$userid];
            $ucc = ($uccid) ? self::get_user_competency_course($uccid) : null;
        } else {
            if (!array_key_exists($competencyid, $cc->usercompetenciescourse)) {
                $cc->usercompetenciescourse[$competencyid] = [];
            }

            $params = ['courseid' => $courseid, 'userid' => $userid, 'competencyid' => $competencyid];
            $ucc = user_competency_course::get_record($params);

            if ($ucc) {
                $uccid = $ucc->get('id');
                $cc->usercompetenciescourse[$competencyid][$userid] = $uccid;
                self::$usercompetenciescourse[$uccid] = $ucc;
            } else {
                $cc->usercompetenciescourse[$competencyid][$userid] = null;
            }
        }

        // Create empty user_competency if none found.
        if (!$ucc) {
            $ucc = user_competency_course::create_relation($userid, $competencyid, $courseid);
        }

        return $ucc;
    }

    /**
     * Preload all course competencies
     *
     * @param studyplan $plan The studyplan to preload for
     * @return void
     */
    public static function preload_competencies(studyplan $plan) {
        global $DB;

        // Retrieve relevant course competencies.
        $sql = "SELECT cc.*
                FROM {competency_coursecomp} cc
                INNER JOIN {local_treestudyplan_item} ti ON ti.course_id = cc.courseid
                INNER JOIN {local_treestudyplan_line} tl ON tl.id = ti.line_id
                INNER JOIN {local_treestudyplan_page} tp ON tp.id = tl.page_id
                INNER JOIN {local_treestudyplan} p ON p.id = tp.studyplan_id
                WHERE p.id = :studyplanid
                ORDER BY cc.courseid ASC, cc.sortorder ASC
                ";
        $rs = $DB->get_recordset_sql($sql, ["studyplanid" => $plan->id()]);
        foreach ($rs as $r) {
            $cc = new course_competency(0, $r); // Create new item from record.
            self::$coursecompetencies[$r->id] = $cc; // Store in cache.
            self::coursecollection($r->courseid)->coursecompetencies[] = $r->id; // Add id to course's id list.
        }
        $rs->close();

        // Retrieve relevant competencies.
        $sql = "SELECT c.*, cc.courseid AS courseid
                FROM {competency} c
                INNER JOIN {competency_coursecomp} cc ON cc.competencyid = c.id
                INNER JOIN {local_treestudyplan_item} ti ON ti.course_id = cc.courseid
                INNER JOIN {local_treestudyplan_line} tl ON tl.id = ti.line_id
                INNER JOIN {local_treestudyplan_page} tp ON tp.id = tl.page_id
                INNER JOIN {local_treestudyplan} p ON p.id = tp.studyplan_id
                WHERE p.id = :studyplanid
                ORDER BY cc.courseid ASC, cc.sortorder ASC
                ";
        $rs = $DB->get_recordset_sql($sql, ["studyplanid" => $plan->id()]);
        foreach ($rs as $r) {
            $cm = new competency(0, $r); // Create new item from record.
            self::$competencies[$r->id] = $cm; // Store in cache.
            self::coursecollection($r->courseid)->competencies[] = $r->id; // Add id to course's id list.
        }
        $rs->close();
    }

    /**
     * Preload all course competencies
     *
     * @param studyplan $plan
     * @param array|int $userids Id's of the user to load the data for
     * @return void
     */
    public static function preload_user_competencies(studyplan $plan, $userids) {
        global $DB;
        if (is_numeric($userids)) {
            $userids = [$userids];
        }
        if (count($userids) > 0) {
            [$insql, $params] = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uid');
            $params["studyplanid"] = $plan->id();

            $sql = "SELECT uc.*, cc.courseid AS courseid
                    FROM {competency_usercomp} uc
                    INNER JOIN {competency_coursecomp} cc ON cc.competencyid = uc.competencyid
                    INNER JOIN {local_treestudyplan_item} ti ON ti.course_id = cc.courseid
                    INNER JOIN {local_treestudyplan_line} tl ON tl.id = ti.line_id
                    INNER JOIN {local_treestudyplan_page} tp ON tp.id = tl.page_id
                    INNER JOIN {local_treestudyplan} p ON p.id = tp.studyplan_id
                    WHERE p.id = :studyplanid
                    AND uc.userid {$insql}
                    ORDER BY cc.courseid ASC, cc.sortorder ASC
                    ";
            $rs = $DB->get_recordset_sql($sql, $params);
            foreach ($rs as $r) {
                $uc = new user_competency(0, $r); // Create new item from record.
                self::$usercompetencies[$r->id] = $uc; // Store in cache.
                self::coursecollection($r->courseid)->usercompetencies[] = $r->id; // Add id to course's id list.
                if (!array_key_exists($r->competencyid, self::$usercompetencymap)) {
                    self::$usercompetencymap[$r->competencyid] = [];
                }
                self::$usercompetencymap[$r->competencyid][$r->userid] = $r->id;
            }
            $rs->close();

            $sql = "SELECT ucc.*
                    FROM {competency_usercompcourse} ucc
                    INNER JOIN {local_treestudyplan_item} ti ON ti.course_id = ucc.courseid
                    INNER JOIN {local_treestudyplan_line} tl ON tl.id = ti.line_id
                    INNER JOIN {local_treestudyplan_page} tp ON tp.id = tl.page_id
                    INNER JOIN {local_treestudyplan} p ON p.id = tp.studyplan_id
                    WHERE p.id = :studyplanid
                    AND ucc.userid {$insql}
                    ORDER BY ucc.courseid ASC
                    ";
            $rs = $DB->get_recordset_sql($sql, $params);
            foreach ($rs as $r) {
                $ucc = new user_competency_course(0, $r); // Create new item from record.
                self::$usercompetenciescourse[$r->id] = $ucc; // Store in cache.

                $cc = self::coursecollection($r->courseid);
                if (!array_key_exists($r->competencyid, $cc->usercompetenciescourse)) {
                    $cc->usercompetenciescourse[$r->competencyid] = [];
                }
                $cc->usercompetenciescourse[$r->competencyid][$r->userid] = $r->id;
            }
            $rs->close();
        }
    }
}
