<?php
// This file is part of the Studyplan plugin for Moodle
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <https://www.gnu.org/licenses/>.

/**
 * Handle badge information
 * @package    local_treestudyplan
 * @copyright  2023 P.M. Kuipers
 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

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

require_once($CFG->libdir . '/externallib.php');

use award_criteria;
use core_badges\badge;
use moodle_url;
use core_external\external_description;
use core_external\external_value;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core\context\system as context_system;
use core\context\course as context_course;

/**
 * Handle badge information in the same style as the other classes
 */
class badgeinfo {
    /**
     * Holds database record
     * @var \core_badges\badge
     */
    private $badge;

    /**
     * Maps badge status to strings
     * @var array
     */
    private const STATUSINFO = [
        BADGE_STATUS_INACTIVE => 'inactive',
        BADGE_STATUS_ACTIVE => 'active',
        BADGE_STATUS_INACTIVE_LOCKED => 'inactive',
        BADGE_STATUS_ACTIVE_LOCKED => 'active',
        BADGE_STATUS_ARCHIVED => 'archived',
    ];

    /**
     * Maps badge status to locked or not
     * @var array
     */
    private const LOCKEDINFO = [
        BADGE_STATUS_INACTIVE => 0,
        BADGE_STATUS_ACTIVE => 0,
        BADGE_STATUS_INACTIVE_LOCKED => 1,
        BADGE_STATUS_ACTIVE_LOCKED => 1,
        BADGE_STATUS_ARCHIVED => 1, // We don't want to edit archived badges anyway.....
    ];

    /**
     * Construct new badgeinfo object
     * @param badge $badge Badge object to use
     */
    public function __construct(badge $badge) {
        $this->badge = $badge;
    }

    /**
     * Return full name
     * @return string
     */
    public function name() {
        return $this->badge->name;
    }

    /**
     * Get the badge id from the badge name
     * @param string $name Badge name
     * @return int Badge id
     */
    public static function id_from_name($name) {
        global $DB;

        return $DB->get_field("badge", "id", ['name' => $name]);
    }

    /**
     * Check if a given badge exists
     * @param int $id Badge id
     * @return bool
     */
    public static function exists($id) {
        global $DB;
        return is_numeric($id) && $DB->record_exists('badge', ['id' => $id]);
    }

    /**
     * Webservice structure for editor info
     * @param int $value Webservice requirement constant
     */
    public static function simple_structure($value = VALUE_REQUIRED): external_description {
        return new external_single_structure([
            "id" => new external_value(PARAM_INT, 'id of badge'),
            "infolink" => new external_value(PARAM_RAW, 'badge issue information link', VALUE_OPTIONAL),
            "name" => new external_value(PARAM_RAW, 'badge name'),
            "status" => new external_value(PARAM_TEXT, 'badge status'),
            "locked" => new external_value(PARAM_TEXT, 'badge lock status'),
            "description" => new external_value(PARAM_RAW, 'badge description'),
            "imageurl" => new external_value(PARAM_RAW, 'url of badge image'),
            "active" => new external_value(PARAM_BOOL, 'badge is available'),
        ], "Badge basic info", $value);
    }

    /**
     * Webservice model for editor info
     * @return array Webservice data model
     */
    public function simple_model() {
        if ($this->badge->type == BADGE_TYPE_SITE) {
            $context = context_system::instance();
        } else {
            $context = context_course::instance($this->badge->courseid);
        }
        $model = [
            'id' => $this->badge->id,
            'infolink' => (new \moodle_url('/badges/overview.php', ['id' => $this->badge->id]))->out(false),
            'name' => $this->badge->name,
            'status' => self::STATUSINFO[$this->badge->status],
            'locked' => self::LOCKEDINFO[$this->badge->status],
            'description' => $this->badge->description,
            'imageurl' => \moodle_url::make_pluginfile_url(
                $context->id,
                'badges',
                'badgeimage',
                $this->badge->id,
                '/',
                'f1'
            )->out(false),
            "active" => $this->badge->is_active(),
        ];
        return $model;
    }

    /**
     * Create a simple model from a database recode
     * @param object $r Webservice requirement constant
     * @return array Webservice data model
     */
    public static function simple_model_from_db($r) {
        if ($r->type == BADGE_TYPE_SITE) {
            $context = context_system::instance();
        } else {
            $context = context_course::instance($r->courseid);
        }
        $model = [
            'id' => $r->id,
            'infolink' => (new \moodle_url('/badges/overview.php', ['id' => $r->id]))->out(false),
            'name' => $r->name,
            'status' => self::STATUSINFO[$r->status],
            'locked' => self::LOCKEDINFO[$r->status],
            'description' => $r->description,
            'imageurl' => \moodle_url::make_pluginfile_url(
                $context->id,
                'badges',
                'badgeimage',
                $r->id,
                '/',
                'f1'
            )->out(false),
            "active" => ($r->status == BADGE_STATUS_ACTIVE) || ($r->status == BADGE_STATUS_ACTIVE_LOCKED),
        ];
        return $model;
    }

