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

/**
 * Generate random grades and feedback to initialize development environment
 * @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;

use Exception;
/**
 * Generate random grades and feedback to initialize development environment
 */
class gradegenerator {
    /**
     * Table to store ficional skill data for students
     * @var array
     */
    private $table = [];

    /**
     * Lorem ipsum text to use if fortune is not available
     * @var string[]
     */
    private static $loremipsum = [
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
        "Etiam scelerisque ligula porttitor velit sollicitudin blandit.",
        "Praesent laoreet nisi id lacus laoreet volutpat.",
        "Donec rutrum tortor tempus lectus malesuada, ut pretium eros vehicula.",
        "Phasellus vulputate tortor vehicula mauris porta ultricies.",
        "Ut et lacus sit amet nisl facilisis elementum.",
        "Vestibulum ut mauris ac justo tincidunt hendrerit.",
        "Fusce congue nulla quis elit facilisis malesuada.",
        "Aenean ornare eros placerat ipsum fringilla, sed imperdiet felis imperdiet.",
        "Ut malesuada risus ultricies arcu dapibus, quis lobortis eros maximus.",
        "Nam ullamcorper dolor ac est tristique, vel blandit tortor tristique.",
        "Quisque quis lorem vitae leo lobortis posuere.",
        "Nulla ac enim consectetur, rhoncus eros sed, malesuada enim.",
        "Vestibulum lobortis lacus ac dolor pulvinar, gravida tincidunt dolor bibendum.",
        "Maecenas fringilla urna eget sem bibendum, non lacinia lorem tempus.",
        "Nullam quis metus sagittis, pharetra orci eget, ultrices nunc.",
        "Morbi et ante at ipsum sodales porta.",
        "Morbi vel neque in urna vestibulum vestibulum eu quis lectus.",
        "Nam consequat dolor at enim vestibulum, ac gravida nisl consequat.",
        "Phasellus ac libero vestibulum, vulputate tellus at, viverra dui.",
        "Vivamus venenatis magna a nunc cursus, eget laoreet velit malesuada.",
        "Cras fermentum velit vitae tellus sodales, vulputate semper purus porta.",
        "Cras ultricies orci in est elementum, at laoreet erat tempus.",
        "In non magna et lorem sagittis sollicitudin sit amet et est.",
        "Etiam vitae augue ac turpis volutpat iaculis a vitae enim.",
        "Integer pharetra quam ac tortor porta dignissim.",
        "Pellentesque ullamcorper neque vitae ligula rhoncus accumsan.",
        "Nullam in lectus sit amet est faucibus elementum vitae vel risus.",
        "Aenean vehicula libero ut convallis blandit.",
        "Aenean id mi facilisis, tristique enim vel, egestas lorem.",
        "Mauris suscipit dui eget neque gravida, vel pellentesque leo gravida.",
        "Quisque quis elit at velit maximus viverra ultricies in nisi.",
        "Vivamus et orci nec magna hendrerit egestas sed quis arcu.",
        "Suspendisse semper tortor sed justo iaculis volutpat.",
        "Praesent interdum dolor nec ultricies imperdiet.",
        "Vivamus tristique justo quis tellus commodo, at faucibus justo auctor.",
        "Praesent pharetra tellus vel nunc mattis pharetra.",
        "Cras a dui quis arcu rutrum ullamcorper sit amet et sem.",
        "Aenean porttitor risus ac enim tempor posuere.",
        "Mauris bibendum augue ac vehicula mattis.",
        "Vestibulum nec justo vehicula, euismod enim sed, convallis magna.",
        "Praesent ultrices elit vitae velit dignissim dignissim.",
        "Curabitur vehicula velit vitae tortor consequat consectetur sit amet at leo.",
        "Sed lobortis neque a magna facilisis aliquam.",
        "Phasellus a libero in sem aliquam varius.",
        "Mauris tincidunt ligula a risus efficitur euismod.",
        "Sed pharetra diam ac neque tempus convallis.",
        "Donec at ipsum elementum ex hendrerit laoreet mollis non elit.",
        "Praesent eu arcu sollicitudin, fermentum tellus at, blandit dolor.",
        "Curabitur in lectus consequat, bibendum ligula vitae, semper lacus.",
        "Aenean eu risus non sem pretium dictum.",
        "Praesent nec risus vestibulum quam venenatis tempor.",
        "Nullam rhoncus ex a quam egestas, eu auctor enim lobortis.",
        "Nam luctus ante id lacus scelerisque, quis blandit ante elementum.",
    ];

