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

namespace tool_usertours;

/**
 * Tour class.
 *
 * @package    tool_usertours
 * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class tour {
    /**
     * The tour is currently disabled
     *
     * @var DISABLED
     */
    const DISABLED = 0;

    /**
     * The tour is currently disabled
     *
     * @var DISABLED
     */
    const ENABLED = 1;

    /**
     * The user preference value to indicate the time of completion of the tour for a user.
     *
     * @var TOUR_LAST_COMPLETED_BY_USER
     */
    const TOUR_LAST_COMPLETED_BY_USER   = 'tool_usertours_tour_completion_time_';

    /**
     * The user preference value to indicate the time that a user last requested to see the tour.
     *
     * @var TOUR_REQUESTED_BY_USER
     */
    const TOUR_REQUESTED_BY_USER        = 'tool_usertours_tour_reset_time_';

    /** @var int Whether to show the tour only until it has been marked complete */
    const SHOW_TOUR_UNTIL_COMPLETE = 1;

    /** @var int Whether to show the tour every time a page matches */
    const SHOW_TOUR_ON_EACH_PAGE_VISIT = 2;

    /**
     * @var $id The tour ID.
     */
    protected $id;

    /**
     * @var $name The tour name.
     */
    protected $name;

    /**
     * @var $description The tour description.
     */
    protected $description;

    /**
     * @var $pathmatch The tour pathmatch.
     */
    protected $pathmatch;

    /**
     * @var $enabled The tour enabled state.
     */
    protected $enabled;

    /**
     * @var $endtourlabel The end tour label.
     */
    protected $endtourlabel;

    /**
     * @var $sortorder The sort order.
     */
    protected $sortorder;

    /**
     * @var $dirty Whether the current view of the tour has been modified.
     */
    protected $dirty = false;

    /**
     * @var $config The configuration object for the tour.
     */
    protected $config;

    /**
     * @var $filtervalues The filter configuration object for the tour.
     */
    protected $filtervalues;

    /**
     * @var $steps  The steps in this tour.
     */
    protected $steps = [];

    /**
     * @var bool $displaystepnumbers Display the step numbers in this tour.
     */
    protected $displaystepnumbers = true;

    /**
     * Create an instance of the specified tour.
     *
     * @param   int         $id         The ID of the tour to load.
     * @return  tour
     */
    public static function instance($id) {
        $tour = new self();
        return $tour->fetch($id);
    }

    /**
     * Create an instance of tour from its provided DB record.
     *
     * @param   stdClass    $record     The record of the tour to load.
     * @param   boolean     $clean      Clean the values.
     * @return  tour
     */
    public static function load_from_record($record, $clean = false) {
        $tour = new self();
        return $tour->reload_from_record($record, $clean);
    }

    /**
     * Fetch the specified tour into the current object.
     *
     * @param   int         $id         The ID of the tour to fetch.
     * @return  tour
     */
    protected function fetch($id) {
        global $DB;

        return $this->reload_from_record(
            $DB->get_record('tool_usertours_tours', ['id' => $id], '*', MUST_EXIST)
        );
    }

    /**
     * Reload the current tour from database.
     *
     * @return  tour
     */
    protected function reload() {
        return $this->fetch($this->id);
    }

    /**
     * Reload the tour into the current object.
     *
     * @param   stdClass    $record     The record to reload.
     * @param   boolean     $clean      Clean the values.
     * @return  tour
     */
    protected function reload_from_record($record, $clean = false) {
        $this->id           = $record->id;
        if (!property_exists($record, 'description')) {
            if (property_exists($record, 'comment')) {
                $record->description = $record->comment;
                unset($record->comment);
            }
        }
        if ($clean) {
            $this->name         = clean_param($record->name, PARAM_TEXT);
            $this->description  = clean_text($record->description);
        } else {
            $this->name         = $record->name;
            $this->description  = $record->description;
        }
        $this->pathmatch    = $record->pathmatch;
        $this->enabled      = $record->enabled;
        if (isset($record->sortorder)) {
            $this->sortorder = $record->sortorder;
        }
        $this->endtourlabel = $record->endtourlabel ?? null;
        $this->config       = json_decode($record->configdata);
        $this->dirty        = false;
        $this->steps        = [];
        $this->displaystepnumbers = !empty($record->displaystepnumbers);

        return $this;
    }

    /**
     * Fetch all steps in the tour.
     *
     * @return  step[]
     */
    public function get_steps() {
        if (empty($this->steps)) {
            $this->steps = helper::get_steps($this->id);
        }

        return $this->steps;
    }

    /**
     * Count the number of steps in the tour.
     *
     * @return  int
     */
    public function count_steps() {
        return count($this->get_steps());
    }

    /**
     * The ID of the tour.
     *
     * @return  int
     */
    public function get_id() {
        return $this->id;
    }

    /**
     * The name of the tour.
     *
     * @return  string
     */
    public function get_name() {
        return $this->name;
    }

    /**
     * Set the name of the tour to the specified value.
     *
     * @param   string      $value      The new name.
     * @return  $this
     */
    public function set_name($value) {
        $this->name = clean_param($value, PARAM_TEXT);
        $this->dirty = true;

        return $this;
    }

    /**
     * The description associated with the tour.
     *
     * @return  string
     */
    public function get_description() {
        return $this->description;
    }

    /**
     * Set the description of the tour to the specified value.
     *
     * @param   string      $value      The new description.
     * @return  $this
     */
    public function set_description($value) {
        $this->description = clean_text($value);
        $this->dirty = true;

        return $this;
    }

    /**
     * The path match for the tour.
     *
     * @return  string
     */
    public function get_pathmatch() {
        return $this->pathmatch;
    }

    /**
     * Set the patchmatch of the tour to the specified value.
     *
     * @param   string      $value      The new patchmatch.
     * @return  $this
     */
    public function set_pathmatch($value) {
        $this->pathmatch = $value;
        $this->dirty = true;

        return $this;
    }

    /**
     * The enabled state of the tour.
     *
     * @return  int
     */
    public function get_enabled() {
        return $this->enabled;
    }

    /**
     * Whether the tour is currently enabled.
     *
     * @return  boolean
     */
    public function is_enabled() {
        return ($this->enabled == self::ENABLED);
    }

    /**
     * Set the enabled state of the tour to the specified value.
     *
     * @param   boolean     $value      The new state.
     * @return  $this
     */
    public function set_enabled($value) {
        $this->enabled = $value;
        $this->dirty = true;

        return $this;
    }

    /**
     * The end tour label for the tour.
     *
     * @return string
     */
    public function get_endtourlabel(): string {
        if ($this->endtourlabel) {
            $label = helper::get_string_from_input($this->endtourlabel);
        } else if ($this->count_steps() == 1) {
            $label = get_string('endonesteptour', 'tool_usertours');
        } else {
            $label = get_string('endtour', 'tool_usertours');
        }

        return $label;
    }

    /**
     * Set the endtourlabel of the tour to the specified value.
     *
     * @param string $value
     * @return $this
     */
    public function set_endtourlabel(string $value): tour {
        $this->endtourlabel = $value;
        $this->dirty = true;

        return $this;
    }

    /**
     * The link to view this tour.
     *
     * @return  \moodle_url
     */
    public function get_view_link() {
        return helper::get_view_tour_link($this->id);
    }

    /**
     * The link to edit this tour.
     *
     * @return  \moodle_url
     */
    public function get_edit_link() {
        return helper::get_edit_tour_link($this->id);
    }

    /**
     * The link to reset the state of this tour for all users.
     *
     * @return  moodle_url
     */
    public function get_reset_link() {
        return helper::get_reset_tour_for_all_link($this->id);
    }

    /**
     * The link to export this tour.
     *
     * @return  moodle_url
     */
    public function get_export_link() {
        return helper::get_export_tour_link($this->id);
    }

    /**
     * The link to duplicate this tour.
     *
     * @return  moodle_url
     */
    public function get_duplicate_link() {
        return helper::get_duplicate_tour_link($this->id);
    }

    /**
     * The link to remove this tour.
     *
     * @return  moodle_url
     */
    public function get_delete_link() {
        return helper::get_delete_tour_link($this->id);
    }

    /**
     * Prepare this tour for saving to the database.
     *
     * @return  object
     */
    public function to_record() {
        return (object) [
            'id'            => $this->id,
            'name'          => $this->name,
            'description'   => $this->description,
            'pathmatch'     => $this->pathmatch,
            'enabled'       => $this->enabled,
            'sortorder'     => $this->sortorder,
            'endtourlabel'  => $this->endtourlabel,
            'configdata'    => json_encode($this->config),
            'displaystepnumbers' => $this->displaystepnumbers,
        ];
    }

    /**
     * Get the current sortorder for this tour.
     *
     * @return  int
     */
    public function get_sortorder() {
        return (int) $this->sortorder;
    }

    /**
     * Whether this tour is the first tour.
     *
     * @return  boolean
     */
    public function is_first_tour() {
        return ($this->get_sortorder() === 0);
    }

    /**
     * Whether this tour is the last tour.
     *
     * @param   int         $tourcount  The pre-fetched count of tours
     * @return  boolean
     */
    public function is_last_tour($tourcount = null) {
        if ($tourcount === null) {
            $tourcount = helper::count_tours();
        }
        return ($this->get_sortorder() === ($tourcount - 1));
    }

    /**
     * Set the sortorder for this tour.
     *
     * @param   int         $value      The new sortorder to use.
     * @return  $this
     */
    public function set_sortorder($value) {
        $this->sortorder = $value;
        $this->dirty = true;

        return $this;
    }

    /**
     * Calculate the next sort-order value.
     *
     * @return  int
     */
    protected function calculate_sortorder() {
        $this->sortorder = helper::count_tours();

        return $this;
    }

    /**
     * Get the link to move this tour up in the sortorder.
     *
     * @return  moodle_url
     */
    public function get_moveup_link() {
        return helper::get_move_tour_link($this->get_id(), helper::MOVE_UP);
    }

    /**
     * Get the link to move this tour down in the sortorder.
     *
     * @return  moodle_url
     */
    public function get_movedown_link() {
        return helper::get_move_tour_link($this->get_id(), helper::MOVE_DOWN);
    }

    /**
     * Get the value of the specified configuration item.
     *
     * @param   string      $key        The configuration key to set.
     * @param   mixed       $default    The default value to use if a value was not found.
     * @return  mixed
     */
    public function get_config($key = null, $default = null) {
        if ($this->config === null) {
            $this->config = (object) [];
        }
        if ($key === null) {
            return $this->config;
        }

        if (property_exists($this->config, $key)) {
            return $this->config->$key;
        }

        if ($default !== null) {
            return $default;
        }

        return configuration::get_default_value($key);
    }

    /**
     * Set the configuration item as specified.
     *
     * @param   string      $key        The configuration key to set.
     * @param   mixed       $value      The new value for the configuration item.
     * @return  $this
     */
    public function set_config($key, $value) {
        if ($this->config === null) {
            $this->config = (object) [];
        }
        $this->config->$key = $value;
        $this->dirty = true;

        return $this;
    }

    /**
     * Save the tour and it's configuration to the database.
     *
     * @param   boolean     $force      Whether to force writing to the database.
     * @return  $this
     */
    public function persist($force = false) {
        global $DB;

        if (!$this->dirty && !$force) {
            return $this;
        }

        if ($this->id) {
            $record = $this->to_record();
            $DB->update_record('tool_usertours_tours', $record);
        } else {
            $this->calculate_sortorder();
            $record = $this->to_record();
            unset($record->id);
            $this->id = $DB->insert_record('tool_usertours_tours', $record);
        }

        $this->reload();

        // Notify the cache that a tour has changed.
        cache::notify_tour_change();

        return $this;
    }

    /**
     * Remove this step.
     */
    public function remove() {
        global $DB;

        if ($this->id === null) {
            // Nothing to delete - this tour has not been persisted.
            return null;
        }

        // Delete all steps associated with this tour.
        // Note, although they are currently just DB records, there may be other components in the future.
        foreach ($this->get_steps() as $step) {
            $step->remove();
        }

        // Remove the configuration for the tour.
        $DB->delete_records('tool_usertours_tours', ['id' => $this->id]);
        helper::reset_tour_sortorder();

        $this->remove_user_preferences();

        return null;
    }

    /**
     * Reset the sortorder for all steps in the tour.
     *
     * @return  $this
     */
    public function reset_step_sortorder() {
        global $DB;
        $steps = $DB->get_records('tool_usertours_steps', ['tourid' => $this->id], 'sortorder ASC', 'id');

        $index = 0;
        foreach ($steps as $step) {
            $DB->set_field('tool_usertours_steps', 'sortorder', $index, ['id' => $step->id]);
            $index++;
        }

        // Notify of a change to the step configuration.
        // Note: Do not notify of a tour change here. This is only a step change for a tour.
        cache::notify_step_change($this->get_id());

        return $this;
    }

    /**
     * Remove stored user preferences for the tour
     */
    protected function remove_user_preferences(): void {
        global $DB;

        $DB->delete_records('user_preferences', ['name' => self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id()]);
        $DB->delete_records('user_preferences', ['name' => self::TOUR_REQUESTED_BY_USER . $this->get_id()]);
    }

    /**
     * Whether this tour should be displayed to the user.
     *
     * @return  boolean
     */
    public function should_show_for_user() {
        if (!$this->is_enabled()) {
            // The tour is disabled - it should not be shown.
            return false;
        }

        if ($this->get_showtourwhen() === self::SHOW_TOUR_ON_EACH_PAGE_VISIT) {
            // The tour should be shown on every page visit.
            return true;
        }

        if ($tourcompletiondate = get_user_preferences(self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id(), null)) {
            if ($tourresetdate = get_user_preferences(self::TOUR_REQUESTED_BY_USER . $this->get_id(), null)) {
                if ($tourresetdate >= $tourcompletiondate) {
                    return true;
                }
            }
            $lastmajorupdate = $this->get_config('majorupdatetime', time());
            if ($tourcompletiondate > $lastmajorupdate) {
                // The user has completed the tour since the last major update.
                return false;
            }
        }

        return true;
    }

    /**
     * Get the key for this tour.
     * This is used in the session cookie to determine whether the user has seen this tour before.
     */
    public function get_tour_key() {
        global $USER;

        $tourtime = $this->get_config('majorupdatetime', null);

        if ($tourtime === null) {
            // This tour has no majorupdate time.
            // Set one now to prevent repeated displays to the user.
            $this->set_config('majorupdatetime', time());
            $this->persist();
            $tourtime = $this->get_config('majorupdatetime', null);
        }

        if ($userresetdate = get_user_preferences(self::TOUR_REQUESTED_BY_USER . $this->get_id(), null)) {
            $tourtime = max($tourtime, $userresetdate);
        }

        return sprintf('tool_usertours_%d_%d_%s', $USER->id, $this->get_id(), $tourtime);
    }

    /**
     * Reset the requested by user date.
     *
     * @return  $this
     */
    public function request_user_reset() {
        set_user_preference(self::TOUR_REQUESTED_BY_USER . $this->get_id(), time());

        return $this;
    }

    /**
     * Mark this tour as completed for this user.
     *
     * @return  $this
     */
    public function mark_user_completed() {
        set_user_preference(self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id(), time());

        return $this;
    }

    /**
     * Update a tour giving it a new major update time.
     * This will ensure that it is displayed to all users, even those who have already seen it.
     *
     * @return  $this
     */
    public function mark_major_change() {
        // Clear old reset and completion notes.
        $this->remove_user_preferences();

        $this->set_config('majorupdatetime', time());
        $this->persist();

        return $this;
    }

    /**
     * Add the step configuration to the form.
     *
     * @param   MoodleQuickForm $mform      The form to add configuration to.
     * @return  $this
     */
    public function add_config_to_form(\MoodleQuickForm &$mform) {
        $options = configuration::get_placement_options();
        $mform->addElement('select', 'placement', get_string('placement', 'tool_usertours'), $options);
        $mform->addHelpButton('placement', 'placement', 'tool_usertours');

        $this->add_config_field_to_form($mform, 'orphan');
        $this->add_config_field_to_form($mform, 'backdrop');
        $this->add_config_field_to_form($mform, 'reflex');

        return $this;
    }

    /**
     * Add the specified step field configuration to the form.
     *
     * @param   MoodleQuickForm $mform      The form to add configuration to.
     * @param   string          $key        The key to add.
     * @return  $this
     */
    protected function add_config_field_to_form(\MoodleQuickForm &$mform, $key) {
        $options = [
            true    => get_string('yes'),
            false   => get_string('no'),
        ];
        $mform->addElement('select', $key, get_string($key, 'tool_usertours'), $options);
        $mform->setDefault($key, configuration::get_default_value($key));
        $mform->addHelpButton($key, $key, 'tool_usertours');

        return $this;
    }

    /**
     * Prepare the configuration data for the moodle form.
     *
     * @return  object
     */
    public function prepare_data_for_form() {
        $data = $this->to_record();
        $data->showtourwhen = $this->get_showtourwhen();
        foreach (configuration::get_defaultable_keys() as $key) {
            $data->$key = $this->get_config($key, configuration::get_default_value($key));
        }

        return $data;
    }

    /**
     * Get the configured filter values.
     *
     * @param   string      $filter     The filter to retrieve values for.
     * @return  array
     */
    public function get_filter_values($filter) {
        if ($allvalues = (array) $this->get_config('filtervalues')) {
            if (isset($allvalues[$filter])) {
                return $allvalues[$filter];
            }
        }

        return [];
    }

    /**
     * Set the values for the specified filter.
     *
     * @param   string      $filter     The filter to set.
     * @param   array       $values     The values to set.
     * @return  $this
     */
    public function set_filter_values($filter, array $values = []) {
        $allvalues = (array) $this->get_config('filtervalues', []);
        $allvalues[$filter] = $values;

        return $this->set_config('filtervalues', $allvalues);
    }

    /**
     * Check whether this tour matches all filters.
     *
     * @param   \context     $context    The context to check.
     * @param   array|null   $filters    Optional array of filters.
     * @return  bool
     */
    public function matches_all_filters(\context $context, array $filters = null): bool {
        if (!$filters) {
            $filters = helper::get_all_filters();
        }

        // All filters must match.
        // If any one filter fails to match, we return false.
        foreach ($filters as $filterclass) {
            if (!$filterclass::filter_matches($this, $context)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Gets all filter values for use in client side filters.
     *
     * @param   array     $filters    Array of clientside filters.
     * @return  array
     */
    public function get_client_filter_values(array $filters): array {
        $results = [];

        foreach ($filters as $filter) {
            $results[$filter::get_filter_name()] = $filter::get_client_side_values($this);
        }

        return $results;
    }

    /**
     * Set the value for the display step numbers setting.
     *
     * @param bool $value True for enable.
     * @return $this
     */
    public function set_display_step_numbers(bool $value): tour {
        $this->displaystepnumbers = $value;
        $this->dirty = true;

        return $this;
    }

    /**
     * Get the value of the display step numbers setting.
     *
     * @return bool
     */
    public function get_display_step_numbers(): bool {
        return $this->displaystepnumbers;
    }

    /**
     * Set the value for the when to show the tour.
     *
     * @see self::SHOW_TOUR_UNTIL_COMPLETE
     * @see self::SHOW_TOUR_ON_EACH_PAGE_VISIT
     *
     * @param int $value
     * @return self
     */
    public function set_showtourwhen(int $value): tour {
        return $this->set_config('showtourwhen', $value);
    }

    /**
     * When to show the tour.
     *
     * @see self::SHOW_TOUR_UNTIL_COMPLETE
     * @see self::SHOW_TOUR_ON_EACH_PAGE_VISIT
     *
     * @return int
     */
    public function get_showtourwhen(): int {
        return $this->get_config('showtourwhen', self::SHOW_TOUR_UNTIL_COMPLETE);
    }
}