    /**
     * Webservice structure for editor info
     * @param int $value Webservice requirement constant
     */
    public static function editor_structure($value = VALUE_REQUIRED): external_description {
        return new external_single_structure([
            "id" => new external_value(PARAM_INT, 'id of badge'),
            "infolink" => new external_value(PARAM_RAW, 'badge issue information link', VALUE_OPTIONAL),
            "name" => new external_value(PARAM_RAW, 'badge name'),
            "status" => new external_value(PARAM_TEXT, 'badge status'),
            "locked" => new external_value(PARAM_TEXT, 'badge lock status'),
            "criteria" => self::badge_criteria_structure(VALUE_REQUIRED),
            "criteriatext" => new external_multiple_structure(
                new external_value(PARAM_RAW, 'criteria text'),
                'badge criteria',
                VALUE_OPTIONAL
            ),
            "description" => new external_value(PARAM_RAW, 'badge description'),
            "imageurl" => new external_value(PARAM_RAW, 'url of badge image'),
            "studentcount" => new external_value(
                PARAM_INT,
                'number of studyplan students that can get this badge',
                VALUE_OPTIONAL
            ),
            "issuedcount" => new external_value(
                PARAM_INT,
                'number of studyplan students that have got this badge',
                VALUE_OPTIONAL
            ),
            "active" => new external_value(PARAM_BOOL, 'badge is available'),
        ], "Badge info", $value);
    }

    /**
     * Webservice model for editor info
     * @param int[]|null $studentlist List of user id's to use for checking issueing progress within a study plan
     * @return array Webservice data model
     */
    public function editor_model(?array $studentlist = null) {
        if ($this->badge->type == BADGE_TYPE_SITE) {
            $context = context_system::instance();
        } else {
            $context = context_course::instance($this->badge->courseid);
        }
        // If the user is viewing another user's badge and doesn't have the right capability return only part of the data.

        $criteriatext = [];
        foreach ($this->badge->get_criteria() as $bc) {
            $criteriatext[] = $bc->get_title() . " " . $bc->get_details();
        }
        $model = [
            'id' => $this->badge->id,
            'infolink' => (new \moodle_url('/badges/overview.php', ['id' => $this->badge->id]))->out(false),
            'name' => $this->badge->name,
            'status' => self::STATUSINFO[$this->badge->status],
            'locked' => self::LOCKEDINFO[$this->badge->status],
            'criteria' => $this->badge_criteria_model(),
            'criteriatext' => $criteriatext,
            'description' => $this->badge->description,
            'imageurl' => \moodle_url::make_pluginfile_url(
                $context->id,
                'badges',
                'badgeimage',
                $this->badge->id,
                '/',
                'f1'
            )->out(false),
            "active" => $this->badge->is_active(),
        ];

        // Add badge issue stats if a studentlist is attached to the request.
        if (!empty($studentlist) && is_array($studentlist)) {
            $model['studentcount'] = count($studentlist);
            $model['issuedcount'] = $this->count_issued($studentlist);
        }

        return $model;
    }

    /**
     * Webservice structure for userinfo
     * @param int $value Webservice requirement constant
     */
    public static function user_structure($value = VALUE_REQUIRED): external_description {
        return new external_single_structure([
            "id" => new external_value(PARAM_INT, 'id of badge'),
            "infolink" => new external_value(PARAM_TEXT, 'badge issue information link', VALUE_OPTIONAL),
            "name" => new external_value(PARAM_TEXT, 'badge name'),
            "completion" => self::badge_completion_structure(VALUE_OPTIONAL),
            "description" => new external_value(PARAM_TEXT, 'badge description'),
            "imageurl" => new external_value(PARAM_TEXT, 'url of badge image'),
            "issued" => new external_value(PARAM_BOOL, 'badge is issued'),
            "dateissued" => new external_value(PARAM_TEXT, 'date the badge was issued', VALUE_OPTIONAL),
            "dateexpire" => new external_value(PARAM_TEXT, 'date the badge will expire', VALUE_OPTIONAL),
            "uniquehash" => new external_value(PARAM_TEXT, 'badge issue hash', VALUE_OPTIONAL),
            "issuedlink" => new external_value(PARAM_TEXT, 'badge issue information link', VALUE_OPTIONAL),
            "active" => new external_value(PARAM_BOOL, 'badge is available'),
        ], "Badge info", $value);
    }

