<?php
// This file is part of Moodle - https://moodle.org/
//
// 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/>.

namespace theme_snap;

use dml_exception;
use grade_item;
use stdClass;

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

require_once($CFG->dirroot.'/mod/assign/locallib.php');

/**
 * Activity functions.
 * These functions are in a class purely for auto loading convenience.
 *
 * @package   theme_snap
 * @copyright Copyright (c) 2015 Open LMS (https://www.openlms.net)
 * @license   https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class activity {

    public static $phpunitallowcaching = false;

    /**
     * @param \cm_info $mod
     * @return activity_meta
     */
    public static function module_meta(\cm_info $mod) {
        $methodname = $mod->modname . '_meta';
        if (method_exists('theme_snap\\activity', $methodname)) {
            $meta = call_user_func('theme_snap\\activity::' . $methodname, $mod);
        } else {
            $meta = new activity_meta(); // Return empty activity meta.
        }
        return $meta;
    }

    /**
     * Return standard meta data for module
     *
     * @param cm_info $mod
     * @param string $timeopenfld
     * @param string $timeclosefld
     * @param string $keyfield
     * @param string $submissiontable
     * @param string $submittedonfld
     * @param string $submitstrkey
     * @param bool $isgradeable
     * @param string $submitselect - sql to further filter submission row select statement - e.g. st.status='finished'
     * @param bool $submissionnotrequired
     * @return activity_meta
     */
    protected static function std_meta(\cm_info $mod,
                                       $timeopenfld,
                                       $timeclosefld,
                                       $keyfield,
                                       $submissiontable,
                                       $submittedonfld,
                                       $submitstrkey,
                                       $isgradeable = false,
                                       $submitselect = '',
                                       $submissionnotrequired = false
    ) {
        global $USER;

        $courseid = $mod->course;

        // Create meta data object.
        $meta = new activity_meta();
        $meta->submissionnotrequired = $submissionnotrequired;
        $meta->submitstrkey = $submitstrkey;
        $meta->submittedstr = get_string($submitstrkey, 'theme_snap');
        $meta->notsubmittedstr = get_string('not'.$submitstrkey, 'theme_snap');
        if (get_string_manager()->string_exists($mod->modname.'draft', 'theme_snap')) {
            $meta->draftstr = get_string($mod->modname.'draft', 'theme_snap');
        } else {
            $meta->draftstr = get_string('draft', 'theme_snap');
        }

        if (get_string_manager()->string_exists($mod->modname.'reopened', 'theme_snap')) {
            $meta->reopenedstr = get_string($mod->modname.'reopened', 'theme_snap');
        } else {
            $meta->reopenedstr = get_string('reopened', 'theme_snap');
        }

        // If module is not visible to the user then don't bother getting meta data.
        if (!$mod->visibleoncoursepage) {
            return $meta;
        }

        $activitydates = self::instance_activity_dates($courseid, $mod, $timeopenfld, $timeclosefld);
        $meta->timeopen = $activitydates->timeopen;
        $meta->timeclose = $activitydates->timeclose;
        $meta->timesfromcache = !empty($activitydates->fromcache);

        if (isset($activitydates->extension)) {
            $meta->extension = $activitydates->extension;
        }

        // TODO: use activity specific "teacher" capabilities.
        if (has_capability('mod/assign:grade', $mod->context)) {
            $meta->isteacher = true;

            // Teacher - useful teacher meta data.
            $methodnsubmissions = $mod->modname.'_num_submissions';
            $methodnungraded = $mod->modname.'_num_submissions_ungraded';

            if (method_exists('theme_snap\\activity', $methodnsubmissions)) {
                $meta->numsubmissions = call_user_func('theme_snap\\activity::'.$methodnsubmissions, $courseid, $mod->instance);
            }
            if (method_exists('theme_snap\\activity', $methodnungraded)) {
                $meta->numrequiregrading = call_user_func('theme_snap\\activity::'.$methodnungraded, $courseid, $mod->instance);
            }
        } else {
            // Student - useful student meta data - only display if activity is available.
            if (empty($activitydates->timeopen) || $activitydates->timeopen <= time()) {

                $submissionrow = self::get_submission_row($courseid, $mod, $submissiontable, $keyfield, $submitselect);

                if (!empty($submissionrow)) {
                    if ($mod->modname === 'assign' && !empty($submissionrow->status)) {
                        switch ($submissionrow->status) {
                            case ASSIGN_SUBMISSION_STATUS_DRAFT:
                                $meta->draft = true;
                                break;

                            case ASSIGN_SUBMISSION_STATUS_REOPENED:
                                $meta->reopened = true;
                                break;

                            case ASSIGN_SUBMISSION_STATUS_SUBMITTED:
                                $meta->submitted = true;
                                break;
                        }
                    } else {
                        $meta->submitted = true;
                        $meta->timesubmitted = !empty($submissionrow->$submittedonfld) ? $submissionrow->$submittedonfld : null;
                    }
                    // If submitted on field uses modified field then fall back to timecreated if modified is 0.
                    if (empty($meta->timesubmitted) && $submittedonfld = 'timemodified') {
                        if (isset($submissionrow->timemodified)) {
                            $meta->timesubmitted = $submissionrow->timemodified;
                        } else {
                            $meta->timesubmitted = $submissionrow->timecreated;
                        }
                    }
                }
            }

            $graderow = false;
            if ($isgradeable) {
                $graderow = self::grade_row($courseid, $mod);
            }

            if ($graderow) {
                // Looking for a visible grade.
                $gradeitems = grade_item::fetch_all([
                    'courseid'     => $courseid,
                    'itemtype'     => 'mod',
                    'itemmodule'   => $mod->modname,
                    'iteminstance' => $mod->instance,
                ]);

                $coursecontext = \context_course::instance($courseid);
                foreach ($gradeitems as $gradeitem) {
                    $grade = new \grade_grade(['itemid' => $gradeitem->id, 'userid' => $USER->id]);
                    $canviewhiddengrade = has_capability('moodle/grade:viewhidden', $coursecontext);
                    if (!$grade->is_hidden() || $canviewhiddengrade) {
                        $meta->grade = true; // Found a visible grade, item is graded.
                        break;
                    }
                }
            }
        }

        if (!empty($meta->timeclose)) {
            // Submission required?
            $subreqd = empty($meta->submissionnotrequired);

            // Overdue?
            $meta->overdue = $subreqd && empty($meta->submitted) && (time() > $meta->timeclose);
        }

        return $meta;
    }

    /**
     * Get assignment meta data
     *
     * @param cm_info $modinst - module instance
     * @return activity_meta
     */
    public static function assign_meta(\cm_info $modinst) {
        global $DB;
        static $submissionsenabled;
        static $coursequeried;

        $courseid = $modinst->course;

        // Get count of enabled submission plugins grouped by assignment id.
        // Note, under normal circumstances we only run this once but with PHP unit tests, assignments are being
        // created one after the other and so this needs to be run each time during a PHP unit test.
        if (empty($submissionsenabled) || $coursequeried !== $courseid || PHPUNIT_TEST) {
            $sql = "SELECT a.id, count(1) AS submissionsenabled
                      FROM {assign} a
                      JOIN {assign_plugin_config} ac ON ac.assignment = a.id
                     WHERE a.course = ?
                       AND ac.name='enabled'
                       AND ac.value = '1'
                       AND ac.subtype='assignsubmission'
                       AND plugin!='comments'
                  GROUP BY a.id;";
            $submissionsenabled = $DB->get_records_sql($sql, array($courseid));
            $coursequeried = $courseid;
        }

        $submitselect = '';

        // If there aren't any submission plugins enabled for this module, then submissions are not required.
        if (empty($submissionsenabled[$modinst->instance])) {
            $submissionnotrequired = true;
        } else {
            $submissionnotrequired = false;
        }

        $meta = self::std_meta($modinst, 'allowsubmissionsfromdate', 'duedate', 'assignment', 'submission',
            'timemodified', 'submitted', true, $submitselect, $submissionnotrequired);

        return ($meta);
    }

    /**
     * Get choice module meta data
     *
     * @param cm_info $modinst - module instance
     * @return string
     */
    public static function choice_meta(\cm_info $modinst) {
        return  self::std_meta($modinst, 'timeopen', 'timeclose', 'choiceid', 'answers', 'timeseen', 'answered');
    }

    /**
     * Get database module meta data
     *
     * @param cm_info $modinst - module instance
     * @return string
     */
    public static function data_meta(\cm_info $modinst) {
        return self::std_meta($modinst, 'timeavailablefrom', 'timeavailableto', 'dataid', 'records', 'timemodified', 'contributed');
    }

    /**
     * Get feedback module meta data
     *
     * @param cm_info $modinst - module instance
     * @return string
     */
    public static function feedback_meta(\cm_info $modinst) {
        return self::std_meta($modinst, 'timeopen', 'timeclose', 'feedback', 'completed', 'timemodified', 'submitted');
    }

    /**
     * Get lesson module meta data
     *
     * @param cm_info $modinst - module instance
     * @return string
     */
    public static function lesson_meta(\cm_info $modinst) {
        $meta = self::std_meta($modinst, 'available', 'deadline', 'lessonid', 'attempts', 'timeseen', 'attempted', true);
        $meta->submissionnotrequired = true;
        return $meta;
    }

    /**
     * Get quiz module meta data
     *
     * @param cm_info $modinst - module instance
     * @return string
     */
    public static function quiz_meta(\cm_info $modinst) {
        return self::std_meta($modinst, 'timeopen', 'timeclose', 'quiz',
            'attempts', 'timemodified', 'attempted', true, 'AND st.state=\'finished\'');
    }

    /**
     * Get forum module meta data
     *
     * @param cm_info $modinst - module instance
     * @return string
     */
    public static function forum_meta(\cm_info $modinst) {
        $meta = new activity_meta();
        $courseid = $modinst->course;
        $activitydates = self::instance_activity_dates($courseid, $modinst, 'timemodified', 'duedate');
        $meta->timeopen = $activitydates->timeopen;
        $meta->timeclose = $activitydates->timeclose;
        $meta->timesfromcache = !empty($activitydates->fromcache);
        return $meta;
    }

    /**
     * Get scorm module meta data
     *
     * @param cm_info $modinst - module instance
     * @return string
     */
    public static function scorm_meta(\cm_info $modinst) {
        $meta = new activity_meta();
        $courseid = $modinst->course;
        $activitydates = self::instance_activity_dates($courseid, $modinst, 'timeopen', 'timeclose');
        $meta->timeopen = $activitydates->timeopen;
        $meta->timeclose = $activitydates->timeclose;
        $meta->timesfromcache = !empty($activitydates->fromcache);
        return $meta;
    }

    /**
     * Get workshop module meta data
     *
     * @param cm_info $modinst - module instance
     * @return string
     */
    public static function workshop_meta(\cm_info $modinst) {
        $meta = new activity_meta();
        $courseid = $modinst->course;
        $activitydates = self::instance_activity_dates($courseid, $modinst, 'submissionstart', 'submissionend');
        $meta->timeopen = $activitydates->timeopen;
        $meta->timeclose = $activitydates->timeclose;
        $meta->timesfromcache = !empty($activitydates->fromcache);
        return $meta;
    }

    /**
     * Get all assignments (for all courses) waiting to be graded.
     *
     * @param array $courseids
     * @param int $since
     * @return array $ungraded
     */
    public static function assign_ungraded($courseids, $since = null) {
        global $DB;

        $ungraded = array();

        if ($since === null) {
            $since = time() - (12 * WEEKSECS);
        }

        // Limit to assignments with grades.
        $gradetypelimit = 'AND gi.gradetype NOT IN (' . GRADE_TYPE_NONE . ',' . GRADE_TYPE_TEXT . ')';

        foreach ($courseids as $courseid) {

            // Get the assignments that need grading.
            [$esql, $params] = get_enrolled_sql(\context_course::instance($courseid), 'mod/assign:submit', 0, true);
            $params['courseid'] = $courseid;

            [$sqlgroupsjoin, $sqlgroupswhere, $groupparams] = self::get_groups_sql($courseid);

            $sql = "-- Snap sql
                    SELECT cm.id AS coursemoduleid, a.id AS instanceid, a.course,
                           a.allowsubmissionsfromdate AS opentime, a.duedate AS closetime,
                           count(DISTINCT sb.userid) AS ungraded
                      FROM {assign} a
                      JOIN {course} c ON c.id = a.course
                      JOIN {modules} m ON m.name = 'assign'

                      JOIN {course_modules} cm
                        ON cm.module = m.id
                       AND cm.instance = a.id

                      JOIN {assign_submission} sb
                        ON sb.assignment = a.id
                       AND sb.latest = 1

                      JOIN ($esql) e
                        ON e.id = sb.userid

 -- Start of join required to make assignments marked via gradebook not show as requiring grading
 -- Note: This will lead to disparity between the assignment page (mod/assign/view.php[questionmark]id=[id])
 -- and the module page will still say that 1 item requires grading.

                 LEFT JOIN {assign_grades} ag
                        ON ag.assignment = sb.assignment
                       AND ag.userid = sb.userid
                       AND ag.attemptnumber = sb.attemptnumber

                 LEFT JOIN {grade_items} gi
                        ON gi.courseid = a.course
                       AND gi.itemtype = 'mod'
                       AND gi.itemmodule = 'assign'
                       AND gi.itemnumber = 0
                       AND gi.iteminstance = cm.instance

                 LEFT JOIN {grade_grades} gg
                        ON gg.itemid = gi.id
                       AND gg.userid = sb.userid
                       $sqlgroupsjoin

-- End of join required to make assignments classed as graded when done via gradebook

                     WHERE sb.status = 'submitted'
                       AND a.course = :courseid

                       AND (
                           sb.timemodified > gg.timemodified
                           OR gg.finalgrade IS NULL
                       )

                       AND (a.duedate = 0 OR a.duedate > $since)
                       $sqlgroupswhere
                 $gradetypelimit
                 GROUP BY instanceid, a.course, opentime, closetime, coursemoduleid ORDER BY a.duedate ASC";
            $rs = $DB->get_records_sql($sql, array_merge($params, $groupparams));
            $ungraded = array_merge($ungraded, $rs);
        }

        return $ungraded;
    }

    /**
     * Get Quizzes waiting to be graded.
     *
     * @param array $courseids
     * @param int $since
     * @return array $ungraded
     */
    public static function quiz_ungraded($courseids, $since = null) {
        global $DB;

        if ($since === null) {
            $since = time() - (12 * WEEKSECS);
        }

        $ungraded = array();

        foreach ($courseids as $courseid) {

            // Get people who are typically not students (people who can view grader report) so that we can exclude them!
            [$graderids, $params] = get_enrolled_sql(\context_course::instance($courseid), 'moodle/grade:viewall');
            $params['courseid'] = $courseid;

            $sql = "-- Snap SQL
                    SELECT cm.id AS coursemoduleid, q.id AS instanceid, q.course,
                           q.timeopen AS opentime, q.timeclose AS closetime,
                           count(DISTINCT qa.userid) AS ungraded
                      FROM {quiz} q
                      JOIN {course} c ON c.id = q.course AND q.course = :courseid
                      JOIN {modules} m ON m.name = 'quiz'
                      JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = q.id

-- Get ALL ungraded attempts for this quiz

					  JOIN {quiz_attempts} qa ON qa.quiz = q.id
					   AND qa.sumgrades IS NULL
					   AND qa.preview = 0

-- Exclude those people who can grade quizzes and suspended users

          		      JOIN {enrol} en ON en.courseid = q.course
                      JOIN {user_enrolments} ue ON en.id = ue.enrolid
                       AND qa.userid = ue.userid
                     WHERE ue.status = 0
                       AND qa.userid NOT IN ($graderids)
                       AND qa.state = 'finished'
                       AND (q.timeclose = 0 OR q.timeclose > $since)
                  GROUP BY instanceid, q.course, opentime, closetime, coursemoduleid
                  ORDER BY q.timeclose ASC";

            $rs = $DB->get_records_sql($sql, $params);
            $ungraded = array_merge($ungraded, $rs);
        }

        return $ungraded;
    }

    // The lesson_ungraded function has been removed as it was very tricky to implement.
    // This was because it creates a grade record as soon as a student finishes the lesson.

    /**
     * Get number of ungraded submissions for specific assignment
     * Based on count_submissions_need_grading() in mod/assign/locallib.php
     *
     * @param int $courseid
     * @param int $modid
     * @return int
     */
    public static function assign_num_submissions_ungraded($courseid, $modid) {
        global $DB;

        static $hasgrades = null;
        static $totalsbyid;

        // Use cache to see if assign has grades.
        if ($hasgrades != null && !isset($hasgrades[$modid])) {
            return 0;
        }

        // Use cache to return number of assigns yet to be graded.
        if (!empty($totalsbyid) && !local::duringtesting()) {
            if (isset($totalsbyid[$modid])) {
                return intval($totalsbyid[$modid]->total);
            } else {
                return 0;
            }
        }

        // Check to see if this assign is graded.
        $params = array(
            'courseid'      => $courseid,
            'itemtype'      => 'mod',
            'itemmodule'    => 'assign',
            'gradetypenone' => GRADE_TYPE_NONE,
            'gradetypetext' => GRADE_TYPE_TEXT,
        );

        $sql = 'SELECT DISTINCT iteminstance
                FROM {grade_items}
                WHERE courseid = ?
                AND itemtype = ?
                AND itemmodule = ?
                AND gradetype <> ?
                AND gradetype <> ?';

        $hasgrades = $DB->get_records_sql($sql, $params);

        if (!isset($hasgrades[$modid])) {
            return 0;
        }
        list($course, $cm) = (get_course_and_cm_from_instance($modid, 'assign', $courseid));
        $currentgroup = groups_get_activity_group($cm, true);

        // Get grading information for remaining of assigns.
        $coursecontext = \context_course::instance($courseid);
        [$esql, $params] = get_enrolled_sql($coursecontext, 'mod/assign:submit', $currentgroup, true);

        $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $params['courseid'] = $courseid;

        $sql = "-- Snap sql
                 SELECT sb.assignment, count(sb.userid) AS total
                   FROM {assign_submission} sb

                   JOIN {assign} an
                     ON sb.assignment = an.id

              LEFT JOIN {assign_grades} ag
                     ON sb.assignment = ag.assignment
                    AND sb.userid = ag.userid
                    AND sb.attemptnumber = ag.attemptnumber

-- Start of join required to make assignments marked via gradebook not show as requiring grading
-- Note: This will lead to disparity between the assignment page (mod/assign/view.php[questionmark]id=[id])
-- and the module page will still say that 1 item requires grading.

              LEFT JOIN {grade_items} gi
                     ON gi.courseid = an.course
                    AND gi.itemtype = 'mod'
                    AND gi.itemmodule = 'assign'
                    AND gi.itemnumber = 0
                    AND gi.iteminstance = an.id

              LEFT JOIN {grade_grades} gg
                     ON gg.itemid = gi.id
                    AND gg.userid = sb.userid

-- End of join required to make assignments classed as graded when done via gradebook

-- Start of enrolment join to make sure we only include students that are allowed to submit. Note this causes an ALL
-- join on mysql!
                   JOIN ($esql) e
                     ON e.id = sb.userid
-- End of enrolment join

                  WHERE an.course = :courseid
                    AND sb.timemodified IS NOT NULL
                    AND sb.status = :submitted
                    AND sb.latest = 1

                    AND (
                        sb.timemodified > gg.timemodified
                        OR gg.finalgrade IS NULL
                    )

                GROUP BY sb.assignment
               ";

        $totalsbyid = $DB->get_records_sql($sql, $params);
        return isset($totalsbyid[$modid]) ? intval($totalsbyid[$modid]->total) : 0;
    }

    /**
     * Standard function for getting number of submissions (where sql is not complicated and pretty much standard)
     *
     * @param int $courseid
     * @param int $modid
     * @param string $maintable
     * @param string $mainkey
     * @param string $submittable
     * @return int
     */
    protected static function std_num_submissions($courseid,
                                                  $modid,
                                                  $maintable,
                                                  $mainkey,
                                                  $submittable,
                                                  $extraselect = '') {
        global $DB;

        static $modtotalsbyid = array();

        if (!isset($modtotalsbyid[$maintable][$courseid])) {
            // Results are not cached, so lets get them.

            // Get people who are typically not students (people who can view grader report) so that we can exclude them!
            [$graderids, $params] = get_enrolled_sql(\context_course::instance($courseid), 'moodle/grade:viewall');
            $params['courseid'] = $courseid;

            if ($maintable == 'quiz') {
                $quizvalidation = "AND sb.preview = 0";
            } else {
                $quizvalidation = "";
            }
            // Get the number of submissions for all $maintable activities in this course.
            $sql = "-- Snap sql
                    SELECT m.id, COUNT(DISTINCT sb.userid) as totalsubmitted
                      FROM {".$maintable."} m
                      JOIN {".$submittable."} sb ON m.id = sb.$mainkey
                      JOIN {enrol} en ON en.courseid = m.course
                      JOIN {user_enrolments} ue ON en.id = ue.enrolid
                       AND sb.userid = ue.userid
                     WHERE ue.status = 0
                       AND m.course = :courseid
                           AND sb.userid NOT IN ($graderids)
                           $quizvalidation
                           $extraselect
                     GROUP BY m.id";
            $modtotalsbyid[$maintable][$courseid] = $DB->get_records_sql($sql, $params);
        }
        $totalsbyid = $modtotalsbyid[$maintable][$courseid];

        if (!empty($totalsbyid)) {
            if (isset($totalsbyid[$modid])) {
                return intval($totalsbyid[$modid]->totalsubmitted);
            }
        }
        return 0;
    }

    /**
     * Assign module function for getting number of submissions
     *
     * @param int $courseid
     * @param int $modid
     * @return int
     */
    public static function assign_num_submissions($courseid, $modid) {
        global $DB;

        static $modtotalsbyid = array();

        if (!isset($modtotalsbyid['assign'][$courseid])) {
            // Results are not cached, so lets get them.

            [$esql, $params] = get_enrolled_sql(\context_course::instance($courseid), 'mod/assign:submit', 0, true);
            $params['courseid'] = $courseid;
            $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;

            [$sqlgroupsjoin, $sqlgroupswhere, $groupparams] = self::get_groups_sql($courseid);

            // Get the number of submissions for all assign activities in this course.
            $sql = "-- Snap sql
                SELECT m.id, COUNT(sb.userid) as totalsubmitted
                  FROM {assign} m
                  JOIN {assign_submission} sb
                    ON m.id = sb.assignment
                   AND sb.latest = 1

                  JOIN ($esql) e
                    ON e.id = sb.userid
                       $sqlgroupsjoin

                 WHERE m.course = :courseid
                       AND sb.status = :submitted
                       $sqlgroupswhere
                 GROUP by m.id";
            $modtotalsbyid['assign'][$courseid] = $DB->get_records_sql($sql, array_merge($params, $groupparams));
        }
        $totalsbyid = $modtotalsbyid['assign'][$courseid];

        if (!empty($totalsbyid)) {
            if (isset($totalsbyid[$modid])) {
                return intval($totalsbyid[$modid]->totalsubmitted);
            }
        }
        return 0;
    }


    /**
     * Get number of answers for specific choice
     *
     * @param int $courseid
     * @param int $choiceid
     * @return int
     */
    public static function choice_num_submissions($courseid, $modid) {
        return self::std_num_submissions($courseid, $modid, 'choice', 'choiceid', 'choice_answers');
    }

    /**
     * Get number of submissions for feedback activity
     *
     * @param int $courseid
     * @param int $feedbackid
     * @return int
     */
    public static function feedback_num_submissions($courseid, $modid) {
        return self::std_num_submissions($courseid, $modid, 'feedback', 'feedback', 'feedback_completed');
    }

    /**
     * Get number of submissions for lesson activity
     *
     * @param int $courseid
     * @param int $feedbackid
     * @return int
     */
    public static function lesson_num_submissions($courseid, $modid) {
        return self::std_num_submissions($courseid, $modid, 'lesson', 'lessonid', 'lesson_attempts');
    }

    /**
     * Get number of attempts for specific quiz
     *
     * @param int $courseid
     * @param int $quizid
     * @return int
     */
    public static function quiz_num_submissions($courseid, $modid) {
        return self::std_num_submissions($courseid, $modid, 'quiz', 'quiz', 'quiz_attempts');
    }

    /**
     * Get number of contributors to the database
     *
     * @param int $courseid
     * @param int $modid
     * @return int
     */
    public static function data_num_submissions($courseid, $modid) {
        return self::std_num_submissions($courseid, $modid, 'data', 'dataid', 'data_records');
    }

    /**
     * Get number of ungraded quiz attempts for specific quiz
     *
     * @param int $courseid
     * @param int $quizid
     * @return int
     */
    public static function quiz_num_submissions_ungraded($courseid, $quizid) {
        global $DB;

        static $totalsbyquizid;

        $coursecontext = \context_course::instance($courseid);
        // Get people who are typically not students (people who can view grader report) so that we can exclude them!
        [$graderids, $params] = get_enrolled_sql($coursecontext, 'moodle/grade:viewall');
        $params['courseid'] = $courseid;

        if (!isset($totalsbyquizid)) {
            // Results are not cached.
            $sql = "-- Snap sql
                    SELECT q.id, count(DISTINCT qa.userid) as total
                      FROM {quiz} q

-- Get ALL ungraded attempts for this quiz

					  JOIN {quiz_attempts} qa ON qa.quiz = q.id
					   AND qa.sumgrades IS NULL
					   AND qa.preview = 0

-- Exclude those people who can grade quizzes and suspended users

                      JOIN {enrol} en ON en.courseid = q.course
                      JOIN {user_enrolments} ue ON en.id = ue.enrolid
                       AND qa.userid = ue.userid
                     WHERE ue.status = 0
                       AND qa.userid NOT IN ($graderids)
                       AND qa.state = 'finished'
                       AND q.course = :courseid
                     GROUP BY q.id";
            $totalsbyquizid = $DB->get_records_sql($sql, $params);
        }

        if (!empty($totalsbyquizid)) {
            if (isset($totalsbyquizid[$quizid])) {
                return intval($totalsbyquizid[$quizid]->total);
            }
        }

        return 0;
    }

    /**
     * Get activity submission row
     *
     * @param $mod
     * @param $submissiontable
     * @param $modfield
     * @param $tabrow
     * @return mixed
     */
    public static function get_submission_row($courseid, $mod, $submissiontable, $modfield, $extraselect='') {
        global $DB, $USER;

        // Note: Caches all submissions to minimise database transactions.
        static $submissions = array();

        // Pull from cache?
        if (!PHPUNIT_TEST) {
            if (isset($submissions[$courseid.'_'.$mod->modname])) {
                if (isset($submissions[$courseid.'_'.$mod->modname][$mod->instance])) {
                    return $submissions[$courseid.'_'.$mod->modname][$mod->instance];
                } else {
                    return false;
                }
            }
        }

        $submissiontable = $mod->modname.'_'.$submissiontable;

        if ($mod->modname === 'assign') {
            $params = [$courseid, $USER->id];
            $sql = "-- Snap sql
                SELECT a.id AS instanceid, st.*
                    FROM {".$submissiontable."} st

                    JOIN {".$mod->modname."} a
                      ON a.id = st.$modfield

                   WHERE a.course = ?
                     AND st.latest = 1
                     AND st.userid = ? $extraselect
                ORDER BY $modfield DESC, st.id DESC";
        } else {
            // Less effecient general purpose for other module types.
            $params = [$USER->id, $courseid, $USER->id];
            $sql = "-- Snap sql
                SELECT a.id AS instanceid, st.*
                    FROM {".$submissiontable."} st

                    JOIN {".$mod->modname."} a
                      ON a.id = st.$modfield

                    -- Get only the most recent submission.
                    JOIN (SELECT $modfield AS modid, MAX(id) AS maxattempt
                            FROM {".$submissiontable."}
                           WHERE userid = ?
                        GROUP BY modid) AS smx
                      ON smx.modid = st.$modfield
                     AND smx.maxattempt = st.id

                   WHERE a.course = ?
                     AND st.userid = ? $extraselect
                ORDER BY $modfield DESC, st.id DESC";
        }

        // Not every activity has a status field...
        // Add one if it is missing so code assuming there is a status property doesn't explode.
        $result = $DB->get_records_sql($sql, $params);
        if (!$result) {
            unset($submissions[$courseid.'_'.$mod->modname]);
            return false;
        }

        if ($mod->modname === 'assign') {
            $parameter = [$courseid];
            $sqlgrps = "-- Snap sql
                SELECT st.*
                    FROM {".$submissiontable."} st

                    JOIN {".$mod->modname."} a
                      ON a.id = st.$modfield

                   WHERE NOT st.groupid = 0
                     AND a.course = ?
                     AND st.latest = 1
                     AND a.teamsubmission = 1
                ORDER BY $modfield DESC, st.id DESC";
            $grpssubmissions = $DB->get_records_sql($sqlgrps, $parameter);

            foreach ($grpssubmissions as $grpssub) {
                if (groups_is_member($grpssub->groupid, $USER->id)) {
                    if (array_key_exists($grpssub->assignment, $result)) {
                        $result[$grpssub->assignment]->status = $grpssub->status;
                    }
                }
            }
        }

        foreach ($result as $r) {
            if (!isset($r->status)) {
                $r->status = null;
            }
        }

        $submissions[$courseid.'_'.$mod->modname] = $result;

        if (isset($submissions[$courseid.'_'.$mod->modname][$mod->instance])) {
            return $submissions[$courseid.'_'.$mod->modname][$mod->instance];
        } else {
            return false;
        }
    }

    /**
     * Take events array and rehash by modulename instance
     * @param array $events
     * @return array
     */
    protected static function hash_events_by_module_instance(array $events) {
        $tmparr = [];
        foreach ($events as $event) {

            if (!isset($tmparr[$event->modulename])) {
                $tmparr[$event->modulename] = [];
            }

            if (!isset($tmparr[$event->modulename][$event->instance])) {
                $tmparr[$event->modulename][$event->instance] = [];
            }

            $tmparr[$event->modulename][$event->instance][] = $event;
        }
        return $tmparr;
    }

    /**
     * Get the activity open from date for a specific module instance
     *
     * @param $courseid
     * @param \cm_info $mod
     * @param string $timeopenfld
     * @param string $timeclosefld
     *
     * @return bool|stdClass
     */
    public static function instance_activity_dates($courseid, \cm_info $mod, $timeopenfld = '', $timeclosefld = '') {
        global $DB, $USER, $COURSE;

        // Note: Caches all moduledates to minimise database transactions.
        static $moddates = [];

        // Did we use the MUC to get the events from the calendar?
        static $eventsfromcache = false;

        // Note: Caches all moduledates by instance to minimise db transactions.
        static $eventsbymodinst = [];

        $modname = $mod->modname;
        $modinst = $mod->instance;

        $phpunittest = defined('PHPUNIT_TEST') && PHPUNIT_TEST;

        if (!empty($moddates[$courseid.'_'.$modname][$modinst]) && !$phpunittest) {
            return $moddates[$courseid.'_'.$modname][$modinst];
        }

        if ($modname === 'quiz') {
            $timeopenfld = 'timeopen';
            $timeclosefld = 'timeclose';
        } else if ($modname === 'lesson') {
            $timeopenfld = 'available';
            $timeclosefld = 'deadline';
        }

        if ($mod->modname != 'assign') {
            // Get moddates WITHOUT overrides.
            $sql = "-- Snap sql
                    SELECT id, $timeopenfld AS timeopen, $timeclosefld as timeclose
                        FROM {" . $modname . "}
                    WHERE course = ?";
            $params = [$courseid];
        } else {
            // Get assignment moddates + time opening overrides.
            // Assignment doesn't put opening time overrides in the calendar so we need to get them here.
            $groups = groups_get_user_groups($courseid);

            if ($groups[0]) {
                [$groupsql, $params] = $DB->get_in_or_equal($groups[0]);
                $sortorder = count($groups[0]) > 1 ? 'AND maog.sortorder = 1' : "";

                $sql = "-- Snap sql
                    SELECT ma.id,
                      CASE
                      WHEN mao.allowsubmissionsfromdate IS NOT NULL
                      THEN mao.allowsubmissionsfromdate
                      ELSE CASE WHEN maog.allowsubmissionsfromdate IS NOT NULL
                      THEN maog.allowsubmissionsfromdate
                      ELSE ma.allowsubmissionsfromdate
                      END
                      END AS timeopen,
                      CASE
                      WHEN mao.duedate IS NOT NULL
                      THEN mao.duedate
                      ELSE CASE WHEN maog.duedate IS NOT NULL
                      THEN maog.duedate
                      ELSE ma.duedate
                      END
                      END AS timeclose
                    FROM {assign} ma

                LEFT JOIN {assign_overrides} mao ON mao.assignid = ma.id AND mao.userid = ? AND mao.groupid IS NULL
                LEFT JOIN {assign_overrides} maog ON maog.assignid = ma.id AND maog.groupid $groupsql
                      $sortorder

                    WHERE course = ?";

                array_unshift($params, $USER->id);
                $params[] = $courseid;

            } else {

                $sql = "-- Snap sql
                    SELECT ma.id,
                      CASE
                      WHEN mao.allowsubmissionsfromdate IS NOT NULL
                      THEN mao.allowsubmissionsfromdate
                      ELSE ma.allowsubmissionsfromdate
                      END AS timeopen,
                      CASE
                      WHEN mao.duedate IS NOT NULL
                      THEN mao.duedate
                      ELSE ma.duedate
                      END AS timeclose
                     FROM {assign} ma

                LEFT JOIN {assign_overrides} mao ON mao.assignid = ma.id AND mao.userid = ? AND mao.groupid IS NULL

                    WHERE course = ?";

                $params = [$USER->id, $courseid];
            }

        }
        $moddates[$courseid . '_' . $modname] = $DB->get_records_sql($sql, $params);

        // Override moddates with calendar dates.
        // Note - we only get 1 years of dates to use for overrides, etc.
        // This means 6 months after an override date expires it will show the default date.
        $tz = new \DateTimeZone(\core_date::get_user_timezone($USER));
        $today = new \DateTime('today', $tz);
        $todayts = $today->getTimestamp();
        $tstart = $todayts - (YEARSECS / 2);
        $tend = $todayts + (YEARSECS / 2);

        if ($phpunittest || !isset($eventsbymodinst[$courseid])) {
            if ($COURSE->id == $courseid) {
                $coursesparam = [$courseid => $COURSE];
            } else {
                $coursesparam = [$courseid => get_course($courseid)];
            }
            $eventsobj = self::user_activity_events($coursesparam, $tstart, $tend, 'incourse', 1000);
            $events = $eventsobj->events;
            $eventsfromcache = $eventsobj->fromcache;
            $eventsbymodinst[$courseid] = self::hash_events_by_module_instance($events);
        }

        if ($mod->modname == 'assign') {
            foreach ($moddates[$courseid . '_' . $modname] as $assign) {
                $assigninstance = $assign->id;
                $dates = $moddates[$courseid . '_' . $modname][$assigninstance];
                // Check if there is any extension.
                $flags = $DB->get_record('assign_user_flags', array('assignment' => $assigninstance, 'userid' => $USER->id));
                if (!empty($flags->extensionduedate)) {
                    // If there is an extension, then assign the duedate of the extension.
                    $timeclose = $flags->extensionduedate;
                } else {
                    $timeclose = $dates->timeclose;
                }
                $timeopen = $dates->timeopen;
                $instdates = (object)[
                    'timeopen' => $timeopen,
                    'timeclose' => $timeclose,
                    'fromcache' => false,
                ];
                $moddates[$courseid . '_' . $modname][$assigninstance] = $instdates;
            }
            return $moddates[$courseid.'_'.$modname][$modinst];
        }

        // Extract opening time and closing time from events.

        if (!empty($eventsbymodinst[$courseid][$modname])) {
            foreach ($eventsbymodinst[$courseid][$modname] as $modinstevents) {
                $timeopen = null;
                $timeclose = null;
                foreach ($modinstevents as $event) {
                    if ($event->timestart === null) {
                        continue;
                    }

                    if ($event->eventtype === 'open') {
                        $timeopen = $event->timestart;
                    } else if (($event->eventtype === 'close' || $event->eventtype === 'due')) {
                        $timeclose = $event->timestart + $event->timeduration;
                    }

                }

                // If we have a null time open or close, use initial dates gotten from module query.
                $initialdates = null;
                if (!empty($moddates[$courseid . '_' . $modname][$event->instance])) {
                    $initialdates = $moddates[$courseid . '_' . $modname][$event->instance];
                }
                if ($timeopen === null && !empty($initialdates)) {
                    $timeopen = $initialdates->timeopen;
                }
                if ($timeclose === null && !empty($initialdates)) {
                    $timeclose = $initialdates->timeclose;
                }

                $instdates = (object)[
                    'timeopen' => $timeopen,
                    'timeclose' => $timeclose,
                    'fromcache' => $eventsfromcache,
                ];

                if ($event->modulename === $modname) {
                    // Only statically cache for the current module type we are requesting.
                    $moddates[$courseid . '_' . $modname][$event->instance] = $instdates;
                }
            }
        }

        return $moddates[$courseid.'_'.$modname][$modinst];

    }

    /**
     * Return grade row for specific module instance.
     *
     * @param $courseid
     * @param $mod
     * @param $modfield
     * @return bool
     */
    public static function grade_row($courseid, $mod) {
        global $DB, $USER;

        static $grades = array();

        if (isset($grades[$courseid.'_'.$mod->modname])
            && isset($grades[$courseid.'_'.$mod->modname][$mod->instance])
        ) {
            return $grades[$courseid.'_'.$mod->modname][$mod->instance];
        }

        $sql = "-- Snap sql
                  SELECT DISTINCT m.id AS instanceid

                    FROM {".$mod->modname."} m

                    JOIN {grade_items} gi
                      ON m.id = gi.iteminstance
                     AND gi.itemtype = 'mod'
                     AND gi.itemmodule = :modname
                     AND gi.courseid = :courseid1

                    JOIN {grade_grades} gg
                      ON gi.id = gg.itemid

                   WHERE m.course = :courseid2
                     AND gg.userid = :userid
                     AND (
                         gg.rawgrade IS NOT NULL
                         OR gg.finalgrade IS NOT NULL
                         OR gg.feedback IS NOT NULL
                     )
                     ";
        $params = array(
            'modname' => $mod->modname,
            'courseid1' => $courseid,
            'courseid2' => $courseid,
            'userid' => $USER->id,
        );
        $grades[$courseid.'_'.$mod->modname] = $DB->get_records_sql($sql, $params);

        if (isset($grades[$courseid.'_'.$mod->modname][$mod->instance])) {
            return $grades[$courseid.'_'.$mod->modname][$mod->instance];
        } else {
            return false;
        }
    }

    /**
     * Get everything graded from a specific date to the current date.
     *
     * @param bool $onlyactive - only show grades in courses actively enrolled on if true.
     * @param null|int $showfrom - timestamp to show grades from. Note if not set defaults to 1 month ago.
     * @return mixed
     */
    public static function events_graded($onlyactive = true, $showfrom = null) {
        global $DB, $USER;

        $params = [];
        $coursesql = '';
        if ($onlyactive) {
            $courses = enrol_get_my_courses();
            $courseids = array_keys($courses);
            $courseids[] = SITEID;
            [$coursesql, $params] = $DB->get_in_or_equal($courseids);
            $coursesql = 'AND gi.courseid '.$coursesql;
        }

        $onemonthago = time() - (DAYSECS * 31);
        $showfrom = $showfrom !== null ? $showfrom : $onemonthago;

        $sql = "-- Snap sql
                SELECT gg.*, gi.itemmodule, gi.iteminstance, gi.courseid, gi.itemtype
                  FROM {grade_grades} gg
                  JOIN {grade_items} gi
                    ON gg.itemid = gi.id $coursesql
                 WHERE gg.userid = ?
                   AND (gg.timemodified > ?
                    OR gg.timecreated > ?)
                   AND (gg.finalgrade IS NOT NULL
                    OR gg.rawgrade IS NOT NULL
                    OR gg.feedback IS NOT NULL)
                   AND gi.itemtype = 'mod'
                 ORDER BY timemodified DESC";

        $params = array_merge($params, [$USER->id, $showfrom, $showfrom]);
        $grades = $DB->get_records_sql($sql, $params, 0, 5);

        $eventdata = array();
        foreach ($grades as $grade) {
            $eventdata[] = $grade;
        }

        return $eventdata;
    }

    /**
     * Note: This function is not optimised for usage in big loops but it does have the advantage of using core logic
     * for evaluating override priority.
     * Get the most appropriate due date, including overrides and extensions.
     * @param int $assignid
     * @param stdClass | int $userid
     * @return stdClass
     * @throws \coding_exception
     */
    public static function assignment_due_date_info($assignid, $userid) {
        global $CFG;

        require_once($CFG->dirroot.'/mod/assign/locallib.php');

        $duedateinfo = (object) ['duedate' => null, 'extended' => false];

        [$course, $cminfo] = get_course_and_cm_from_instance($assignid, 'assign');
        unset($course);

        // Check overrides.
        $assign = new \assign($cminfo->context, $cminfo, false);
        $overrides = $assign->override_exists($userid);
        if (!empty($overrides->duedate)) {
            $duedate = $overrides->duedate;
        } else {
            $duedate = $assign->get_instance()->duedate;
        }

        // Check deadline extensions.
        $flags = $assign->get_user_flags($userid, true);
        if (!empty($flags->extensionduedate)) {
            // Extension always overwrites duedate, even if it's less than due date or overridden due date.
            $duedate = $flags->extensionduedate;
            $duedateinfo->extended = true;
        }

        $duedateinfo->duedate = $duedate;
        return $duedateinfo;
    }

    /**
     * Get all events restricted by various parameters, taking in to account user and group overrides.
     * Copied from calendar/classes/local/api.php.
     * Uses
     *
     * @param int|null      $timestartfrom         Events with timestart from this value (inclusive).
     * @param int|null      $timestartto           Events with timestart until this value (inclusive).
     * @param int|null      $timesortfrom          Events with timesort from this value (inclusive).
     * @param int|null      $timesortto            Events with timesort until this value (inclusive).
     * @param int|null      $timestartaftereventid Restrict the events in the timestart range to ones after this ID.
     * @param int|null      $timesortaftereventid  Restrict the events in the timesort range to ones after this ID.
     * @param int           $limitnum              Return at most this number of events.
     * @param int|null      $type                  Return only events of this type.
     * @param array|null    $usersfilter           Return only events for these users.
     * @param array|null    $groupsfilter          Return only events for these groups.
     * @param array|null    $coursesfilter         Return only events for these courses.
     * @param bool          $withduration          If true return only events starting within specified
     *                                             timestart otherwise return in progress events as well.
     * @param bool          $ignorehidden          If true don't return hidden events.
     * @return \core_calendar\local\event\entities\event_interface[] Array of event_interfaces.
     */
    public static function get_events(
        $timestartfrom = null,
        $timestartto = null,
        $timesortfrom = null,
        $timesortto = null,
        $timestartaftereventid = null,
        $timesortaftereventid = null,
        $limitnum = 20,
        $type = null,
        array $usersfilter = null,
        array $groupsfilter = null,
        array $coursesfilter = null,
        $withduration = true,
        $ignorehidden = true,
        callable $filter = null,
        ?string $searchvalue = null
    ) {

        \theme_snap\calendar\event\container::ovd_init();
        $vault = \theme_snap\calendar\event\container::get_event_vault();

        $timestartafterevent = null;
        $timesortafterevent = null;

        if ($timestartaftereventid && $event = $vault->get_event_by_id($timestartaftereventid)) {
            $timestartafterevent = $event;
        }

        if ($timesortaftereventid && $event = $vault->get_event_by_id($timesortaftereventid)) {
            $timesortafterevent = $event;
        }

        return $vault->get_events(
            $timestartfrom,
            $timestartto,
            $timesortfrom,
            $timesortto,
            $timestartafterevent,
            $timesortafterevent,
            $limitnum,
            $type,
            $usersfilter,
            $groupsfilter,
            $coursesfilter,
            null,
            $withduration,
            $ignorehidden
        );
    }

    /**
     * Get calendar activity events for specific date range and array of courses.
     * Note - only deals with due, open, close event types.
     * @param int $tstart
     * @param int $tend
     * @param stdClass[] $courses
     * @param string $cachesuffix
     * @param int $limit
     * @return object
     * @throws dml_exception
     */
    public static function get_calendar_activity_events($tstart, $tend, array $courses, $cachesuffix = '', $limit = 200) {
        global $DB, $USER;
        $retobj = (object) [
            'timestamp' => null,
            'events' => [],
            'courses' => [],
            'fromcache' => false,
        ];

        if (empty($courses)) {
            return $retobj;
        }

        // The cache key includes the start and end dates rounded to a day.
        $dstart = strtotime(date('Y-m-d', $tstart));
        $dend = strtotime(date('Y-m-d', $tend));

        // Cache key HAS to have courses.
        $cachekey = self::get_id_indexed_array_cache_key($courses);

        // It also can have group ids for this user within the courses.
        $groupkey = self::get_user_group_cache_key($USER, $courses);
        if (!empty($groupkey)) {
            $cachekey .= '_' . $groupkey;
        }

        // And an optional suffix.
        if (!empty($cachesuffix)) {
            $cachekey .= '_' . $cachesuffix;
        }
        $freshkey = $cachekey.'_'.($dstart + $dend).'_'.$limit;

        if (self::$phpunitallowcaching || !(defined('PHPUNIT_TEST') && PHPUNIT_TEST)) {
            $muc = \cache::make('theme_snap', 'activity_deadlines');
            $cached = $muc->get($cachekey);
            $cachefresh = false;
            if ($cached && $cached->key !== $freshkey) {
                $cachefresh = false;
            } else if ($cached && $cached->timestamp >= time() - HOURSECS) {
                $cachestamps = local::get_calendar_change_stamps();
                $activitiesstamp = $cached->timestamp;
                $cachefresh = true; // Until proven otherwise.
                $coursecache = [];
                foreach ($courses as $courseid => $course) {
                    $coursecache[$courseid] = $course->shortname;

                    if (!isset($cached->courses[$courseid])) {
                        $cachefresh = false;
                    }
                    if (isset($cachestamps[$courseid])) {
                        $stamp = $cachestamps[$courseid];
                        if ($stamp > $activitiesstamp) {
                            $cachefresh = false;
                        }
                    }
                }
                $cmids = [];
                foreach ($cached->events as $event) {
                    if (!empty($event->actionurl)) {
                        $cmids[] = $event->actionurl->get_param("id");
                    }
                    if (!isset($courses[$event->courseid])) {
                        $cachefresh = false;
                    }
                }
                if (!empty($cmids)) {
                    [$insql, $params] = $DB->get_in_or_equal($cmids);
                    $sql = "SELECT deletioninprogress
                          FROM {course_modules}
                         WHERE id $insql
                           AND deletioninprogress = 1";
                    $deletioninprogress = $DB->get_record_sql($sql, $params);
                }
                if (!empty($deletioninprogress)) {
                    $cachefresh = false;
                }
            }

            if ($cachefresh) {
                $cached->fromcache = true; // Useful for debugging and unit testing.
                return $cached;
            }
        }

        $calendar = new \calendar_information(0, 0, 0, $tstart);
        $course = get_course(SITEID);
        $calendar->set_sources($course, $courses);

        $withduration = true;
        $ignorehidden = true;
        $mapper = \core_calendar\local\event\container::get_event_mapper();

        // Normalise the users, groups and courses parameters so that they are compliant with
        // the calendar apis get_events method.
        // Existing functions that were using the old calendar_get_events() were passing a mixture of array, int,
        // boolean for these parameters, but with the new API method, only null and arrays are accepted.
        [$userparam, $groupparam, $courseparam] = array_map(function($param) {
            // If parameter is true, return null.
            if ($param === true) {
                return null;
            }

            // If parameter is false, return an empty array.
            if ($param === false) {
                return [];
            }

            // If the parameter is a scalar value, enclose it in an array.
            if (!is_array($param)) {
                return [$param];
            }

            // No normalisation required.
            return $param;
        }, [$calendar->users, $calendar->groups, $calendar->courses]);

        // This will keep count of the amount of concurrent queries made to the events table.
        // The amount of queries can be stated using the flag $CFG->theme_snap_max_concurrent_deadline_queries.
        // If the amount is reached, an exception will be thrown.
        // It's default to INF, i.e., it's disabled.
        self::start_deadline_query();

        // I present to you, the events query.
        $events = self::get_events(
            $tstart,
            $tend,
            null,
            null,
            null,
            null,
            $limit,
            null,
            $userparam,
            $groupparam,
            $courseparam,
            $withduration,
            $ignorehidden
        );

        // Quickly reduce query count to allow other users to run the query.
        self::end_deadline_query();

        $events = array_reduce($events, function($carry, $event) use ($mapper) {
            return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)];
        }, []);

        if (!isset($coursecache)) {
            foreach ($courses as $courseid => $course) {
                $coursecache[$courseid] = $course->shortname;
            }
        }

        $retobj->timestamp = microtime(true);
        $retobj->events = $events;

        if (self::$phpunitallowcaching || !(defined('PHPUNIT_TEST') && PHPUNIT_TEST)) {
            $retobj->courses = $coursecache;
            $retobj->key = $freshkey;
            $muc->set($cachekey, $retobj);
        }

        return $retobj;
    }

    /**
     * Return deadlines from calendar associated to a set of courses.
     *
     * @param stdClass[] $courses array of courses hashed by course id.
     * @param int $tstart
     * @param int $tend
     * @param string $cachesuffix
     * @param int $limit
     * @param boolean $skipcmchecks Skip course module checks
     * @return object
     */
    public static function user_activity_events(array $courses, $tstart, $tend, $cachesuffix = '', $limit = 500,
                                                $skipcmchecks = false) {
        $retobj = (object) [
            'timestamp' => null,
            'events' => [],
            'courses' => [],
            'fromcache' => false,
        ];

        if (empty($courses)) {
            return $retobj;
        }

        $retobj = self::get_calendar_activity_events($tstart, $tend, $courses, $cachesuffix, $limit);

        if ($skipcmchecks) {
            // Skip CM checks. This should only be done for populating caches or getting data that does not need formatting.
            return $retobj;
        }

        // Filter down array and also modify event name if necessary;
        // Note, filter_array cannot be used here as we need to modify the event name, not just filter.
        $tmparr = [];
        foreach ($retobj->events as $event) {

            // Validation added to prevent array offset.
            $courseid = array_key_exists($event->courseid, $courses) ? $courses[$event->courseid] : 0;

            [$course, $cminfo] = get_course_and_cm_from_instance(
                    $event->instance, $event->modulename,  $courseid, $event->userid);
            unset($course);

            // We are only interested in modules with valid instances.
            if (empty($cminfo)) {
                continue;
            }

            if (!$cminfo->uservisible) {
                continue;
            }
            if ($event->eventtype === 'close') {
                // Revert the addition of e.g. "(Quiz closes)" to the event name.
                $event->name = $cminfo->name;
            }

            if (isset($courses[$event->courseid])) {
                $course = $courses[$event->courseid];
                $event->coursefullname = format_string($course->fullname);
            }

            $tmparr[$event->id] = $event;

        }
        $retobj->events = $tmparr;
        unset($tmparr);

        return $retobj;
    }

    /**
     * Return user's upcoming activity deadlines from the calendar.
     *
     * All deadlines from today, then any from the next 6 months up to the
     * max requested.
     * @param stdClass|integer $userorid
     * @param integer $maxdeadlines
     * @param stdClass|integer $courseorid
     * @param boolean $skipcmchecks Skip course module checks
     * @return stdClass
     */
    public static function upcoming_deadlines($userorid, $maxdeadlines = 500, $courseorid = 0, $skipcmchecks = false) {
        global $USER;
        $origuser = $USER;

        // The user is set here to:
        // * Assign categories to the calendar that the user has access to
        // * Assign groups to the calendar that the user belongs to
        // See: \calendar_information::set_sources.
        // It is safe to assume that b/c the user is enrolled in the courses specified,
        // all enrolled users would need to be able to access the corresponding categories, hence,
        // we can cache deadlines for courses without worrying about categories.
        //
        // However, we do need to worry about groups. Which is why we cache the group ids too.
        // See \theme_snap\activity::user_activity_events.
        $user = local::get_user($userorid);
        $USER = $user;

        $tz = new \DateTimeZone(\core_date::get_user_timezone($user));
        $today = new \DateTime('today', $tz);
        $todayts = $today->getTimestamp();
        $tomorrow = new \DateTime('tomorrow', $tz);
        $tomorrowts = $tomorrow->getTimestamp();

        $courses = enrol_get_users_courses($user->id, true);
        $courses = local::remove_hidden_courses($courses);
        if ($courseorid !== 0) {
            $course = local::get_course($courseorid);
            if ($course !== false &&
                (is_siteadmin() || isset($courses[$course->id]))) {
                $courses = [$course->id => $course];
            } else {
                // The user is not enrolled on this course, let's clean up the course lists.
                $courses = [];
            }
        }

        $eventsobj = self::user_activity_events(
            $courses, $todayts, $todayts + (YEARSECS / 2), 'deadlines', $maxdeadlines, $skipcmchecks);

        $events = $eventsobj->events;
        uasort($events, function($e1, $e2) {
            if ($e1->timestart === $e2->timestart) {
                return 0;
            }
            return ($e1->timestart < $e2->timestart) ? -1 : 1;
        });

        $counteventstoday = 0;

        $tmparr = [];
        foreach ($events as $event) {
            if ($event->timestart >= $todayts) {
                if ($event->eventtype != 'close' && $event->eventtype != 'due' && $event->eventtype != 'expectcompletionon') {
                    continue;
                }

                $tmparr[] = $event;

                if ($event->timestart < $tomorrowts) {
                    $counteventstoday++;
                }
            }
        }
        $events = $tmparr;

        // We have unlimited events for today but a maximum of five events for everything passed today.
        // If we have 10 events today then we will see 10 events, if we have 3 events for today then we will see
        // a maximum of 5 events including all the events that happen beyond today's date.
        $maxevents = $counteventstoday > $maxdeadlines ? $counteventstoday : $maxdeadlines;

        $eventsobj->events = array_slice($events, 0, $maxevents);

        $USER = $origuser;

        return $eventsobj;
    }

    /**
     * Returns the join and where statements required to validate the assignment submissions by groups on a course.
     * @param integer $courseid
     * @return array
     */
    private static function get_groups_sql($courseid) {
        global $USER;

        $sqlgroupsjoin = '';
        $sqlgroupswhere = '';
        $groupparams = array();

        $course = get_course($courseid);
        $groupmode = groups_get_course_groupmode($course);
        $context = \context_course::instance($courseid);

        if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) {
            $groupparams['userid'] = $USER->id;
            $groupparams['courseid2'] = $courseid;

            $sqlgroupsjoin = "
                    JOIN {groups_members} gm
                      ON gm.userid = sb.userid
                    JOIN {groups} g
                      ON gm.groupid = g.id";
            $sqlgroupswhere = "
                     AND gm.groupid
                      IN (SELECT g.id
                    FROM {groups} g
                    JOIN {groups_members} gm ON gm.groupid = g.id
                   WHERE g.courseid = :courseid2
                     AND gm.userid = :userid)";
        }
        return array($sqlgroupsjoin, $sqlgroupswhere, $groupparams);
    }

    /**
     * Generates a hash key from an 'id' indexed array.
     * @param array $idindexedarray
     * @return string
     */
    public static function get_id_indexed_array_cache_key(array $idindexedarray) {
        ksort($idindexedarray);
        return !empty($idindexedarray) ? sha1(implode(',', array_keys($idindexedarray))) : '';
    }

    /**
     * Get the cache key associated to the user's group ids in the specified courses.
     * @param mixed $user
     * @param \stdClass[] $courses Array of courses indexed by id.
     * @return string
     */
    public static function get_user_group_cache_key($user, array $courses) {
        $groupids = array_reduce(array_keys($courses), function($carry, $courseid) use ($user) {
            $groupings = groups_get_user_groups($courseid, $user->id);
            // Grouping 0 is all groups.
            return array_merge($carry, $groupings[0]);
        }, []);
        return self::get_id_indexed_array_cache_key(array_flip($groupids));
    }

    private static function start_deadline_query() {
        $locktype = 'theme_snap_activity_deadlines_cache';
        $resource = 'query_count';
        self::run_within_lock($locktype, $resource, function($lock) {
            global $CFG;
            $muc = \cache::make('theme_snap', 'activity_deadlines');
            $querycount = $muc->get('query_count');

            if ($querycount !== false) {
                $maxqueries = !empty($CFG->theme_snap_max_concurrent_deadline_queries) ?
                    $CFG->theme_snap_max_concurrent_deadline_queries : INF;
                if ($querycount >= $maxqueries) {
                    $lock->release();
                    throw new \moodle_exception(
                        'retryfeed', 'theme_snap', '', get_string('deadlines', 'theme_snap'));
                }
            } else {
                $querycount = 0;
            }
            $muc->set('query_count', $querycount + 1);
        });
    }

    private static function end_deadline_query() {
        $locktype = 'theme_snap_activity_deadlines_cache';
        $resource = 'query_count';
        self::run_within_lock($locktype, $resource, function($lock) {
            $muc = \cache::make('theme_snap', 'activity_deadlines');
            $querycount = $muc->get('query_count');

            if ($querycount !== false) {
                if ($querycount > 0) {
                    $muc->set('query_count', $querycount - 1);
                } else {
                    // We won't fail if the query count is 0 or negative. Just reset it.
                    // There could be locking issues.
                    $muc->set('query_count', 0);
                }
            }
        });
    }

    private static function run_within_lock($locktype, $resource, $callable) {
        $timeout = 5;
        $lockfactory = \core\lock\lock_config::get_lock_factory($locktype);
        if ($lock = $lockfactory->get_lock($resource, $timeout)) {
            $callable($lock);
            $lock->release();
        } else {
            // We did not get access to the resource in time, give up.
            throw new \moodle_exception(
                'retryfeed', 'theme_snap', '', get_string('deadlines', 'theme_snap'));
        }
    }

    public static function reset_deadline_query_count() {
        $muc = \cache::make('theme_snap', 'activity_deadlines');
        $muc->set('query_count', 0);
    }
}
