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

/**
 * This defines the states a question can be in.
 *
 * @package    moodlecore
 * @subpackage questionengine
 * @copyright  2010 The Open University
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */


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


/**
 * An enumeration representing the states a question can be in after a
 * {@link question_attempt_step}.
 *
 * There are also some useful methods for testing and manipulating states.
 *
 * @copyright  2009 The Open University
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
abstract class question_state {
    /**#@+
     * Specific question_state instances.
     */
    public static $notstarted;
    public static $unprocessed;
    public static $todo;
    public static $invalid;
    public static $complete;
    public static $needsgrading;
    public static $finished;
    public static $gaveup;
    public static $gradedwrong;
    public static $gradedpartial;
    public static $gradedright;
    public static $manfinished;
    public static $mangaveup;
    public static $mangrwrong;
    public static $mangrpartial;
    public static $mangrright;
    /**#@+-*/

    protected function __construct() {
    }

    public static function init() {
        $us = new ReflectionClass('question_state');
        foreach ($us->getStaticProperties() as $name => $notused) {
            $class = 'question_state_' . $name;
            $states[$name] = new $class();
            self::$$name = $states[$name];
        }
    }

    /**
     * Get all the states in an array.
     *
     * @return question_state[] of question_state objects.
     */
    public static function get_all() {
        $states = array();
        $us = new ReflectionClass('question_state');
        foreach ($us->getStaticProperties() as $name => $notused) {
            $states[] = self::$$name;
        }
        return $states;
    }

    /**
     * Get all the states in an array.
     * @param string $summarystate one of the four summary states
     * inprogress, needsgrading, manuallygraded or autograded.
     * @return array of the corresponding states.
     */
    public static function get_all_for_summary_state($summarystate) {
        $states = array();
        foreach (self::get_all() as $state) {
            if ($state->get_summary_state() == $summarystate) {
                $states[] = $state;
            }
        }
        if (empty($states)) {
            throw new coding_exception('unknown summary state ' . $summarystate);
        }
        return $states;
    }

    /**
     * @return string convert this state to a string.
     */
    public function __toString() {
        return substr(get_class($this), 15);
    }

    /**
     * Get the instance of this class for a given state name.
     *
     * @param string $name a state name.
     * @return question_state|null the state with that name. (Null only in an exceptional case.)
     */
    public static function get(string $name): ?question_state {
        // In the past, there was a bug where null states got stored
        // in the database as an empty string, which was wrong because
        // the state column should be NOT NULL.
        // That is no longer possible, but we need to avoid exceptions
        // for people with old bad data in their database.
        if ($name === '') {
            debugging('Attempt to create a state from an empty string. ' .
                'This is probably a sign of bad data in your database. See MDL-80127.');
            return null;
        }

        return self::$$name;
    }

    /**
     * Is this state one of the ones that mean the question attempt is in progress?
     * That is, started, but no finished.
     * @return bool
     */
    public function is_active() {
        return false;
    }

    /**
     * Is this state one of the ones that mean the question attempt is finished?
     * That is, no further interaction possible, apart from manual grading.
     * @return bool
     */
    public function is_finished() {
        return true;
    }

    /**
     * Is this state one of the ones that mean the question attempt has been graded?
     * @return bool
     */
    public function is_graded() {
        return false;
    }

    /**
     * Is this state one of the ones that mean the question attempt has been graded?
     * @return bool
     */
    public function is_correct() {
        return false;
    }

    /**
     * Is this state one of the ones that mean the question attempt has been graded?
     * @return bool
     */
    public function is_partially_correct() {
        return false;
    }

    /**
     * Is this state one of the ones that mean the question attempt has been graded?
     * @return bool
     */
    public function is_incorrect() {
        return false;
    }

    /**
     * Is this state one of the ones that mean the question attempt has been graded?
     * @return bool
     */
    public function is_gave_up() {
        return false;
    }

    /**
     * Is this state one of the ones that mean the question attempt has had a manual comment added?
     * @return bool
     */
    public function is_commented() {
        return false;
    }

    /**
     * Each state can be categorised into one of four categories:
     * inprogress, needsgrading, manuallygraded or autograded.
     * @return string which category this state falls into.
     */
    public function get_summary_state() {
        if (!$this->is_finished()) {
            return 'inprogress';
        } else if ($this == self::$needsgrading) {
            return 'needsgrading';
        } else if ($this->is_commented()) {
            return 'manuallygraded';
        } else {
            return 'autograded';
        }
    }

    /**
     * Return the appropriate graded state based on a fraction. That is 0 or less
     * is $graded_incorrect, 1 is $graded_correct, otherwise it is $graded_partcorrect.
     * Appropriate allowance is made for rounding float values.
     *
     * @param number $fraction the grade, on the fraction scale.
     * @return question_state one of the state constants.
     */
    public static function graded_state_for_fraction($fraction) {
        if ($fraction < 0.000001) {
            return self::$gradedwrong;
        } else if ($fraction > 0.999999) {
            return self::$gradedright;
        } else {
            return self::$gradedpartial;
        }
    }

    /**
     * Return the appropriate manually graded state based on a fraction. That is 0 or less
     * is $manually_graded_incorrect, 1 is $manually_graded_correct, otherwise it is
     * $manually_graded_partcorrect. Appropriate allowance is made for rounding float values.
     *
     * @param number $fraction the grade, on the fraction scale.
     * @return int one of the state constants.
     */
    public static function manually_graded_state_for_fraction($fraction) {
        if (is_null($fraction)) {
            return self::$needsgrading;
        } else if ($fraction < 0.000001) {
            return self::$mangrwrong;
        } else if ($fraction > 0.999999) {
            return self::$mangrright;
        } else {
            return self::$mangrpartial;
        }
    }

    /**
     * Compute an appropriate state to move to after a manual comment has been
     * added to this state.
     * @param number $fraction the manual grade (if any) on the fraction scale.
     * @return int the new state.
     */
    public function corresponding_commented_state($fraction) {
        throw new coding_exception('Unexpected question state.');
    }

    /**
     * Return an appropriate CSS class name ''/'correct'/'partiallycorrect'/'incorrect',
     * for a state.
     * @return string
     */
    public function get_feedback_class() {
        return '';
    }

    /**
     * Return the name of an appropriate string to look up in the question
     * language pack for a state. This is used, for example, by
     * {@link question_behaviour::get_state_string()}. However, behaviours
     * sometimes change this default string for soemthing more specific.
     *
     * @param bool $showcorrectness Whether right/partial/wrong states should
     * be distinguised, or just treated as 'complete'.
     * @return string the name of a string that can be looked up in the 'question'
     *      lang pack, or used as a CSS class name, etc.
     */
    public abstract function get_state_class($showcorrectness);

    /**
     * The result of doing get_string on the result of {@link get_state_class()}.
     *
     * @param bool $showcorrectness Whether right/partial/wrong states should
     * be distinguised.
     * @return string a string from the lang pack that can be used in the UI.
     */
    public function default_string($showcorrectness) {
        return get_string($this->get_state_class($showcorrectness), 'question');
    }
}