    /**
     * Webservice model for user info
     * @param int $userid ID of user to check specific info for
     * @return array Webservice data model
     */
    public function user_model($userid) {
        global $DB;
        if ($this->badge->type == BADGE_TYPE_SITE) {
            $context = context_system::instance();
        } else {
            $context = context_course::instance($this->badge->courseid);
        }

        $issued = $this->badge->is_issued($userid);

        // If the user is viewing another user's badge and doesn't have the right capability return only part of the data.

        $badge = [
            'id' => $this->badge->id,
            'name' => $this->badge->name,
            'description' => $this->badge->description,
            'imageurl' => \moodle_url::make_pluginfile_url(
                $context->id,
                'badges',
                'badgeimage',
                $this->badge->id,
                '/',
                'f1'
            )->out(false),
            'completion' => $this->badge_completion_data($userid),
            'issued' => $issued,
            'infolink' => (new \moodle_url('/badges/overview.php', ['id' => $this->badge->id]))->out(false),
            "active" => $this->badge->is_active(),
        ];

        if ($issued) {
            $issueinfo = $DB->get_record('badge_issued', ['badgeid' => $this->badge->id, 'userid' => $userid]);
            $badge['dateissued'] = date("Y-m-d", $issueinfo->dateissued);
            if ($issueinfo->expiredate) {
                $badge['dateexpire'] = date("Y-m-d", $issueinfo->dateexpire);
            }
            $badge['uniquehash'] = $issueinfo->uniquehash;
            $badge['issuedlink'] = (new \moodle_url('/badges/badge.php', ['hash' => $issueinfo->uniquehash]))->out(false);
        }

        return $badge;
    }

    /**
     * Define structure for badge criteria
     * @param object $value VALUE_OPTIONAL or VALUE_REQUIRED
     */
    protected static function badge_criteria_structure($value) {
        return new external_single_structure([
            "types" => new external_multiple_structure(new external_single_structure([
                'criteria' => new external_multiple_structure(new external_single_structure([
                    "title" => new external_value(PARAM_RAW, 'criterion title'),
                    "description" => new external_value(PARAM_RAW, 'criterion description'),
                    "link" => new external_value(PARAM_RAW, 'link to criterion resource', VALUE_OPTIONAL),
                    "requirements" => new external_multiple_structure(new external_single_structure([
                        "title" => new external_value(PARAM_RAW, 'requirment title'),
                    ]), "criterion specific requirements", VALUE_OPTIONAL),
                ]), 'specific criteria'),
                "title" => new external_value(PARAM_RAW, 'type title'),
                "longtitle" => new external_value(PARAM_RAW, 'long type title'),
                "aggregation" => new external_value(PARAM_TEXT, 'any|all'),
            ]), "criteria types", VALUE_OPTIONAL),
            "aggregation" => new external_value(PARAM_TEXT, 'any|all'),
        ], 'badge criteria', $value);
    }

    /**
     * Generate criteria model of this badge
     */
    protected function badge_criteria_model(): array {
        /* TODO: Consider adding a badge criteria preloading system, like the one for grades */
        $badgeagg = $this->badge->get_aggregation_method();
        $types = [];

        foreach ($this->badge->criteria as $type => $bc) {
            if ($type != BADGE_CRITERIA_TYPE_OVERALL) {
                $typeagg = $this->badge->get_aggregation_method($type);
                $typecrit = $this->get_award_subcriteria($bc);

                $aggrgationhandle = ($typeagg == BADGE_CRITERIA_AGGREGATION_ALL) ? "all" : "any";
                $typeinfo = [
                    'title' => ucfirst(get_string("criteria_descr_single_short$type", "badges")),
                    'longtitle' => ucfirst(get_string("criteria_descr_single_$type", "badges")),
                    'aggregation' => $aggrgationhandle,
                    'criteria' => $typecrit,
                ];
                $types[] = $typeinfo;
            }
        }

        $aggrgationhandle = ($badgeagg == BADGE_CRITERIA_AGGREGATION_ALL) ? "all" : "any";
        return [
            "types" => $types,
            "title" => ucfirst(
                get_string(
                    "criteria_descr_0",
                    "badges",
                    \core_text::strtolower(get_string($aggrgationhandle, "core"))
                )
            ),
            "longtitle" => ucfirst(
                get_string(
                    "criteria_descr_0",
                    "badges",
                    \core_text::strtolower(get_string($aggrgationhandle, "core"))
                )
            ),
            "aggregation" => $aggrgationhandle,
        ];
    }
    /**
     * Define structure for badge completion parts
     * @param object $value VALUE_OPTIONAL or VALUE_REQUIRED
     */
    protected static function badge_completion_structure($value) {
        return new external_single_structure([
            "types" => new external_multiple_structure(new external_single_structure([
                'criteria' => new external_multiple_structure(new external_single_structure([
                    "title" => new external_value(PARAM_RAW, 'criterion title'),
                    "description" => new external_value(PARAM_RAW, 'criterion description'),
                    "link" => new external_value(PARAM_RAW, 'link to criterion resource', VALUE_OPTIONAL),
                    "requirements" => new external_multiple_structure(new external_single_structure([
                        "title" => new external_value(PARAM_RAW, 'requirment title'),
                        "completed" => new external_value(PARAM_BOOL, 'criterion is completed or not', VALUE_OPTIONAL),
                    ]), "criterion specific requirements", VALUE_OPTIONAL),
                    "completed" => new external_value(PARAM_BOOL, 'criterion is completed or not'),
                ]), 'specific criteria'),
                "title" => new external_value(PARAM_RAW, 'type title'),
                "aggregation" => new external_value(PARAM_TEXT, 'any|all'),
                "count" => new external_value(PARAM_INT, 'effective number of critera for type progress'),
                "progress" => new external_value(PARAM_INT, 'effective number of completed criteria for type progress'),
                "fraction" => new external_value(PARAM_FLOAT, 'fraction of completed type criteria as float'),
            ]), "criteria types", VALUE_OPTIONAL),
            "aggregation" => new external_value(PARAM_TEXT, 'any|all'),
            "count" => new external_value(PARAM_INT, 'total number of critera for progress'),
            "progress" => new external_value(PARAM_INT, 'number of completed criteria for progress'),
            "fraction" => new external_value(PARAM_FLOAT, 'fraction of completed criteria as float'),
            "title" => new external_value(PARAM_RAW, 'completion title'),
        ], 'badge completion information', $value);
    }