    /**
     * Generate random feedback from fortune or internal loremipsum text
     * @return string Randomized text to use as feedback
     */
    private function generatedfeedback() {
        if (file_exists("/usr/games/fortune")) {
            // Get a fortune if it is available.
            return shell_exec("/usr/games/fortune -n 160 -e disclaimer literature science pratchett wisdom education");
        } else {
            // Get a random loremipsum string.
            return self::$loremipsum[rand(0, count(self::$loremipsum) - 1)];
        }
    }

    /**
     * Constructor
     */
    public function __construct() {
    }

    /**
     * Register a new student in the skill table
     * @param string $student Student identifier
     */
    public function addstudent(string $student) {
        if (!array_key_exists($student, $this->table)) {
            $this->table[$student] = [
                "intelligence" => rand(70, 100),
                "endurance" => rand(70, 100),
                "skills" => [],
            ];
        }
    }

    /**
     * Add a specific skill for a student
     * @param string $student Student identifier
     * @param string $skill Skill identifier
     */
    public function addskill(string $student, string $skill) {
        $this->addstudent($student);
        if (!array_key_exists($skill, $this->table[$student]["skills"])) {
            $int = $this->table[$student]["intelligence"];
            $end = $this->table[$student]["endurance"];
            $this->table[$student]["skills"][$skill] = [
                "intelligence" => min(100, $int + rand(-30, 30)),
                "endurance" => min(100, $end + rand(-10, 10)),
            ];
        }
    }

    /**
     * Register a firstname and lastname for a student identifier for reference in the json file.
     * @param string $student Student identifier
     * @param string $firstname First name for student
     * @param string $lastname Last name for student
     */
    public function add_username_info(string $student, string $firstname, string $lastname) {
        $this->addstudent($student);
        $this->table[$student]["firstname"] = $firstname;
        $this->table[$student]["lastname"] = $lastname;
    }

    /**
     * Generate a number of random results for a given student and skill
     * @param string $student Student identifier
     * @param string $skill Skill identifier
     * @param int $count Number of results to generate
     * @return array Raw outcomes
     */
    public function generateraw($student, $skill, $count) {
        $this->addskill($student, $skill);
        $int = $this->table[$student]["skills"][$skill]["intelligence"];
        $end = $this->table[$student]["skills"][$skill]["endurance"];

        $results = [];
        $gaveup = false;
        for ($i = 0; $i < $count; $i++) {
            $r = new \stdClass();
            if ($gaveup) {
                $r->done = !$gaveup;
            } else {
                $r->done = (rand(0, $end) > 20); // Determine if the assignment was done.
            }
            if ($r->done) {
                $score = rand(0, $int);
                $r->result = ($score > 20); // Determine if the assignment was successful.
                if (!$r->result) {
                    $r->failed = !($score > 10);
                }
            } else {
                $r->result = false; // Make sure a result property is always there.
                $r->failed = true;
            }
            // Aways generate a little feedback.
            $r->fb = $this->generatedfeedback();
            $results[] = $r;

            if (!$gaveup && $i >= 3) {
                /* There is a slight chance the students with low endurance
                   for this course will stop with this course's work entirely.
                */
                $gaveup = (rand(0, $end) < 15);
            }
        }
        return $results;
    }