/**#@+
 * Specific question_state subclasses.
 *
 * @copyright  2009 The Open University
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class question_state_notstarted extends question_state {
    public function is_finished() {
        return false;
    }
    public function get_state_class($showcorrectness) {
        throw new coding_exception('Unexpected question state.');
    }
}
class question_state_unprocessed extends question_state {
    public function is_finished() {
        return false;
    }
    public function get_state_class($showcorrectness) {
        throw new coding_exception('Unexpected question state.');
    }
}
class question_state_todo extends question_state {
    public function is_active() {
        return true;
    }
    public function is_finished() {
        return false;
    }
    public function get_state_class($showcorrectness) {
        return 'notyetanswered';
    }
}
class question_state_invalid extends question_state {
    public function is_active() {
        return true;
    }
    public function is_finished() {
        return false;
    }
    public function get_state_class($showcorrectness) {
        return 'invalidanswer';
    }
}
class question_state_complete extends question_state {
    public function is_active() {
        return true;
    }
    public function is_finished() {
        return false;
    }
    public function get_state_class($showcorrectness) {
        return 'answersaved';
    }
}
class question_state_needsgrading extends question_state {
    public function get_state_class($showcorrectness) {
        if ($showcorrectness) {
            return 'requiresgrading';
        } else {
            return 'complete';
        }
    }
    public function corresponding_commented_state($fraction) {
        return self::manually_graded_state_for_fraction($fraction);
    }
}
class question_state_finished extends question_state {
    public function get_state_class($showcorrectness) {
        return 'complete';
    }
    public function corresponding_commented_state($fraction) {
        return self::$manfinished;
    }
}
class question_state_gaveup extends question_state {
    public function is_gave_up() {
        return true;
    }
    public function get_feedback_class() {
        return 'incorrect';
    }
    public function get_state_class($showcorrectness) {
        return 'notanswered';
    }
    public function corresponding_commented_state($fraction) {
        if (is_null($fraction)) {
            return self::$mangaveup;
        } else {
            return self::manually_graded_state_for_fraction($fraction);
        }
    }
}
abstract class question_state_graded extends question_state {
    public function is_graded() {
        return true;
    }
    public function get_state_class($showcorrectness) {
        if ($showcorrectness) {
            return $this->get_feedback_class();
        } else {
            return 'complete';
        }
    }
    public function corresponding_commented_state($fraction) {
        return self::manually_graded_state_for_fraction($fraction);
    }
}
class question_state_gradedwrong extends question_state_graded {
    public function is_incorrect() {
        return true;
    }
    public function get_feedback_class() {
        return 'incorrect';
    }
}
class question_state_gradedpartial extends question_state_graded {
    public function is_graded() {
        return true;
    }
    public function is_partially_correct() {
        return true;
    }
    public function get_feedback_class() {
        return 'partiallycorrect';
    }
}
class question_state_gradedright extends question_state_graded {
    public function is_graded() {
        return true;
    }
    public function is_correct() {
        return true;
    }
    public function get_feedback_class() {
        return 'correct';
    }
}
class question_state_manfinished extends question_state_finished {
    public function is_commented() {
        return true;
    }
}
class question_state_mangaveup extends question_state_gaveup {
    public function is_commented() {
        return true;
    }
}
abstract class question_state_manuallygraded extends question_state_graded {
    public function is_commented() {
        return true;
    }
}
class question_state_mangrwrong extends question_state_manuallygraded {
    public function is_incorrect() {
        return false;
    }
    public function get_feedback_class() {
        return 'incorrect';
    }
}
class question_state_mangrpartial extends question_state_manuallygraded {
    public function is_partially_correct() {
        return true;
    }
    public function get_feedback_class() {
        return 'partiallycorrect';
    }
}
class question_state_mangrright extends question_state_manuallygraded {
    public function is_correct() {
        return true;
    }
    public function get_feedback_class() {
        return 'correct';
    }
}
/**#@-*/
question_state::init();