    /**
     * Generate completion model of this badge for a given user
     * @param int $userid id of user to check for
     */
    protected function badge_completion_data($userid): array {
        /* TODO: Consider adding a badge criteria preloading system, like the one for grades */
        $count = 0;
        $progress = 0;
        $fraction = 0;

        $badgeagg = $this->badge->get_aggregation_method();
        $types = [];

        foreach ($this->badge->criteria as $type => $bc) {
            if ($type != BADGE_CRITERIA_TYPE_OVERALL) {
                $typeagg = $this->badge->get_aggregation_method($type);
                $typecrit = $this->get_award_subcriteria($bc, $userid);

                $typecount = count($typecrit);
                $typeprogress = 0;
                foreach ($typecrit as $subcrit) {
                    // Quickly check if the subcriteria are completed.
                    if ($subcrit["completed"]) {
                        $typeprogress++;
                    }
                }

                // Determine how to process the progress data, depending on the TYPE's aggregation.
                if ($typeagg == BADGE_CRITERIA_AGGREGATION_ANY) {
                    $typecount = 1;
                    $typeprogress = ($typeprogress > 0) ? 1 : 0;
                }

                // Determine how to patch this data into the overall progress numbers, depending on the OVERALL aggregation.
                if ($badgeagg == BADGE_CRITERIA_AGGREGATION_ANY) {
                    /*  If ANY completion overall, count only the criteria type with the highest completion percentage -.
                        Overwrite data if current type is more complete */
                    $typefraction = ($typecount > 0) ? ($typeprogress / $typecount) : ($typeprogress / $typecount);
                    if ($typefraction > $fraction || ($fraction == 0 && $typecount > $count)) {
                        $fraction = $typefraction;
                        $count = $typecount;
                        $progress = $typeprogress;
                    }
                } else {
                    /* If ALL completion overall, just add it up to the total */
                    $count += $typecount;
                    $progress += $typeprogress;
                    $typefraction = floatval($typeprogress) / floatval($typecount);
                }

                $aggrgationhandle = ($typeagg == BADGE_CRITERIA_AGGREGATION_ALL) ? "all" : "any";
                $typeinfo = [
                    'title' => ucfirst(get_string("criteria_descr_$type", "badges", get_string($aggrgationhandle, "core"))),
                    'aggregation' => $aggrgationhandle,
                    'criteria' => $typecrit,
                    'count' => $typecount,
                    'progress' => $typeprogress,
                    "fraction" => $typefraction,
                ];
                $types[] = $typeinfo;
            }
        }

        $aggrgationhandle = ($badgeagg == BADGE_CRITERIA_AGGREGATION_ALL) ? "all" : "any";
        return [
            "types" => $types,
            "title" => ucfirst(
                get_string(
                    "criteria_descr_0",
                    "badges",
                    \core_text::strtolower(get_string($aggrgationhandle, "core"))
                )
            ),
            "aggregation" => $aggrgationhandle,
            "count" => $count,
            "progress" => $progress,
            "fraction" => $fraction,
        ];
    }

    /**
     * Count how many of the students in the array have this badge issued
     * @param int[] $studentids List of user id's to check
     * @return int
     */
    public function count_issued(array $studentids) {
        $issuecount = 0;

        foreach ($studentids as $userid) {
            if ($this->badge->is_issued($userid)) {
                $issuecount++;
            }
        }
        return $issuecount;
    }


