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

/**
 * Date condition.
 *
 * @package availability_date
 * @copyright 2014 The Open University
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace availability_date;

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

/**
 * Date condition.
 *
 * @package availability_date
 * @copyright 2014 The Open University
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class condition extends \core_availability\condition {
    /** @var string Availabile only from specified date. */
    const DIRECTION_FROM = '>=';

    /** @var string Availabile only until specified date. */
    const DIRECTION_UNTIL = '<';

    /** @var string One of the DIRECTION_xx constants. */
    private $direction;

    /** @var int Time (Unix epoch seconds) for condition. */
    private $time;

    /** @var int Forced current time (for unit tests) or 0 for normal. */
    private static $forcecurrenttime = 0;

    /**
     * Constructor.
     *
     * @param \stdClass $structure Data structure from JSON decode
     * @throws \coding_exception If invalid data structure.
     */
    public function __construct($structure) {
        // Get direction.
        if (isset($structure->d) && in_array($structure->d,
                array(self::DIRECTION_FROM, self::DIRECTION_UNTIL))) {
            $this->direction = $structure->d;
        } else {
            throw new \coding_exception('Missing or invalid ->d for date condition');
        }

        // Get time.
        if (isset($structure->t) && is_int($structure->t)) {
            $this->time = $structure->t;
        } else {
            throw new \coding_exception('Missing or invalid ->t for date condition');
        }
    }

    public function save() {
        return (object)array('type' => 'date',
                'd' => $this->direction, 't' => $this->time);
    }

    /**
     * Returns a JSON object which corresponds to a condition of this type.
     *
     * Intended for unit testing, as normally the JSON values are constructed
     * by JavaScript code.
     *
     * @param string $direction DIRECTION_xx constant
     * @param int $time Time in epoch seconds
     * @return stdClass Object representing condition
     */
    public static function get_json($direction, $time) {
        return (object)array('type' => 'date', 'd' => $direction, 't' => (int)$time);
    }

    public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
        return $this->is_available_for_all($not);
    }

    public function is_available_for_all($not = false) {
        // Check condition.
        $now = self::get_time();
        switch ($this->direction) {
            case self::DIRECTION_FROM:
                $allow = $now >= $this->time;
                break;
            case self::DIRECTION_UNTIL:
                $allow = $now < $this->time;
                break;
            default:
                throw new \coding_exception('Unexpected direction');
        }
        if ($not) {
            $allow = !$allow;
        }

        return $allow;
    }

    /**
     * Obtains the actual direction of checking based on the $not value.
     *
     * @param bool $not True if condition is negated
     * @return string Direction constant
     * @throws \coding_exception
     */
    protected function get_logical_direction($not) {
        switch ($this->direction) {
            case self::DIRECTION_FROM:
                return $not ? self::DIRECTION_UNTIL : self::DIRECTION_FROM;
            case self::DIRECTION_UNTIL:
                return $not ? self::DIRECTION_FROM : self::DIRECTION_UNTIL;
            default:
                throw new \coding_exception('Unexpected direction');
        }
    }

    public function get_description($full, $not, \core_availability\info $info) {
        return $this->get_either_description($not, false);
    }

    public function get_standalone_description(
            $full, $not, \core_availability\info $info) {
        return $this->get_either_description($not, true);
    }

    /**
     * Shows the description using the different lang strings for the standalone
     * version or the full one.
     *
     * @param bool $not True if NOT is in force
     * @param bool $standalone True to use standalone lang strings
     */
    protected function get_either_description($not, $standalone) {
        $direction = $this->get_logical_direction($not);
        $midnight = self::is_midnight($this->time);
        $midnighttag = $midnight ? '_date' : '';
        $satag = $standalone ? 'short_' : 'full_';
        switch ($direction) {
            case self::DIRECTION_FROM:
                return get_string($satag . 'from' . $midnighttag, 'availability_date',
                        self::show_time($this->time, $midnight, false));
            case self::DIRECTION_UNTIL:
                return get_string($satag . 'until' . $midnighttag, 'availability_date',
                        self::show_time($this->time, $midnight, true));
        }
    }

    protected function get_debug_string() {
        return $this->direction . ' ' . gmdate('Y-m-d H:i:s', $this->time);
    }

    /**
     * Gets time. This function is implemented here rather than calling time()
     * so that it can be overridden in unit tests. (Would really be nice if
     * Moodle had a generic way of doing that, but it doesn't.)
     *
     * @return int Current time (seconds since epoch)
     */
    protected static function get_time() {
        if (self::$forcecurrenttime) {
            return self::$forcecurrenttime;
        } else {
            return time();
        }
    }

    /**
     * Forces the current time for unit tests.
     *
     * @param int $forcetime Time to return from the get_time function
     */
    public static function set_current_time_for_test($forcetime = 0) {
        self::$forcecurrenttime = $forcetime;
    }

    /**
     * Shows a time either as a date or a full date and time, according to
     * user's timezone.
     *
     * @param int $time Time
     * @param bool $dateonly If true, uses date only
     * @param bool $until If true, and if using date only, shows previous date
     * @return string Date
     */
    protected function show_time($time, $dateonly, $until = false) {
        // For 'until' dates that are at midnight, e.g. midnight 5 March, it
        // is better to word the text as 'until end 4 March'.
        $daybefore = false;
        if ($until && $dateonly) {
            $daybefore = true;
            $time = strtotime('-1 day', $time);
        }
        return userdate($time,
                get_string($dateonly ? 'strftimedate' : 'strftimedatetime', 'langconfig'));
    }

    /**
     * Checks whether a given time refers exactly to midnight (in current user
     * timezone).
     *
     * @param int $time Time
     * @return bool True if time refers to midnight, false otherwise
     */
    protected static function is_midnight($time) {
        return usergetmidnight($time) == $time;
    }

    public function update_after_restore(
            $restoreid, $courseid, \base_logger $logger, $name) {
        // Update the date, if restoring with changed date.
        $dateoffset = \core_availability\info::get_restore_date_offset($restoreid);
        if ($dateoffset) {
            $this->time += $dateoffset;
            return true;
        }
        return false;
    }

    /**
     * Changes all date restrictions on a course by the specified shift amount.
     * Used by the course reset feature.
     *
     * @param int $courseid Course id
     * @param int $timeshift Offset in seconds
     */
    public static function update_all_dates($courseid, $timeshift) {
        global $DB;

        $modinfo = get_fast_modinfo($courseid);
        $anychanged = false;

        // Adjust dates from all course modules.
        foreach ($modinfo->cms as $cm) {
            if (!$cm->availability) {
                continue;
            }
            $info = new \core_availability\info_module($cm);
            $tree = $info->get_availability_tree();
            $dates = $tree->get_all_children('availability_date\condition');
            $changed = false;
            foreach ($dates as $date) {
                $date->time += $timeshift;
                $changed = true;
            }

            // Save the updated course module.
            if ($changed) {
                $DB->set_field('course_modules', 'availability', json_encode($tree->save()),
                        array('id' => $cm->id));
                $anychanged = true;
            }
        }

        // Adjust dates from all course sections.
        foreach ($modinfo->get_section_info_all() as $section) {
            if (!$section->availability) {
                continue;
            }

            $info = new \core_availability\info_section($section);
            $tree = $info->get_availability_tree();
            $dates = $tree->get_all_children('availability_date\condition');
            $changed = false;
            foreach ($dates as $date) {
                $date->time += $timeshift;
                $changed = true;
            }

            // Save the updated course module.
            if ($changed) {
                $updatesection = new \stdClass();
                $updatesection->id = $section->id;
                $updatesection->availability = json_encode($tree->save());
                $updatesection->timemodified = time();
                $DB->update_record('course_sections', $updatesection);
                // Invalidate the section cache by given section id.
                \course_modinfo::purge_course_section_cache_by_id($courseid, $section->id);

                $anychanged = true;
            }
        }

        if ($anychanged) {
            // Partial rebuild the sections which have been invalidated.
            rebuild_course_cache($courseid, true, true);
        }
    }
}
