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

/**
 * DM - Availability Window
 *
 * @package availability_dmavailabilitywindow
 * @copyright 2020 NED
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace availability_dmavailabilitywindow;

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

use availability_dmavailabilitywindow\shared_lib as NED;

/**
 * dmavailabilitywindow condition
 */
class condition extends \core_availability\condition {
    /** @var array[] - [[time_window_start0, time_window_end0], [time_window_start1, time_window_end1],... ]  */
    protected $_windows = [];

    /**
     * Saves tree data back to a structure object.
     *
     * @return \stdClass Structure object (ready to be made into JSON format)
     */
    public function save(){
        return (object)['type' => 'dmavailabilitywindow'];
    }

    /**
     * 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.
     *
     * @return \stdClass Object representing condition
     */
    public static function get_json(){
        return (object)['type' => 'dmavailabilitywindow'];
    }

    /**
     * Determines whether a particular item is currently available
     * according to this availability condition.
     *
     * If implementations require a course or modinfo, they should use
     * the get methods in $info.
     *
     * The $not option is potentially confusing. This option always indicates
     * the 'real' value of NOT. For example, a condition inside a 'NOT AND'
     * group will get this called with $not = true, but if you put another
     * 'NOT OR' group inside the first group, then a condition inside that will
     * be called with $not = false. We need to use the real values, rather than
     * the more natural use of the current value at this point inside the tree,
     * so that the information displayed to users makes sense.
     *
     * @param bool                    $not        Set true if we are inverting the condition
     * @param \core_availability\info $info       Item we're checking
     * @param bool                    $grabthelot Performance hint: if true, caches information
     *                                            required for all course-modules, to make the front page and similar
     *                                            pages work more quickly (works only for current user)
     * @param int                     $userid     User ID to check availability for
     *
     * @return bool True if available
     */
    public function is_available($not, \core_availability\info $info, $grabthelot, $userid){
        $this->_windows = [];
        $use_only_window_start = false;
        // only for situation, when $use_only_window_start = true
        $window_starts = [];

        /** @var array[] $cfg_windows - [[cfg_window_start0, cfg_window_end0], [cfg_window_start1, cfg_window_end1],... ] */
        $cfg_windows = [];
        $course = $info->get_course();
        $context = $info->get_context();
        $allow_res = true;

        do {
            if ($context->contextlevel != CONTEXT_MODULE) break;
            if (NED::is_tt_exists() && has_capability('block/ned_teacher_tools:overriderestrictions', $context, $userid)) break;

            $cm = NED::get_cm_by_cmorid($context->instanceid, $course);
            if (empty($cm)) break;

            $role_windows = $this->get_role_windows($cm);
            if (empty($role_windows)) break;

            $roles = get_user_roles($context, $userid, true);
            foreach ($roles as $role){
                if (!empty($role_windows[$role->shortname])){
                    $cfg_windows[] = $role_windows[$role->shortname];
                }
            }
            if (empty($cfg_windows)) break;

            $deadlines = $this->get_user_deadlines($cm, $course, $userid);
            if (empty($deadlines)) break;

            // we have done CFG and DM prechecks => next we will check DM windows, so we need some more info about user
            $check_after_time = 0;
            $is_teacher = NED::has_capability('mod/assign:grade', $context, $userid) ||
                NED::has_capability('applyteacherrestriction', $context, $userid);
            if (!$is_teacher){
                $gg = NED::get_grade_grade($cm, $userid, false);
                $check_after_time = (empty($gg) || is_null($gg->finalgrade) || empty($gg->timemodified)) ? 0 : $gg->timemodified;
                if (empty($check_after_time) && $cm->modname == NED::QUIZ){
                    $check_after_time = NED::quiz_get_submitted_time_by_cm($cm, $userid);
                }
                $use_only_window_start = empty($check_after_time);
            }

            // we have all we need, let's create real window limits and check them
            // note: we need all windows, even if we know restriction result
            $allow_res = false;
            $now = time();
            foreach ($cfg_windows as $cfg_window){
                [$days_before, $days_after] = $cfg_window;
                foreach ($deadlines as $deadline){
                    $window_start = $deadline - ($days_before * DAYSECS);
                    $allow_start = $window_start < $now;

                    if ($use_only_window_start){
                        $window_starts[] = $window_start;
                        $allow_end = true;
                    } else {
                        $check_end_time = $is_teacher ? $deadline : $check_after_time;
                        if ($check_end_time){
                            $window_end = $check_end_time + ($days_after * DAYSECS);
                            $allow_end = $window_end > $now;
                        } else {
                            // normally shouldn't be this situation
                            $window_end = 0;
                            $allow_end = true;
                        }

                        $window_key = $window_start;
                        while (isset($this->_windows[$window_key])){
                            $window_key++;
                        }
                        $this->_windows[$window_key] = [$window_start, $window_end];
                    }

                    $allow_res = $allow_res || ($allow_start && $allow_end);
                }
            }
        } while(false);

        if ($use_only_window_start && !empty($window_starts)){
            $window_start = NED::min(...$window_starts);
            $this->_windows = [$window_start => [$window_start, 0]];
        }
        if (!empty($this->_windows)){
            ksort($this->_windows);
        }

        return $not ? !$allow_res : $allow_res;
    }