    /**
     * Generate results for a number of gradeinfo
     * Returns (object)[ "gi" => <gradeinfo object>,
     *                   "grade" => <randomized grade>
     *                   "fb" => <randomized feedback>
     *                 ];
     * @param string $student Student identifier
     * @param string $skill Skill identifier
     * @param gradeinfo[] $gradeinfos
     * @return array List of gradeinfo related results ready to add
     */
    public function generate(string $student, string $skill, array $gradeinfos) {
        global $DB;
        $table = "local_treestudyplan_gradecfg";

        $rlist = [];
        $gen = $this->generateraw($student, $skill, count($gradeinfos));
        $length = count($gradeinfos);
        for ($i = 0; $i < $length; $i++) {
            $g = $gradeinfos[$i];
            $gi = $g->get_gradeitem();
            $gr = $gen[$i];

            // First get the configured interpretation for this scale or grade.
            $scale = $gi->load_scale();
            if (isset($scale)) {
                $gradecfg = $DB->get_record($table, ["scale_id" => $scale->id]);
            } else if ($gi->grademin == 0) {
                $gradecfg = $DB->get_record($table, ["grade_points" => $gi->grademax]);
            } else {
                $gradecfg = null;
            }

            // Next generate the grade.
            if ($gradecfg) {
                if (!$gr->done) {
                    // INCOMPLETE.
                    // Fair chance of teacher forgetting to set incomplete to "no evidence".
                    $grade = 0;
                    $r = (object)["gi" => $g, "grade" => $grade, "fb" => "" ];
                } else if (!$gr->result) {
                    $grade = rand($gradecfg->min_progress, $gradecfg->min_completed - 1);
                    $r = (object)["gi" => $g, "grade" => $grade, "fb" => $gr->fb ];
                } else {
                    // COMPLETED.
                    $r = (object)["gi" => $g, "grade" => rand($gradecfg->min_completed, $gi->grademax), "fb" => $gr->fb];
                }

                $r->gradetext = $r->grade;
                if (isset($scale)) {
                    if ($r->grade > 0) {
                        $r->gradetext = trim($scale->get_nearest_item($r->grade));
                    } else {
                        $r->gradetext = "-";
                    }
                }
            } else if ($gi->gradepass > 0) {
                if (!$gr->done) {
                    // INCOMPLETe or FAILED.
                    $grade = rand(0, $gi->gradepass / 2);
                    $r = (object)["gi" => $g, "grade" => $grade, "fb" => ($grade > 0) ? $gr->fb : "" ];
                } else if (!$gr->result) {
                    // PROGRESS.
                    $r = (object)["gi" => $g, "grade" => rand(round($gi->gradepass / 2), $gi->gradepass - 1), "fb" => $gr->fb];
                } else {
                    // COMPLETED.
                    $r = (object)["gi" => $g, "grade" => rand($gi->gradepass, $gi->grademax), "fb" => $gr->fb];
                }

                $r->gradetext = $r->grade;
            } else {
                // Blind assumptions if nothing is provided.
                // Over 55% of range is completed.
                // Under 35% is not done.
                $range = floatval($gi->grademax - $gi->grademin);

                if (!$gr->done) {
                    // INCOMPLETe or FAILED.
                    $grade = rand(0, round($range * 0.35) - 1);
                    $r = (object)[
                            "gi" => $g,
                            "grade" => $gi->grademin + $grade,
                            "fb" => ($grade > 0) ? $gr->fb : "",
                         ];
                } else if (!$gr->result) {
                    // PROGRESS.
                    $r = (object)[
                            "gi" => $g,
                            "grade" => $gi->grademin + rand(round($range * 0.35), round($range * 0.55) - 1),
                            "fb" => $gr->fb,
                         ];
                } else {
                    // COMPLETED.
                    $r = (object)[
                            "gi" => $g,
                            "grade" => $gi->grademin + rand(round($range * 0.55), $range),
                            "fb" => $gr->fb,
                         ];
                }

                $r->gradetext = $r->grade;
            }

            $rlist[] = $r;
        }

        return $rlist;
    }

    /**
     * Get a fictional student's performance stats
     * @param string $student Student identifier
     * @return array Used stats for student
     */
    public function getstats($student) {
        return $this->table[$student];
    }

    /**
     * Store skills table into a string
     * @return string|null Json encoded string of the skills table
     */
    public function serialize(): string {
        $s = json_encode(["table" => $this->table], JSON_PRETTY_PRINT);
        if (!is_string($s)) {
            return "";
        }
        return $s;
    }

    /**
     * Load skills table from stored data
     * @param string $data Json encoded string of the skills table
     */
    public function unserialize(string $data): void {
        $o = json_decode($data, true);
        $this->table = $o["table"];
    }

    /**
     * Store skills table to file
     * @param string $filename The file to use
     */
    public function to_file(string $filename) {
        $filename = self::expand_tilde($filename);
        file_put_contents($filename, $this->serialize());
    }

    /**
     * Load skills table from file
     * @param string $filename The file to use
     */
    public function from_file(string $filename) {
        $filename = self::expand_tilde($filename);
        if (file_exists($filename)) {
            try {
                $json = file_get_contents($filename);
                $this->unserialize($json);
            } catch (Exception $x) {
                cli_problem("ERROR loading from file");
                throw $x; // Throw X up again to show the output.
            }
        }
    }

    /**
     * Internal function to properly handle the ~ symbol in unix context
     * @param string $path A unix path
     * @return string Unix path with ~ symbol properly expanded to user home dir
     */
    private static function expand_tilde($path) {
        if (function_exists('posix_getuid') && strpos($path, '~') !== false) {
            $info = posix_getpwuid(posix_getuid());
            $path = str_replace('~', $info['dir'], $path);
        }

        return $path;
    }
}