    /**
     * Gets the module instance from the database and returns it.
     * If no module instance exists this function returns false.
     * @param int $cmid Course module id
     * @return object|null
     */
    private static function get_mod_instance($cmid) {
        global $DB;
        $rec = $DB->get_record_sql("SELECT md.name
                               FROM {course_modules} cm,
                                    {modules} md
                               WHERE cm.id = ? AND
                                     md.id = cm.module", [$cmid]);

        if ($rec) {
            return get_coursemodule_from_id($rec->name, $cmid);
        } else {
            return null;
        }
    }

    /**
     * Gets role name.
     * If no such role exists this function returns null.
     * @param int $rid Role id
     * @return string|null
     */
    private static function get_role_name($rid) {
        global $DB;
        $rec = $DB->get_record('role', ['id' => $rid]);

        if ($rec) {
            return role_get_name($rec, context_system::instance(), ROLENAME_BOTH);
        } else {
            return null;
        }
    }

    /**
     * Get subcriteria info for awarding a badge for a user
     * @param award_criteria $crit The criteria to build info for
     * @param int|null $userid Optional userid to check subcriteria completion for
     * @return array
     */
    protected function get_award_subcriteria(award_criteria $crit, $userid = null): array {
        global $DB, $CFG;
        $list = [];

        if ($crit->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) {
            foreach ($crit->params as $p) {
                $mod = self::get_mod_instance($p["module"]);
                if (!is_object($mod)) {
                    $title = get_string('error:nosuchmod', 'badges');
                    $description = get_string('error:nosuchmod', 'badges');
                    continue;
                } else {
                    $title = get_string('modulename', $mod->modname) . ' - ' . format_string($mod->name, true);
                    $description = \html_writer::tag(
                        'b',
                        '"' . get_string('modulename', $mod->modname) . ' - ' . format_string($mod->name, true) . '"'
                    );
                    if (isset($p['bydate'])) {
                        $description .= get_string(
                            'criteria_descr_bydate',
                            'badges',
                            userdate($p['bydate'], get_string('strftimedate', 'core_langconfig'))
                        );
                    }
                }

                $subcrit = [
                    "title" => $title,
                    "description" => $description,
                    "requirements" => [
                        'completion' => [
                            'title' => get_string('completeactivity', 'core') .
                                        get_string('modulename', $mod->modname) . ' - ' . $mod->name,
                        ],
                    ],
                ];

                if (isset($p["bydate"])) {
                    $subcrit["requirements"]["bydate"] = [
                        'title' => get_string(
                            'criteria_descr_bydate',
                            'badges',
                            userdate($p['bydate'], get_string('strftimedate', 'core_langconfig'))
                        ),
                    ];
                }

                if (isset($userid)) {
                    $info = new \completion_info($crit->course);
                    $cm = new \stdClass();
                    $cm->id = $p['module'];
                    $data = $info->get_data($cm, false, $userid);
                    // Successfull completion states depend on the completion settings.
                    if (isset($data->passgrade)) {
                        // Passing grade is required. Don't issue a badge when state is COMPLETION_COMPLETE_FAIL.
                        $completionstates = [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS];
                    } else {
                        // Any grade is required. Issue a badge even when state is COMPLETION_COMPLETE_FAIL.
                        $completionstates = [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL];
                    }

                    $modcompleted = in_array($data->completionstate, $completionstates);

                    $subcrit["requirements"]["completion"]["completed"] = $modcompleted;

                    $checkdate = true;
                    if (isset($p["bydate"])) {
                        $date = $data->timemodified;
                        $checkdate = ($date <= $p['bydate']);
                        $subcrit["requirements"]["bydate"]["completed"] = $checkdate;
                    }

                    $subcrit["completed"] = $modcompleted && $checkdate;
                }
                $list[] = $subcrit;
            }
        } else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
            foreach ($crit->params as $p) {
                $role = self::get_role_name($p['role']);

                if (!$role) {
                    $title = get_string('error:nosuchrole', 'badges');
                    $description = get_string('error:nosuchrole', 'badges');
                } else {
                    $title = $role;
                    $description = $role;
                }

                $subcrit = [
                    "title" => $title,
                    "description" => $description,
                    "requirements" => [],
                ];

                if (isset($userid)) {
                    $crit = $DB->get_record(
                        'badge_manual_award',
                        [
                            'issuerrole' => $p['role'],
                            'recipientid' => $userid,
                            'badgeid' => $crit->badgeid,
                        ]
                    );
                    $subcrit["completed"] = $crit !== false;
                }
                $list[] = $subcrit;
            }
        } else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_COURSE || $crit->criteriatype == BADGE_CRITERIA_TYPE_COURSESET) {
            if ($crit->criteriatype == BADGE_CRITERIA_TYPE_COURSE) {
                $params = [reset($crit->params)]; // Only use the first parameter.
            } else {
                $params = $crit->params;
            }

            foreach ($params as $p) {
                $course = preloader::get_course($p["course"]);
                if (!$course) {
                    $description = get_string('error:nosuchcourse', "badges");
                } else {
                    $description = \html_writer::tag('b', '"' . $course->fullname . '"');
                    if (isset($p['bydate'])) {
                        $description .= get_string(
                            'criteria_descr_bydate',
                            'badges',
                            userdate($p['bydate'], get_string('strftimedate', 'core_langconfig'))
                        );
                    }
                    if (isset($p['grade'])) {
                        $description .= get_string('criteria_descr_grade', 'badges', $p['grade']);
                    }
                }

                $subcrit = [
                    "title" => format_string($course->fullname, true),
                    "link" => (new moodle_url($CFG->wwwroot . "/course/view.php", ["id" => $course->id]))->out(),
                    "description" => format_string($description, true),
                    "requirements" => [
                        'completion' => [
                            'title' => get_string('coursecompleted', 'completion'),
                        ],
                    ],
                ];

                if (isset($p["grade"])) {
                    $subcrit["requirements"]["grade"] = [
                        'title' => get_string('criteria_descr_grade', 'badges', $p["grade"]),
                    ];
                }
                if (isset($p["bydate"])) {
                    $subcrit["requirements"]["bydate"] = [
                        'title' => get_string(
                            'criteria_descr_bydate',
                            'badges',
                            userdate($p['bydate'], get_string('strftimedate', 'core_langconfig'))
                        ),
                    ];
                }

                if (isset($userid)) {
                    $coursecompletion = new \completion_completion(["userid" => $userid, "course" => $course->id]);
                    $coursecompleted = $coursecompletion->is_complete();
                    $subcrit["requirements"]["completion"]["completed"] = (bool) $coursecompleted;

                    $checkgrade = true;
                    if (isset($p["grade"])) {
                        $grade = \grade_get_course_grade($userid, $course->id);
                        $checkgrade = ($grade->grade >= $p['grade']);
                        $subcrit["requirements"]["grade"]["completed"] = (bool) $checkgrade;
                    }
                    $checkdate = true;
                    if (isset($p["bydate"])) {
                        $checkdate = ((bool) $coursecompletion->timecompleted) &&
                                             ($coursecompletion->timecompleted <= $p["bydate"]);
                        $subcrit["requirements"]["bydate"]["completed"] = (bool) $checkdate;
                    }

                    $subcrit["completed"] = $coursecompleted && $checkgrade && $checkdate;
                }
                $list[] = $subcrit;
            }
        } else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_PROFILE) {
            foreach ($crit->params as $p) {
                if (is_numeric($p['field'])) {
                    $fields = profile_get_custom_fields();
                    // Get formatted field name if such field exists.
                    $fieldname = isset($fields[$p['field']]->name) ?
                        format_string($fields[$p['field']]->name) : null;
                } else {
                    $fieldname = \core_user\fields::get_display_name($p['field']);
                }

                if (!$fieldname) {
                    $title = get_string('error:nosuchfield', 'badges');
                    $description = get_string('error:nosuchfield', 'badges');
                } else {
                    $title = $fieldname;
                    $description = $fieldname;
                }

                $subcrit = [
                    "title" => $title,
                    "description" => $description,
                    "requirements" => [],
                ];

                if (isset($userid)) {
                    $join = '';
                    $where = '';
                    $sqlparams = [ 'userid' => $userid];

                    if (is_numeric($p['field'])) {
                        // This is a custom field.
                        $join .= " LEFT JOIN {user_info_data} uid ON uid.userid = u.id AND uid.fieldid = :fieldid ";
                        $sqlparams["fieldid"] = $p['field'];
                        $where = "uid.id IS NOT NULL";
                    } else if (in_array($p['field'], $this->allowed_default_fields)) {
                        // This is a valid field from {user} table.
                        if ($p['field'] == 'picture') {
                            // The picture field is numeric and requires special handling.
                            $where = "u.{$p['field']} != 0";
                        } else {
                            $where = $DB->sql_isnotempty('u', "u.{$p['field']}", false, true);
                        }
                    }

                    $sql = "SELECT 1 FROM {user} u " . $join . " WHERE u.id = :userid $where";
                    $completed = $DB->record_exists_sql($sql, $sqlparams);
                    $subcrit["completed"] = (bool) $completed;
                }
                $list[] = $subcrit;
            }
        } else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_BADGE) {
            foreach ($crit->params as $p) {
                $badgename = $DB->get_field('badge', 'name', ['id' => $p['badge']]);
                if (!$badgename) {
                    $title = get_string('error:nosuchbadge', 'badges');
                    $description = get_string('error:nosuchbadge', 'badges');
                } else {
                    $title = format_string($badgename, true);
                    $description = \html_writer::tag('b', '"' . $badgename . '"');
                }

                $subcrit = [
                    "title" => $title,
                    "description" => $description,
                    "link" => (new \moodle_url($CFG->wwwroot . "/badges/overview.php", ["id" => $p["badge"]]))->out(),
                    "requirements" => [],
                ];

                if (isset($userid)) {
                    $badge = $DB->get_record('badge', ['id' => $p['badge']]);
                    // See if the user has earned this badge.
                    if ($badge) {
                        $awarded = $DB->get_record('badge_issued', ['badgeid' => $p['badge'], 'userid' => $userid]);
                        $awarded = !empty($awarded);
                    } else {
                        $awarded = false;
                    }
                    $subcrit["completed"] = (bool)$awarded;
                }
                $list[] = $subcrit;
            }
        } else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_COHORT) {
            foreach ($crit->params as $p) {
                $cohortname = $DB->get_field('cohort', 'name', ['id' => $p['cohort']]);
                if (!$cohortname) {
                    $title = get_string('error:nosuchcohort', 'badges');
                    $description = get_string('error:nosuchcohort', 'badges');
                } else {
                    $title = format_string($cohortname, true);
                    $description = \html_writer::tag('b', '"' . format_string($cohortname, true) . '"');
                }

                $subcrit = [
                    "title" => $title,
                    "description" => $description,
                    "requirements" => [],
                ];

                if (isset($userid)) {
                    $cohort = $DB->get_record('cohort', ['id' => $p['cohort']]);
                    $ismember = (bool) \cohort_is_member($cohort->id, $userid);
                    $subcrit["completed"] = $ismember;
                }
                $list[] = $subcrit;
            }
        } else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_COMPETENCY) {
            foreach ($crit->params as $p) {
                $competency = new \core_competency\competency($p['competency']);
                $competency->set('description', ''); // Will use 'short' description.

                /*  Below must be one of the most convoluted ways of calling tool_lp_render_competency_summary.
                    Since I'm pretty sure it's not an actually good idea to implement <plugin>_render_competency_summary
                    in a custom plugin, even though the system appearently allows you to add competency descriptions,
                    it's probably a case of premature optimization. Still, since this is the way that
                    'badges/criteria/award_criteria_competency.php does it, we'll copy its example, to avoid
                    differences in display....
                */
                $output = [];
                // Render the competency even if competencies are not currently enabled.
                \core_competency\api::skip_enabled();
                if ($pluginsfunction = get_plugins_with_function('render_competency_summary')) {
                    foreach ($pluginsfunction as $plugins) {
                        foreach ($plugins as $pluginfunction) {
                            $output[] = $pluginfunction($competency, $competency->get_framework(), false, false, true);
                        }
                    }
                }
                \core_competency\api::check_enabled();

                if (count($output) < 1) {
                    $title = get_string('error:nosuchcompetency', 'local_treestudyplan');
                    $description = get_string('error:nosuchcompetency', 'local_treestudyplan');
                } else {
                    /*  Might as wel implode the output, just in case someone was actually stupid enough to implement
                        <plugin>_render_competency_summary in a custom plugin...
                    */
                    $title = format_string($competency->get("shortname"), true);
                    $description = implode(" ", $output);
                }

                $subcrit = [
                    "title" => $title,
                    "description" => $description,
                    "requirements" => [],
                ];

                if (isset($userid)) {
                    /*  Slightly modified from award_criteria_competency.php::review()
                        to use criteria api class instead of direct calls....
                    */
                    $proficiency = false;
                    $badge = $DB->get_record('badge', ['id' => $crit->badgeid]);
                    if ($badge->type == BADGE_TYPE_SITE) {
                        $uc = \core_competency\api::get_user_competency($userid, $p['competency']);
                        $proficiency = $uc->get('proficiency');
                    } else if ($badge->type == BADGE_TYPE_COURSE) {
                        $uc = \core_competency\api::get_user_competency_in_course($badge->courseid, $userid, $p['competency']);
                        $proficiency = $uc->get('proficiency');
                    }

                    $subcrit["completed"] = (bool) $proficiency;
                }
                $list[] = $subcrit;
            }
        }

        return $list;
    }

    /**
     * Find all badges that have a relation to a certain course
     * @param int|stdClass $course The course to relate to or it's id
     * @param string $search An optional search string to filter by
     * @return array of int
     */
    public static function find_badges_by_course_relation($course, $search = '') {
        global $DB;
        if (is_numeric($course)) {
            $courseid = (int)$course;
        } else if (is_object($course)) {
            $courseid = $course->id;
        } else {
            throw new \moodle_exception(
                "\$course argument must be course id or course object",
                "local_treestudyplan",
                '',
                null,
                var_export($course, true)
            );
        }

        $search = \core_text::strtolower(trim($search));
        $conditions = "";
        $basesqlparams = ['courseid' => $courseid];

        if (strlen($search) > 0) {
            $conditions .= " AND " . $DB->sql_like('LOWER( b.name )', ':search');
            $basesqlparams["search"] = "%$search%";
        }
        if (false) { /* TODO: Add config option to only include active badges. */
            [$insql, $inparams] = $DB->get_in_or_equal([BADGE_STATUS_ACTIVE, BADGE_STATUS_ACTIVE_LOCKED], SQL_PARAMS_NAMED);
            $conditions .= " AND b.status $insql";
            $basesqlparams = array_merge($basesqlparams, $inparams);
        }

        [$ctypesql, $ctypeinparams] = $DB->get_in_or_equal(
            [BADGE_CRITERIA_TYPE_COURSESET, BADGE_CRITERIA_TYPE_COURSE],
            SQL_PARAMS_NAMED
        );
        $sqlparams = array_merge($basesqlparams, $ctypeinparams);

        // IMPORTANT: p.value in query below is varchar(255), but contains an int.
        $sql = "SELECT DISTINCT b.id from {badge} b
                INNER JOIN {badge_criteria} crit ON b.id = crit.badgeid
                INNER JOIN {badge_criteria_param} p on p.critid = crit.id
                WHERE p.value = :courseid AND crit.criteriatype $ctypesql $conditions";

        $courserelids = $DB->get_fieldset_sql($sql, $sqlparams);

        [$ctypesql, $ctypeinparams] = $DB->get_in_or_equal([BADGE_CRITERIA_TYPE_COMPETENCY], SQL_PARAMS_NAMED);
        $sqlparams = array_merge($basesqlparams, $ctypeinparams);
        $sql = "SELECT DISTINCT b.id from {badge} b
            INNER JOIN {badge_criteria} crit ON b.id = crit.badgeid
            INNER JOIN {badge_criteria_param} p on p.critid = crit.id
            INNER JOIN {competency_coursecomp} cc on cc.competencyid = CAST(p.value as INTEGER)
            WHERE cc.courseid = :courseid AND crit.criteriatype $ctypesql $conditions";

        $competencyrelids = $DB->get_fieldset_sql($sql, $sqlparams);

        $badgeids = [];
        foreach ([$courserelids, $competencyrelids] as $list) {
            foreach ($list as $id) {
                $badgeids[] = $id;
            }
        }

        return $badgeids;
    }

    /**
     * Find course badges
     * @param int|stdClass $course The course to relate to or it's id
     * @param string $search An optional search string to filter by
     * @return array of int
     */
    public static function find_course_badges($course, $search = '') {
        global $DB;
        if (is_numeric($course)) {
            $courseid = $course;
        } else if (is_object($course)) {
            $courseid = $course->id;
        } else {
            throw new \moodle_exception("\$course argument must be course id or course object", "local_treestudyplan");
        }

        $search = \core_text::strtolower(trim($search));
        $conditions = "";
        $basesqlparams = ['courseid' => (int)$courseid];

        if (strlen($search) > 0) {
            $conditions .= " AND " . $DB->sql_like('LOWER( b.name )', ':search');
            $basesqlparams["search"] = "%$search%";
        }
        if (false) { /* TODO: Add config option to only include active badges. */
            [$insql, $inparams] = $DB->get_in_or_equal([BADGE_STATUS_ACTIVE, BADGE_STATUS_ACTIVE_LOCKED]);
            $conditions .= " AND b.status $insql";
            $basesqlparams = array_merge($basesqlparams, $inparams);
        }

        // Get all course badges for this course.
        $badgesids = [];
        $sql = "SELECT DISTINCT b.id from {badge} b
            WHERE b.courseid = :courseid $conditions";

        $badgesids = $DB->get_fieldset_sql($sql, $basesqlparams);

        return $badgesids;
    }

    /**
     * Find badges related to a page
     * @param studyplanpage $page Page to search for/in
     * @param mixed $search Search string to use
     * @return array of badgeinfo simple models
     */
    public static function find_page_related_badges(studyplanpage $page, $search = "") {
        global $DB;
        $badgeids = [];

        if (true) {
            $coursesql =
                "SELECT DISTINCT i.course_id
                 FROM {local_treestudyplan_item} i
                 INNER JOIN {local_treestudyplan_line} l ON l.id = i.line_id
                 WHERE l.page_id = :pageid AND i.type = :coursetype
               ";
            $courseids = $DB->get_fieldset_sql($coursesql, ["pageid" => $page->id(), "coursetype" => studyitem::COURSE]);
            foreach ($courseids as $courseid) {
                $relatedbadges = self::find_badges_by_course_relation($courseid, $search);
                foreach ($relatedbadges as $id) {
                    $badgeids[] = $id;
                }
                $coursebadges = self::find_course_badges($courseid, $search);
                foreach ($coursebadges as $id) {
                    $badgeids[] = $id;
                }
            }
        }
        $badges = [];
        if (count($badgeids) > 0) {
            [$insql, $inparams] = $DB->get_in_or_equal($badgeids, SQL_PARAMS_NAMED);
            $sql = "SELECT b.* FROM {badge} b WHERE b.id {$insql}";

            $rs = $DB->get_recordset_sql($sql, $inparams);
            foreach ($rs as $r) {
                $badges[] = self::simple_model_from_db($r);
            }
            $rs->close();

            usort($badges, function ($a, $b) {
                return $a['name'] <=> $b['name'];
            });
        }
        return $badges;
    }

    /**
     * Search site badges
     * @param string $search An optional search string to filter by
     * @return array of badgeinfo simple models
     */
    public static function search_site_badges($search = '') {
        global $DB;

        $search = \core_text::strtolower(trim($search));
        $conditions = "TRUE";
        $basesqlparams = ['type' => \BADGE_TYPE_SITE];

        if (strlen($search) > 0) {
            $conditions .= " AND " . $DB->sql_like('LOWER( b.name )', ':search');
            $basesqlparams["search"] = "%$search%";
        }
        if (false) { /* TODO: Add config option to only include active badges.*/
            [$insql, $inparams] = $DB->get_in_or_equal([BADGE_STATUS_ACTIVE, BADGE_STATUS_ACTIVE_LOCKED]);
            $conditions .= " AND b.status $insql";
            $basesqlparams = array_merge($basesqlparams, $inparams);
        }

        // Get all course badges for this course.
        $sql = "SELECT b.* from {badge} b
            WHERE $conditions";

        $badges = [];
        $rs = $DB->get_recordset_sql($sql, $basesqlparams);
        foreach ($rs as $r) {
            $badges[] = self::simple_model_from_db($r);
        }
        $rs->close();

        usort($badges, function ($a, $b) {
            return $a['name'] <=> $b['name'];
        });

        return $badges;
    }
}
