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

/**
 * Base class for aggregators
 * @package    local_treestudyplan
 * @copyright  2023 P.M. Kuipers
 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace local_treestudyplan;

use moodle_exception;
use core_external\external_description;
use core_external\external_value;
use core_external\external_multiple_structure;
use core_external\external_single_structure;

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

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

/**
 * Base class for aggregators
 */
abstract class aggregator {
    /** Fallback aggregator if method not found
     * @var string
     * */
    private const FALLBACK = "core";
    /** @var string[] */
    private static $methodsupported = [];

    /**
     * Check if aggregation method is supported
     * @param string $method Name of aggregation method
     */
    public static function supported($method) {
        if (!array_key_exists($method, self::$methodsupported)) {
            self::$methodsupported[$method] = class_exists(self::aggregator_name($method));
        }
        return self::$methodsupported[$method];
    }

    /**
     * Get class name for aggregation method
     * @param string $method Name of aggregation method
     */
    private static function aggregator_name($method) {
        return "\local_treestudyplan\\local\\aggregators\\{$method}_aggregator";
    }

    /**
     * Get names of supported aggregation methods
     * @return string[] Names of aggregation methods
     */
    public static function list() {
        // Static list, since we'd need to implement a lot of static data for new aggregation methods anyway.
        // And this is faster than any dynamic method.
        return [
            "core", // Use moodle core completion.
            "competency",
            "bistate",
            "tristate", // Deprecated.
        ];
    }

    /**
     * Create a new aggregatior object based on the specified method
     * @param mixed $method Aggregation method
     * @param mixed $configstr Configuration string for aggregator
     * @throws moodle_exception If method is not found
     */
    public static function create($method, $configstr): object {
        if (self::supported($method)) {
            $agclass = self::aggregator_name($method);
            return new $agclass($configstr);
        } else {
            throw new moodle_exception("Cannot find aggregator '{$method}'");
        }
    }

    /**
     * Create a new aggregatior object based on the specified method,
     * but return a default aggregator if the method is not found
     * @param mixed $method Aggregation method
     * @param mixed $configstr Configuration string for aggregator
     */
    public static function create_or_default($method, $configstr): object {
        try {
            return self::create($method, $configstr);
        } catch (\moodle_exception $x) {
            return self::create(self::FALLBACK, "");
        }
    }

    /**
     * Create new instance of aggregation method
     * @param string $configstr Aggregation configuration string
     */
    private function __construct($configstr) {
        $this->initialize($configstr);
    }

    /**
     * Initialize the aggregation method
     * @param string $configstr Aggregation configuration string
     */
    protected function initialize($configstr) {
    }

    /**
     * Determine if aggregation method wants to select gradables
     * @return bool True if aggregation method needs gradables to be selected
     */
    abstract public function select_gradables();
    /**
     * Determine if aggregation method is deprecated
     * @return bool True if aggregation method is deprecated
     */
    abstract public function deprecated();

    /**
     * Aggregate all completions in a course into one final course completion
     * Possible states:
     *   completion::EXCELLENT  - Completed with excellent results
     *   completion::GOOD       - Completed with good results
     *   completion::COMPLETED  - Completed
     *   completion::PROGRESS   - Started, but not completed yey
     *   completion::FAILED     - Failed
     *   completion::INCOMPLETE - Not yet started
     * @param courseinfo $courseinfo Courseinfo object for the course to check
     * @param studyitem $studyitem Studyitem object for the course to check
     * @param int $userid Id of user to check this course for
     * @return int Aggregated completion as completion class constant
     */
    abstract public function aggregate_course(courseinfo $courseinfo, studyitem $studyitem, $userid);

    /**
     * Aggregate juncton/filter inputs into one final junction outcome
     * @param int[] $completion List of completion inputs
     * @param studyitem $studyitem Studyitem object for the junction
     * @param int $userid Id of user to check completion for
     * @return int Aggregated completion as completion class constant
     */
    abstract public function aggregate_junction(array $completion, studyitem $studyitem, $userid);

    /**
     * Determine completion for a single grade and user
     * @param gradeinfo $gradeinfo Gradeinfo object for grade to check
     * @param mixed $userid Id of user to check completion for
     * @return int Aggregated completion as completion class constant
     */
    abstract public function grade_completion(gradeinfo $gradeinfo, $userid);

    /**
     * Determine if the aggregation method uses core_completion, or treestudyplan custom completion.
     * @return bool True if the aggregation method uses core_completion
     */
    public function use_corecompletioninfo() {
        return false;
    }

    /**
     * Determine if the aggregation method uses course competencies,
     * @return bool True if the aggregation method uses course competencies
     */
    public function use_coursecompetencies() {
        return false;
    }

    /**
     * Determine if the aggregation method uses manual activity selection,
     * @return bool True if the aggregation method uses manual activity selection
     */
    public function use_manualactivityselection() {
        return false;
    }

    /**
     * Determine if Aggregation method makes use of "required grades" in a course/module.
     * @return bool True if Aggregation method makes use of "required grades" in a course/module.
     */
    public function use_required_grades() {
        return false;
    }

    /**
     * Determine if aggregation method makes use of item conditions
     * @return bool True if aggregation method makes use of
     */
    public function use_item_conditions() {
        return false;
    }

    /**
     * Return the current configuration string.
     * @return string Configuration string
     */
    public function config_string() {
        return "";
    }

    /**
     * Webservice structure for basic aggregator info
     * @param int $value Webservice requirement constant
     */
    public static function basic_structure($value = VALUE_REQUIRED): external_description {
        return new external_single_structure([
            "useRequiredGrades" => new external_value(PARAM_BOOL, 'id of studyplan'),
            "useItemConditions" => new external_value(PARAM_BOOL, 'name of studyplan'),
        ], "Aggregator requirements", $value);
    }

    /**
     * Webservice model for basic aggregator info
     * @return array Webservice data model
     */
    public function basic_model() {
        return [
            "useRequiredGrades" => $this->use_required_grades(),
            "useItemConditions" => $this->use_item_conditions(),
        ];
    }

    /**
     * Webservice structure list of aggregators
     * @param int $value Webservice requirement constant
     * @return mixed Webservice output structure
     */
    public static function list_structure($value = VALUE_REQUIRED): external_description {
        return  new external_multiple_structure(new external_single_structure([
            "id" => new external_value(PARAM_TEXT, 'id of aggregator'),
            "name" => new external_value(PARAM_TEXT, 'name of agregator'),
            "deprecated" => new external_value(PARAM_BOOL, 'if method is deprecated'),
            "defaultconfig" => new external_value(PARAM_TEXT, 'default config of agregator'),

        ], "Available aggregators", $value));
    }

    /**
     * Webservice model for list of aggregators
     * @return array Webservice data model
     */
    public static function list_model() {

        $list = [];
        foreach (self::list() as $agid) {
            $a = self::create($agid, ""); // Create new one with empty config string.
            $list[] = [
                'id' => $agid,
                'name' => get_string("{$agid}_aggregator_title", "local_treestudyplan"),
                'deprecated' => $a->deprecated(),
                'defaultconfig' => $a->config_string(),
            ];
        }

        return $list;
    }
}