    /**
     * Obtains a string describing this restriction (whether or not
     * it actually applies). Used to obtain information that is displayed to
     * students if the activity is not available to them, and for staff to see
     * what conditions are.
     *
     * The $full parameter can be used to distinguish between 'staff' cases
     * (when displaying all information about the activity) and 'student' cases
     * (when displaying only conditions they don't meet).
     *
     * If implementations require a course or modinfo, they should use
     * the get methods in $info. They should not use any other functions that
     * might rely on modinfo, such as format_string.
     *
     * To work around this limitation, use the functions:
     *
     * description_cm_name()
     * description_format_string()
     * description_callback()
     *
     * These return special markers which will be added to the string and processed
     * later after modinfo is complete.
     *
     * @param bool $full Set true if this is the 'full information' view
     * @param bool $not  Set true if we are inverting the condition
     * @param \core_availability\info $info Item we're checking
     *
     * @return string Information string (for admin) about all restrictions on
     *   this item
     */
    public function get_description($full, $not, \core_availability\info $info){
        if (empty($this->_windows)){
            return NED::str('window_description_none');
        }

        $str_date_none = NED::str('date_none');
        $str_date_format = NED::str('date_format');
        $f_date = function($time) use ($str_date_none, $str_date_format){
            if (empty($time)) return $str_date_none;
            return userdate($time, $str_date_format);
        };

        if (count($this->_windows) == 1){
            [$window_start, $window_end] = reset($this->_windows);
            if (empty($window_end)){
                $res = NED::str('window_description_after', $f_date($window_start));
            } else {
                $res = NED::str('window_description_single', ['start' => $f_date($window_start), 'end' => $f_date($window_end)]);
            }
        } else {
            $str_windows = [];
            foreach ($this->_windows as $window){
                [$window_start, $window_end] = $window;
                $str_windows[] = $f_date($window_start).' – '.$f_date($window_end);
            }
            $res = NED::str('window_description_list', NED::arr2str($str_windows, '', ', '));
        }

        return $res;
    }

    /**
     * Obtains a representation of the options of this condition as a string,
     * for debugging.
     *
     * @return string Text representation of parameters
     */
    protected function get_debug_string(){
        return gmdate('Y-m-d H:i:s');
    }

    /**
     * Obtains a string describing this restriction, used when there is only
     * a single restriction to display. (I.e. this provides a 'short form'
     * rather than showing in a list.)
     *
     * Default behaviour sticks the prefix text, normally displayed above
     * the list, in front of the standard get_description call.
     *
     * If implementations require a course or modinfo, they should use
     * the get methods in $info. They should not use any other functions that
     * might rely on modinfo, such as format_string.
     *
     * To work around this limitation, use the functions:
     *
     * description_cm_name()
     * description_format_string()
     * description_callback()
     *
     * These return special markers which will be added to the string and processed
     * later after modinfo is complete.
     *
     * @param bool                    $full Set true if this is the 'full information' view
     * @param bool                    $not  Set true if we are inverting the condition
     * @param \core_availability\info $info Item we're checking
     *
     * @return string Information string (for admin) about all restrictions on
     *   this item
     */
    public function get_standalone_description($full, $not, \core_availability\info $info){
        return $this->get_description($full, $not, $info);
    }

    /**
     * Get array of config texts by tag ids
     *
     * @return array - [tag_id => text_config_block(multiline)]
     */
    static public function get_plugin_restrictions(){
        $plugin_restrictions = [];
        $config = NED::get_config();

        if ($config->numberofrestrictions ?? 0){
            for ($i = 1;  $i <= $config->numberofrestrictions; $i++){
                $tagid = $config->{'tag' . $i} ?? 0;
                $availability_window_cfg = $config->{'availabilitywindow' . $i} ?? null;
                if (is_null($availability_window_cfg)) continue;

                $plugin_restrictions[$tagid] = $availability_window_cfg;
            }
        }

        return $plugin_restrictions;
    }

