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

declare(strict_types=1);

namespace core_reportbuilder\local\filters;

use DateTimeImmutable;
use lang_string;
use MoodleQuickForm;
use core_reportbuilder\local\helpers\database;

/**
 * Date report filter
 *
 * This filter accepts a unix timestamp to perform date filtering on
 *
 * @package     core_reportbuilder
 * @copyright   2021 Paul Holden <paulh@moodle.com>
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class date extends base {

    /** @var int Any value */
    public const DATE_ANY = 0;

    /** @var int Non-empty (positive) value */
    public const DATE_NOT_EMPTY = 1;

    /** @var int Empty (zero) value */
    public const DATE_EMPTY = 2;

    /** @var int Date within defined range */
    public const DATE_RANGE = 3;

    /** @var int Date in the last [X relative date unit(s)] */
    public const DATE_LAST = 4;

    /** @var int Date in the previous [X relative date unit(s)] Kept for backwards compatibility */
    public const DATE_PREVIOUS = self::DATE_LAST;

    /** @var int Date in current [relative date unit] */
    public const DATE_CURRENT = 5;

    /** @var int Date in the next [X relative date unit(s)] */
    public const DATE_NEXT = 6;

    /** @var int Date in the past */
    public const DATE_PAST = 7;

    /** @var int Date in the future */
    public const DATE_FUTURE = 8;

    /** @var int Date before [X relative date unit(s)] */
    public const DATE_BEFORE = 9;

    /** @var int Relative date unit for an hour */
    public const DATE_UNIT_HOUR = 0;

    /** @var int Relative date unit for a day */
    public const DATE_UNIT_DAY = 1;

    /** @var int Relative date unit for a week */
    public const DATE_UNIT_WEEK = 2;

    /** @var int Relative date unit for a month */
    public const DATE_UNIT_MONTH = 3;

    /** @var int Relative date unit for a month */
    public const DATE_UNIT_YEAR = 4;

    /**
     * Return an array of operators available for this filter
     *
     * @return lang_string[]
     */
    private function get_operators(): array {
        $operators = [
            self::DATE_ANY => new lang_string('filterisanyvalue', 'core_reportbuilder'),
            self::DATE_NOT_EMPTY => new lang_string('filterisnotempty', 'core_reportbuilder'),
            self::DATE_EMPTY => new lang_string('filterisempty', 'core_reportbuilder'),
            self::DATE_RANGE => new lang_string('filterrange', 'core_reportbuilder'),
            self::DATE_BEFORE => new lang_string('filterdatebefore', 'core_reportbuilder'),
            self::DATE_LAST => new lang_string('filterdatelast', 'core_reportbuilder'),
            self::DATE_CURRENT => new lang_string('filterdatecurrent', 'core_reportbuilder'),
            self::DATE_NEXT => new lang_string('filterdatenext', 'core_reportbuilder'),
            self::DATE_PAST => new lang_string('filterdatepast', 'core_reportbuilder'),
            self::DATE_FUTURE => new lang_string('filterdatefuture', 'core_reportbuilder'),
        ];

        return $this->filter->restrict_limited_operators($operators);
    }

    /**
     * Setup form
     *
     * @param MoodleQuickForm $mform
     */
    public function setup_form(MoodleQuickForm $mform): void {
        // Operator selector.
        $operatorlabel = get_string('filterfieldoperator', 'core_reportbuilder', $this->get_header());
        $typesnounit = [self::DATE_ANY, self::DATE_NOT_EMPTY, self::DATE_EMPTY, self::DATE_RANGE,
            self::DATE_PAST, self::DATE_FUTURE];

        $elements[] = $mform->createElement('select', "{$this->name}_operator", $operatorlabel, $this->get_operators());
        $mform->setType("{$this->name}_operator", PARAM_INT);
        $mform->setDefault("{$this->name}_operator", self::DATE_ANY);

        // Value selector for last and next operators.
        $valuelabel = get_string('filterfieldvalue', 'core_reportbuilder', $this->get_header());

        $elements[] = $mform->createElement('text', "{$this->name}_value", $valuelabel, ['size' => 3]);
        $mform->setType("{$this->name}_value", PARAM_INT);
        $mform->setDefault("{$this->name}_value", 1);
        $mform->hideIf("{$this->name}_value", "{$this->name}_operator", 'in', array_merge($typesnounit, [self::DATE_CURRENT]));

        // Unit selector for last and next operators.
        $unitlabel = get_string('filterdurationunit', 'core_reportbuilder', $this->get_header());
        $units = [
            self::DATE_UNIT_HOUR => get_string('filterdatehours', 'core_reportbuilder'),
            self::DATE_UNIT_DAY => get_string('filterdatedays', 'core_reportbuilder'),
            self::DATE_UNIT_WEEK => get_string('filterdateweeks', 'core_reportbuilder'),
            self::DATE_UNIT_MONTH => get_string('filterdatemonths', 'core_reportbuilder'),
            self::DATE_UNIT_YEAR => get_string('filterdateyears', 'core_reportbuilder'),
        ];

        $elements[] = $mform->createElement('select', "{$this->name}_unit", $unitlabel, $units);
        $mform->setType("{$this->name}_unit", PARAM_INT);
        $mform->setDefault("{$this->name}_unit", self::DATE_UNIT_DAY);
        $mform->hideIf("{$this->name}_unit", "{$this->name}_operator", 'in', $typesnounit);

        // Add operator/value/unit group.
        $mform->addGroup($elements, "{$this->name}_group", $this->get_header(), '', false)
            ->setHiddenLabel(true);

        // Date selectors for range operator.
        $mform->addElement('date_selector', "{$this->name}_from", get_string('filterdatefrom', 'core_reportbuilder'),
            ['optional' => true]);
        $mform->setType("{$this->name}_from", PARAM_INT);
        $mform->setDefault("{$this->name}_from", 0);
        $mform->hideIf("{$this->name}_from", "{$this->name}_operator", 'neq', self::DATE_RANGE);

        $mform->addElement('date_selector', "{$this->name}_to", get_string('filterdateto', 'core_reportbuilder'),
            ['optional' => true]);
        $mform->setType("{$this->name}_to", PARAM_INT);
        $mform->setDefault("{$this->name}_to", 0);
        $mform->hideIf("{$this->name}_to", "{$this->name}_operator", 'neq', self::DATE_RANGE);
    }

    /**
     * Return filter SQL
     *
     * @param array $values
     * @return array
     */
    public function get_sql_filter(array $values): array {
        $fieldsql = $this->filter->get_field_sql();
        $params = $this->filter->get_field_params();

        $operator = (int) ($values["{$this->name}_operator"] ?? self::DATE_ANY);
        $dateunitvalue = (int) ($values["{$this->name}_value"] ?? 1);
        $dateunit = (int) ($values["{$this->name}_unit"] ?? self::DATE_UNIT_DAY);

        switch ($operator) {
            case self::DATE_NOT_EMPTY:
                $sql = "COALESCE({$fieldsql}, 0) <> 0";
                break;
            case self::DATE_EMPTY:
                $sql = "COALESCE({$fieldsql}, 0) = 0";
                break;
            case self::DATE_RANGE:
                $sql = '';

                $datefrom = (int)($values["{$this->name}_from"] ?? 0);
                $dateto = (int)($values["{$this->name}_to"] ?? 0);

                [$paramdatefrom, $paramdateto] = database::generate_param_names(2);

                if ($datefrom > 0 && $dateto > 0) {
                    $sql = "{$fieldsql} BETWEEN :{$paramdatefrom} AND :{$paramdateto}";
                    $params[$paramdatefrom] = $datefrom;
                    $params[$paramdateto] = $dateto;
                } else if ($datefrom > 0) {
                    $sql = "{$fieldsql} >= :{$paramdatefrom}";
                    $params[$paramdatefrom] = $datefrom;
                } else if ($dateto > 0) {
                    $sql = "{$fieldsql} < :{$paramdateto}";
                    $params[$paramdateto] = $dateto;
                }

                break;
            case self::DATE_BEFORE:
                $param = database::generate_param_name();

                // We can use the start date of the "Last" operator as the end date here.
                $sql = "{$fieldsql} < :{$param}";
                $params[$param] = self::get_relative_timeframe(self::DATE_LAST, $dateunitvalue, $dateunit)[0];
                break;
            // Relative helper method can handle these three cases.
            case self::DATE_LAST:
            case self::DATE_CURRENT:
            case self::DATE_NEXT:

                // Last and next operators require a unit value greater than zero.
                if ($operator !== self::DATE_CURRENT && $dateunitvalue === 0) {
                    return ['', []];
                }

                // Generate parameters and SQL clause for the relative date comparison.
                [$paramdatefrom, $paramdateto] = database::generate_param_names(2);
                $sql = "{$fieldsql} BETWEEN :{$paramdatefrom} AND :{$paramdateto}";

                [
                    $params[$paramdatefrom],
                    $params[$paramdateto],
                ] = self::get_relative_timeframe($operator, $dateunitvalue, $dateunit);

                break;
            case self::DATE_PAST:
                $param = database::generate_param_name();
                $sql = "{$fieldsql} < :{$param}";
                $params[$param] = time();
                break;
            case self::DATE_FUTURE:
                $param = database::generate_param_name();
                $sql = "{$fieldsql} > :{$param}";
                $params[$param] = time();
                break;
            default:
                // Invalid or inactive filter.
                return ['', []];
        }

        return [$sql, $params];
    }

    /**
     * Return start and end time of given relative date period
     *
     * @param int $operator One of the ::DATE_LAST/CURRENT/NEXT constants
     * @param int $dateunitvalue Unit multiplier of the date unit
     * @param int $dateunit One of the ::DATE_UNIT_* constants
     * @return int[] Timestamps representing the start/end of timeframe
     */
    private static function get_relative_timeframe(int $operator, int $dateunitvalue, int $dateunit): array {
        // Initialise start/end time to now.
        $datestart = $dateend = new DateTimeImmutable();

        switch ($dateunit) {
            case self::DATE_UNIT_HOUR:
                if ($operator === self::DATE_CURRENT) {
                    $hour = (int) $datestart->format('G');
                    $datestart = $datestart->setTime($hour, 0);
                    $dateend = $dateend->setTime($hour, 59, 59);
                } else if ($operator === self::DATE_LAST) {
                    $datestart = $datestart->modify("-{$dateunitvalue} hour");
                } else if ($operator === self::DATE_NEXT) {
                    $dateend = $dateend->modify("+{$dateunitvalue} hour");
                }
                break;
            case self::DATE_UNIT_DAY:
                if ($operator === self::DATE_CURRENT) {
                    $datestart = $datestart->setTime(0, 0);
                    $dateend = $dateend->setTime(23, 59, 59);
                } else if ($operator === self::DATE_LAST) {
                    $datestart = $datestart->modify("-{$dateunitvalue} day");
                } else if ($operator === self::DATE_NEXT) {
                    $dateend = $dateend->modify("+{$dateunitvalue} day");
                }

                break;
            case self::DATE_UNIT_WEEK:
                if ($operator === self::DATE_CURRENT) {
                    // The first day of the week is determined by site calendar configuration/preferences.
                    $startweekday = \core_calendar\type_factory::get_calendar_instance()->get_starting_weekday();
                    $weekdays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];

                    // If calculated start of week is after today (today is Tues/start of week is Weds), move back a week.
                    $datestartnow = $datestart->getTimestamp();
                    $datestart = $datestart->modify($weekdays[$startweekday] . ' this week')->setTime(0, 0);
                    if ($datestart->getTimestamp() > $datestartnow) {
                        $datestart = $datestart->modify('-1 week');
                    }

                    $dateend = $datestart->modify('+6 day')->setTime(23, 59, 59);
                } else if ($operator === self::DATE_LAST) {
                    $datestart = $datestart->modify("-{$dateunitvalue} week");
                } else if ($operator === self::DATE_NEXT) {
                    $dateend = $dateend->modify("+{$dateunitvalue} week");
                }

                break;
            case self::DATE_UNIT_MONTH:
                if ($operator === self::DATE_CURRENT) {
                    $datestart = $datestart->modify('first day of this month')->setTime(0, 0);
                    $dateend = $dateend->modify('last day of this month')->setTime(23, 59, 59);
                } else if ($operator === self::DATE_LAST) {
                    $datestart = $datestart->modify("-{$dateunitvalue} month");
                } else if ($operator === self::DATE_NEXT) {
                    $dateend = $dateend->modify("+{$dateunitvalue} month");
                }

                break;
            case self::DATE_UNIT_YEAR:
                if ($operator === self::DATE_CURRENT) {
                    $datestart = $datestart->modify('first day of january this year')->setTime(0, 0);
                    $dateend = $dateend->modify('last day of december this year')->setTime(23, 59, 59);
                } else if ($operator === self::DATE_LAST) {
                    $datestart = $datestart->modify("-{$dateunitvalue} year");
                } else if ($operator === self::DATE_NEXT) {
                    $dateend = $dateend->modify("+{$dateunitvalue} year");
                }

                break;
        }

        return [
            $datestart->getTimestamp(),
            $dateend->getTimestamp(),
        ];
    }

    /**
     * Return sample filter values
     *
     * @return array
     */
    public function get_sample_values(): array {
        return [
            "{$this->name}_operator" => self::DATE_CURRENT,
            "{$this->name}_unit" => self::DATE_UNIT_WEEK,
        ];
    }
}
