<?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/>.
/**
 *
 * @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 \local_treestudyplan\courseinfo;
use \local_treestudyplan\gradeinfo;
use \local_treestudyplan\studyitem;
use \local_treestudyplan\completion;
use \local_treestudyplan\debug;

class bistate_aggregator extends \local_treestudyplan\aggregator {
    public const DEPRECATED = false;
    private const DEFAULT_CONDITION = "50";

    private $threshexcellent = 1.0;            // Minimum fraction that must be completed to aggregate as excellent (usually 1.0).
    private $threshgood = 0.8;                 // Minimum fraction that must be completed to aggregate as good.
    private $threshcompleted = 0.66;           // Minimum fraction that must be completed to aggregate as completed.
    private $usefailed = True;                 // Support failed completion yes/no.
    private $threshprogress = 0.33;            // Minimum fraction that must be failed to aggregate as failed instead of progress.
    private $acceptpending_as_submitted = False;  // Also count ungraded but submitted .

    public function __construct($configstr) {
        // Allow public constructor for testing purposes.
        $this->initialize($configstr);
    }

    protected function initialize($configstr) {
        // First initialize with the defaults.

        foreach (["thresh_excellent", "thresh_good", "thresh_completed", "thresh_progress", ] as $key) {
            $val = intval(get_config('local_treestudyplan', "bistate_{$key}"));
            if ($val >= 0 && $val <= 100) {
                $this->$key = floatval($val)/100;
            }
        }
        foreach (["use_failed", "accept_pending_as_submitted"] as $key) {
            $this->$key = boolval(get_config('local_treestudyplan', "bistate_{$key}"));
        }

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

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

        }
    }

    // Return active configuration model.
    public function config_string() {
        return json_encode([
            "thresh_excellent" => 100*$this->thresh_excellent,
            "thresh_good" => 100*$this->thresh_good,
            "thresh_completed" => 100*$this->thresh_completed,
            "thresh_progress" => 100*$this->thresh_progress,
            "use_failed" => $this->use_failed,
            "accept_pending_as_submitted" => $this->accept_pending_as_submitted,
        ]);
    }

    public function needSelectGradables() { return True;}
    public function isDeprecated() { return self::DEPRECATED;}
    public function useRequiredGrades() { return True;}
    public function useItemConditions() { return False;}

    public function aggregate_binary_goals(array $completions, array $required = []) {
        // Function is public to allow access for the testing code.

        // Return te following conditions.
        // Possible states:.
        // - completion::EXCELLENT  - At least $threshexcellent fraction of goals are complete and all required goals are met.
        // - completion::GOOD       - At least $threshgood fraction of goals are complete and all required goals are met.
        // - completion::COMPLETED  - At least $threshcomplete fraction of goals are completed and all required goals are met.
        // - completion::FAILED     - At least $threshprogress fraction of goals is not failed.
        // - completion::INCOMPLETE - No goals have been started.
        // - completion::PROGRESS   - All other states.

        $total = count($completions);
        $completed = 0;
        $progress = 0;
        $failed = 0;
        $started = 0;

        $totalrequired = 0;
        $requiredmet = 0;

        $MINPROGRESS = ($this->accept_pending_as_submitted) ? completion::PENDING : completion::PROGRESS;

        foreach ($completions as $index => $c) {

            $completed += ($c >= completion::COMPLETED)?1:0;
            $progress  += ($c >= $MINPROGRESS)?1:0;
            $failed    += ($c <= completion::FAILED)?1:0;
        }
        $started = $progress + $failed;
        $allrequiredmet = ($requiredmet >= $totalrequired);

        $fractioncompleted = ($total >0) ? (floatval($completed)/floatval($total)) : 0.0;
        $fractionprogress  = ($total >0) ? (floatval($progress)/floatval($total)) : 0.0;
        $fractionfailed    = ($total >0) ? (floatval($failed)/floatval($total)) : 0.0;
        $fractionstarted   = ($total >0) ? (floatval($started)/floatval($total)) : 0.0;

        if ($total == 0) {
            return completion::INCOMPLETE;
        }
        if ($fractioncompleted >= $this->thresh_excellent && $allrequiredmet) {
            return completion::EXCELLENT;
        } else if ($fractioncompleted >= $this->thresh_good && $allrequiredmet) {
            return completion::GOOD;
        } else if ($fractioncompleted >= $this->thresh_completed && $allrequiredmet) {
            return completion::COMPLETED;
        } else if ($started == 0) {
            return completion::INCOMPLETE;
        } else {
            return completion::PROGRESS;
        }
    }

    public function aggregate_course(courseinfo $courseinfo, studyitem $studyitem, $userid) {
        $course = $courseinfo->course();
        $coursefinished = ($course->enddate) ? ($course->enddate < time()) : false;
        // Note: studyitem condition config is not used in this aggregator.
        // Loop through all associated gradables and count the totals, completed, etc..
        $completions = [];
        $required = [];
        foreach (gradeinfo::list_studyitem_gradables($studyitem) as $gi) {
            $completions[] = $this->grade_completion($gi, $userid);
            if ($gi->is_required()) {
                // If it's a required grade .
                // Also add it's index in the completion list to the list of required grades .
                $required[] = count($completions) - 1;
            }
        }

        // Combine the aquired completions into one.
        $result = self::aggregate_binary_goals($completions, $required);
        if ($this->use_failed && $result == completion::PROGRESS && $coursefinished) {
            return completion::FAILED;
        } else {
            return $result;
        }

    }

    public function aggregate_junction(array $completion, studyitem $studyitem = null, $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.

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

        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;
        }
    }

    public function grade_completion(gradeinfo $gradeinfo, $userid) {
        global $DB;
        $table = "local_treestudyplan_gradecfg";
        $gradeitem = $gradeinfo->getGradeitem();
        $grade = $gradeitem->get_final($userid);
        $course = \get_course($gradeitem->courseid); // Fetch course from cache.
        $coursefinished = ($course->enddate) ? ($course->enddate < time()) : false;

        if (empty($grade)) {
            return completion::INCOMPLETE;
        } else if ($grade->finalgrade === NULL) {
            // On assignments, grade NULL means a submission has not yet been graded,.
            // But on quizes this can also mean a quiz might have been started.
            // Therefor, we treat a NULL result as a reason to check the relevant gradingscanner for presence of pending items.

            // Since we want old results to be visible until a pending item was graded, we only use this state here.
            // Pending items are otherwise expressly indicated by the "pendingsubmission" field in the user model.
            if ($gradeinfo->getGradingscanner()->pending($userid)) {
                return completion::PENDING;
            } else {
                return completion::INCOMPLETE;
            }
        } else {
            $grade = $gradeitem->get_final($userid);
            // First determine if we have a grade_config for this scale or this maximum grade.
            $finalgrade = $grade->finalgrade;
            $scale = $gradeinfo->getScale();
            if ( isset($scale)) {
                $gradecfg = $DB->get_record($table, ["scale_id" => $scale->id]);
            } else if ($gradeitem->grademin == 0) {
                $gradecfg = $DB->get_record($table, ["grade_points" => $gradeitem->grademax]);
            } else {
                $gradecfg = null;
            }

            // For point grades, a provided grade pass overrides the defaults in the gradeconfig.
            // For scales, the configuration in the gradeconfig is leading.

            if ($gradecfg && (isset($scale) || $gradeitem->gradepass == 0)) {
                // If so, we need to know if the grade is .
                if ($finalgrade >= $gradecfg->min_completed) {
                    // Return completed if completed.
                    return completion::COMPLETED;
                } else if ($this->use_failed && $coursefinished) {
                    // Return failed if failed is enabled and the grade is less than the minimum grade for progress.
                    return completion::FAILED;
                } else {
                    return completion::PROGRESS;
                }
            } else if ($gradeitem->gradepass > 0) {
                $range = floatval($gradeitem->grademax - $gradeitem->grademin);
                // If no gradeconfig and gradepass is set, use that one to determine config.
                if ($finalgrade >= $gradeitem->gradepass) {
                    return completion::COMPLETED;
                } else if ($this->use_failed && $coursefinished) {
                    // Return failed if failed is enabled and the grade is 1, while there are at leas 3 states.
                    return completion::FAILED;
                } else {
                    return completion::PROGRESS;
                }
            } else {
                // Blind assumptions if nothing is provided.
                // Over 55% of range is completed.
                // If range >= 3 and failed is enabled, assume that this means failed.
                $g = floatval($finalgrade - $gradeitem->grademin);
                $range = floatval($gradeitem->grademax - $gradeitem->grademin);
                $score = $g / $range;

                if ($score > 0.55) {
                    return completion::COMPLETED;
                } else if ($this->use_failed && $coursefinished) {
                    // Return failed if failed is enabled and the grade is 1, while there are at leas 3 states.
                    return completion::FAILED;
                } else {
                    return completion::PROGRESS;
                }
            }
        }
    }
}