<?php
// This file is part of Moodle - http://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 <http://www.gnu.org/licenses/>.

/**
 * Class for loading/storing competency frameworks from the DB.
 *
 * @package    core_competency
 * @copyright  2015 Damyon Wiese
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
namespace core_competency;
defined('MOODLE_INTERNAL') || die();

use core\invalid_persistent_exception;
use stdClass;
use cm_info;
use context;
use context_helper;
use context_system;
use context_course;
use context_module;
use context_user;
use coding_exception;
use require_login_exception;
use moodle_exception;
use moodle_url;
use required_capability_exception;

/**
 * Class for doing things with competency frameworks.
 *
 * @copyright  2015 Damyon Wiese
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class api {

    /** @var boolean Allow api functions even if competencies are not enabled for the site. */
    private static $skipenabled = false;

    /**
     * Returns whether competencies are enabled.
     *
     * This method should never do more than checking the config setting, the reason
     * being that some other code could be checking the config value directly
     * to avoid having to load this entire file into memory.
     *
     * @return boolean True when enabled.
     */
    public static function is_enabled() {
        return self::$skipenabled || get_config('core_competency', 'enabled');
    }

    /**
     * When competencies used to be enabled, we can show the text but do not include links.
     *
     * @return boolean True means show links.
     */
    public static function show_links() {
        return isloggedin() && !isguestuser() && get_config('core_competency', 'enabled');
    }

    /**
     * Allow calls to competency api functions even if competencies are not currently enabled.
     */
    public static function skip_enabled() {
        self::$skipenabled = true;
    }

    /**
     * Restore the checking that competencies are enabled with any api function.
     */
    public static function check_enabled() {
        self::$skipenabled = false;
    }

    /**
     * Throws an exception if competencies are not enabled.
     *
     * @return void
     * @throws moodle_exception
     */
    public static function require_enabled() {
        if (!static::is_enabled()) {
            throw new moodle_exception('competenciesarenotenabled', 'core_competency');
        }
    }

    /**
     * Checks whether a scale is used anywhere in the plugin.
     *
     * This public API has two exceptions:
     * - It MUST NOT perform any capability checks.
     * - It MUST ignore whether competencies are enabled or not ({@link self::is_enabled()}).
     *
     * @param int $scaleid The scale ID.
     * @return bool
     */
    public static function is_scale_used_anywhere($scaleid) {
        global $DB;
        $sql = "SELECT s.id
                  FROM {scale} s
             LEFT JOIN {" . competency_framework::TABLE ."} f
                    ON f.scaleid = :scaleid1
             LEFT JOIN {" . competency::TABLE ."} c
                    ON c.scaleid = :scaleid2
                 WHERE f.id IS NOT NULL
                    OR c.id IS NOT NULL";
        return $DB->record_exists_sql($sql, ['scaleid1' => $scaleid, 'scaleid2' => $scaleid]);
    }

    /**
     * Validate if current user has access to the course_module if hidden.
     *
     * @param mixed $cmmixed The cm_info class, course module record or its ID.
     * @param bool $throwexception Throw an exception or not.
     * @return bool
     */
    protected static function validate_course_module($cmmixed, $throwexception = true) {
        $cm = $cmmixed;
        if (!is_object($cm)) {
            $cmrecord = get_coursemodule_from_id(null, $cmmixed);
            $modinfo = get_fast_modinfo($cmrecord->course);
            $cm = $modinfo->get_cm($cmmixed);
        } else if (!$cm instanceof cm_info) {
            // Assume we got a course module record.
            $modinfo = get_fast_modinfo($cm->course);
            $cm = $modinfo->get_cm($cm->id);
        }

        if (!$cm->uservisible) {
            if ($throwexception) {
                throw new require_login_exception('Course module is hidden');
            } else {
                return false;
            }
        }

        return true;
    }

    /**
     * Validate if current user has access to the course if hidden.
     *
     * @param mixed $courseorid The course or it ID.
     * @param bool $throwexception Throw an exception or not.
     * @return bool
     */
    protected static function validate_course($courseorid, $throwexception = true) {
        $course = $courseorid;
        if (!is_object($course)) {
            $course = get_course($course);
        }

        $coursecontext = context_course::instance($course->id);
        if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
            if ($throwexception) {
                throw new require_login_exception('Course is hidden');
            } else {
                return false;
            }
        }

        return true;
    }

    /**
     * Create a competency from a record containing all the data for the class.
     *
     * Requires moodle/competency:competencymanage capability at the system context.
     *
     * @param stdClass $record Record containing all the data for an instance of the class.
     * @return competency
     */
    public static function create_competency(stdClass $record) {
        static::require_enabled();
        $competency = new competency(0, $record);

        // First we do a permissions check.
        require_capability('moodle/competency:competencymanage', $competency->get_context());

        // Reset the sortorder, use reorder instead.
        $competency->set('sortorder', 0);
        $competency->create();

        \core\event\competency_created::create_from_competency($competency)->trigger();

        // Reset the rule of the parent.
        $parent = $competency->get_parent();
        if ($parent) {
            $parent->reset_rule();
            $parent->update();
        }

        return $competency;
    }

    /**
     * Delete a competency by id.
     *
     * Requires moodle/competency:competencymanage capability at the system context.
     *
     * @param int $id The record to delete. This will delete alot of related data - you better be sure.
     * @return boolean
     */
    public static function delete_competency($id) {
        global $DB;
        static::require_enabled();
        $competency = new competency($id);

        // First we do a permissions check.
        require_capability('moodle/competency:competencymanage', $competency->get_context());

        $events = array();
        $competencyids = array(intval($competency->get('id')));
        $contextid = $competency->get_context()->id;
        $competencyids = array_merge(competency::get_descendants_ids($competency), $competencyids);
        if (!competency::can_all_be_deleted($competencyids)) {
            return false;
        }
        $transaction = $DB->start_delegated_transaction();

        try {

            // Reset the rule of the parent.
            $parent = $competency->get_parent();
            if ($parent) {
                $parent->reset_rule();
                $parent->update();
            }

            // Delete the competency separately so the after_delete event can be triggered.
            $competency->delete();

            // Delete the competencies.
            competency::delete_multiple($competencyids);

            // Delete the competencies relation.
            related_competency::delete_multiple_relations($competencyids);

            // Delete competency evidences.
            user_evidence_competency::delete_by_competencyids($competencyids);

            // Register the competencies deleted events.
            $events = \core\event\competency_deleted::create_multiple_from_competencyids($competencyids, $contextid);

        } catch (\Exception $e) {
            $transaction->rollback($e);
        }

        $transaction->allow_commit();
        // Trigger events.
        foreach ($events as $event) {
            $event->trigger();
        }

        return true;
    }

    /**
     * Reorder this competency.
     *
     * Requires moodle/competency:competencymanage capability at the system context.
     *
     * @param int $id The id of the competency to move.
     * @return boolean
     */
    public static function move_down_competency($id) {
        static::require_enabled();
        $current = new competency($id);

        // First we do a permissions check.
        require_capability('moodle/competency:competencymanage', $current->get_context());

        $max = self::count_competencies(array('parentid' => $current->get('parentid'),
                                              'competencyframeworkid' => $current->get('competencyframeworkid')));
        if ($max > 0) {
            $max--;
        }

        $sortorder = $current->get('sortorder');
        if ($sortorder >= $max) {
            return false;
        }
        $sortorder = $sortorder + 1;
        $current->set('sortorder', $sortorder);

        $filters = array('parentid' => $current->get('parentid'),
                         'competencyframeworkid' => $current->get('competencyframeworkid'),
                         'sortorder' => $sortorder);
        $children = self::list_competencies($filters, 'id');
        foreach ($children as $needtoswap) {
            $needtoswap->set('sortorder', $sortorder - 1);
            $needtoswap->update();
        }

        // OK - all set.
        $result = $current->update();

        return $result;
    }

    /**
     * Reorder this competency.
     *
     * Requires moodle/competency:competencymanage capability at the system context.
     *
     * @param int $id The id of the competency to move.
     * @return boolean
     */
    public static function move_up_competency($id) {
        static::require_enabled();
        $current = new competency($id);

        // First we do a permissions check.
        require_capability('moodle/competency:competencymanage', $current->get_context());

        $sortorder = $current->get('sortorder');
        if ($sortorder == 0) {
            return false;
        }

        $sortorder = $sortorder - 1;
        $current->set('sortorder', $sortorder);

        $filters = array('parentid' => $current->get('parentid'),
                         'competencyframeworkid' => $current->get('competencyframeworkid'),
                         'sortorder' => $sortorder);
        $children = self::list_competencies($filters, 'id');
        foreach ($children as $needtoswap) {
            $needtoswap->set('sortorder', $sortorder + 1);
            $needtoswap->update();
        }

        // OK - all set.
        $result = $current->update();

        return $result;
    }

    /**
     * Move this competency so it sits in a new parent.
     *
     * Requires moodle/competency:competencymanage capability at the system context.
     *
     * @param int $id The id of the competency to move.
     * @param int $newparentid The new parent id for the competency.
     * @return boolean
     */
    public static function set_parent_competency($id, $newparentid) {
        global $DB;
        static::require_enabled();
        $current = new competency($id);

        // First we do a permissions check.
        require_capability('moodle/competency:competencymanage', $current->get_context());
        if ($id == $newparentid) {
            throw new coding_exception('Can not set a competency as a parent of itself.');
        } if ($newparentid == $current->get('parentid')) {
            throw new coding_exception('Can not move a competency to the same location.');
        }

        // Some great variable assignment right here.
        $currentparent = $current->get_parent();
        $parent = !empty($newparentid) ? new competency($newparentid) : null;
        $parentpath = !empty($parent) ? $parent->get('path') : '/0/';

        // We're going to change quite a few things.
        $transaction = $DB->start_delegated_transaction();

        // If we are moving a node to a child of itself:
        // - promote all the child nodes by one level.
        // - remove the rule on self.
        // - re-read the parent.
        $newparents = explode('/', $parentpath);
        if (in_array($current->get('id'), $newparents)) {
            $children = competency::get_records(array('parentid' => $current->get('id')), 'id');
            foreach ($children as $child) {
                $child->set('parentid', $current->get('parentid'));
                $child->update();
            }

            // Reset the rule on self as our children have changed.
            $current->reset_rule();

            // The destination parent is one of our descendants, we need to re-fetch its values (path, parentid).
            $parent->read();
        }

        // Reset the rules of initial parent and destination.
        if (!empty($currentparent)) {
            $currentparent->reset_rule();
            $currentparent->update();
        }
        if (!empty($parent)) {
            $parent->reset_rule();
            $parent->update();
        }

        // Do the actual move.
        $current->set('parentid', $newparentid);
        $result = $current->update();

        // All right, let's commit this.
        $transaction->allow_commit();

        return $result;
    }

    /**
     * Update the details for a competency.
     *
     * Requires moodle/competency:competencymanage capability at the system context.
     *
     * @param stdClass $record The new details for the competency.
     *                         Note - must contain an id that points to the competency to update.
     *
     * @return boolean
     */
    public static function update_competency($record) {
        static::require_enabled();
        $competency = new competency($record->id);

        // First we do a permissions check.
        require_capability('moodle/competency:competencymanage', $competency->get_context());

        // Some things should not be changed in an update - they should use a more specific method.
        $record->sortorder = $competency->get('sortorder');
        $record->parentid = $competency->get('parentid');
        $record->competencyframeworkid = $competency->get('competencyframeworkid');

        $competency->from_record($record);
        require_capability('moodle/competency:competencymanage', $competency->get_context());

        // OK - all set.
        $result = $competency->update();

        // Trigger the update event.
        \core\event\competency_updated::create_from_competency($competency)->trigger();

        return $result;
    }

    /**
     * Read the details for a single competency and return a record.
     *
     * Requires moodle/competency:competencyview capability at the system context.
     *
     * @param int $id The id of the competency to read.
     * @param bool $includerelated Include related tags or not.
     * @return competency
     */
    public static function read_competency($id, $includerelated = false) {
        static::require_enabled();
        $competency = new competency($id);

        // First we do a permissions check.
        $context = $competency->get_context();
        if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
             throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
        }

        // OK - all set.
        if ($includerelated) {
            $relatedcompetency = new related_competency();
            if ($related = $relatedcompetency->list_relations($id)) {
                $competency->relatedcompetencies = $related;
            }
        }

        return $competency;
    }

    /**
     * Perform a search based on a text and return all results and their parents.
     *
     * Requires moodle/competency:competencyview capability at the framework context.
     *
     * @param string $textsearch A string to search for.
     * @param int $competencyframeworkid The id of the framework to limit the search.
     * @return array of competencies
     */
    public static function search_competencies($textsearch, $competencyframeworkid) {
        static::require_enabled();
        $framework = new competency_framework($competencyframeworkid);

        // First we do a permissions check.
        $context = $framework->get_context();
        if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
             throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
        }

        // OK - all set.
        $competencies = competency::search($textsearch, $competencyframeworkid);
        return $competencies;
    }

    /**
     * Perform a search based on the provided filters and return a paginated list of records.
     *
     * Requires moodle/competency:competencyview capability at some context.
     *
     * @param array $filters A list of filters to apply to the list.
     * @param string $sort The column to sort on
     * @param string $order ('ASC' or 'DESC')
     * @param int $skip Number of records to skip (pagination)
     * @param int $limit Max of records to return (pagination)
     * @return array of competencies
     */
    public static function list_competencies($filters, $sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
        static::require_enabled();
        if (!isset($filters['competencyframeworkid'])) {
            $context = context_system::instance();
        } else {
            $framework = new competency_framework($filters['competencyframeworkid']);
            $context = $framework->get_context();
        }

        // First we do a permissions check.
        if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
             throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
        }

        // OK - all set.
        return competency::get_records($filters, $sort, $order, $skip, $limit);
    }

    /**
     * Perform a search based on the provided filters and return a paginated list of records.
     *
     * Requires moodle/competency:competencyview capability at some context.
     *
     * @param array $filters A list of filters to apply to the list.
     * @return int
     */
    public static function count_competencies($filters) {
        static::require_enabled();
        if (!isset($filters['competencyframeworkid'])) {
            $context = context_system::instance();
        } else {
            $framework = new competency_framework($filters['competencyframeworkid']);
            $context = $framework->get_context();
        }

        // First we do a permissions check.
        if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
             throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
        }

        // OK - all set.
        return competency::count_records($filters);
    }

    /**
     * Create a competency framework from a record containing all the data for the class.
     *
     * Requires moodle/competency:competencymanage capability at the system context.
     *
     * @param stdClass $record Record containing all the data for an instance of the class.
     * @return competency_framework
     */
    public static function create_framework(stdClass $record) {
        static::require_enabled();
        $framework = new competency_framework(0, $record);
        require_capability('moodle/competency:competencymanage', $framework->get_context());

        // Account for different formats of taxonomies.
        if (isset($record->taxonomies)) {
            $framework->set('taxonomies', $record->taxonomies);
        }

        $framework = $framework->create();

        // Trigger a competency framework created event.
        \core\event\competency_framework_created::create_from_framework($framework)->trigger();

        return $framework;
    }

    /**
     * Duplicate a competency framework by id.
     *
     * Requires moodle/competency:competencymanage capability at the system context.
     *
     * @param int $id The record to duplicate. All competencies associated and related will be duplicated.
     * @return competency_framework the framework duplicated
     */
    public static function duplicate_framework($id) {
        global $DB;
        static::require_enabled();

        $framework = new competency_framework($id);
        require_capability('moodle/competency:competencymanage', $framework->get_context());
        // Starting transaction.
        $transaction = $DB->start_delegated_transaction();

        try {
            // Get a uniq idnumber based on the origin framework.
            $idnumber = competency_framework::get_unused_idnumber($framework->get('idnumber'));
            $framework->set('idnumber', $idnumber);
            // Adding the suffix copy to the shortname.
            $framework->set('shortname', get_string('duplicateditemname', 'core_competency', $framework->get('shortname')));
            $framework->set('id', 0);
            $framework = $framework->create();

            // Array that matches the old competencies ids with the new one to use when copying related competencies.
            $frameworkcompetency = competency::get_framework_tree($id);
            $matchids = self::duplicate_competency_tree($framework->get('id'), $frameworkcompetency, 0, 0);

            // Copy the related competencies.
            $relcomps = related_competency::get_multiple_relations(array_keys($matchids));

            foreach ($relcomps as $relcomp) {
                $compid = $relcomp->get('competencyid');
                $relcompid = $relcomp->get('relatedcompetencyid');
                if (isset($matchids[$compid]) && isset($matchids[$relcompid])) {
                    $newcompid = $matchids[$compid]->get('id');
                    $newrelcompid = $matchids[$relcompid]->get('id');
                    if ($newcompid < $newrelcompid) {
                        $relcomp->set('competencyid', $newcompid);
                        $relcomp->set('relatedcompetencyid', $newrelcompid);
                    } else {
                        $relcomp->set('competencyid', $newrelcompid);
                        $relcomp->set('relatedcompetencyid', $newcompid);
                    }
                    $relcomp->set('id', 0);
                    $relcomp->create();
                } else {
                    // Debugging message when there is no match found.
                    debugging('related competency id not found');
                }
            }

            // Setting rules on duplicated competencies.
            self::migrate_competency_tree_rules($frameworkcompetency, $matchids);

            $transaction->allow_commit();

        } catch (\Exception $e) {
            $transaction->rollback($e);
        }

        // Trigger a competency framework created event.
        \core\event\competency_framework_created::create_from_framework($framework)->trigger();

        return $framework;
    }

    /**
     * Delete a competency framework by id.
     *
     * Requires moodle/competency:competencymanage capability at the system context.
     *
     * @param int $id The record to delete. This will delete alot of related data - you better be sure.
     * @return boolean
     */
    public static function delete_framework($id) {
        global $DB;
        static::require_enabled();
        $framework = new competency_framework($id);
        require_capability('moodle/competency:competencymanage', $framework->get_context());

        $events = array();
        $competenciesid = competency::get_ids_by_frameworkid($id);
        $contextid = $framework->get('contextid');
        if (!competency::can_all_be_deleted($competenciesid)) {
            return false;
        }
        $transaction = $DB->start_delegated_transaction();
        try {
            if (!empty($competenciesid)) {
                // Delete competencies.
                competency::delete_by_frameworkid($id);

                // Delete the related competencies.
                related_competency::delete_multiple_relations($competenciesid);

                // Delete the evidences for competencies.
                user_evidence_competency::delete_by_competencyids($competenciesid);
            }

            // Create a competency framework deleted event.
            $event = \core\event\competency_framework_deleted::create_from_framework($framework);
            $result = $framework->delete();

            // Register the deleted events competencies.
            $events = \core\event\competency_deleted::create_multiple_from_competencyids($competenciesid, $contextid);

        } catch (\Exception $e) {
            $transaction->rollback($e);
        }

        // Commit the transaction.
        $transaction->allow_commit();

        // If all operations are successful then trigger the delete event.
        $event->trigger();

        // Trigger deleted event competencies.
        foreach ($events as $event) {
            $event->trigger();
        }

        return $result;
    }

    /**
     * Update the details for a competency framework.
     *
     * Requires moodle/competency:competencymanage capability at the system context.
     *
     * @param stdClass $record The new details for the framework. Note - must contain an id that points to the framework to update.
     * @return boolean
     */
    public static function update_framework($record) {
        static::require_enabled();
        $framework = new competency_framework($record->id);

        // Check the permissions before update.
        require_capability('moodle/competency:competencymanage', $framework->get_context());

        // Account for different formats of taxonomies.
        $framework->from_record($record);
        if (isset($record->taxonomies)) {
            $framework->set('taxonomies', $record->taxonomies);
        }

        // Trigger a competency framework updated event.
        \core\event\competency_framework_updated::create_from_framework($framework)->trigger();

        return $framework->update();
    }

    /**
     * Read the details for a single competency framework and return a record.
     *
     * Requires moodle/competency:competencyview capability at the system context.
     *
     * @param int $id The id of the framework to read.
     * @return competency_framework
     */
    public static function read_framework($id) {
        static::require_enabled();
        $framework = new competency_framework($id);
        if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
                $framework->get_context())) {
            throw new required_capability_exception($framework->get_context(), 'moodle/competency:competencyview',
                'nopermissions', '');
        }
        return $framework;
    }

    /**
     * Log the competency framework viewed event.
     *
     * @param competency_framework|int $frameworkorid The competency_framework object or competency framework id
     * @return bool
     */
    public static function competency_framework_viewed($frameworkorid) {
        static::require_enabled();
        $framework = $frameworkorid;
        if (!is_object($framework)) {
            $framework = new competency_framework($framework);
        }
        if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
                $framework->get_context())) {
            throw new required_capability_exception($framework->get_context(), 'moodle/competency:competencyview',
                'nopermissions', '');
        }
        \core\event\competency_framework_viewed::create_from_framework($framework)->trigger();
        return true;
    }

    /**
     * Log the competency viewed event.
     *
     * @param competency|int $competencyorid The competency object or competency id
     * @return bool
     */
    public static function competency_viewed($competencyorid) {
        static::require_enabled();
        $competency = $competencyorid;
        if (!is_object($competency)) {
            $competency = new competency($competency);
        }

        if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
                $competency->get_context())) {
            throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview',
                'nopermissions', '');
        }

        \core\event\competency_viewed::create_from_competency($competency)->trigger();
        return true;
    }

    /**
     * Perform a search based on the provided filters and return a paginated list of records.
     *
     * Requires moodle/competency:competencyview capability at the system context.
     *
     * @param string $sort The column to sort on
     * @param string $order ('ASC' or 'DESC')
     * @param int $skip Number of records to skip (pagination)
     * @param int $limit Max of records to return (pagination)
     * @param context $context The parent context of the frameworks.
     * @param string $includes Defines what other contexts to fetch frameworks from.
     *                         Accepted values are:
     *                          - children: All descendants
     *                          - parents: All parents, grand parents, etc...
     *                          - self: Context passed only.
     * @param bool $onlyvisible If true return only visible frameworks
     * @param string $query A string to use to filter down the frameworks.
     * @return array of competency_framework
     */
    public static function list_frameworks($sort, $order, $skip, $limit, $context, $includes = 'children',
                                           $onlyvisible = false, $query = '') {
        global $DB;
        static::require_enabled();

        // Get all the relevant contexts.
        $contexts = self::get_related_contexts($context, $includes,
            array('moodle/competency:competencyview', 'moodle/competency:competencymanage'));

        if (empty($contexts)) {
            throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
        }

        // OK - all set.
        list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
        $select = "contextid $insql";
        if ($onlyvisible) {
            $select .= " AND visible = :visible";
            $inparams['visible'] = 1;
        }

        if (!empty($query) || is_numeric($query)) {
            $sqlnamelike = $DB->sql_like('shortname', ':namelike', false);
            $sqlidnlike = $DB->sql_like('idnumber', ':idnlike', false);

            $select .= " AND ($sqlnamelike OR $sqlidnlike) ";
            $inparams['namelike'] = '%' . $DB->sql_like_escape($query) . '%';
            $inparams['idnlike'] = '%' . $DB->sql_like_escape($query) . '%';
        }

        return competency_framework::get_records_select($select, $inparams, $sort . ' ' . $order, '*', $skip, $limit);
    }

    /**
     * Perform a search based on the provided filters and return a paginated list of records.
     *
     * Requires moodle/competency:competencyview capability at the system context.
     *
     * @param context $context The parent context of the frameworks.
     * @param string $includes Defines what other contexts to fetch frameworks from.
     *                         Accepted values are:
     *                          - children: All descendants
     *                          - parents: All parents, grand parents, etc...
     *                          - self: Context passed only.
     * @return int
     */
    public static function count_frameworks($context, $includes) {
        global $DB;
        static::require_enabled();

        // Get all the relevant contexts.
        $contexts = self::get_related_contexts($context, $includes,
            array('moodle/competency:competencyview', 'moodle/competency:competencymanage'));

        if (empty($contexts)) {
            throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
        }

        // OK - all set.
        list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
        return competency_framework::count_records_select("contextid $insql", $inparams);
    }

    /**
     * Fetches all the relevant contexts.
     *
     * Note: This currently only supports system, category and user contexts. However user contexts
     * behave a bit differently and will fallback on the system context. This is what makes the most
     * sense because a user context does not have descendants, and only has system as a parent.
     *
     * @param context $context The context to start from.
     * @param string $includes Defines what other contexts to find.
     *                         Accepted values are:
     *                          - children: All descendants
     *                          - parents: All parents, grand parents, etc...
     *                          - self: Context passed only.
     * @param array $hasanycapability Array of capabilities passed to {@link has_any_capability()} in each context.
     * @return context[] An array of contexts where keys are context IDs.
     */
    public static function get_related_contexts($context, $includes, ?array $hasanycapability = null) {
        global $DB;
        static::require_enabled();

        if (!in_array($includes, array('children', 'parents', 'self'))) {
            throw new coding_exception('Invalid parameter value for \'includes\'.');
        }

        // If context user swap it for the context_system.
        if ($context->contextlevel == CONTEXT_USER) {
            $context = context_system::instance();
        }

        $contexts = array($context->id => $context);

        if ($includes == 'children') {
            $params = array('coursecatlevel' => CONTEXT_COURSECAT, 'path' => $context->path . '/%');
            $pathlike = $DB->sql_like('path', ':path');
            $sql = "contextlevel = :coursecatlevel AND $pathlike";
            $rs = $DB->get_recordset_select('context', $sql, $params);
            foreach ($rs as $record) {
                $ctxid = $record->id;
                context_helper::preload_from_record($record);
                $contexts[$ctxid] = context::instance_by_id($ctxid);
            }
            $rs->close();

        } else if ($includes == 'parents') {
            $children = $context->get_parent_contexts();
            foreach ($children as $ctx) {
                $contexts[$ctx->id] = $ctx;
            }
        }

        // Filter according to the capabilities required.
        if (!empty($hasanycapability)) {
            foreach ($contexts as $key => $ctx) {
                if (!has_any_capability($hasanycapability, $ctx)) {
                    unset($contexts[$key]);
                }
            }
        }

        return $contexts;
    }

    /**
     * Count all the courses using a competency.
     *
     * @param int $competencyid The id of the competency to check.
     * @return int
     */
    public static function count_courses_using_competency($competencyid) {
        static::require_enabled();

        // OK - all set.
        $courses = course_competency::list_courses_min($competencyid);
        $count = 0;

        // Now check permissions on each course.
        foreach ($courses as $course) {
            if (!self::validate_course($course, false)) {
                continue;
            }

            $context = context_course::instance($course->id);
            $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
            if (!has_any_capability($capabilities, $context)) {
                continue;
            }

            $count++;
        }

        return $count;
    }

    /**
     * List all the courses modules using a competency in a course.
     *
     * @param int $competencyid The id of the competency to check.
     * @param int $courseid The id of the course to check.
     * @return array[int] Array of course modules ids.
     */
    public static function list_course_modules_using_competency($competencyid, $courseid) {
        static::require_enabled();

        $result = array();
        self::validate_course($courseid);

        $coursecontext = context_course::instance($courseid);

        // We will not check each module - course permissions should be enough.
        $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
        if (!has_any_capability($capabilities, $coursecontext)) {
            throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
        }

        $cmlist = course_module_competency::list_course_modules($competencyid, $courseid);
        foreach ($cmlist as $cmid) {
            if (self::validate_course_module($cmid, false)) {
                array_push($result, $cmid);
            }
        }

        return $result;
    }

    /**
     * List all the competencies linked to a course module.
     *
     * @param mixed $cmorid The course module, or its ID.
     * @return array[competency] Array of competency records.
     */
    public static function list_course_module_competencies_in_course_module($cmorid) {
        static::require_enabled();
        $cm = $cmorid;
        if (!is_object($cmorid)) {
            $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
        }

        // Check the user has access to the course module.
        self::validate_course_module($cm);
        $context = context_module::instance($cm->id);

        $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
        if (!has_any_capability($capabilities, $context)) {
            throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
        }

        $result = array();

        $cmclist = course_module_competency::list_course_module_competencies($cm->id);
        foreach ($cmclist as $id => $cmc) {
            array_push($result, $cmc);
        }

        return $result;
    }

    /**
     * List all the courses using a competency.
     *
     * @param int $competencyid The id of the competency to check.
     * @return array[stdClass] Array of stdClass containing id and shortname.
     */
    public static function list_courses_using_competency($competencyid) {
        static::require_enabled();

        // OK - all set.
        $courses = course_competency::list_courses($competencyid);
        $result = array();

        // Now check permissions on each course.
        foreach ($courses as $id => $course) {
            $context = context_course::instance($course->id);
            $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
            if (!has_any_capability($capabilities, $context)) {
                unset($courses[$id]);
                continue;
            }
            if (!self::validate_course($course, false)) {
                unset($courses[$id]);
                continue;
            }
            array_push($result, $course);
        }

        return $result;
    }

    /**
     * Count the proficient competencies in a course for one user.
     *
     * @param int $courseid The id of the course to check.
     * @param int $userid The id of the user to check.
     * @return int
     */
    public static function count_proficient_competencies_in_course_for_user($courseid, $userid) {
        static::require_enabled();
        // Check the user has access to the course.
        self::validate_course($courseid);

        // First we do a permissions check.
        $context = context_course::instance($courseid);

        $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
        if (!has_any_capability($capabilities, $context)) {
             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
        }

        // OK - all set.
        return user_competency_course::count_proficient_competencies($courseid, $userid);
    }

    /**
     * Count all the competencies in a course.
     *
     * @param int $courseid The id of the course to check.
     * @return int
     */
    public static function count_competencies_in_course($courseid) {
        static::require_enabled();
        // Check the user has access to the course.
        self::validate_course($courseid);

        // First we do a permissions check.
        $context = context_course::instance($courseid);

        $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
        if (!has_any_capability($capabilities, $context)) {
             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
        }

        // OK - all set.
        return course_competency::count_competencies($courseid);
    }

    /**
     * List the competencies associated to a course.
     *
     * @param mixed $courseorid The course, or its ID.
     * @return array( array(
     *                   'competency' => \core_competency\competency,
     *                   'coursecompetency' => \core_competency\course_competency
     *              ))
     */
    public static function list_course_competencies($courseorid) {
        static::require_enabled();
        $course = $courseorid;
        if (!is_object($courseorid)) {
            $course = get_course($courseorid);
        }

        // Check the user has access to the course.
        self::validate_course($course);
        $context = context_course::instance($course->id);

        $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
        if (!has_any_capability($capabilities, $context)) {
            throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
        }

        $result = array();

        // TODO We could improve the performance of this into one single query.
        $coursecompetencies = course_competency::list_course_competencies($course->id);
        $competencies = course_competency::list_competencies($course->id);

        // Build the return values.
        foreach ($coursecompetencies as $key => $coursecompetency) {
            $result[] = array(
                'competency' => $competencies[$coursecompetency->get('competencyid')],
                'coursecompetency' => $coursecompetency
            );
        }

        return $result;
    }

    /**
     * Get a user competency.
     *
     * @param int $userid The user ID.
     * @param int $competencyid The competency ID.
     * @return user_competency
     */
    public static function get_user_competency($userid, $competencyid) {
        static::require_enabled();
        $existing = user_competency::get_multiple($userid, array($competencyid));
        $uc = array_pop($existing);

        if (!$uc) {
            $uc = user_competency::create_relation($userid, $competencyid);
            $uc->create();
        }

        if (!$uc->can_read()) {
            throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
                'nopermissions', '');
        }
        return $uc;
    }

    /**
     * Get a user competency by ID.
     *
     * @param int $usercompetencyid The user competency ID.
     * @return user_competency
     */
    public static function get_user_competency_by_id($usercompetencyid) {
        static::require_enabled();
        $uc = new user_competency($usercompetencyid);
        if (!$uc->can_read()) {
            throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
                'nopermissions', '');
        }
        return $uc;
    }

    /**
     * Count the competencies associated to a course module.
     *
     * @param mixed $cmorid The course module, or its ID.
     * @return int
     */
    public static function count_course_module_competencies($cmorid) {
        static::require_enabled();
        $cm = $cmorid;
        if (!is_object($cmorid)) {
            $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
        }

        // Check the user has access to the course module.
        self::validate_course_module($cm);
        $context = context_module::instance($cm->id);

        $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
        if (!has_any_capability($capabilities, $context)) {
            throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
        }

        return course_module_competency::count_competencies($cm->id);
    }

    /**
     * List the competencies associated to a course module.
     *
     * @param mixed $cmorid The course module, or its ID.
     * @return array( array(
     *                   'competency' => \core_competency\competency,
     *                   'coursemodulecompetency' => \core_competency\course_module_competency
     *              ))
     */
    public static function list_course_module_competencies($cmorid) {
        static::require_enabled();
        $cm = $cmorid;
        if (!is_object($cmorid)) {
            $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
        }

        // Check the user has access to the course module.
        self::validate_course_module($cm);
        $context = context_module::instance($cm->id);

        $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
        if (!has_any_capability($capabilities, $context)) {
            throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
        }

        $result = array();

        // TODO We could improve the performance of this into one single query.
        $coursemodulecompetencies = course_module_competency::list_course_module_competencies($cm->id);
        $competencies = course_module_competency::list_competencies($cm->id);

        // Build the return values.
        foreach ($coursemodulecompetencies as $key => $coursemodulecompetency) {
            $result[] = array(
                'competency' => $competencies[$coursemodulecompetency->get('competencyid')],
                'coursemodulecompetency' => $coursemodulecompetency
            );
        }

        return $result;
    }

    /**
     * Get a user competency in a course.
     *
     * @param int $courseid The id of the course to check.
     * @param int $userid The id of the course to check.
     * @param int $competencyid The id of the competency.
     * @return user_competency_course
     */
    public static function get_user_competency_in_course($courseid, $userid, $competencyid) {
        static::require_enabled();
        // First we do a permissions check.
        $context = context_course::instance($courseid);

        $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
        if (!has_any_capability($capabilities, $context)) {
            throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
        } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
            throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
        }

        // This will throw an exception if the competency does not belong to the course.
        $competency = course_competency::get_competency($courseid, $competencyid);

        $params = array('courseid' => $courseid, 'userid' => $userid, 'competencyid' => $competencyid);
        $exists = user_competency_course::get_record($params);
        // Create missing.
        if ($exists) {
            $ucc = $exists;
        } else {
            $ucc = user_competency_course::create_relation($userid, $competency->get('id'), $courseid);
            $ucc->create();
        }

        return $ucc;
    }

    /**
     * List all the user competencies in a course.
     *
     * @param int $courseid The id of the course to check.
     * @param int $userid The id of the course to check.
     * @return array of user_competency_course objects
     */
    public static function list_user_competencies_in_course($courseid, $userid) {
        static::require_enabled();
        // First we do a permissions check.
        $context = context_course::instance($courseid);
        $onlyvisible = 1;

        $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
        if (!has_any_capability($capabilities, $context)) {
            throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
        } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
            throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
        }

        // OK - all set.
        $competencylist = course_competency::list_competencies($courseid, false);

        $existing = user_competency_course::get_multiple($userid, $courseid, $competencylist);
        // Create missing.
        $orderedusercompetencycourses = array();

        $somemissing = false;
        foreach ($competencylist as $coursecompetency) {
            $found = false;
            foreach ($existing as $usercompetencycourse) {
                if ($usercompetencycourse->get('competencyid') == $coursecompetency->get('id')) {
                    $found = true;
                    $orderedusercompetencycourses[$usercompetencycourse->get('id')] = $usercompetencycourse;
                    break;
                }
            }
            if (!$found) {
                $ucc = user_competency_course::create_relation($userid, $coursecompetency->get('id'), $courseid);
                $ucc->create();
                $orderedusercompetencycourses[$ucc->get('id')] = $ucc;
            }
        }

        return $orderedusercompetencycourses;
    }

    /**
     * List the user competencies to review.
     *
     * The method returns values in this format:
     *
     * array(
     *     'competencies' => array(
     *         (stdClass)(
     *             'usercompetency' => (user_competency),
     *             'competency' => (competency),
     *             'user' => (user)
     *         )
     *     ),
     *     'count' => (int)
     * )
     *
     * @param int $skip The number of records to skip.
     * @param int $limit The number of results to return.
     * @param int $userid The user we're getting the competencies to review for.
     * @return array Containing the keys 'count', and 'competencies'. The 'competencies' key contains an object
     *               which contains 'competency', 'usercompetency' and 'user'.
     */
    public static function list_user_competencies_to_review($skip = 0, $limit = 50, $userid = null) {
        global $DB, $USER;
        static::require_enabled();
        if ($userid === null) {
            $userid = $USER->id;
        }

        $capability = 'moodle/competency:usercompetencyreview';
        $ucfields = user_competency::get_sql_fields('uc', 'uc_');
        $compfields = competency::get_sql_fields('c', 'c_');
        $usercols = array('id') + get_user_fieldnames();
        $userfields = array();
        foreach ($usercols as $field) {
            $userfields[] = "u." . $field . " AS usr_" . $field;
        }
        $userfields = implode(',', $userfields);

        $select = "SELECT $ucfields, $compfields, $userfields";
        $countselect = "SELECT COUNT('x')";
        $sql = "  FROM {" . user_competency::TABLE . "} uc
                  JOIN {" . competency::TABLE . "} c
                    ON c.id = uc.competencyid
                  JOIN {user} u
                    ON u.id = uc.userid
                 WHERE (uc.status = :waitingforreview
                    OR (uc.status = :inreview AND uc.reviewerid = :reviewerid))
                   AND u.deleted = 0";
        $ordersql = " ORDER BY c.shortname ASC";
        $params = array(
            'inreview' => user_competency::STATUS_IN_REVIEW,
            'reviewerid' => $userid,
            'waitingforreview' => user_competency::STATUS_WAITING_FOR_REVIEW,
        );
        $countsql = $countselect . $sql;

        // Primary check to avoid the hard work of getting the users in which the user has permission.
        $count = $DB->count_records_sql($countselect . $sql, $params);
        if ($count < 1) {
            return array('count' => 0, 'competencies' => array());
        }

        // TODO MDL-52243 Use core function.
        list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql(
            $capability, $userid, SQL_PARAMS_NAMED);
        $params += $inparams;
        $countsql = $countselect . $sql . " AND uc.userid $insql";
        $getsql = $select . $sql . " AND uc.userid $insql " . $ordersql;

        // Extracting the results.
        $competencies = array();
        $records = $DB->get_recordset_sql($getsql, $params, $skip, $limit);
        foreach ($records as $record) {
            $objects = (object) array(
                'usercompetency' => new user_competency(0, user_competency::extract_record($record, 'uc_')),
                'competency' => new competency(0, competency::extract_record($record, 'c_')),
                'user' => persistent::extract_record($record, 'usr_'),
            );
            $competencies[] = $objects;
        }
        $records->close();

        return array(
            'count' => $DB->count_records_sql($countsql, $params),
            'competencies' => $competencies
        );
    }

    /**
     * Add a competency to this course module.
     *
     * @param mixed $cmorid The course module, or id of the course module
     * @param int $competencyid The id of the competency
     * @return bool
     */
    public static function add_competency_to_course_module($cmorid, $competencyid) {
        static::require_enabled();
        $cm = $cmorid;
        if (!is_object($cmorid)) {
            $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
        }

        // Check the user has access to the course module.
        self::validate_course_module($cm);

        // First we do a permissions check.
        $context = context_module::instance($cm->id);

        require_capability('moodle/competency:coursecompetencymanage', $context);

        // Check that the competency belongs to the course.
        $exists = course_competency::get_records(array('courseid' => $cm->course, 'competencyid' => $competencyid));
        if (!$exists) {
            throw new coding_exception('Cannot add a competency to a module if it does not belong to the course');
        }

        $record = new stdClass();
        $record->cmid = $cm->id;
        $record->competencyid = $competencyid;

        $coursemodulecompetency = new course_module_competency();
        $exists = $coursemodulecompetency->get_records(array('cmid' => $cm->id, 'competencyid' => $competencyid));
        if (!$exists) {
            $coursemodulecompetency->from_record($record);
            if ($coursemodulecompetency->create()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Remove a competency from this course module.
     *
     * @param mixed $cmorid The course module, or id of the course module
     * @param int $competencyid The id of the competency
     * @return bool
     */
    public static function remove_competency_from_course_module($cmorid, $competencyid) {
        static::require_enabled();
        $cm = $cmorid;
        if (!is_object($cmorid)) {
            $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
        }
        // Check the user has access to the course module.
        self::validate_course_module($cm);

        // First we do a permissions check.
        $context = context_module::instance($cm->id);

        require_capability('moodle/competency:coursecompetencymanage', $context);

        $record = new stdClass();
        $record->cmid = $cm->id;
        $record->competencyid = $competencyid;

        $competency = new competency($competencyid);
        $exists = course_module_competency::get_record(array('cmid' => $cm->id, 'competencyid' => $competencyid));
        if ($exists) {
            return $exists->delete();
        }
        return false;
    }

    /**
     * Move the course module competency up or down in the display list.
     *
     * Requires moodle/competency:coursecompetencymanage capability at the course module context.
     *
     * @param mixed $cmorid The course module, or id of the course module
     * @param int $competencyidfrom The id of the competency we are moving.
     * @param int $competencyidto The id of the competency we are moving to.
     * @return boolean
     */
    public static function reorder_course_module_competency($cmorid, $competencyidfrom, $competencyidto) {
        static::require_enabled();
        $cm = $cmorid;
        if (!is_object($cmorid)) {
            $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
        }
        // Check the user has access to the course module.
        self::validate_course_module($cm);

        // First we do a permissions check.
        $context = context_module::instance($cm->id);

        require_capability('moodle/competency:coursecompetencymanage', $context);

        $down = true;
        $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidfrom));
        if (count($matches) == 0) {
             throw new coding_exception('The link does not exist');
        }

        $competencyfrom = array_pop($matches);
        $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidto));
        if (count($matches) == 0) {
             throw new coding_exception('The link does not exist');
        }

        $competencyto = array_pop($matches);

        $all = course_module_competency::get_records(array('cmid' => $cm->id), 'sortorder', 'ASC', 0, 0);

        if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
            // We are moving up, so put it before the "to" item.
            $down = false;
        }

        foreach ($all as $id => $coursemodulecompetency) {
            $sort = $coursemodulecompetency->get('sortorder');
            if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
                $coursemodulecompetency->set('sortorder', $coursemodulecompetency->get('sortorder') - 1);
                $coursemodulecompetency->update();
            } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
                $coursemodulecompetency->set('sortorder', $coursemodulecompetency->get('sortorder') + 1);
                $coursemodulecompetency->update();
            }
        }
        $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
        return $competencyfrom->update();
    }

    /**
     * Update ruleoutcome value for a course module competency.
     *
     * @param int|course_module_competency $coursemodulecompetencyorid The course_module_competency, or its ID.
     * @param int $ruleoutcome The value of ruleoutcome.
     * @param bool $overridegrade If true, will override existing grades in related competencies.
     * @return bool True on success.
     */
    public static function set_course_module_competency_ruleoutcome($coursemodulecompetencyorid, $ruleoutcome,
        $overridegrade = false) {
        static::require_enabled();
        $coursemodulecompetency = $coursemodulecompetencyorid;
        if (!is_object($coursemodulecompetency)) {
            $coursemodulecompetency = new course_module_competency($coursemodulecompetencyorid);
        }

        $cm = get_coursemodule_from_id('', $coursemodulecompetency->get('cmid'), 0, true, MUST_EXIST);

        self::validate_course_module($cm);
        $context = context_module::instance($cm->id);

        require_capability('moodle/competency:coursecompetencymanage', $context);

        $coursemodulecompetency->set('ruleoutcome', $ruleoutcome);
        $coursemodulecompetency->set('overridegrade', $overridegrade);

        return $coursemodulecompetency->update();
    }

    /**
     * Add a competency to this course.
     *
     * @param int $courseid The id of the course
     * @param int $competencyid The id of the competency
     * @return bool
     */
    public static function add_competency_to_course($courseid, $competencyid) {
        static::require_enabled();
        // Check the user has access to the course.
        self::validate_course($courseid);

        // First we do a permissions check.
        $context = context_course::instance($courseid);

        require_capability('moodle/competency:coursecompetencymanage', $context);

        $record = new stdClass();
        $record->courseid = $courseid;
        $record->competencyid = $competencyid;

        $competency = new competency($competencyid);

        // Can not add a competency that belongs to a hidden framework.
        if ($competency->get_framework()->get('visible') == false) {
            throw new coding_exception('A competency belonging to hidden framework can not be linked to course');
        }

        $coursecompetency = new course_competency();
        $exists = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyid));
        if (!$exists) {
            $coursecompetency->from_record($record);
            if ($coursecompetency->create()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Remove a competency from this course.
     *
     * @param int $courseid The id of the course
     * @param int $competencyid The id of the competency
     * @return bool
     */
    public static function remove_competency_from_course($courseid, $competencyid) {
        static::require_enabled();
        // Check the user has access to the course.
        self::validate_course($courseid);

        // First we do a permissions check.
        $context = context_course::instance($courseid);

        require_capability('moodle/competency:coursecompetencymanage', $context);

        $record = new stdClass();
        $record->courseid = $courseid;
        $record->competencyid = $competencyid;

        $coursecompetency = new course_competency();
        $exists = course_competency::get_record(array('courseid' => $courseid, 'competencyid' => $competencyid));
        if ($exists) {
            // Delete all course_module_competencies for this competency in this course.
            $cmcs = course_module_competency::get_records_by_competencyid_in_course($competencyid, $courseid);
            foreach ($cmcs as $cmc) {
                $cmc->delete();
            }
            return $exists->delete();
        }
        return false;
    }

    /**
     * Move the course competency up or down in the display list.
     *
     * Requires moodle/competency:coursecompetencymanage capability at the course context.
     *
     * @param int $courseid The course
     * @param int $competencyidfrom The id of the competency we are moving.
     * @param int $competencyidto The id of the competency we are moving to.
     * @return boolean
     */
    public static function reorder_course_competency($courseid, $competencyidfrom, $competencyidto) {
        static::require_enabled();
        // Check the user has access to the course.
        self::validate_course($courseid);

        // First we do a permissions check.
        $context = context_course::instance($courseid);

        require_capability('moodle/competency:coursecompetencymanage', $context);

        $down = true;
        $coursecompetency = new course_competency();
        $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidfrom));
        if (count($matches) == 0) {
             throw new coding_exception('The link does not exist');
        }

        $competencyfrom = array_pop($matches);
        $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidto));
        if (count($matches) == 0) {
             throw new coding_exception('The link does not exist');
        }

        $competencyto = array_pop($matches);

        $all = $coursecompetency->get_records(array('courseid' => $courseid), 'sortorder', 'ASC', 0, 0);

        if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
            // We are moving up, so put it before the "to" item.
            $down = false;
        }

        foreach ($all as $id => $coursecompetency) {
            $sort = $coursecompetency->get('sortorder');
            if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
                $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') - 1);
                $coursecompetency->update();
            } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
                $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') + 1);
                $coursecompetency->update();
            }
        }
        $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
        return $competencyfrom->update();
    }

    /**
     * Update ruleoutcome value for a course competency.
     *
     * @param int|course_competency $coursecompetencyorid The course_competency, or its ID.
     * @param int $ruleoutcome The value of ruleoutcome.
     * @return bool True on success.
     */
    public static function set_course_competency_ruleoutcome($coursecompetencyorid, $ruleoutcome) {
        static::require_enabled();
        $coursecompetency = $coursecompetencyorid;
        if (!is_object($coursecompetency)) {
            $coursecompetency = new course_competency($coursecompetencyorid);
        }

        $courseid = $coursecompetency->get('courseid');
        self::validate_course($courseid);
        $coursecontext = context_course::instance($courseid);

        require_capability('moodle/competency:coursecompetencymanage', $coursecontext);

        $coursecompetency->set('ruleoutcome', $ruleoutcome);
        return $coursecompetency->update();
    }

    /**
     * Create a learning plan template from a record containing all the data for the class.
     *
     * Requires moodle/competency:templatemanage capability.
     *
     * @param stdClass $record Record containing all the data for an instance of the class.
     * @return template
     */
    public static function create_template(stdClass $record) {
        static::require_enabled();
        $template = new template(0, $record);

        // First we do a permissions check.
        if (!$template->can_manage()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
                'nopermissions', '');
        }

        // OK - all set.
        $template = $template->create();

        // Trigger a template created event.
        \core\event\competency_template_created::create_from_template($template)->trigger();

        return $template;
    }

    /**
     * Duplicate a learning plan template.
     *
     * Requires moodle/competency:templatemanage capability at the template context.
     *
     * @param int $id the template id.
     * @return template
     */
    public static function duplicate_template($id) {
        static::require_enabled();
        $template = new template($id);

        // First we do a permissions check.
        if (!$template->can_manage()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
                'nopermissions', '');
        }

        // OK - all set.
        $competencies = template_competency::list_competencies($id, false);

        // Adding the suffix copy.
        $template->set('shortname', get_string('duplicateditemname', 'core_competency', $template->get('shortname')));
        $template->set('id', 0);

        $duplicatedtemplate = $template->create();

        // Associate each competency for the duplicated template.
        foreach ($competencies as $competency) {
            self::add_competency_to_template($duplicatedtemplate->get('id'), $competency->get('id'));
        }

        // Trigger a template created event.
        \core\event\competency_template_created::create_from_template($duplicatedtemplate)->trigger();

        return $duplicatedtemplate;
    }

    /**
     * Delete a learning plan template by id.
     * If the learning plan template has associated cohorts they will be deleted.
     *
     * Requires moodle/competency:templatemanage capability.
     *
     * @param int $id The record to delete.
     * @param boolean $deleteplans True to delete plans associaated to template, false to unlink them.
     * @return boolean
     */
    public static function delete_template($id, $deleteplans = true) {
        global $DB;
        static::require_enabled();
        $template = new template($id);

        // First we do a permissions check.
        if (!$template->can_manage()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
                'nopermissions', '');
        }

        $transaction = $DB->start_delegated_transaction();
        $success = true;

        // Check if there are cohorts associated.
        $templatecohorts = template_cohort::get_relations_by_templateid($template->get('id'));
        foreach ($templatecohorts as $templatecohort) {
            $success = $templatecohort->delete();
            if (!$success) {
                break;
            }
        }

        // Still OK, delete or unlink the plans from the template.
        if ($success) {
            $plans = plan::get_records(array('templateid' => $template->get('id')));
            foreach ($plans as $plan) {
                $success = $deleteplans ? self::delete_plan($plan->get('id')) : self::unlink_plan_from_template($plan);
                if (!$success) {
                    break;
                }
            }
        }

        // Still OK, delete the template comptencies.
        if ($success) {
            $success = template_competency::delete_by_templateid($template->get('id'));
        }

        // OK - all set.
        if ($success) {
            // Create a template deleted event.
            $event = \core\event\competency_template_deleted::create_from_template($template);

            $success = $template->delete();
        }

        if ($success) {
            // Trigger a template deleted event.
            $event->trigger();

            // Commit the transaction.
            $transaction->allow_commit();
        } else {
            $transaction->rollback(new moodle_exception('Error while deleting the template.'));
        }

        return $success;
    }

    /**
     * Update the details for a learning plan template.
     *
     * Requires moodle/competency:templatemanage capability.
     *
     * @param stdClass $record The new details for the template. Note - must contain an id that points to the template to update.
     * @return boolean
     */
    public static function update_template($record) {
        global $DB;
        static::require_enabled();
        $template = new template($record->id);

        // First we do a permissions check.
        if (!$template->can_manage()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
                'nopermissions', '');

        } else if (isset($record->contextid) && $record->contextid != $template->get('contextid')) {
            // We can never change the context of a template.
            throw new coding_exception('Changing the context of an existing tempalte is forbidden.');

        }

        $updateplans = false;
        $before = $template->to_record();

        $template->from_record($record);
        $after = $template->to_record();

        // Should we update the related plans?
        if ($before->duedate != $after->duedate ||
                $before->shortname != $after->shortname ||
                $before->description != $after->description ||
                $before->descriptionformat != $after->descriptionformat) {
            $updateplans = true;
        }

        $transaction = $DB->start_delegated_transaction();
        $success = $template->update();

        if (!$success) {
            $transaction->rollback(new moodle_exception('Error while updating the template.'));
            return $success;
        }

        // Trigger a template updated event.
        \core\event\competency_template_updated::create_from_template($template)->trigger();

        if ($updateplans) {
            plan::update_multiple_from_template($template);
        }

        $transaction->allow_commit();

        return $success;
    }

    /**
     * Read the details for a single learning plan template and return a record.
     *
     * Requires moodle/competency:templateview capability at the system context.
     *
     * @param int $id The id of the template to read.
     * @return template
     */
    public static function read_template($id) {
        static::require_enabled();
        $template = new template($id);
        $context = $template->get_context();

        // First we do a permissions check.
        if (!$template->can_read()) {
             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
                'nopermissions', '');
        }

        // OK - all set.
        return $template;
    }

    /**
     * Perform a search based on the provided filters and return a paginated list of records.
     *
     * Requires moodle/competency:templateview capability at the system context.
     *
     * @param string $sort The column to sort on
     * @param string $order ('ASC' or 'DESC')
     * @param int $skip Number of records to skip (pagination)
     * @param int $limit Max of records to return (pagination)
     * @param context $context The parent context of the frameworks.
     * @param string $includes Defines what other contexts to fetch frameworks from.
     *                         Accepted values are:
     *                          - children: All descendants
     *                          - parents: All parents, grand parents, etc...
     *                          - self: Context passed only.
     * @param bool $onlyvisible If should list only visible templates
     * @return array of competency_framework
     */
    public static function list_templates($sort, $order, $skip, $limit, $context, $includes = 'children', $onlyvisible = false) {
        global $DB;
        static::require_enabled();

        // Get all the relevant contexts.
        $contexts = self::get_related_contexts($context, $includes,
            array('moodle/competency:templateview', 'moodle/competency:templatemanage'));

        // First we do a permissions check.
        if (empty($contexts)) {
             throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
        }

        // Make the order by.
        $orderby = '';
        if (!empty($sort)) {
            $orderby = $sort . ' ' . $order;
        }

        // OK - all set.
        $template = new template();
        list($insql, $params) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
        $select = "contextid $insql";

        if ($onlyvisible) {
            $select .= " AND visible = :visible";
            $params['visible'] = 1;
        }
        return $template->get_records_select($select, $params, $orderby, '*', $skip, $limit);
    }

    /**
     * Perform a search based on the provided filters and return how many results there are.
     *
     * Requires moodle/competency:templateview capability at the system context.
     *
     * @param context $context The parent context of the frameworks.
     * @param string $includes Defines what other contexts to fetch frameworks from.
     *                         Accepted values are:
     *                          - children: All descendants
     *                          - parents: All parents, grand parents, etc...
     *                          - self: Context passed only.
     * @return int
     */
    public static function count_templates($context, $includes) {
        global $DB;
        static::require_enabled();

        // First we do a permissions check.
        $contexts = self::get_related_contexts($context, $includes,
            array('moodle/competency:templateview', 'moodle/competency:templatemanage'));

        if (empty($contexts)) {
             throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
        }

        // OK - all set.
        $template = new template();
        list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
        return $template->count_records_select("contextid $insql", $inparams);
    }

    /**
     * Count all the templates using a competency.
     *
     * @param int $competencyid The id of the competency to check.
     * @return int
     */
    public static function count_templates_using_competency($competencyid) {
        static::require_enabled();
        // First we do a permissions check.
        $context = context_system::instance();
        $onlyvisible = 1;

        $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
        if (!has_any_capability($capabilities, $context)) {
             throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
        }

        if (has_capability('moodle/competency:templatemanage', $context)) {
            $onlyvisible = 0;
        }

        // OK - all set.
        return template_competency::count_templates($competencyid, $onlyvisible);
    }

    /**
     * List all the learning plan templatesd using a competency.
     *
     * @param int $competencyid The id of the competency to check.
     * @return array[stdClass] Array of stdClass containing id and shortname.
     */
    public static function list_templates_using_competency($competencyid) {
        static::require_enabled();
        // First we do a permissions check.
        $context = context_system::instance();
        $onlyvisible = 1;

        $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
        if (!has_any_capability($capabilities, $context)) {
             throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
        }

        if (has_capability('moodle/competency:templatemanage', $context)) {
            $onlyvisible = 0;
        }

        // OK - all set.
        return template_competency::list_templates($competencyid, $onlyvisible);

    }

    /**
     * Count all the competencies in a learning plan template.
     *
     * @param  template|int $templateorid The template or its ID.
     * @return int
     */
    public static function count_competencies_in_template($templateorid) {
        static::require_enabled();
        // First we do a permissions check.
        $template = $templateorid;
        if (!is_object($template)) {
            $template = new template($template);
        }

        if (!$template->can_read()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
                'nopermissions', '');
        }

        // OK - all set.
        return template_competency::count_competencies($template->get('id'));
    }

    /**
     * Count all the competencies in a learning plan template with no linked courses.
     *
     * @param  template|int $templateorid The template or its ID.
     * @return int
     */
    public static function count_competencies_in_template_with_no_courses($templateorid) {
        // First we do a permissions check.
        $template = $templateorid;
        if (!is_object($template)) {
            $template = new template($template);
        }

        if (!$template->can_read()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
                'nopermissions', '');
        }

        // OK - all set.
        return template_competency::count_competencies_with_no_courses($template->get('id'));
    }

    /**
     * List all the competencies in a template.
     *
     * @param  template|int $templateorid The template or its ID.
     * @return array of competencies
     */
    public static function list_competencies_in_template($templateorid) {
        static::require_enabled();
        // First we do a permissions check.
        $template = $templateorid;
        if (!is_object($template)) {
            $template = new template($template);
        }

        if (!$template->can_read()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
                'nopermissions', '');
        }

        // OK - all set.
        return template_competency::list_competencies($template->get('id'));
    }

    /**
     * Add a competency to this template.
     *
     * @param int $templateid The id of the template
     * @param int $competencyid The id of the competency
     * @return bool
     */
    public static function add_competency_to_template($templateid, $competencyid) {
        static::require_enabled();
        // First we do a permissions check.
        $template = new template($templateid);
        if (!$template->can_manage()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
                'nopermissions', '');
        }

        $record = new stdClass();
        $record->templateid = $templateid;
        $record->competencyid = $competencyid;

        $competency = new competency($competencyid);

        // Can not add a competency that belongs to a hidden framework.
        if ($competency->get_framework()->get('visible') == false) {
            throw new coding_exception('A competency belonging to hidden framework can not be added');
        }

        $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
        if (!$exists) {
            $templatecompetency = new template_competency(0, $record);
            $templatecompetency->create();
            return true;
        }
        return false;
    }

    /**
     * Remove a competency from this template.
     *
     * @param int $templateid The id of the template
     * @param int $competencyid The id of the competency
     * @return bool
     */
    public static function remove_competency_from_template($templateid, $competencyid) {
        static::require_enabled();
        // First we do a permissions check.
        $template = new template($templateid);
        if (!$template->can_manage()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
                'nopermissions', '');
        }

        $record = new stdClass();
        $record->templateid = $templateid;
        $record->competencyid = $competencyid;

        $competency = new competency($competencyid);

        $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
        if ($exists) {
            $link = array_pop($exists);
            return $link->delete();
        }
        return false;
    }

    /**
     * Move the template competency up or down in the display list.
     *
     * Requires moodle/competency:templatemanage capability at the system context.
     *
     * @param int $templateid The template id
     * @param int $competencyidfrom The id of the competency we are moving.
     * @param int $competencyidto The id of the competency we are moving to.
     * @return boolean
     */
    public static function reorder_template_competency($templateid, $competencyidfrom, $competencyidto) {
        static::require_enabled();
        $template = new template($templateid);

        // First we do a permissions check.
        if (!$template->can_manage()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
                'nopermissions', '');
        }

        $down = true;
        $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidfrom));
        if (count($matches) == 0) {
            throw new coding_exception('The link does not exist');
        }

        $competencyfrom = array_pop($matches);
        $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidto));
        if (count($matches) == 0) {
            throw new coding_exception('The link does not exist');
        }

        $competencyto = array_pop($matches);

        $all = template_competency::get_records(array('templateid' => $templateid), 'sortorder', 'ASC', 0, 0);

        if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
            // We are moving up, so put it before the "to" item.
            $down = false;
        }

        foreach ($all as $id => $templatecompetency) {
            $sort = $templatecompetency->get('sortorder');
            if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
                $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') - 1);
                $templatecompetency->update();
            } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
                $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') + 1);
                $templatecompetency->update();
            }
        }
        $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
        return $competencyfrom->update();
    }

    /**
     * Create a relation between a template and a cohort.
     *
     * This silently ignores when the relation already existed.
     *
     * @param  template|int $templateorid The template or its ID.
     * @param  stdClass|int $cohortorid   The cohort ot its ID.
     * @return template_cohort
     */
    public static function create_template_cohort($templateorid, $cohortorid) {
        global $DB;
        static::require_enabled();

        $template = $templateorid;
        if (!is_object($template)) {
            $template = new template($template);
        }
        require_capability('moodle/competency:templatemanage', $template->get_context());

        $cohort = $cohortorid;
        if (!is_object($cohort)) {
            $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
        }

        // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
        $cohortcontext = context::instance_by_id($cohort->contextid);
        if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
            throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
        }

        $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id);
        if (!$tplcohort->get('id')) {
            $tplcohort->create();
        }

        return $tplcohort;
    }

    /**
     * Remove a relation between a template and a cohort.
     *
     * @param  template|int $templateorid The template or its ID.
     * @param  stdClass|int $cohortorid   The cohort ot its ID.
     * @return boolean True on success or when the relation did not exist.
     */
    public static function delete_template_cohort($templateorid, $cohortorid) {
        global $DB;
        static::require_enabled();

        $template = $templateorid;
        if (!is_object($template)) {
            $template = new template($template);
        }
        require_capability('moodle/competency:templatemanage', $template->get_context());

        $cohort = $cohortorid;
        if (!is_object($cohort)) {
            $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
        }

        $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id);
        if (!$tplcohort->get('id')) {
            return true;
        }

        return $tplcohort->delete();
    }

    /**
     * Lists user plans.
     *
     * @param int $userid
     * @return \core_competency\plan[]
     */
    public static function list_user_plans($userid) {
        global $DB, $USER;
        static::require_enabled();
        $select = 'userid = :userid';
        $params = array('userid' => $userid);
        $context = context_user::instance($userid);

        // Check that we can read something here.
        if (!plan::can_read_user($userid) && !plan::can_read_user_draft($userid)) {
            throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
        }

        // The user cannot view the drafts.
        if (!plan::can_read_user_draft($userid)) {
            list($insql, $inparams) = $DB->get_in_or_equal(plan::get_draft_statuses(), SQL_PARAMS_NAMED, 'param', false);
            $select .= " AND status $insql";
            $params += $inparams;
        }
        // The user cannot view the non-drafts.
        if (!plan::can_read_user($userid)) {
            list($insql, $inparams) = $DB->get_in_or_equal(array(plan::STATUS_ACTIVE, plan::STATUS_COMPLETE),
                SQL_PARAMS_NAMED, 'param', false);
            $select .= " AND status $insql";
            $params += $inparams;
        }

        return plan::get_records_select($select, $params, 'name ASC');
    }

    /**
     * List the plans to review.
     *
     * The method returns values in this format:
     *
     * array(
     *     'plans' => array(
     *         (stdClass)(
     *             'plan' => (plan),
     *             'template' => (template),
     *             'owner' => (stdClass)
     *         )
     *     ),
     *     'count' => (int)
     * )
     *
     * @param int $skip The number of records to skip.
     * @param int $limit The number of results to return.
     * @param int $userid The user we're getting the plans to review for.
     * @return array Containing the keys 'count', and 'plans'. The 'plans' key contains an object
     *               which contains 'plan', 'template' and 'owner'.
     */
    public static function list_plans_to_review($skip = 0, $limit = 100, $userid = null) {
        global $DB, $USER;
        static::require_enabled();

        if ($userid === null) {
            $userid = $USER->id;
        }

        $planfields = plan::get_sql_fields('p', 'plan_');
        $tplfields = template::get_sql_fields('t', 'tpl_');
        $usercols = array('id') + get_user_fieldnames();
        $userfields = array();
        foreach ($usercols as $field) {
            $userfields[] = "u." . $field . " AS usr_" . $field;
        }
        $userfields = implode(',', $userfields);

        $select = "SELECT $planfields, $tplfields, $userfields";
        $countselect = "SELECT COUNT('x')";

        $sql = "  FROM {" . plan::TABLE . "} p
                  JOIN {user} u
                    ON u.id = p.userid
             LEFT JOIN {" . template::TABLE . "} t
                    ON t.id = p.templateid
                 WHERE (p.status = :waitingforreview
                    OR (p.status = :inreview AND p.reviewerid = :reviewerid))
                   AND p.userid != :userid";

        $params = array(
            'waitingforreview' => plan::STATUS_WAITING_FOR_REVIEW,
            'inreview' => plan::STATUS_IN_REVIEW,
            'reviewerid' => $userid,
            'userid' => $userid
        );

        // Primary check to avoid the hard work of getting the users in which the user has permission.
        $count = $DB->count_records_sql($countselect . $sql, $params);
        if ($count < 1) {
            return array('count' => 0, 'plans' => array());
        }

        // TODO MDL-52243 Use core function.
        list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql('moodle/competency:planreview',
            $userid, SQL_PARAMS_NAMED);
        $sql .= " AND p.userid $insql";
        $params += $inparams;

        // Order by ID just to have some ordering in place.
        $ordersql = " ORDER BY p.id ASC";

        $plans = array();
        $records = $DB->get_recordset_sql($select . $sql . $ordersql, $params, $skip, $limit);
        foreach ($records as $record) {
            $plan = new plan(0, plan::extract_record($record, 'plan_'));
            $template = null;

            if ($plan->is_based_on_template()) {
                $template = new template(0, template::extract_record($record, 'tpl_'));
            }

            $plans[] = (object) array(
                'plan' => $plan,
                'template' => $template,
                'owner' => persistent::extract_record($record, 'usr_'),
            );
        }
        $records->close();

        return array(
            'count' => $DB->count_records_sql($countselect . $sql, $params),
            'plans' => $plans
        );
    }

    /**
     * Creates a learning plan based on the provided data.
     *
     * @param stdClass $record
     * @return \core_competency\plan
     */
    public static function create_plan(stdClass $record) {
        global $USER;
        static::require_enabled();
        $plan = new plan(0, $record);

        if ($plan->is_based_on_template()) {
            throw new coding_exception('To create a plan from a template use api::create_plan_from_template().');
        } else if ($plan->get('status') == plan::STATUS_COMPLETE) {
            throw new coding_exception('A plan cannot be created as complete.');
        }

        if (!$plan->can_manage()) {
            $context = context_user::instance($plan->get('userid'));
            throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
        }

        $plan->create();

        // Trigger created event.
        \core\event\competency_plan_created::create_from_plan($plan)->trigger();
        return $plan;
    }

    /**
     * Create a learning plan from a template.
     *
     * @param  mixed $templateorid The template object or ID.
     * @param  int $userid
     * @return false|\core_competency\plan Returns false when the plan already exists.
     */
    public static function create_plan_from_template($templateorid, $userid) {
        static::require_enabled();
        $template = $templateorid;
        if (!is_object($template)) {
            $template = new template($template);
        }

        // The user must be able to view the template to use it as a base for a plan.
        if (!$template->can_read()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
                'nopermissions', '');
        }
        // Can not create plan from a hidden template.
        if ($template->get('visible') == false) {
            throw new coding_exception('A plan can not be created from a hidden template');
        }

        // Convert the template to a plan.
        $record = $template->to_record();
        $record->templateid = $record->id;
        $record->userid = $userid;
        $record->name = $record->shortname;
        $record->status = plan::STATUS_ACTIVE;

        unset($record->id);
        unset($record->timecreated);
        unset($record->timemodified);
        unset($record->usermodified);

        // Remove extra keys.
        $properties = plan::properties_definition();
        foreach ($record as $key => $value) {
            if (!array_key_exists($key, $properties)) {
                unset($record->$key);
            }
        }

        $plan = new plan(0, $record);
        if (!$plan->can_manage()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage',
                'nopermissions', '');
        }

        // We first apply the permission checks as we wouldn't want to leak information by returning early that
        // the plan already exists.
        if (plan::record_exists_select('templateid = :templateid AND userid = :userid', array(
                'templateid' => $template->get('id'), 'userid' => $userid))) {
            return false;
        }

        $plan->create();

        // Trigger created event.
        \core\event\competency_plan_created::create_from_plan($plan)->trigger();
        return $plan;
    }

    /**
     * Create learning plans from a template and cohort.
     *
     * @param  mixed $templateorid The template object or ID.
     * @param  int $cohortid The cohort ID.
     * @param  bool $recreateunlinked When true the plans that were unlinked from this template will be re-created.
     * @return int The number of plans created.
     */
    public static function create_plans_from_template_cohort($templateorid, $cohortid, $recreateunlinked = false) {
        global $DB, $CFG;
        static::require_enabled();
        require_once($CFG->dirroot . '/cohort/lib.php');

        $template = $templateorid;
        if (!is_object($template)) {
            $template = new template($template);
        }

        // The user must be able to view the template to use it as a base for a plan.
        if (!$template->can_read()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
                'nopermissions', '');
        }

        // Can not create plan from a hidden template.
        if ($template->get('visible') == false) {
            throw new coding_exception('A plan can not be created from a hidden template');
        }

        // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
        $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
        $cohortcontext = context::instance_by_id($cohort->contextid);
        if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
            throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
        }

        // Convert the template to a plan.
        $recordbase = $template->to_record();
        $recordbase->templateid = $recordbase->id;
        $recordbase->name = $recordbase->shortname;
        $recordbase->status = plan::STATUS_ACTIVE;

        unset($recordbase->id);
        unset($recordbase->timecreated);
        unset($recordbase->timemodified);
        unset($recordbase->usermodified);

        // Remove extra keys.
        $properties = plan::properties_definition();
        foreach ($recordbase as $key => $value) {
            if (!array_key_exists($key, $properties)) {
                unset($recordbase->$key);
            }
        }

        // Create the plans.
        $created = 0;
        $userids = template_cohort::get_missing_plans($template->get('id'), $cohortid, $recreateunlinked);
        foreach ($userids as $userid) {
            $record = (object) (array) $recordbase;
            $record->userid = $userid;

            $plan = new plan(0, $record);
            if (!$plan->can_manage()) {
                // Silently skip members where permissions are lacking.
                continue;
            }

            $plan->create();
            // Trigger created event.
            \core\event\competency_plan_created::create_from_plan($plan)->trigger();
            $created++;
        }

        return $created;
    }

    /**
     * Unlink a plan from its template.
     *
     * @param  \core_competency\plan|int $planorid The plan or its ID.
     * @return bool
     */
    public static function unlink_plan_from_template($planorid) {
        global $DB;
        static::require_enabled();

        $plan = $planorid;
        if (!is_object($planorid)) {
            $plan = new plan($planorid);
        }

        // The user must be allowed to manage the plans of the user, nothing about the template.
        if (!$plan->can_manage()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
        }

        // Only plan with status DRAFT or ACTIVE can be unlinked..
        if ($plan->get('status') == plan::STATUS_COMPLETE) {
            throw new coding_exception('Only draft or active plan can be unlinked from a template');
        }

        // Early exit, it's already done...
        if (!$plan->is_based_on_template()) {
            return true;
        }

        // Fetch the template.
        $template = new template($plan->get('templateid'));

        // Now, proceed by copying all competencies to the plan, then update the plan.
        $transaction = $DB->start_delegated_transaction();
        $competencies = template_competency::list_competencies($template->get('id'), false);
        $i = 0;
        foreach ($competencies as $competency) {
            $record = (object) array(
                'planid' => $plan->get('id'),
                'competencyid' => $competency->get('id'),
                'sortorder' => $i++
            );
            $pc = new plan_competency(null, $record);
            $pc->create();
        }
        $plan->set('origtemplateid', $template->get('id'));
        $plan->set('templateid', null);
        $success = $plan->update();
        $transaction->allow_commit();

        // Trigger unlinked event.
        \core\event\competency_plan_unlinked::create_from_plan($plan)->trigger();

        return $success;
    }

    /**
     * Updates a plan.
     *
     * @param stdClass $record
     * @return \core_competency\plan
     */
    public static function update_plan(stdClass $record) {
        static::require_enabled();

        $plan = new plan($record->id);

        // Validate that the plan as it is can be managed.
        if (!$plan->can_manage()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');

        } else if ($plan->get('status') == plan::STATUS_COMPLETE) {
            // A completed plan cannot be edited.
            throw new coding_exception('Completed plan cannot be edited.');

        } else if ($plan->is_based_on_template()) {
            // Prevent a plan based on a template to be edited.
            throw new coding_exception('Cannot update a plan that is based on a template.');

        } else if (isset($record->templateid) && $plan->get('templateid') != $record->templateid) {
            // Prevent a plan to be based on a template.
            throw new coding_exception('Cannot base a plan on a template.');

        } else if (isset($record->userid) && $plan->get('userid') != $record->userid) {
            // Prevent change of ownership as the capabilities are checked against that.
            throw new coding_exception('A plan cannot be transfered to another user');

        } else if (isset($record->status) && $plan->get('status') != $record->status) {
            // Prevent change of status.
            throw new coding_exception('To change the status of a plan use the appropriate methods.');

        }

        $plan->from_record($record);
        $plan->update();

        // Trigger updated event.
        \core\event\competency_plan_updated::create_from_plan($plan)->trigger();

        return $plan;
    }

    /**
     * Returns a plan data.
     *
     * @param int $id
     * @return \core_competency\plan
     */
    public static function read_plan($id) {
        static::require_enabled();
        $plan = new plan($id);

        if (!$plan->can_read()) {
            $context = context_user::instance($plan->get('userid'));
            throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
        }

        return $plan;
    }

    /**
     * Plan event viewed.
     *
     * @param mixed $planorid The id or the plan.
     * @return boolean
     */
    public static function plan_viewed($planorid) {
        static::require_enabled();
        $plan = $planorid;
        if (!is_object($plan)) {
            $plan = new plan($plan);
        }

        // First we do a permissions check.
        if (!$plan->can_read()) {
            $context = context_user::instance($plan->get('userid'));
            throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
        }

        // Trigger a template viewed event.
        \core\event\competency_plan_viewed::create_from_plan($plan)->trigger();

        return true;
    }

    /**
     * Deletes a plan.
     *
     * Plans based on a template can be removed just like any other one.
     *
     * @param int $id
     * @return bool Success?
     */
    public static function delete_plan($id) {
        global $DB;
        static::require_enabled();

        $plan = new plan($id);

        if (!$plan->can_manage()) {
            $context = context_user::instance($plan->get('userid'));
            throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
        }

        // Wrap the suppression in a DB transaction.
        $transaction = $DB->start_delegated_transaction();

        // Delete plan competencies.
        $plancomps = plan_competency::get_records(array('planid' => $plan->get('id')));
        foreach ($plancomps as $plancomp) {
            $plancomp->delete();
        }

        // Delete archive user competencies if the status of the plan is complete.
        if ($plan->get('status') == plan::STATUS_COMPLETE) {
            self::remove_archived_user_competencies_in_plan($plan);
        }
        $event = \core\event\competency_plan_deleted::create_from_plan($plan);
        $success = $plan->delete();

        $transaction->allow_commit();

        // Trigger deleted event.
        $event->trigger();

        return $success;
    }

    /**
     * Cancel the review of a plan.
     *
     * @param int|plan $planorid The plan, or its ID.
     * @return bool
     */
    public static function plan_cancel_review_request($planorid) {
        static::require_enabled();
        $plan = $planorid;
        if (!is_object($plan)) {
            $plan = new plan($plan);
        }

        // We need to be able to view the plan at least.
        if (!$plan->can_read()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
        }

        if ($plan->is_based_on_template()) {
            throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
        } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) {
            throw new coding_exception('The plan review cannot be cancelled at this stage.');
        } else if (!$plan->can_request_review()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
        }

        $plan->set('status', plan::STATUS_DRAFT);
        $result = $plan->update();

        // Trigger review request cancelled event.
        \core\event\competency_plan_review_request_cancelled::create_from_plan($plan)->trigger();

        return $result;
    }

    /**
     * Request the review of a plan.
     *
     * @param int|plan $planorid The plan, or its ID.
     * @return bool
     */
    public static function plan_request_review($planorid) {
        static::require_enabled();
        $plan = $planorid;
        if (!is_object($plan)) {
            $plan = new plan($plan);
        }

        // We need to be able to view the plan at least.
        if (!$plan->can_read()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
        }

        if ($plan->is_based_on_template()) {
            throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
        } else if ($plan->get('status') != plan::STATUS_DRAFT) {
            throw new coding_exception('The plan cannot be sent for review at this stage.');
        } else if (!$plan->can_request_review()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
        }

        $plan->set('status', plan::STATUS_WAITING_FOR_REVIEW);
        $result = $plan->update();

        // Trigger review requested event.
        \core\event\competency_plan_review_requested::create_from_plan($plan)->trigger();

        return $result;
    }

    /**
     * Start the review of a plan.
     *
     * @param int|plan $planorid The plan, or its ID.
     * @return bool
     */
    public static function plan_start_review($planorid) {
        global $USER;
        static::require_enabled();
        $plan = $planorid;
        if (!is_object($plan)) {
            $plan = new plan($plan);
        }

        // We need to be able to view the plan at least.
        if (!$plan->can_read()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
        }

        if ($plan->is_based_on_template()) {
            throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
        } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) {
            throw new coding_exception('The plan review cannot be started at this stage.');
        } else if (!$plan->can_review()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
        }

        $plan->set('status', plan::STATUS_IN_REVIEW);
        $plan->set('reviewerid', $USER->id);
        $result = $plan->update();

        // Trigger review started event.
        \core\event\competency_plan_review_started::create_from_plan($plan)->trigger();

        return $result;
    }

    /**
     * Stop reviewing a plan.
     *
     * @param  int|plan $planorid The plan, or its ID.
     * @return bool
     */
    public static function plan_stop_review($planorid) {
        static::require_enabled();
        $plan = $planorid;
        if (!is_object($plan)) {
            $plan = new plan($plan);
        }

        // We need to be able to view the plan at least.
        if (!$plan->can_read()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
        }

        if ($plan->is_based_on_template()) {
            throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
        } else if ($plan->get('status') != plan::STATUS_IN_REVIEW) {
            throw new coding_exception('The plan review cannot be stopped at this stage.');
        } else if (!$plan->can_review()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
        }

        $plan->set('status', plan::STATUS_DRAFT);
        $plan->set('reviewerid', null);
        $result = $plan->update();

        // Trigger review stopped event.
        \core\event\competency_plan_review_stopped::create_from_plan($plan)->trigger();

        return $result;
    }

    /**
     * Approve a plan.
     *
     * This means making the plan active.
     *
     * @param  int|plan $planorid The plan, or its ID.
     * @return bool
     */
    public static function approve_plan($planorid) {
        static::require_enabled();
        $plan = $planorid;
        if (!is_object($plan)) {
            $plan = new plan($plan);
        }

        // We need to be able to view the plan at least.
        if (!$plan->can_read()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
        }

        // We can approve a plan that is either a draft, in review, or waiting for review.
        if ($plan->is_based_on_template()) {
            throw new coding_exception('Template plans are already approved.');   // This should never happen.
        } else if (!$plan->is_draft()) {
            throw new coding_exception('The plan cannot be approved at this stage.');
        } else if (!$plan->can_review()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
        }

        $plan->set('status', plan::STATUS_ACTIVE);
        $plan->set('reviewerid', null);
        $result = $plan->update();

        // Trigger approved event.
        \core\event\competency_plan_approved::create_from_plan($plan)->trigger();

        return $result;
    }

    /**
     * Unapprove a plan.
     *
     * This means making the plan draft.
     *
     * @param  int|plan $planorid The plan, or its ID.
     * @return bool
     */
    public static function unapprove_plan($planorid) {
        static::require_enabled();
        $plan = $planorid;
        if (!is_object($plan)) {
            $plan = new plan($plan);
        }

        // We need to be able to view the plan at least.
        if (!$plan->can_read()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
        }

        if ($plan->is_based_on_template()) {
            throw new coding_exception('Template plans are always approved.');   // This should never happen.
        } else if ($plan->get('status') != plan::STATUS_ACTIVE) {
            throw new coding_exception('The plan cannot be sent back to draft at this stage.');
        } else if (!$plan->can_review()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
        }

        $plan->set('status', plan::STATUS_DRAFT);
        $result = $plan->update();

        // Trigger unapproved event.
        \core\event\competency_plan_unapproved::create_from_plan($plan)->trigger();

        return $result;
    }

    /**
     * Complete a plan.
     *
     * @param int|plan $planorid The plan, or its ID.
     * @return bool
     */
    public static function complete_plan($planorid) {
        global $DB;
        static::require_enabled();

        $plan = $planorid;
        if (!is_object($planorid)) {
            $plan = new plan($planorid);
        }

        // Validate that the plan can be managed.
        if (!$plan->can_manage()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
        }

        // Check if the plan was already completed.
        if ($plan->get('status') == plan::STATUS_COMPLETE) {
            throw new coding_exception('The plan is already completed.');
        }

        $originalstatus = $plan->get('status');
        $plan->set('status', plan::STATUS_COMPLETE);

        // The user should also be able to manage the plan when it's completed.
        if (!$plan->can_manage()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
        }

        // Put back original status because archive needs it to extract competencies from the right table.
        $plan->set('status', $originalstatus);

        // Do the things.
        $transaction = $DB->start_delegated_transaction();
        self::archive_user_competencies_in_plan($plan);
        $plan->set('status', plan::STATUS_COMPLETE);
        $success = $plan->update();

        if (!$success) {
            $transaction->rollback(new moodle_exception('The plan could not be updated.'));
            return $success;
        }

        $transaction->allow_commit();

        // Trigger updated event.
        \core\event\competency_plan_completed::create_from_plan($plan)->trigger();

        return $success;
    }

    /**
     * Reopen a plan.
     *
     * @param int|plan $planorid The plan, or its ID.
     * @return bool
     */
    public static function reopen_plan($planorid) {
        global $DB;
        static::require_enabled();

        $plan = $planorid;
        if (!is_object($planorid)) {
            $plan = new plan($planorid);
        }

        // Validate that the plan as it is can be managed.
        if (!$plan->can_manage()) {
            $context = context_user::instance($plan->get('userid'));
            throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
        }

        $beforestatus = $plan->get('status');
        $plan->set('status', plan::STATUS_ACTIVE);

        // Validate if status can be changed.
        if (!$plan->can_manage()) {
            $context = context_user::instance($plan->get('userid'));
            throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
        }

        // Wrap the updates in a DB transaction.
        $transaction = $DB->start_delegated_transaction();

        // Delete archived user competencies if the status of the plan is changed from complete to another status.
        $mustremovearchivedcompetencies = ($beforestatus == plan::STATUS_COMPLETE && $plan->get('status') != plan::STATUS_COMPLETE);
        if ($mustremovearchivedcompetencies) {
            self::remove_archived_user_competencies_in_plan($plan);
        }

        // If duedate less than or equal to duedate_threshold unset it.
        if ($plan->get('duedate') <= time() + plan::DUEDATE_THRESHOLD) {
            $plan->set('duedate', 0);
        }

        $success = $plan->update();

        if (!$success) {
            $transaction->rollback(new moodle_exception('The plan could not be updated.'));
            return $success;
        }

        $transaction->allow_commit();

        // Trigger reopened event.
        \core\event\competency_plan_reopened::create_from_plan($plan)->trigger();

        return $success;
    }

    /**
     * Get a single competency from the user plan.
     *
     * @param  int $planorid The plan, or its ID.
     * @param  int $competencyid The competency id.
     * @return (object) array(
     *                      'competency' => \core_competency\competency,
     *                      'usercompetency' => \core_competency\user_competency
     *                      'usercompetencyplan' => \core_competency\user_competency_plan
     *                  )
     *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
     */
    public static function get_plan_competency($planorid, $competencyid) {
        static::require_enabled();
        $plan = $planorid;
        if (!is_object($planorid)) {
            $plan = new plan($planorid);
        }

        if (!user_competency::can_read_user($plan->get('userid'))) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:usercompetencyview',
                'nopermissions', '');
        }

        $competency = $plan->get_competency($competencyid);

        // Get user competencies from user_competency_plan if the plan status is set to complete.
        $iscompletedplan = $plan->get('status') == plan::STATUS_COMPLETE;
        if ($iscompletedplan) {
            $usercompetencies = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), array($competencyid));
            $ucresultkey = 'usercompetencyplan';
        } else {
            $usercompetencies = user_competency::get_multiple($plan->get('userid'), array($competencyid));
            $ucresultkey = 'usercompetency';
        }

        $found = count($usercompetencies);

        if ($found) {
            $uc = array_pop($usercompetencies);
        } else {
            if ($iscompletedplan) {
                throw new coding_exception('A user competency plan is missing');
            } else {
                $uc = user_competency::create_relation($plan->get('userid'), $competency->get('id'));
                $uc->create();
            }
        }

        $plancompetency = (object) array(
            'competency' => $competency,
            'usercompetency' => null,
            'usercompetencyplan' => null
        );
        $plancompetency->$ucresultkey = $uc;

        return $plancompetency;
    }

    /**
     * List the plans with a competency.
     *
     * @param  int $userid The user id we want the plans for.
     * @param  int $competencyorid The competency, or its ID.
     * @return array[plan] Array of learning plans.
     */
    public static function list_plans_with_competency($userid, $competencyorid) {
        global $USER;

        static::require_enabled();
        $competencyid = $competencyorid;
        $competency = null;
        if (is_object($competencyid)) {
            $competency = $competencyid;
            $competencyid = $competency->get('id');
        }

        $plans = plan::get_by_user_and_competency($userid, $competencyid);
        foreach ($plans as $index => $plan) {
            // Filter plans we cannot read.
            if (!$plan->can_read()) {
                unset($plans[$index]);
            }
        }
        return $plans;
    }

    /**
     * List the competencies in a user plan.
     *
     * @param  int $planorid The plan, or its ID.
     * @return array((object) array(
     *                            'competency' => \core_competency\competency,
     *                            'usercompetency' => \core_competency\user_competency
     *                            'usercompetencyplan' => \core_competency\user_competency_plan
     *                        ))
     *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
     */
    public static function list_plan_competencies($planorid) {
        static::require_enabled();
        $plan = $planorid;
        if (!is_object($planorid)) {
            $plan = new plan($planorid);
        }

        if (!$plan->can_read()) {
            $context = context_user::instance($plan->get('userid'));
            throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
        }

        $result = array();
        $competencies = $plan->get_competencies();

        // Get user competencies from user_competency_plan if the plan status is set to complete.
        $iscompletedplan = $plan->get('status') == plan::STATUS_COMPLETE;
        if ($iscompletedplan) {
            $usercompetencies = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), $competencies);
            $ucresultkey = 'usercompetencyplan';
        } else {
            $usercompetencies = user_competency::get_multiple($plan->get('userid'), $competencies);
            $ucresultkey = 'usercompetency';
        }

        // Build the return values.
        foreach ($competencies as $key => $competency) {
            $found = false;

            foreach ($usercompetencies as $uckey => $uc) {
                if ($uc->get('competencyid') == $competency->get('id')) {
                    $found = true;
                    unset($usercompetencies[$uckey]);
                    break;
                }
            }

            if (!$found) {
                if ($iscompletedplan) {
                    throw new coding_exception('A user competency plan is missing');
                } else {
                    $uc = user_competency::create_relation($plan->get('userid'), $competency->get('id'));
                }
            }

            $plancompetency = (object) array(
                'competency' => $competency,
                'usercompetency' => null,
                'usercompetencyplan' => null
            );
            $plancompetency->$ucresultkey = $uc;
            $result[] = $plancompetency;
        }

        return $result;
    }

    /**
     * Add a competency to a plan.
     *
     * @param int $planid The id of the plan
     * @param int $competencyid The id of the competency
     * @return bool
     */
    public static function add_competency_to_plan($planid, $competencyid) {
        static::require_enabled();
        $plan = new plan($planid);

        // First we do a permissions check.
        if (!$plan->can_manage()) {
            throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');

        } else if ($plan->is_based_on_template()) {
            throw new coding_exception('A competency can not be added to a learning plan based on a template');
        }

        if (!$plan->can_be_edited()) {
            throw new coding_exception('A competency can not be added to a completed learning plan');
        }

        $competency = new competency($competencyid);

        // Can not add a competency that belong to a hidden framework.
        if ($competency->get_framework()->get('visible') == false) {
            throw new coding_exception('A competency belonging to hidden framework can not be added');
        }

        $exists = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
        if (!$exists) {
            $record = new stdClass();
            $record->planid = $planid;
            $record->competencyid = $competencyid;
            $plancompetency = new plan_competency(0, $record);
            $plancompetency->create();
        }

        return true;
    }

    /**
     * Remove a competency from a plan.
     *
     * @param int $planid The plan id
     * @param int $competencyid The id of the competency
     * @return bool
     */
    public static function remove_competency_from_plan($planid, $competencyid) {
        static::require_enabled();
        $plan = new plan($planid);

        // First we do a permissions check.
        if (!$plan->can_manage()) {
            $context = context_user::instance($plan->get('userid'));
            throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');

        } else if ($plan->is_based_on_template()) {
            throw new coding_exception('A competency can not be removed from a learning plan based on a template');
        }

        if (!$plan->can_be_edited()) {
            throw new coding_exception('A competency can not be removed from a completed learning plan');
        }

        $link = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
        if ($link) {
            return $link->delete();
        }
        return false;
    }

    /**
     * Move the plan competency up or down in the display list.
     *
     * Requires moodle/competency:planmanage capability at the system context.
     *
     * @param int $planid The plan  id
     * @param int $competencyidfrom The id of the competency we are moving.
     * @param int $competencyidto The id of the competency we are moving to.
     * @return boolean
     */
    public static function reorder_plan_competency($planid, $competencyidfrom, $competencyidto) {
        static::require_enabled();
        $plan = new plan($planid);

        // First we do a permissions check.
        if (!$plan->can_manage()) {
            $context = context_user::instance($plan->get('userid'));
            throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');

        } else if ($plan->is_based_on_template()) {
            throw new coding_exception('A competency can not be reordered in a learning plan based on a template');
        }

        if (!$plan->can_be_edited()) {
            throw new coding_exception('A competency can not be reordered in a completed learning plan');
        }

        $down = true;
        $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidfrom));
        if (count($matches) == 0) {
            throw new coding_exception('The link does not exist');
        }

        $competencyfrom = array_pop($matches);
        $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidto));
        if (count($matches) == 0) {
            throw new coding_exception('The link does not exist');
        }

        $competencyto = array_pop($matches);

        $all = plan_competency::get_records(array('planid' => $planid), 'sortorder', 'ASC', 0, 0);

        if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
            // We are moving up, so put it before the "to" item.
            $down = false;
        }

        foreach ($all as $id => $plancompetency) {
            $sort = $plancompetency->get('sortorder');
            if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
                $plancompetency->set('sortorder', $plancompetency->get('sortorder') - 1);
                $plancompetency->update();
            } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
                $plancompetency->set('sortorder', $plancompetency->get('sortorder') + 1);
                $plancompetency->update();
            }
        }
        $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
        return $competencyfrom->update();
    }

    /**
     * Cancel a user competency review request.
     *
     * @param  int $userid       The user ID.
     * @param  int $competencyid The competency ID.
     * @return bool
     */
    public static function user_competency_cancel_review_request($userid, $competencyid) {
        static::require_enabled();
        $context = context_user::instance($userid);
        $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
        if (!$uc || !$uc->can_read()) {
            throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
        } else if ($uc->get('status') != user_competency::STATUS_WAITING_FOR_REVIEW) {
            throw new coding_exception('The competency review request can not be cancelled at this stage.');
        } else if (!$uc->can_request_review()) {
            throw new required_capability_exception($context, 'moodle/competency:usercompetencyrequestreview', 'nopermissions', '');
        }

        $uc->set('status', user_competency::STATUS_IDLE);
        $result = $uc->update();
        if ($result) {
            \core\event\competency_user_competency_review_request_cancelled::create_from_user_competency($uc)->trigger();
        }
        return $result;
    }

    /**
     * Request a user competency review.
     *
     * @param  int $userid       The user ID.
     * @param  int $competencyid The competency ID.
     * @return bool
     */
    public static function user_competency_request_review($userid, $competencyid) {
        static::require_enabled();
        $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
        if (!$uc) {
            $uc = user_competency::create_relation($userid, $competencyid);
            $uc->create();
        }

        if (!$uc->can_read()) {
            throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
                'nopermissions', '');
        } else if ($uc->get('status') != user_competency::STATUS_IDLE) {
            throw new coding_exception('The competency can not be sent for review at this stage.');
        } else if (!$uc->can_request_review()) {
            throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyrequestreview',
                'nopermissions', '');
        }

        $uc->set('status', user_competency::STATUS_WAITING_FOR_REVIEW);
        $result = $uc->update();
        if ($result) {
            \core\event\competency_user_competency_review_requested::create_from_user_competency($uc)->trigger();
        }
        return $result;
    }

    /**
     * Start a user competency review.
     *
     * @param  int $userid       The user ID.
     * @param  int $competencyid The competency ID.
     * @return bool
     */
    public static function user_competency_start_review($userid, $competencyid) {
        global $USER;
        static::require_enabled();

        $context = context_user::instance($userid);
        $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
        if (!$uc || !$uc->can_read()) {
            throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
        } else if ($uc->get('status') != user_competency::STATUS_WAITING_FOR_REVIEW) {
            throw new coding_exception('The competency review can not be started at this stage.');
        } else if (!$uc->can_review()) {
            throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', '');
        }

        $uc->set('status', user_competency::STATUS_IN_REVIEW);
        $uc->set('reviewerid', $USER->id);
        $result = $uc->update();
        if ($result) {
            \core\event\competency_user_competency_review_started::create_from_user_competency($uc)->trigger();
        }
        return $result;
    }

    /**
     * Stop a user competency review.
     *
     * @param  int $userid       The user ID.
     * @param  int $competencyid The competency ID.
     * @return bool
     */
    public static function user_competency_stop_review($userid, $competencyid) {
        static::require_enabled();
        $context = context_user::instance($userid);
        $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
        if (!$uc || !$uc->can_read()) {
            throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
        } else if ($uc->get('status') != user_competency::STATUS_IN_REVIEW) {
            throw new coding_exception('The competency review can not be stopped at this stage.');
        } else if (!$uc->can_review()) {
            throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', '');
        }

        $uc->set('status', user_competency::STATUS_IDLE);
        $result = $uc->update();
        if ($result) {
            \core\event\competency_user_competency_review_stopped::create_from_user_competency($uc)->trigger();
        }
        return $result;
    }

    /**
     * Log user competency viewed event.
     *
     * @param user_competency|int $usercompetencyorid The user competency object or user competency id
     * @return bool
     */
    public static function user_competency_viewed($usercompetencyorid) {
        static::require_enabled();
        $uc = $usercompetencyorid;
        if (!is_object($uc)) {
            $uc = new user_competency($uc);
        }

        if (!$uc || !$uc->can_read()) {
            throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
                'nopermissions', '');
        }

        \core\event\competency_user_competency_viewed::create_from_user_competency_viewed($uc)->trigger();
        return true;
    }

    /**
     * Log user competency viewed in plan event.
     *
     * @param user_competency|int $usercompetencyorid The user competency object or user competency id
     * @param int $planid The plan ID
     * @return bool
     */
    public static function user_competency_viewed_in_plan($usercompetencyorid, $planid) {
        static::require_enabled();
        $uc = $usercompetencyorid;
        if (!is_object($uc)) {
            $uc = new user_competency($uc);
        }

        if (!$uc || !$uc->can_read()) {
            throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
                'nopermissions', '');
        }
        $plan = new plan($planid);
        if ($plan->get('status') == plan::STATUS_COMPLETE) {
            throw new coding_exception('To log the user competency in a completed plan use the user_competency_plan_viewed method.');
        }

        \core\event\competency_user_competency_viewed_in_plan::create_from_user_competency_viewed_in_plan($uc, $planid)->trigger();
        return true;
    }

    /**
     * Log user competency viewed in course event.
     *
     * @param user_competency_course|int $usercoursecompetencyorid The user competency course object or its ID.
     * @param int $courseid The course ID
     * @return bool
     */
    public static function user_competency_viewed_in_course($usercoursecompetencyorid) {
        static::require_enabled();
        $ucc = $usercoursecompetencyorid;
        if (!is_object($ucc)) {
            $ucc = new user_competency_course($ucc);
        }

        if (!$ucc || !user_competency::can_read_user_in_course($ucc->get('userid'), $ucc->get('courseid'))) {
            throw new required_capability_exception($ucc->get_context(), 'moodle/competency:usercompetencyview',
                'nopermissions', '');
        }

        // Validate the course, this will throw an exception if not valid.
        self::validate_course($ucc->get('courseid'));

        \core\event\competency_user_competency_viewed_in_course::create_from_user_competency_viewed_in_course($ucc)->trigger();
        return true;
    }

    /**
     * Log user competency plan viewed event.
     *
     * @param user_competency_plan|int $usercompetencyplanorid The user competency plan object or user competency plan id
     * @return bool
     */
    public static function user_competency_plan_viewed($usercompetencyplanorid) {
        static::require_enabled();
        $ucp = $usercompetencyplanorid;
        if (!is_object($ucp)) {
            $ucp = new user_competency_plan($ucp);
        }

        if (!$ucp || !user_competency::can_read_user($ucp->get('userid'))) {
            throw new required_capability_exception($ucp->get_context(), 'moodle/competency:usercompetencyview',
                'nopermissions', '');
        }
        $plan = new plan($ucp->get('planid'));
        if ($plan->get('status') != plan::STATUS_COMPLETE) {
            throw new coding_exception('To log the user competency in non-completed plan use '
                . 'user_competency_viewed_in_plan method.');
        }

        \core\event\competency_user_competency_plan_viewed::create_from_user_competency_plan($ucp)->trigger();
        return true;
    }

    /**
     * Check if template has related data.
     *
     * @param int $templateid The id of the template to check.
     * @return boolean
     */
    public static function template_has_related_data($templateid) {
        static::require_enabled();
        // First we do a permissions check.
        $template = new template($templateid);

        if (!$template->can_read()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
                'nopermissions', '');
        }

        // OK - all set.
        return $template->has_plans();
    }

    /**
     * List all the related competencies.
     *
     * @param int $competencyid The id of the competency to check.
     * @return competency[]
     */
    public static function list_related_competencies($competencyid) {
        static::require_enabled();
        $competency = new competency($competencyid);

        if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
                $competency->get_context())) {
            throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview',
                'nopermissions', '');
        }

        return $competency->get_related_competencies();
    }

    /**
     * Add a related competency.
     *
     * @param int $competencyid The id of the competency
     * @param int $relatedcompetencyid The id of the related competency.
     * @return bool False when create failed, true on success, or if the relation already existed.
     */
    public static function add_related_competency($competencyid, $relatedcompetencyid) {
        static::require_enabled();
        $competency1 = new competency($competencyid);
        $competency2 = new competency($relatedcompetencyid);

        require_capability('moodle/competency:competencymanage', $competency1->get_context());

        $relatedcompetency = related_competency::get_relation($competency1->get('id'), $competency2->get('id'));
        if (!$relatedcompetency->get('id')) {
            $relatedcompetency->create();
            return true;
        }

        return true;
    }

    /**
     * Remove a related competency.
     *
     * @param int $competencyid The id of the competency.
     * @param int $relatedcompetencyid The id of the related competency.
     * @return bool True when it was deleted, false when it wasn't or the relation doesn't exist.
     */
    public static function remove_related_competency($competencyid, $relatedcompetencyid) {
        static::require_enabled();
        $competency = new competency($competencyid);

        // This only checks if we have the permission in either competency because both competencies
        // should belong to the same framework.
        require_capability('moodle/competency:competencymanage', $competency->get_context());

        $relatedcompetency = related_competency::get_relation($competencyid, $relatedcompetencyid);
        if ($relatedcompetency->get('id')) {
            return $relatedcompetency->delete();
        }

        return false;
    }

    /**
     * Read a user evidence.
     *
     * @param int $id
     * @return user_evidence
     */
    public static function read_user_evidence($id) {
        static::require_enabled();
        $userevidence = new user_evidence($id);

        if (!$userevidence->can_read()) {
            $context = $userevidence->get_context();
            throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', '');
        }

        return $userevidence;
    }

    /**
     * Create a new user evidence.
     *
     * @param  object $data        The data.
     * @param  int    $draftitemid The draft ID in which files have been saved.
     * @return user_evidence
     */
    public static function create_user_evidence($data, $draftitemid = null) {
        static::require_enabled();
        $userevidence = new user_evidence(null, $data);
        $context = $userevidence->get_context();

        if (!$userevidence->can_manage()) {
            throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
        }

        $userevidence->create();
        if (!empty($draftitemid)) {
            $fileareaoptions = array('subdirs' => true);
            $itemid = $userevidence->get('id');
            file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions);
        }

        // Trigger an evidence of prior learning created event.
        \core\event\competency_user_evidence_created::create_from_user_evidence($userevidence)->trigger();

        return $userevidence;
    }

    /**
     * Create a new user evidence.
     *
     * @param  object $data        The data.
     * @param  int    $draftitemid The draft ID in which files have been saved.
     * @return user_evidence
     */
    public static function update_user_evidence($data, $draftitemid = null) {
        static::require_enabled();
        $userevidence = new user_evidence($data->id);
        $context = $userevidence->get_context();

        if (!$userevidence->can_manage()) {
            throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');

        } else if (property_exists($data, 'userid') && $data->userid != $userevidence->get('userid')) {
            throw new coding_exception('Can not change the userid of a user evidence.');
        }

        $userevidence->from_record($data);
        $userevidence->update();

        if (!empty($draftitemid)) {
            $fileareaoptions = array('subdirs' => true);
            $itemid = $userevidence->get('id');
            file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions);
        }

        // Trigger an evidence of prior learning updated event.
        \core\event\competency_user_evidence_updated::create_from_user_evidence($userevidence)->trigger();

        return $userevidence;
    }

    /**
     * Delete a user evidence.
     *
     * @param  int $id The user evidence ID.
     * @return bool
     */
    public static function delete_user_evidence($id) {
        static::require_enabled();
        $userevidence = new user_evidence($id);
        $context = $userevidence->get_context();

        if (!$userevidence->can_manage()) {
            throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
        }

        // Delete the user evidence.
        $userevidence->delete();

        // Delete associated files.
        $fs = get_file_storage();
        $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $id);

        // Delete relation between evidence and competencies.
        $userevidence->set('id', $id);     // Restore the ID to fully mock the object.
        $competencies = user_evidence_competency::get_competencies_by_userevidenceid($id);
        foreach ($competencies as $competency) {
            static::delete_user_evidence_competency($userevidence, $competency->get('id'));
        }

        // Trigger an evidence of prior learning deleted event.
        \core\event\competency_user_evidence_deleted::create_from_user_evidence($userevidence)->trigger();

        $userevidence->set('id', 0);       // Restore the object.

        return true;
    }

    /**
     * List the user evidence of a user.
     *
     * @param  int $userid The user ID.
     * @return user_evidence[]
     */
    public static function list_user_evidence($userid) {
        static::require_enabled();
        if (!user_evidence::can_read_user($userid)) {
            $context = context_user::instance($userid);
            throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', '');
        }

        $evidence = user_evidence::get_records(array('userid' => $userid), 'name');
        return $evidence;
    }

    /**
     * Link a user evidence with a competency.
     *
     * @param  user_evidence|int $userevidenceorid User evidence or its ID.
     * @param  int $competencyid Competency ID.
     * @return user_evidence_competency
     */
    public static function create_user_evidence_competency($userevidenceorid, $competencyid) {
        global $USER;
        static::require_enabled();

        $userevidence = $userevidenceorid;
        if (!is_object($userevidence)) {
            $userevidence = self::read_user_evidence($userevidence);
        }

        // Perform user evidence capability checks.
        if (!$userevidence->can_manage()) {
            $context = $userevidence->get_context();
            throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
        }

        // Perform competency capability checks.
        $competency = self::read_competency($competencyid);

        // Get (and create) the relation.
        $relation = user_evidence_competency::get_relation($userevidence->get('id'), $competency->get('id'));
        if (!$relation->get('id')) {
            $relation->create();

            $link = url::user_evidence($userevidence->get('id'));
            self::add_evidence(
                $userevidence->get('userid'),
                $competency,
                $userevidence->get_context(),
                evidence::ACTION_LOG,
                'evidence_evidenceofpriorlearninglinked',
                'core_competency',
                $userevidence->get('name'),
                false,
                $link->out(false),
                null,
                $USER->id
            );
        }

        return $relation;
    }

    /**
     * Delete a relationship between a user evidence and a competency.
     *
     * @param  user_evidence|int $userevidenceorid User evidence or its ID.
     * @param  int $competencyid Competency ID.
     * @return bool
     */
    public static function delete_user_evidence_competency($userevidenceorid, $competencyid) {
        global $USER;
        static::require_enabled();

        $userevidence = $userevidenceorid;
        if (!is_object($userevidence)) {
            $userevidence = self::read_user_evidence($userevidence);
        }

        // Perform user evidence capability checks.
        if (!$userevidence->can_manage()) {
            $context = $userevidence->get_context();
            throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
        }

        // Get (and delete) the relation.
        $relation = user_evidence_competency::get_relation($userevidence->get('id'), $competencyid);
        if (!$relation->get('id')) {
            return true;
        }

        $success = $relation->delete();
        if ($success) {
            self::add_evidence(
                $userevidence->get('userid'),
                $competencyid,
                $userevidence->get_context(),
                evidence::ACTION_LOG,
                'evidence_evidenceofpriorlearningunlinked',
                'core_competency',
                $userevidence->get('name'),
                false,
                null,
                null,
                $USER->id
            );
        }

        return $success;
    }

    /**
     * Send request review for user evidence competencies.
     *
     * @param  int $id The user evidence ID.
     * @return bool
     */
    public static function request_review_of_user_evidence_linked_competencies($id) {
        $userevidence = new user_evidence($id);
        $context = $userevidence->get_context();
        $userid = $userevidence->get('userid');

        if (!$userevidence->can_manage()) {
            throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
        }

        $usercompetencies = user_evidence_competency::get_user_competencies_by_userevidenceid($id);
        foreach ($usercompetencies as $usercompetency) {
            if ($usercompetency->get('status') == user_competency::STATUS_IDLE) {
                static::user_competency_request_review($userid, $usercompetency->get('competencyid'));
            }
        }

        return true;
    }

    /**
     * Recursively duplicate competencies from a tree, we start duplicating from parents to children to have a correct path.
     * This method does not copy the related competencies.
     *
     * @param int $frameworkid - framework id
     * @param stdClass[] $tree - list of framework competency nodes
     * @param int $oldparent - old parent id
     * @param int $newparent - new parent id
     * @return competency[] $matchids - List of old competencies ids matched with new competencies object.
     */
    protected static function duplicate_competency_tree($frameworkid, $tree, $oldparent = 0, $newparent = 0) {
        $matchids = array();
        foreach ($tree as $node) {
            if ($node->competency->get('parentid') == $oldparent) {
                $parentid = $node->competency->get('id');

                // Create the competency.
                $competency = new competency(0, $node->competency->to_record());
                $competency->set('competencyframeworkid', $frameworkid);
                $competency->set('parentid', $newparent);
                $competency->set('path', '');
                $competency->set('id', 0);
                $competency->reset_rule();
                $competency->create();

                // Trigger the created event competency.
                \core\event\competency_created::create_from_competency($competency)->trigger();

                // Match the old id with the new one.
                $matchids[$parentid] = $competency;

                if (!empty($node->children)) {
                    // Duplicate children competency.
                    $childrenids = self::duplicate_competency_tree($frameworkid, $node->children, $parentid, $competency->get('id'));
                    // Array_merge does not keep keys when merging so we use the + operator.
                    $matchids = $matchids + $childrenids;
                }
            }
        }
        return $matchids;
    }

    /**
     * Recursively migrate competency rules.
     *
     * @param array $tree - array of competencies object
     * @param competency[] $matchids - List of old competencies ids matched with new competencies object
     */
    protected static function migrate_competency_tree_rules($tree, $matchids) {

        foreach ($tree as $node) {
            $oldcompid = $node->competency->get('id');
            if ($node->competency->get('ruletype') && array_key_exists($oldcompid, $matchids)) {
                try {
                    // Get the new competency.
                    $competency = $matchids[$oldcompid];
                    $class = $node->competency->get('ruletype');
                    $newruleconfig = $class::migrate_config($node->competency->get('ruleconfig'), $matchids);
                    $competency->set('ruleconfig', $newruleconfig);
                    $competency->set('ruletype', $class);
                    $competency->set('ruleoutcome', $node->competency->get('ruleoutcome'));
                    $competency->update();
                } catch (\Exception $e) {
                    debugging('Could not migrate competency rule from: ' . $oldcompid . ' to: ' . $competency->get('id') . '.' .
                        ' Exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
                    $competency->reset_rule();
                }
            }

            if (!empty($node->children)) {
                self::migrate_competency_tree_rules($node->children, $matchids);
            }
        }
    }

    /**
     * Archive user competencies in a plan.
     *
     * @param plan $plan The plan object.
     * @return void
     */
    protected static function archive_user_competencies_in_plan($plan) {

        // Check if the plan was already completed.
        if ($plan->get('status') == plan::STATUS_COMPLETE) {
            throw new coding_exception('The plan is already completed.');
        }

        $competencies = $plan->get_competencies();
        $usercompetencies = user_competency::get_multiple($plan->get('userid'), $competencies);

        $i = 0;
        foreach ($competencies as $competency) {
            $found = false;

            foreach ($usercompetencies as $uckey => $uc) {
                if ($uc->get('competencyid') == $competency->get('id')) {
                    $found = true;

                    $ucprecord = $uc->to_record();
                    $ucprecord->planid = $plan->get('id');
                    $ucprecord->sortorder = $i;
                    unset($ucprecord->id);
                    unset($ucprecord->status);
                    unset($ucprecord->reviewerid);

                    $usercompetencyplan = new user_competency_plan(0, $ucprecord);
                    $usercompetencyplan->create();

                    unset($usercompetencies[$uckey]);
                    break;
                }
            }

            // If the user competency doesn't exist, we create a new relation in user_competency_plan.
            if (!$found) {
                $usercompetencyplan = user_competency_plan::create_relation($plan->get('userid'), $competency->get('id'),
                        $plan->get('id'));
                $usercompetencyplan->set('sortorder', $i);
                $usercompetencyplan->create();
            }
            $i++;
        }
    }

    /**
     * Delete archived user competencies in a plan.
     *
     * @param plan $plan The plan object.
     * @return void
     */
    protected static function remove_archived_user_competencies_in_plan($plan) {
        $competencies = $plan->get_competencies();
        $usercompetenciesplan = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), $competencies);

        foreach ($usercompetenciesplan as $ucpkey => $ucp) {
            $ucp->delete();
        }
    }

    /**
     * List all the evidence for a user competency.
     *
     * @param int $userid The user id - only used if usercompetencyid is 0.
     * @param int $competencyid The competency id - only used it usercompetencyid is 0.
     * @param int $planid The plan id - not used yet - but can be used to only list archived evidence if a plan is completed.
     * @param string $sort The field to sort the evidence by.
     * @param string $order The ordering of the sorting.
     * @param int $skip Number of records to skip.
     * @param int $limit Number of records to return.
     * @return \core_competency\evidence[]
     * @return array of \core_competency\evidence
     */
    public static function list_evidence($userid = 0, $competencyid = 0, $planid = 0, $sort = 'timecreated',
                                         $order = 'DESC', $skip = 0, $limit = 0) {
        static::require_enabled();

        if (!user_competency::can_read_user($userid)) {
            $context = context_user::instance($userid);
            throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
        }

        $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
        if (!$usercompetency) {
            return array();
        }

        $plancompleted = false;
        if ($planid != 0) {
            $plan = new plan($planid);
            if ($plan->get('status') == plan::STATUS_COMPLETE) {
                $plancompleted = true;
            }
        }

        $select = 'usercompetencyid = :usercompetencyid';
        $params = array('usercompetencyid' => $usercompetency->get('id'));
        if ($plancompleted) {
            $select .= ' AND timecreated <= :timecompleted';
            $params['timecompleted'] = $plan->get('timemodified');
        }

        $orderby = $sort . ' ' . $order;
        $orderby .= !empty($orderby) ? ', id DESC' : 'id DESC'; // Prevent random ordering.

        $evidence = evidence::get_records_select($select, $params, $orderby, '*', $skip, $limit);
        return $evidence;
    }

    /**
     * List all the evidence for a user competency in a course.
     *
     * @param int $userid The user ID.
     * @param int $courseid The course ID.
     * @param int $competencyid The competency ID.
     * @param string $sort The field to sort the evidence by.
     * @param string $order The ordering of the sorting.
     * @param int $skip Number of records to skip.
     * @param int $limit Number of records to return.
     * @return \core_competency\evidence[]
     */
    public static function list_evidence_in_course($userid = 0, $courseid = 0, $competencyid = 0, $sort = 'timecreated',
                                                   $order = 'DESC', $skip = 0, $limit = 0) {
        static::require_enabled();

        if (!user_competency::can_read_user_in_course($userid, $courseid)) {
            $context = context_user::instance($userid);
            throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
        }

        $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
        if (!$usercompetency) {
            return array();
        }

        $context = context_course::instance($courseid);
        return evidence::get_records_for_usercompetency($usercompetency->get('id'), $context, $sort, $order, $skip, $limit);
    }

    /**
     * Create an evidence from a list of parameters.
     *
     * Requires no capability because evidence can be added in many situations under any user.
     *
     * @param int $userid The user id for which evidence is added.
     * @param competency|int $competencyorid The competency, or its id for which evidence is added.
     * @param context|int $contextorid The context in which the evidence took place.
     * @param int $action The type of action to take on the competency. \core_competency\evidence::ACTION_*.
     * @param string $descidentifier The strings identifier.
     * @param string $desccomponent The strings component.
     * @param mixed $desca Any arguments the string requires.
     * @param bool $recommend When true, the user competency will be sent for review.
     * @param string $url The url the evidence may link to.
     * @param int $grade The grade, or scale ID item.
     * @param int $actionuserid The ID of the user who took the action of adding the evidence. Null when system.
     *                          This should be used when the action was taken by a real person, this will allow
     *                          to keep track of all the evidence given by a certain person.
     * @param string $note A note to attach to the evidence.
     * @return evidence
     * @throws coding_exception
     * @throws invalid_persistent_exception
     * @throws moodle_exception
     */
    public static function add_evidence($userid, $competencyorid, $contextorid, $action, $descidentifier, $desccomponent,
                                        $desca = null, $recommend = false, $url = null, $grade = null, $actionuserid = null,
                                        $note = null, $overridegrade = false) {
        global $DB;
        static::require_enabled();

        // Some clearly important variable assignments right there.
        $competencyid = $competencyorid;
        $competency = null;
        if (is_object($competencyid)) {
            $competency = $competencyid;
            $competencyid = $competency->get('id');
        }
        $contextid = $contextorid;
        $context = $contextorid;
        if (is_object($contextorid)) {
            $contextid = $contextorid->id;
        } else {
            $context = context::instance_by_id($contextorid);
        }
        $setucgrade = false;
        $ucgrade = null;
        $ucproficiency = null;
        $usercompetencycourse = null;

        // Fetch or create the user competency.
        $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
        if (!$usercompetency) {
            $usercompetency = user_competency::create_relation($userid, $competencyid);
            $usercompetency->create();
        }

        // What should we be doing?
        switch ($action) {

            // Completing a competency.
            case evidence::ACTION_COMPLETE:
                // The logic here goes like this:
                //
                // if rating outside a course
                // - set the default grade and proficiency ONLY if there is no current grade
                // else we are in a course
                // - set the default grade and proficiency in the course ONLY if there is no current grade in the course
                // - then check the course settings to see if we should push the rating outside the course
                // - if we should push it
                // --- push it only if the user_competency (outside the course) has no grade
                // Done.

                if ($grade !== null) {
                    throw new coding_exception("The grade MUST NOT be set with a 'completing' evidence.");
                }

                // Fetch the default grade to attach to the evidence.
                if (empty($competency)) {
                    $competency = new competency($competencyid);
                }
                list($grade, $proficiency) = $competency->get_default_grade();

                // Add user_competency_course record when in a course or module.
                if (in_array($context->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE))) {
                    $coursecontext = $context->get_course_context();
                    $courseid = $coursecontext->instanceid;
                    $filterparams = array(
                        'userid' => $userid,
                        'competencyid' => $competencyid,
                        'courseid' => $courseid
                    );
                    // Fetch or create user competency course.
                    $usercompetencycourse = user_competency_course::get_record($filterparams);
                    if (!$usercompetencycourse) {
                        $usercompetencycourse = user_competency_course::create_relation($userid, $competencyid, $courseid);
                        $usercompetencycourse->create();
                    }
                    // Only update the grade and proficiency if there is not already a grade or the override option is enabled.
                    if ($usercompetencycourse->get('grade') === null || $overridegrade) {
                        // Set grade.
                        $usercompetencycourse->set('grade', $grade);
                        // Set proficiency.
                        $usercompetencycourse->set('proficiency', $proficiency);
                    }

                    // Check the course settings to see if we should push to user plans.
                    $coursesettings = course_competency_settings::get_by_courseid($courseid);
                    $setucgrade = $coursesettings->get('pushratingstouserplans');

                    if ($setucgrade) {
                        // Only push to user plans if there is not already a grade or the override option is enabled.
                        if ($usercompetency->get('grade') !== null && !$overridegrade) {
                            $setucgrade = false;
                        } else {
                            $ucgrade = $grade;
                            $ucproficiency = $proficiency;
                        }
                    }
                } else {

                    // When completing the competency we fetch the default grade from the competency. But we only mark
                    // the user competency when a grade has not been set yet or if override option is enabled.
                    // Complete is an action to use with automated systems.
                    if ($usercompetency->get('grade') === null || $overridegrade) {
                        $setucgrade = true;
                        $ucgrade = $grade;
                        $ucproficiency = $proficiency;
                    }
                }

                break;

            // We override the grade, even overriding back to not set.
            case evidence::ACTION_OVERRIDE:
                $setucgrade = true;
                $ucgrade = $grade;
                if (empty($competency)) {
                    $competency = new competency($competencyid);
                }
                if ($ucgrade !== null) {
                    $ucproficiency = $competency->get_proficiency_of_grade($ucgrade);
                }

                // Add user_competency_course record when in a course or module.
                if (in_array($context->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE))) {
                    $coursecontext = $context->get_course_context();
                    $courseid = $coursecontext->instanceid;
                    $filterparams = array(
                        'userid' => $userid,
                        'competencyid' => $competencyid,
                        'courseid' => $courseid
                    );
                    // Fetch or create user competency course.
                    $usercompetencycourse = user_competency_course::get_record($filterparams);
                    if (!$usercompetencycourse) {
                        $usercompetencycourse = user_competency_course::create_relation($userid, $competencyid, $courseid);
                        $usercompetencycourse->create();
                    }
                    // Get proficiency.
                    $proficiency = $ucproficiency;
                    if ($proficiency === null) {
                        if (empty($competency)) {
                            $competency = new competency($competencyid);
                        }
                        $proficiency = $competency->get_proficiency_of_grade($grade);
                    }
                    // Set grade.
                    $usercompetencycourse->set('grade', $grade);
                    // Set proficiency.
                    $usercompetencycourse->set('proficiency', $proficiency);

                    $coursesettings = course_competency_settings::get_by_courseid($courseid);
                    if (!$coursesettings->get('pushratingstouserplans')) {
                        $setucgrade = false;
                    }
                }

                break;

            // Simply logging an evidence.
            case evidence::ACTION_LOG:
                if ($grade !== null) {
                    throw new coding_exception("The grade MUST NOT be set when 'logging' an evidence.");
                }
                break;

            // Whoops, this is not expected.
            default:
                throw new coding_exception('Unexpected action parameter when registering an evidence.');
                break;
        }

        // Should we recommend?
        if ($recommend && $usercompetency->get('status') == user_competency::STATUS_IDLE) {
            $usercompetency->set('status', user_competency::STATUS_WAITING_FOR_REVIEW);
        }

        // Setting the grade and proficiency for the user competency.
        $wascompleted = false;
        if ($setucgrade == true) {
            if (!$usercompetency->get('proficiency') && $ucproficiency) {
                $wascompleted = true;
            }
            $usercompetency->set('grade', $ucgrade);
            $usercompetency->set('proficiency', $ucproficiency);
        }

        // Prepare the evidence.
        $record = new stdClass();
        $record->usercompetencyid = $usercompetency->get('id');
        $record->contextid = $contextid;
        $record->action = $action;
        $record->descidentifier = $descidentifier;
        $record->desccomponent = $desccomponent;
        $record->grade = $grade;
        $record->actionuserid = $actionuserid;
        $record->note = $note;
        $evidence = new evidence(0, $record);
        $evidence->set('desca', $desca);
        $evidence->set('url', $url);

        // Validate both models, we should not operate on one if the other will not save.
        if (!$usercompetency->is_valid()) {
            throw new invalid_persistent_exception($usercompetency->get_errors());
        } else if (!$evidence->is_valid()) {
            throw new invalid_persistent_exception($evidence->get_errors());
        }

        // Save the user_competency_course record.
        if ($usercompetencycourse !== null) {
            // Validate and update.
            if (!$usercompetencycourse->is_valid()) {
                throw new invalid_persistent_exception($usercompetencycourse->get_errors());
            }
            $usercompetencycourse->update();
        }

        // Finally save. Pheww!
        $usercompetency->update();
        $evidence->create();

        // Trigger the evidence_created event.
        \core\event\competency_evidence_created::create_from_evidence($evidence, $usercompetency, $recommend)->trigger();

        // The competency was marked as completed, apply the rules.
        if ($wascompleted) {
            self::apply_competency_rules_from_usercompetency($usercompetency, $competency, $overridegrade);
        }

        return $evidence;
    }

    /**
     * Read an evidence.
     * @param int $evidenceid The evidence ID.
     * @return evidence
     */
    public static function read_evidence($evidenceid) {
        static::require_enabled();

        $evidence = new evidence($evidenceid);
        $uc = new user_competency($evidence->get('usercompetencyid'));
        if (!$uc->can_read()) {
            throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
                'nopermissions', '');
        }

        return $evidence;
    }

    /**
     * Delete an evidence.
     *
     * @param evidence|int $evidenceorid The evidence, or its ID.
     * @return bool
     */
    public static function delete_evidence($evidenceorid) {
        $evidence = $evidenceorid;
        if (!is_object($evidence)) {
            $evidence = new evidence($evidenceorid);
        }

        $uc = new user_competency($evidence->get('usercompetencyid'));
        if (!evidence::can_delete_user($uc->get('userid'))) {
            throw new required_capability_exception($uc->get_context(), 'moodle/competency:evidencedelete', 'nopermissions', '');
        }

        return $evidence->delete();
    }

    /**
     * Apply the competency rules from a user competency.
     *
     * The user competency passed should be one that was recently marked as complete.
     * A user competency is considered 'complete' when it's proficiency value is true.
     *
     * This method will check if the parent of this usercompetency's competency has any
     * rules and if so will see if they match. When matched it will take the required
     * step to add evidence and trigger completion, etc...
     *
     * @param  user_competency $usercompetency The user competency recently completed.
     * @param  competency|null $competency     The competency of the user competency, useful to avoid unnecessary read.
     * @return void
     */
    protected static function apply_competency_rules_from_usercompetency(user_competency $usercompetency,
                                                                         ?competency $competency = null, $overridegrade = false) {

        // Perform some basic checks.
        if (!$usercompetency->get('proficiency')) {
            throw new coding_exception('The user competency passed is not completed.');
        }
        if ($competency === null) {
            $competency = $usercompetency->get_competency();
        }
        if ($competency->get('id') != $usercompetency->get('competencyid')) {
            throw new coding_exception('Mismatch between user competency and competency.');
        }

        // Fetch the parent.
        $parent = $competency->get_parent();
        if ($parent === null) {
            return;
        }

        // The parent should have a rule, and a meaningful outcome.
        $ruleoutcome = $parent->get('ruleoutcome');
        if ($ruleoutcome == competency::OUTCOME_NONE) {
            return;
        }
        $rule = $parent->get_rule_object();
        if ($rule === null) {
            return;
        }

        // Fetch or create the user competency for the parent.
        $userid = $usercompetency->get('userid');
        $parentuc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $parent->get('id')));
        if (!$parentuc) {
            $parentuc = user_competency::create_relation($userid, $parent->get('id'));
            $parentuc->create();
        }

        // Does the rule match?
        if (!$rule->matches($parentuc)) {
            return;
        }

        // Figuring out what to do.
        $recommend = false;
        if ($ruleoutcome == competency::OUTCOME_EVIDENCE) {
            $action = evidence::ACTION_LOG;

        } else if ($ruleoutcome == competency::OUTCOME_RECOMMEND) {
            $action = evidence::ACTION_LOG;
            $recommend = true;

        } else if ($ruleoutcome == competency::OUTCOME_COMPLETE) {
            $action = evidence::ACTION_COMPLETE;

        } else {
            throw new moodle_exception('Unexpected rule outcome: ' + $ruleoutcome);
        }

        // Finally add an evidence.
        static::add_evidence(
            $userid,
            $parent,
            $parent->get_context()->id,
            $action,
            'evidence_competencyrule',
            'core_competency',
            null,
            $recommend,
            null,
            null,
            null,
            null,
            $overridegrade
        );
    }

    /**
     * Observe when a course module is marked as completed.
     *
     * Note that the user being logged in while this happens may be anyone.
     * Do not rely on capability checks here!
     *
     * @param  \core\event\course_module_completion_updated $event
     * @return void
     */
    public static function observe_course_module_completion_updated(\core\event\course_module_completion_updated $event) {
        if (!static::is_enabled()) {
            return;
        }

        $eventdata = $event->get_record_snapshot('course_modules_completion', $event->objectid);

        if ($eventdata->completionstate == COMPLETION_COMPLETE
                || $eventdata->completionstate == COMPLETION_COMPLETE_PASS) {
            $coursemodulecompetencies = course_module_competency::list_course_module_competencies($eventdata->coursemoduleid);

            $cm = get_coursemodule_from_id(null, $eventdata->coursemoduleid);
            $fastmodinfo = get_fast_modinfo($cm->course)->cms[$cm->id];

            $cmname = $fastmodinfo->name;
            $url = $fastmodinfo->url;

            foreach ($coursemodulecompetencies as $coursemodulecompetency) {
                $outcome = $coursemodulecompetency->get('ruleoutcome');
                $action = null;
                $recommend = false;
                $strdesc = 'evidence_coursemodulecompleted';
                $overridegrade = $coursemodulecompetency->get('overridegrade');

                if ($outcome == course_module_competency::OUTCOME_NONE) {
                    continue;
                }
                if ($outcome == course_module_competency::OUTCOME_EVIDENCE) {
                    $action = evidence::ACTION_LOG;

                } else if ($outcome == course_module_competency::OUTCOME_RECOMMEND) {
                    $action = evidence::ACTION_LOG;
                    $recommend = true;

                } else if ($outcome == course_module_competency::OUTCOME_COMPLETE) {
                    $action = evidence::ACTION_COMPLETE;

                } else {
                    throw new moodle_exception('Unexpected rule outcome: ' + $outcome);
                }

                static::add_evidence(
                    $event->relateduserid,
                    $coursemodulecompetency->get('competencyid'),
                    $event->contextid,
                    $action,
                    $strdesc,
                    'core_competency',
                    $cmname,
                    $recommend,
                    $url,
                    null,
                    null,
                    null,
                    $overridegrade
                );
            }
        }
    }

    /**
     * Observe when a course is marked as completed.
     *
     * Note that the user being logged in while this happens may be anyone.
     * Do not rely on capability checks here!
     *
     * @param  \core\event\course_completed $event
     * @return void
     */
    public static function observe_course_completed(\core\event\course_completed $event) {
        if (!static::is_enabled()) {
            return;
        }

        $sql = 'courseid = :courseid AND ruleoutcome != :nooutcome';
        $params = array(
            'courseid' => $event->courseid,
            'nooutcome' => course_competency::OUTCOME_NONE
        );
        $coursecompetencies = course_competency::get_records_select($sql, $params);

        $course = get_course($event->courseid);
        $courseshortname = format_string($course->shortname, null, array('context' => $event->contextid));

        foreach ($coursecompetencies as $coursecompetency) {

            $outcome = $coursecompetency->get('ruleoutcome');
            $action = null;
            $recommend = false;
            $strdesc = 'evidence_coursecompleted';

            if ($outcome == course_module_competency::OUTCOME_NONE) {
                continue;
            }
            if ($outcome == course_competency::OUTCOME_EVIDENCE) {
                $action = evidence::ACTION_LOG;

            } else if ($outcome == course_competency::OUTCOME_RECOMMEND) {
                $action = evidence::ACTION_LOG;
                $recommend = true;

            } else if ($outcome == course_competency::OUTCOME_COMPLETE) {
                $action = evidence::ACTION_COMPLETE;

            } else {
                throw new moodle_exception('Unexpected rule outcome: ' + $outcome);
            }

            static::add_evidence(
                $event->relateduserid,
                $coursecompetency->get('competencyid'),
                $event->contextid,
                $action,
                $strdesc,
                'core_competency',
                $courseshortname,
                $recommend,
                $event->get_url()
            );
        }
    }

    /**
     * Action to perform when a course module is deleted.
     *
     * Do not call this directly, this is reserved for core use.
     *
     * @param stdClass $cm The CM object.
     * @return void
     */
    public static function hook_course_module_deleted(stdClass $cm) {
        global $DB;
        $DB->delete_records(course_module_competency::TABLE, array('cmid' => $cm->id));
        $context = context_module::instance($cm->id);
        $DB->delete_records(evidence::TABLE, ['contextid' => $context->id]);
    }

    /**
     * Action to perform when a course is deleted.
     *
     * Do not call this directly, this is reserved for core use.
     *
     * @param stdClass $course The course object.
     * @return void
     */
    public static function hook_course_deleted(stdClass $course) {
        global $DB;
        $DB->delete_records(course_competency::TABLE, array('courseid' => $course->id));
        $DB->delete_records(course_competency_settings::TABLE, array('courseid' => $course->id));
        $DB->delete_records(user_competency_course::TABLE, array('courseid' => $course->id));
    }

    /**
     * Action to perform when a course is being reset.
     *
     * Do not call this directly, this is reserved for core use.
     *
     * @param int $courseid The course ID.
     * @return void
     */
    public static function hook_course_reset_competency_ratings($courseid) {
        global $DB;
        $DB->delete_records(user_competency_course::TABLE, array('courseid' => $courseid));
    }

    /**
     * Action to perform when a cohort is deleted.
     *
     * Do not call this directly, this is reserved for core use.
     *
     * @param \stdClass $cohort The cohort object.
     * @return void
     */
    public static function hook_cohort_deleted(\stdClass $cohort) {
        global $DB;
        $DB->delete_records(template_cohort::TABLE, array('cohortid' => $cohort->id));
    }

    /**
     * Action to perform when a user is deleted.
     *
     * @param int $userid The user id.
     */
    public static function hook_user_deleted($userid) {
        global $DB;

        $usercompetencies = $DB->get_records(user_competency::TABLE, ['userid' => $userid], '', 'id');
        foreach ($usercompetencies as $usercomp) {
            $DB->delete_records(evidence::TABLE, ['usercompetencyid' => $usercomp->id]);
        }

        $DB->delete_records(user_competency::TABLE, ['userid' => $userid]);
        $DB->delete_records(user_competency_course::TABLE, ['userid' => $userid]);
        $DB->delete_records(user_competency_plan::TABLE, ['userid' => $userid]);

        // Delete any associated files.
        $fs = get_file_storage();
        $context = context_user::instance($userid);
        $userevidences = $DB->get_records(user_evidence::TABLE, ['userid' => $userid], '', 'id');
        foreach ($userevidences as $userevidence) {
            $DB->delete_records(user_evidence_competency::TABLE, ['userevidenceid' => $userevidence->id]);
            $DB->delete_records(user_evidence::TABLE, ['id' => $userevidence->id]);
            $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $userevidence->id);
        }

        $userplans = $DB->get_records(plan::TABLE, ['userid' => $userid], '', 'id');
        foreach ($userplans as $userplan) {
            $DB->delete_records(plan_competency::TABLE, ['planid' => $userplan->id]);
            $DB->delete_records(plan::TABLE, ['id' => $userplan->id]);
        }
    }

    /**
     * Manually grade a user competency.
     *
     * @param int $userid
     * @param int $competencyid
     * @param int $grade
     * @param string $note A note to attach to the evidence
     * @return array of \core_competency\user_competency
     */
    public static function grade_competency($userid, $competencyid, $grade, $note = null) {
        global $USER;
        static::require_enabled();

        $uc = static::get_user_competency($userid, $competencyid);
        $context = $uc->get_context();
        if (!user_competency::can_grade_user($uc->get('userid'))) {
            throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', '');
        }

        // Throws exception if competency not in plan.
        $competency = $uc->get_competency();
        $competencycontext = $competency->get_context();
        if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
                $competencycontext)) {
            throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', '');
        }

        $action = evidence::ACTION_OVERRIDE;
        $desckey = 'evidence_manualoverride';

        $result = self::add_evidence($uc->get('userid'),
                                  $competency,
                                  $context->id,
                                  $action,
                                  $desckey,
                                  'core_competency',
                                  null,
                                  false,
                                  null,
                                  $grade,
                                  $USER->id,
                                  $note);
        if ($result) {
            $uc->read();
            $event = \core\event\competency_user_competency_rated::create_from_user_competency($uc);
            $event->trigger();
        }
        return $result;
    }

    /**
     * Manually grade a user competency from the plans page.
     *
     * @param mixed $planorid
     * @param int $competencyid
     * @param int $grade
     * @param string $note A note to attach to the evidence
     * @return array of \core_competency\user_competency
     */
    public static function grade_competency_in_plan($planorid, $competencyid, $grade, $note = null) {
        global $USER;
        static::require_enabled();

        $plan = $planorid;
        if (!is_object($planorid)) {
            $plan = new plan($planorid);
        }

        $context = $plan->get_context();
        if (!user_competency::can_grade_user($plan->get('userid'))) {
            throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', '');
        }

        // Throws exception if competency not in plan.
        $competency = $plan->get_competency($competencyid);
        $competencycontext = $competency->get_context();
        if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
                $competencycontext)) {
            throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', '');
        }

        $action = evidence::ACTION_OVERRIDE;
        $desckey = 'evidence_manualoverrideinplan';

        $result = self::add_evidence($plan->get('userid'),
                                  $competency,
                                  $context->id,
                                  $action,
                                  $desckey,
                                  'core_competency',
                                  $plan->get('name'),
                                  false,
                                  null,
                                  $grade,
                                  $USER->id,
                                  $note);
        if ($result) {
            $uc = static::get_user_competency($plan->get('userid'), $competency->get('id'));
            $event = \core\event\competency_user_competency_rated_in_plan::create_from_user_competency($uc, $plan->get('id'));
            $event->trigger();
        }
        return $result;
    }

    /**
     * Manually grade a user course competency from the course page.
     *
     * This may push the rating to the user competency
     * if the course is configured this way.
     *
     * @param mixed $courseorid
     * @param int $userid
     * @param int $competencyid
     * @param int $grade
     * @param string $note A note to attach to the evidence
     * @return array of \core_competency\user_competency
     */
    public static function grade_competency_in_course($courseorid, $userid, $competencyid, $grade, $note = null) {
        global $USER, $DB;
        static::require_enabled();

        $course = $courseorid;
        if (!is_object($courseorid)) {
            $course = $DB->get_record('course', array('id' => $courseorid));
        }
        $context = context_course::instance($course->id);

        // Check that we can view the user competency details in the course.
        if (!user_competency::can_read_user_in_course($userid, $course->id)) {
            throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
        }

        // Validate the permission to grade.
        if (!user_competency::can_grade_user_in_course($userid, $course->id)) {
            throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', '');
        }

        // Check that competency is in course and visible to the current user.
        $competency = course_competency::get_competency($course->id, $competencyid);
        $competencycontext = $competency->get_context();
        if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
                $competencycontext)) {
            throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', '');
        }

        // Check that the user is enrolled in the course, and is "gradable".
        if (!is_enrolled($context, $userid, 'moodle/competency:coursecompetencygradable')) {
            throw new coding_exception('The competency may not be rated at this time.');
        }

        $action = evidence::ACTION_OVERRIDE;
        $desckey = 'evidence_manualoverrideincourse';

        $result = self::add_evidence($userid,
                                  $competency,
                                  $context->id,
                                  $action,
                                  $desckey,
                                  'core_competency',
                                  $context->get_context_name(),
                                  false,
                                  null,
                                  $grade,
                                  $USER->id,
                                  $note);
        if ($result) {
            $all = user_competency_course::get_multiple($userid, $course->id, array($competency->get('id')));
            $uc = reset($all);
            $event = \core\event\competency_user_competency_rated_in_course::create_from_user_competency_course($uc);
            $event->trigger();
        }
        return $result;
    }

    /**
     * Count the plans in the template, filtered by status.
     *
     * Requires moodle/competency:templateview capability at the system context.
     *
     * @param mixed $templateorid The id or the template.
     * @param int $status One of the plan status constants (or 0 for all plans).
     * @return int
     */
    public static function count_plans_for_template($templateorid, $status = 0) {
        static::require_enabled();
        $template = $templateorid;
        if (!is_object($template)) {
            $template = new template($template);
        }

        // First we do a permissions check.
        if (!$template->can_read()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
                'nopermissions', '');
        }

        return plan::count_records_for_template($template->get('id'), $status);
    }

    /**
     * Count the user-completency-plans in the template, optionally filtered by proficiency.
     *
     * Requires moodle/competency:templateview capability at the system context.
     *
     * @param mixed $templateorid The id or the template.
     * @param mixed $proficiency If true, filter by proficiency, if false filter by not proficient, if null - no filter.
     * @return int
     */
    public static function count_user_competency_plans_for_template($templateorid, $proficiency = null) {
        static::require_enabled();
        $template = $templateorid;
        if (!is_object($template)) {
            $template = new template($template);
        }

        // First we do a permissions check.
        if (!$template->can_read()) {
             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
                'nopermissions', '');
        }

        return user_competency_plan::count_records_for_template($template->get('id'), $proficiency);
    }

    /**
     * List the plans in the template, filtered by status.
     *
     * Requires moodle/competency:templateview capability at the system context.
     *
     * @param mixed $templateorid The id or the template.
     * @param int $status One of the plan status constants (or 0 for all plans).
     * @param int $skip The number of records to skip
     * @param int $limit The max number of records to return
     * @return plan[]
     */
    public static function list_plans_for_template($templateorid, $status = 0, $skip = 0, $limit = 100) {
        $template = $templateorid;
        if (!is_object($template)) {
            $template = new template($template);
        }

        // First we do a permissions check.
        if (!$template->can_read()) {
             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
                'nopermissions', '');
        }

        return plan::get_records_for_template($template->get('id'), $status, $skip, $limit);
    }

    /**
     * Get the most often not completed competency for this course.
     *
     * Requires moodle/competency:coursecompetencyview capability at the course context.
     *
     * @param int $courseid The course id
     * @param int $skip The number of records to skip
     * @param int $limit The max number of records to return
     * @return competency[]
     */
    public static function get_least_proficient_competencies_for_course($courseid, $skip = 0, $limit = 100) {
        static::require_enabled();
        $coursecontext = context_course::instance($courseid);

        if (!has_any_capability(array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'),
                $coursecontext)) {
            throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
        }

        return user_competency_course::get_least_proficient_competencies_for_course($courseid, $skip, $limit);
    }

    /**
     * Get the most often not completed competency for this template.
     *
     * Requires moodle/competency:templateview capability at the system context.
     *
     * @param mixed $templateorid The id or the template.
     * @param int $skip The number of records to skip
     * @param int $limit The max number of records to return
     * @return competency[]
     */
    public static function get_least_proficient_competencies_for_template($templateorid, $skip = 0, $limit = 100) {
        static::require_enabled();
        $template = $templateorid;
        if (!is_object($template)) {
            $template = new template($template);
        }

        // First we do a permissions check.
        if (!$template->can_read()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
                'nopermissions', '');
        }

        return user_competency_plan::get_least_proficient_competencies_for_template($template->get('id'), $skip, $limit);
    }

    /**
     * Template event viewed.
     *
     * Requires moodle/competency:templateview capability at the system context.
     *
     * @param mixed $templateorid The id or the template.
     * @return boolean
     */
    public static function template_viewed($templateorid) {
        static::require_enabled();
        $template = $templateorid;
        if (!is_object($template)) {
            $template = new template($template);
        }

        // First we do a permissions check.
        if (!$template->can_read()) {
            throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
                'nopermissions', '');
        }

        // Trigger a template viewed event.
        \core\event\competency_template_viewed::create_from_template($template)->trigger();

        return true;
    }

    /**
     * Get the competency settings for a course.
     *
     * Requires moodle/competency:coursecompetencyview capability at the course context.
     *
     * @param int $courseid The course id
     * @return course_competency_settings
     */
    public static function read_course_competency_settings($courseid) {
        static::require_enabled();

        // First we do a permissions check.
        if (!course_competency_settings::can_read($courseid)) {
            $context = context_course::instance($courseid);
            throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
        }

        return course_competency_settings::get_by_courseid($courseid);
    }

    /**
     * Update the competency settings for a course.
     *
     * Requires moodle/competency:coursecompetencyconfigure capability at the course context.
     *
     * @param int $courseid The course id
     * @param stdClass $settings List of settings. The only valid setting ATM is pushratginstouserplans (boolean).
     * @return bool
     */
    public static function update_course_competency_settings($courseid, $settings) {
        static::require_enabled();

        $settings = (object) $settings;

        // Get all the valid settings.
        $pushratingstouserplans = isset($settings->pushratingstouserplans) ? $settings->pushratingstouserplans : false;

        // First we do a permissions check.
        if (!course_competency_settings::can_manage_course($courseid)) {
            $context = context_course::instance($courseid);
            throw new required_capability_exception($context, 'moodle/competency:coursecompetencyconfigure', 'nopermissions', '');
        }

        $exists = course_competency_settings::get_record(array('courseid' => $courseid));

        // Now update or insert.
        if ($exists) {
            $settings = $exists;
            $settings->set('pushratingstouserplans', $pushratingstouserplans);
            return $settings->update();
        } else {
            $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $pushratingstouserplans);
            $settings = new course_competency_settings(0, $data);
            $result = $settings->create();
            return !empty($result);
        }
    }


    /**
     * Function used to return a list of users where the given user has a particular capability.
     *
     * This is used e.g. to find all the users where someone is able to manage their learning plans,
     * it also would be useful for mentees etc.
     *
     * @param string $capability - The capability string we are filtering for. If '' is passed,
     *                             an always matching filter is returned.
     * @param int $userid - The user id we are using for the access checks. Defaults to current user.
     * @param int $type - The type of named params to return (passed to $DB->get_in_or_equal).
     * @param string $prefix - The type prefix for the db table (passed to $DB->get_in_or_equal).
     * @return list($sql, $params) Same as $DB->get_in_or_equal().
     * @todo MDL-52243 Move this function to lib/accesslib.php
     */
    public static function filter_users_with_capability_on_user_context_sql($capability, $userid = 0, $type = SQL_PARAMS_QM,
                                                                            $prefix='param') {

        global $USER, $DB;
        $allresultsfilter = array('> 0', array());
        $noresultsfilter = array('= -1', array());

        if (empty($capability)) {
            return $allresultsfilter;
        }

        if (!$capinfo = get_capability_info($capability)) {
            throw new coding_exception('Capability does not exist: ' . $capability);
        }

        if (empty($userid)) {
            $userid = $USER->id;
        }

        // Make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
        if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
            if (isguestuser($userid) or $userid == 0) {
                return $noresultsfilter;
            }
        }

        if (is_siteadmin($userid)) {
            // No filtering for site admins.
            return $allresultsfilter;
        }

        // Check capability on system level.
        $syscontext = context_system::instance();
        $hassystem = has_capability($capability, $syscontext, $userid);

        $access = get_user_roles_sitewide_accessdata($userid);
        // Build up a list of level 2 contexts (candidates to be user context).
        $filtercontexts = array();
        // Build list of roles to check overrides.
        $roles = array();

        foreach ($access['ra'] as $path => $role) {
            $parts = explode('/', $path);
            if (count($parts) == 3) {
                $filtercontexts[$parts[2]] = $parts[2];
            } else if (count($parts) > 3) {
                // We know this is not a user context because there is another path with more than 2 levels.
                unset($filtercontexts[$parts[2]]);
            }
            $roles = array_merge($roles, $role);
        }

        // Add all contexts in which a role may be overidden.
        $rdefs = get_role_definitions($roles);
        foreach ($rdefs as $roledef) {
            foreach ($roledef as $path => $caps) {
                if (!isset($caps[$capability])) {
                    // The capability is not mentioned, we can ignore.
                    continue;
                }
                $parts = explode('/', $path);
                if (count($parts) === 3) {
                    // Only get potential user contexts, they only ever have 2 slashes /parentId/Id.
                    $filtercontexts[$parts[2]] = $parts[2];
                }
            }
        }

        // No interesting contexts - return all or no results.
        if (empty($filtercontexts)) {
            if ($hassystem) {
                return $allresultsfilter;
            } else {
                return $noresultsfilter;
            }
        }
        // Fetch all interesting contexts for further examination.
        list($insql, $params) = $DB->get_in_or_equal($filtercontexts, SQL_PARAMS_NAMED);
        $params['level'] = CONTEXT_USER;
        $fields = context_helper::get_preload_record_columns_sql('ctx');
        $interestingcontexts = $DB->get_recordset_sql('SELECT ' . $fields . '
                                                       FROM {context} ctx
                                                       WHERE ctx.contextlevel = :level
                                                         AND ctx.id ' . $insql . '
                                                       ORDER BY ctx.id', $params);
        if ($hassystem) {
            // If allowed at system, search for exceptions prohibiting the capability at user context.
            $excludeusers = array();
            foreach ($interestingcontexts as $contextrecord) {
                $candidateuserid = $contextrecord->ctxinstance;
                context_helper::preload_from_record($contextrecord);
                $usercontext = context_user::instance($candidateuserid);
                // Has capability should use the data already preloaded.
                if (!has_capability($capability, $usercontext, $userid)) {
                    $excludeusers[$candidateuserid] = $candidateuserid;
                }
            }

            // Construct SQL excluding users with this role assigned for this user.
            if (empty($excludeusers)) {
                $interestingcontexts->close();
                return $allresultsfilter;
            }
            list($sql, $params) = $DB->get_in_or_equal($excludeusers, $type, $prefix, false);
        } else {
            // If not allowed at system, search for exceptions allowing the capability at user context.
            $allowusers = array();
            foreach ($interestingcontexts as $contextrecord) {
                $candidateuserid = $contextrecord->ctxinstance;
                context_helper::preload_from_record($contextrecord);
                $usercontext = context_user::instance($candidateuserid);
                // Has capability should use the data already preloaded.
                if (has_capability($capability, $usercontext, $userid)) {
                    $allowusers[$candidateuserid] = $candidateuserid;
                }
            }

            // Construct SQL excluding users with this role assigned for this user.
            if (empty($allowusers)) {
                $interestingcontexts->close();
                return $noresultsfilter;
            }
            list($sql, $params) = $DB->get_in_or_equal($allowusers, $type, $prefix);
        }
        $interestingcontexts->close();

        // Return the goods!.
        return array($sql, $params);
    }

}
