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

/**
 * Aggregate course results with moodle course completion
 * @package    local_treestudyplan
 * @copyright  2023 P.M. Kuipers
 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace local_treestudyplan\local\aggregators;

use core_competency\api as c_api;
use local_treestudyplan\courseinfo;
use local_treestudyplan\gradeinfo;
use local_treestudyplan\studyitem;
use local_treestudyplan\completion;
use local_treestudyplan\coursecompetencyinfo;

/**
 * Aggregate course results with moodle course completion
 */
class competency_aggregator extends \local_treestudyplan\aggregator {
    /** @var bool */
    public const DEPRECATED = false;
    /** @var stdClass */
    private $agcfg = null;

    /**
     * Retrieve or initialize current config object
     * @return stdClass
     */
    private function cfg() {
        if (empty($this->agcfg)) {
            $this->agcfg = (object)[
                'thresh_completed' => 0.66, // Minimum fraction that must be completed to aggregate as completed.
                'use_failed' => true, // Support failed completion yes/no.
            ];
        }
        return $this->agcfg;
    }

    /**
     * Create new instance of aggregation method
     * @param string $configstr Aggregation configuration string
     */
    public function __construct($configstr) {
        // Allow public constructor for testing purposes.
        $this->initialize($configstr);
    }

    /**
     * Initialize the aggregation method
     * @param string $configstr Aggregation configuration string
     */
    protected function initialize($configstr) {
        // First initialize with the defaults.
        foreach (["thresh_completed" ] as $key) {
            $val = intval(get_config('local_treestudyplan', "competency_{$key}"));
            if ($val >= 0 && $val <= 100) {
                $this->cfg()->$key = floatval($val) / 100;
            }
        }
        foreach (["use_failed" ] as $key) {
            $this->cfg()->$key = boolval(get_config('local_treestudyplan', "competency_{$key}"));
        }

        // Next, decode json.
        $config = \json_decode($configstr, true);

        if (is_array($config)) {
            // Copy all valid config settings to this item.
            foreach (["thresh_completed" ] as $key) {
                if (array_key_exists($key, $config)) {
                    $val = $config[$key];
                    if ($val >= 0 && $val <= 100) {
                        $this->cfg()->$key = floatval($val) / 100;
                    }
                }
            }
            foreach (["use_failed" ] as $key) {
                if (array_key_exists($key, $config)) {
                    $this->cfg()->$key = boolval($config[$key]);
                }
            }
        }
    }

    /**
     * Return the current configuration string.
     * @return string Configuration string
     */
    public function config_string() {
        return json_encode([
            "thresh_completed" => 100 * $this->cfg()->thresh_completed,
            "use_failed" => $this->cfg()->use_failed,
        ]);
    }

    /**
     * Determine if aggregation method wants to select gradables
     * @return bool True if aggregation method needs gradables to be selected
     */
    public function select_gradables() {
        return false;
    }

    /**
     * Determine if aggregation method is deprecated
     * @return bool True if aggregation method is deprecated
     */
    public function deprecated() {
        return self::DEPRECATED;
    }

    /**
     * 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 true;
    }

    /**
     * Determine if aggregation method makes use of required grades
     * @return bool True if aggregation method makes use of
     */
    public function use_item_conditions() {
        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 true;
    }

    /**
     * Return course completion information based on the core completion infromation
     * 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
     */
    public function aggregate_course(courseinfo $courseinfo, studyitem $studyitem, $userid) {
        // Retrieve the course competencies.
        $course = $courseinfo->course();
        $cci = new coursecompetencyinfo($course, $studyitem);

        $competencies = $cci->course_competencies();
        if (count($competencies) == 0) {
            return completion::INCOMPLETE;
        }
        $count = 0;
        $proficient = 0;
        $requiredmet = 0;
        $requiredcount = 0;
        foreach ($competencies as $c) {
            $count += 1;
            $p = $cci->proficiency($c, $userid);
            if ($p->proficient) {
                $proficient += 1;
            }
            if ($cci->is_required(($c))) {
                $requiredcount += 1;
                if ($p->proficient) {
                    $requiredmet += 1;
                }
            }
        }

        // Determine minimum for.
        $limit = $this->cfg()->thresh_completed * $count;
        $coursefinished = ($course->enddate) ? ($course->enddate < time()) : false;

        if ($proficient >= $count && $requiredmet >= $requiredcount) {
            if ($limit < $count) {
                return completion::EXCELLENT;
            } else {
                return completion::COMPLETED;
            }
        } else if ($proficient > $limit && $requiredmet >= $requiredcount) {
            return completion::COMPLETED;
        } else if ($proficient > 0) {
            if ($this->cfg()->use_failed && $coursefinished) {
                return completion::FAILED;
            } else {
                return completion::PROGRESS;
            }
        } else {
            if ($this->cfg()->use_failed && $coursefinished) {
                return completion::FAILED;
            } else {
                return completion::INCOMPLETE;
            }
        }
    }

    /**
     * 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
     */
    public function aggregate_junction(array $completion, studyitem $studyitem, $userid = 0) {
        // Aggregate multiple incoming states into one junction or finish.
        // Possible states:.
        // - completion::EXCELLENT  - All incoming states are excellent.
        // - completion::GOOD       - All incoming states are at least good.
        // - completion::COMPLETED  - All incoming states are at least completed.
        // - completion::FAILED     - All incoming states are failed.
        // - completion::INCOMPLETE - All incoming states are incomplete.
        // - completion::PROGRESS   - All other states.

        $method = strtoupper($studyitem->conditions()); // One of ANY or ALL.

        // First count all states.
        $statecount = completion::count_states($completion);
        $total = count($completion);

        if ($method == "ANY") {
            if ($statecount[completion::EXCELLENT] >= 1) {
                return completion::EXCELLENT;
            } else if ($statecount[completion::GOOD] >= 1) {
                return completion::GOOD;
            } else if ($statecount[completion::COMPLETED] >= 1) {
                return completion::COMPLETED;
            } else if ($statecount[completion::PROGRESS] >= 1) {
                return completion::PROGRESS;
            } else if ($statecount[completion::FAILED] >= 1) {
                return completion::FAILED;
            } else {
                return completion::INCOMPLETE;
            }
        } else { /* default value of ALL */
            if ($total == $statecount[completion::EXCELLENT]) {
                return completion::EXCELLENT;
            } else if (
                $total == (
                    $statecount[completion::EXCELLENT]
                    + $statecount[completion::GOOD]
                )
            ) {
                return completion::GOOD;
            } else if (
                $total == (
                    $statecount[completion::EXCELLENT]
                    + $statecount[completion::GOOD]
                    + $statecount[completion::COMPLETED]
                )
            ) {
                return completion::COMPLETED;
            } else if ($statecount[completion::FAILED]) {
                return completion::FAILED;
            } else if ($total == $statecount[completion::INCOMPLETE]) {
                return completion::INCOMPLETE;
            } else {
                return completion::PROGRESS;
            }
        }
    }

    /**
     * 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
     */
    public function grade_completion(gradeinfo $gradeinfo, $userid) {
        // COURSE COMPETENCIES DOESN'T REALLY USE THIS FUNCTION.
        return 0;
    }
}