    /**
     * Get array of config texts by tag ids for the current activity (by $cm_or_id)
     *
     * @param int|\cm_info|object $cm_or_id - Id of course-module, or database object
     *
     * @return array - [tag_id => text_config_block(multiline)]
     */
    public function get_activity_restrictions($cm_or_id){
        $activity_restrictions = [];

        do {
            $tags = NED::cm_get_tags($cm_or_id);
            if (empty($tags)) break;

            $plugin_restrictions = static::get_plugin_restrictions();
            if (empty($plugin_restrictions)) break;

            foreach ($tags as $tagid => $tag){
                if (!empty($plugin_restrictions[$tagid])){
                    $activity_restrictions[$tagid] = $plugin_restrictions[$tagid];
                }
            }
        } while (false);

        return $activity_restrictions;
    }

    /**
     * Get window settings by role names for the current activity (by $cm_or_id)
     *
     * @param int|\cm_info|object $cm_or_id - Id of course-module, or database object
     *
     * @return array[] - [role_shortname => [$window_start, $window_end]]
     */
    public function get_role_windows($cm_or_id){
        $role_windows = [];
        $activity_restrictions = $this->get_activity_restrictions($cm_or_id);

        foreach ($activity_restrictions as $cfg){
            if (empty($cfg)) continue;

            $lines = explode("\n", $cfg);
            if (empty($lines)) continue;

            foreach ($lines as $line){
                $line = str_replace(' ', '', $line);
                $role_args = explode(',', $line);
                if (empty($role_args) || count($role_args) !== 3) continue;

                foreach ($role_args as $i => $role_arg){
                    $role_args[$i] = trim($role_arg) ?: 0;
                }

                [$role_name, $window_start, $window_end] = $role_args;
                if (empty($role_name)) continue;

                [$window_start, $window_end] = [(int)$window_start, (int)$window_end];
                if (isset($role_windows[$role_name])){
                    [$window_start_prev, $window_end_prev] = $role_windows[$role_name];
                    $window_start = min($window_start_prev, $window_start);
                    $window_end = max($window_end_prev, $window_end);
                }

                $role_windows[$role_name] = [$window_start, $window_end];
            }
        }

        return $role_windows;
    }

    /**
     * Get list of important deadlines for the user-activity
     *
     * @param \cm_info|numeric|object $cm_or_id
     * @param numeric|object|null     $course_or_id - if null, uses global $COURSE
     * @param numeric|object|null     $user_or_id - if null, uses global $USER
     *
     * @return array - [deadline0 => deadline0, deadline1 => deadline1, ...]
     */
    public function get_user_deadlines($cm_or_id, $course_or_id=null, $user_or_id=null){
        $courseid = NED::get_courseid_or_global($course_or_id);
        $userid = NED::get_userid_or_global($user_or_id);

        $deadlines = [];
        $add_dm = function($dm) use (&$deadlines){
            if (!empty($dm)){
                $dm = usergetmidnight($dm);
                $deadlines[$dm] = $dm;
            }
        };
        $get_usual_deadline = true;

        do {
            $DM = NED::get_DM();
            if (!$DM) break;

            $groupids = NED::get_user_groupids($courseid, $userid);
            if (empty($groupids) || count($groupids) == 1) break;

            if (!$DM::is_cm_enabled($cm_or_id, $courseid)) break;

            $dmm = $DM::get_dmm_by_cm($cm_or_id, $course_or_id);
            if (!$dmm) break;

            // done all prechecks, so we sure to get user deadline(s) through DM
            $get_usual_deadline = false;

            $user_override_dm = $dmm->get_user_override_date($userid);
            if (!empty($user_override_dm)){
                // user has several groups, but user-overridden deadline override them all
                $deadlines = [$user_override_dm => $user_override_dm];
                break;
            }

            foreach ($groupids as $groupid){
                $group_dm = $dmm->get_group_override_date($groupid);
                $add_dm($group_dm);
            }

            if (empty($deadlines)){
                // there are no user or group deadline => try to get activity deadline
                $activity_dm = $dmm->get_default_deadline();
                $add_dm($activity_dm);
            }
        } while(false);

        if ($get_usual_deadline){
            // get simple single deadline
            $dm = NED::dm_get_deadline_by_cm($cm_or_id, $userid, $courseid);
            $add_dm($dm);
        }

        return $deadlines;
    }
}
