<?php
// This file is part of Moodle - http://moodle.org/
//
// 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 <http://www.gnu.org/licenses/>.

/**
 * STACK question type restore code.
 * @package   qtype_stack
 * @copyright 2012 The Open University
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */


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

require_once($CFG->dirroot . '/question/type/stack/stack/graphlayout/graph.php');
require_once($CFG->dirroot . '/question/type/stack/stack/utils.class.php');


/**
 * Provides the information to restore STACK questions
 *
 * @package    qtype_stack
 * @copyright 2012 The Open University
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class restore_qtype_stack_plugin extends restore_qtype_plugin {

    /** @var string the name of the PRT we are currently restoring. */
    protected $currentprtname = null;

    /** @var int the name of the PRT we are currently restoring. */
    protected $currenttestcase = null;

    /**
     * Returns the paths to be handled by the plugin at question level
     */
    protected function define_question_plugin_structure() {

        $paths = [];

        // List the relevant paths in the XML.
        $elements = [
            'qtype_stack_options'        => '/stackoptions',
            'qtype_stack_input'          => '/stackinputs/stackinput',
            'qtype_stack_prt'            => '/stackprts/stackprt',
            'qtype_stack_prt_node'       => '/stackprts/stackprt/stackprtnodes/stackprtnode',
            'qtype_stack_qtest'          => '/stackqtests/stackqtest',
            'qtype_stack_qtest_input'    => '/stackqtests/stackqtest/stackqtestinputs/stackqtestinput',
            'qtype_stack_qtest_expected' => '/stackqtests/stackqtest/stackqtestexpecteds/stackqtestexpected',
            'qtype_stack_deployed_seed'  => '/stackdeployedseeds/stackdeployedseed',
        ];
        foreach ($elements as $elename => $path) {
            $paths[] = new restore_path_element($elename, $this->get_pathfor($path));
        }

        return $paths;
    }

    /**
     * Creates the data to be hashed in restore_questions_parser_processor::generate_question_identity_hash().
     * Resulting hash must match that obtained using output from qtype_stack->get_question_options() in
     * order to detect duplicate questions.
     * @param array $backupdata
     * @return stdClass
     */
    public static function convert_backup_to_questiondata(array $backupdata): stdClass {
        $questiondata = parent::convert_backup_to_questiondata($backupdata);
        if (isset($backupdata["plugin_qtype_stack_question"]['stackoptions'])) {
            $questiondata->options = (object) array_merge(
                (array) $questiondata->options,
                $backupdata["plugin_qtype_stack_question"]['stackoptions'][0],
            );
        }
        if (!property_exists($questiondata->options, 'stackversion')) {
            $questiondata->options->stackversion = '';
        }
        if (!property_exists($questiondata->options, 'isbroken')) {
            $questiondata->options->isbroken = 0;
        }

        if (!property_exists($questiondata->options, 'inversetrig')) {
            $questiondata->options->inversetrig = 'cos-1';
        }

        if (!property_exists($questiondata->options, 'logicsymbol')) {
            $questiondata->options->logicsymbol = 'lang';
        }

        if (!property_exists($questiondata->options, 'matrixparens')) {
            $questiondata->options->matrixparens = '[';
        }

        if (!property_exists($questiondata->options, 'questiondescription')) {
            $questiondata->options->questiondescription = '';
        }

        $questiondata->inputs = [];
        foreach ($backupdata["plugin_qtype_stack_question"]['stackinputs']['stackinput'] ?? [] as $input) {
            $questiondata->inputs[$input['name']] = (object) $input;
        }

        foreach ($questiondata->inputs as $input) {
            if (!property_exists($input, 'options')) {
                $input->options = '';
            }
            unset($input->id);
        }

        $questiondata->prts = [];
        foreach ($backupdata["plugin_qtype_stack_question"]['stackprts']['stackprt'] ?? [] as $prt) {
            $prt['name'] = (isset($prt['name'])) ? $prt['name'] : '';
            $nodes = [];
            unset($prt['id']);
            if (isset($prt['stackprtnodes']['stackprtnode'])) {
                foreach ($prt['stackprtnodes']['stackprtnode'] as $node) {
                    $node['prtname'] = $prt['name'];
                    unset($node['id']);
                    unset($node['questionid']);
                    $node['description'] = (isset($node['description'])) ? $node['description'] : '';
                    $node['truenextnode'] = (isset($node['truenextnode'])) ? $node['truenextnode'] : null;
                    $node['falsenextnode'] = (isset($node['falsenextnode'])) ? $node['falsenextnode'] : null;
                    $node['nodename'] = (isset($node['nodename'])) ? $node['nodename'] : '';
                    $nodes[] = $node;
                }
                unset($prt['stackprtnodes']);
            }
            if (!isset($prt['firstnodename'])) {
                $graph = new stack_abstract_graph();
                foreach ($nodes as $node) {
                    if ($node['truenextnode'] == -1) {
                        $left = null;
                    } else {
                        $left = (int) $node['truenextnode'] + 1;
                    }
                    if ($node['falsenextnode'] == -1) {
                        $right = null;
                    } else {
                        $right = (int) $node['falsenextnode'] + 1;
                    }
                    $graph->add_node((int) $node['nodename'] + 1, $node['description'], $left, $right);
                }
                try {
                    // If any of this goes wrong, we'll catch it all properly later in after_execute().
                    // Here we just need to match our later result.
                    $graph->layout();
                } catch (Exception $e) {
                    $prt['firstnodename'] = -1;
                    continue;
                }

                $roots = $graph->get_roots();
                reset($roots);
                $prt['firstnodename'] = key($roots) - 1;
            }
            $prt['nodes'] = $nodes;
            $questiondata->prts[$prt['name']] = (object) $prt;
        }

        $questiondata->deployedseeds = [];
        foreach ($backupdata["plugin_qtype_stack_question"]['stackdeployedseeds']['stackdeployedseed'] ?? [] as $seed) {
            $questiondata->deployedseeds[] = $seed['seed'];
        }

        return $questiondata;
    }

    /**
     * Fields to exclude from hash to detect duplicate questions.
     * @return string[]
     */
    protected function define_excluded_identity_hash_fields(): array {
        return [
            '/options/compiledcache',
            '/prts/questionid',
            '/prts/id',
            '/prts/nodes/questionid',
            '/prts/nodes/id',
            '/inputs/questionid',
            '/inputs/id',
        ];
    }

    /**
     * Process the STACK options.
     * @param array/object $data the data from the backup file.
     */
    public function process_qtype_stack_options($data) {
        global $DB;

        $data = (object)$data;

        if (!property_exists($data, 'stackversion')) {
            $data->stackversion = '';
        }

        if (!property_exists($data, 'isbroken')) {
            $data->isbroken = 0;
        }

        if (!property_exists($data, 'inversetrig')) {
            $data->inversetrig = 'cos-1';
        }

        if (!property_exists($data, 'logicsymbol')) {
            $data->logicsymbol = 'lang';
        }

        if (!property_exists($data, 'matrixparens')) {
            $data->matrixparens = '[';
        }

        if (!property_exists($data, 'questiondescription')) {
            $data->questiondescription = '';
        }

        // Detect if the question is created or mapped.
        $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));

        // If the question is being created by restore, save the stack options.
        if ($questioncreated) {
            $oldid = $data->id;
            $data->questionid = $this->get_new_parentid('question');
            $newitemid = $DB->insert_record('qtype_stack_options', $data);
            $this->set_mapping('qtype_stack_options', $oldid, $newitemid);
        }
    }

    /**
     * Process the STACK inputs.
     * @param array/object $data the data from the backup file.
     */
    public function process_qtype_stack_input($data) {
        global $DB;

        $data = (object)$data;

        if (!property_exists($data, 'options')) {
            $data->options = '';
        }

        // Detect if the question is created or mapped.
        $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));

        // If the question is being created, save this input.
        if ($questioncreated) {
            $data->questionid = $this->get_new_parentid('question');
            $DB->insert_record('qtype_stack_inputs', $data, false);
        }
    }

    /**
     * Process the STACK PRTs.
     * @param array/object $data the data from the backup file.
     */
    public function process_qtype_stack_prt($data) {
        global $DB;

        $data = (object)$data;

        $this->currentprtname = $data->name;

        if (!property_exists($data, 'firstnodename')) {
            $data->firstnodename = -1; // This will be fixed later.
        }

        // Detect if the question is created or mapped.
        $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));

        // If the question is being created, save this PRT.
        if ($questioncreated) {
            $data->questionid = $this->get_new_parentid('question');
            $DB->insert_record('qtype_stack_prts', $data, false);
        }
    }

    /**
     * Process the STACK PRT nodes.
     * @param array/object $data the data from the backup file.
     */
    public function process_qtype_stack_prt_node($data) {
        global $DB;

        $data = (object)$data;

        // Detect if the question is created or mapped.
        $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));

        // If the question is being created, save this input.
        if ($questioncreated) {
            $oldid = $data->id;

            $data->questionid = $this->get_new_parentid('question');
            $data->prtname = $this->currentprtname;
            $data->truepenalty = stack_utils::fix_approximate_thirds($data->truepenalty);
            $data->falsepenalty = stack_utils::fix_approximate_thirds($data->falsepenalty);

            $newitemid = $DB->insert_record('qtype_stack_prt_nodes', $data);
            $this->set_mapping('qtype_stack_prt_nodes', $oldid, $newitemid);
        }
    }

    /**
     * Process the STACK question tests.
     * @param array/object $data the data from the backup file.
     */
    public function process_qtype_stack_qtest($data) {
        global $DB;

        $data = (object)$data;

        $this->currenttestcase = $data->testcase;

        // Detect if the question is created or mapped.
        $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));

        // If the question is being created, save this input.
        if ($questioncreated) {
            $data->questionid = $this->get_new_parentid('question');
            $DB->insert_record('qtype_stack_qtests', $data, false);
        }
    }

    /**
     * Process the STACK question test input data.
     * @param array/object $data the data from the backup file.
     */
    public function process_qtype_stack_qtest_input($data) {
        global $DB;

        $data = (object)$data;

        // Detect if the question is created or mapped.
        $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));

        // If the question is being created, save this input.
        if ($questioncreated) {
            $data->questionid = $this->get_new_parentid('question');
            $data->testcase = $this->currenttestcase;
            $DB->insert_record('qtype_stack_qtest_inputs', $data, false);
        }
    }

    /**
     * Process the STACK question test expected results.
     * @param array/object $data the data from the backup file.
     */
    public function process_qtype_stack_qtest_expected($data) {
        global $DB;

        $data = (object)$data;

        // Detect if the question is created or mapped.
        $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));

        // If the question is being created, save this input.
        if ($questioncreated) {
            $data->questionid = $this->get_new_parentid('question');
            $data->testcase = $this->currenttestcase;
            $data->expectedpenalty = stack_utils::fix_approximate_thirds($data->expectedpenalty);
            $DB->insert_record('qtype_stack_qtest_expected', $data, false);
        }
    }

    /**
     * Process the STACK question tests.
     * @param array/object $data the data from the backup file.
     */
    public function process_qtype_stack_deployed_seed($data) {
        global $DB;

        $data = (object)$data;

        // Detect if the question is created or mapped.
        $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));

        // If the question is being created, save this input.
        if ($questioncreated) {
            $data->questionid = $this->get_new_parentid('question');
            $DB->insert_record('qtype_stack_deployed_seeds', $data, false);
        }
    }

    /**
     * Return the contents of this qtype to be processed by the links decoder
     */
    public static function define_decode_contents() {
        return [
            new restore_decode_content('qtype_stack_options',
                    ['specificfeedback', 'prtcorrect', 'prtpartiallycorrect', 'prtincorrect'],
                    'qtype_stack_options'),
            new restore_decode_content('qtype_stack_prt_nodes', ['truefeedback', 'falsefeedback'],
                    'qtype_stack_prt_nodes'),
        ];
    }

    /**
     * When restoring old data, that does not have the essay options information
     * in the XML, supply defaults.
     */
    protected function after_execute_question() {
        global $DB;

        $prtswithoutfirstnode = $DB->get_records('qtype_stack_prts',
                ['firstnodename' => -1], '', 'id, questionid, name');

        foreach ($prtswithoutfirstnode as $prt) {
            $nodes = $DB->get_records('qtype_stack_prt_nodes',
                    ['questionid' => $prt->questionid, 'prtname' => $prt->name], '',
                    'nodename, description, truenextnode, falsenextnode');

            // Find the root node of the PRT.
            // Otherwise, if an existing question is being edited, and this is an
            // existing PRT, base things on the existing question definition.
            $graph = new stack_abstract_graph();
            foreach ($nodes as $node) {

                if ($node->truenextnode == -1) {
                    $left = null;
                } else {
                    $left = (int) $node->truenextnode + 1;
                }
                if ($node->falsenextnode == -1) {
                    $right = null;
                } else {
                    $right = (int) $node->falsenextnode + 1;
                }

                $graph->add_node((int) $node->nodename + 1, $node->description, $left, $right);
            }
            try {
                $graph->layout();
            } catch (coding_exception $ce) {
                $question = $DB->get_record('question', ['id' => $prt->questionid]);
                $this->step->log('The PRT named "' . $prt->name .
                        '" is malformed in question id ' . $prt->questionid .
                        ' and cannot be laid out, (Question name "' . $question->name .
                        '").  Error reported: ' . $ce->getMessage(), backup::LOG_WARNING);
                continue;
            }

            $roots = $graph->get_roots();
            if (count($roots) != 1 || $graph->get_broken_cycles()) {
                $question = $DB->get_record('question', ['id' => $prt->questionid]);
                if (count($roots) != 1) {
                    $err = 'abnormal root count: '.count($roots).'(<>1)';
                } else {
                    $err = 'broken cycles: '.implode('.', $graph->get_broken_cycles());
                }
                $this->step->log('The PRT named "' . $prt->name .
                        '" is malformed in question id ' . $prt->questionid .
                        ' and does not have a single root, (Question name "' . $question->name .
                        '").  Error reported: ' . $err, backup::LOG_WARNING);
            }
            reset($roots);
            $firstnode = key($roots) - 1;

            $DB->set_field('qtype_stack_prts', 'firstnodename', $firstnode,
                    ['id' => $prt->id]);
        }
    }
}
