<?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 core_calendar\output;

use DateInterval;
use DateTimeInterface;
use DateTimeImmutable;
use core\output\pix_icon;
use core\output\templatable;
use core\output\renderable;
use core\output\renderer_base;
use core\clock;
use core\url;

/**
 * Class humandate.
 *
 * This class is used to render a timestamp as a human readable date.
 * The main difference between userdate and this class is that this class
 * will render the date as "Today", "Yesterday", "Tomorrow" if the date is
 * close to the current date. Also, it will add alert styling if the date
 * is near.
 *
 * @package    core_calendar
 * @copyright  2024 Ferran Recio <ferran@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class humandate implements renderable, templatable {

    /** @var int|null The number of seconds within which a date is considered near. 1 day by default. */
    protected ?int $near = DAYSECS;

    /** @var bool Whether we should show time only or date and time. */
    protected bool $timeonly = false;

    /** @var url|null Link for the date. */
    protected ?url $link = null;

    /** @var string|null An optional date format to apply.  */
    protected ?string $langtimeformat = null;

    /** @var bool Whether to use human relative terminology. */
    protected bool $userelatives = true;

    /** @var clock The clock interface to handle time. */
    protected clock $clock;

    /**
     * Class constructor.
     *
     * Use the factory methods, such as create_from_timestamp or create_from_datetime, instead.
     *
     * @param DateTimeImmutable $datetime The datetime.
     */
    protected function __construct(
        /** @var DateTimeImmutable $datetime The datetime. **/
        protected DateTimeImmutable $datetime,
    ) {
        $this->clock = \core\di::get(clock::class);
    }

    /**
     * Creates a new humandate instance from a timestamp.
     *
     * @param int $timestamp The timestamp.
     * @param int|null $near The number of seconds within which a date is considered near. 1 day by default.
     * @param bool $timeonly Whether we should show time only or date and time.
     * @param url|null $link Link for the date.
     * @param string|null $langtimeformat An optional date format to apply.
     * @param bool $userelatives Whether to use human relative terminology.
     * @return humandate The new instance.
     */
    public static function create_from_timestamp(
        int $timestamp,
        ?int $near = DAYSECS,
        bool $timeonly = false,
        ?url $link = null,
        ?string $langtimeformat = null,
        bool $userelatives = true,
    ): self {

        return self::create_from_datetime(
            (new DateTimeImmutable("@{$timestamp}")),
            $near,
            $timeonly,
            $link,
            $langtimeformat,
            $userelatives
        );
    }

    /**
     * Creates a new humandate instance from a datetime.
     *
     * @param DateTimeInterface $datetime The datetime.
     * @param int|null $near The number of seconds within which a date is considered near. 1 day by default.
     * @param bool $timeonly Whether we should show time only or date and time.
     * @param url|null $link Link for the date.
     * @param string|null $langtimeformat An optional date format to apply.
     * @param bool $userelatives Whether to use human relative terminology.
     * @return humandate The new instance.
     */
    public static function create_from_datetime(
        DateTimeInterface $datetime,
        ?int $near = DAYSECS,
        bool $timeonly = false,
        ?url $link = null,
        ?string $langtimeformat = null,
        bool $userelatives = true,
    ): self {

        if (!($datetime instanceof DateTimeImmutable)) {
            // Always use an Immutable object to ensure that the value does not change externally before it is rendered.
            $datetime = DateTimeImmutable::createFromInterface($datetime);
        }

        return (new self($datetime))
            ->set_near_limit($near)
            ->set_display_time_only($timeonly)
            ->set_link($link)
            ->set_lang_time_format($langtimeformat)
            ->set_use_relatives($userelatives);
    }

    /**
     * Sets the number of seconds within which a date is considered near.
     *
     * @param int|null $near The number of seconds within which a date is considered near.
     * @return humandate The instance.
     */
    public function set_near_limit(?int $near): self {
        $this->near = $near;
        return $this;
    }

    /**
     * Sets whether we should show time only or date and time.
     *
     * @param bool $timeonly Whether we should show time only or date and time.
     * @return humandate The instance.
     */
    public function set_display_time_only(bool $timeonly): self {
        $this->timeonly = $timeonly;
        return $this;
    }

    /**
     * Sets the link for the date. If null, no link will be added.
     *
     * @param url|null $link The link for the date.
     * @return humandate The instance.
     */
    public function set_link(?url $link): self {
        $this->link = $link;
        return $this;
    }

    /**
     * Sets an optional date format to apply.
     *
     * @param string|null $langtimeformat Lang date and time format to use to format the date.
     * @return humandate The instance.
     */
    public function set_lang_time_format(?string $langtimeformat): self {
        $this->langtimeformat = $langtimeformat;
        return $this;
    }

    /**
     * Sets whether to use human relative terminology.
     *
     * @param bool $userelatives Whether to use human relative terminology.
     * @return humandate The instance.
     */
    public function set_use_relatives(bool $userelatives): self {
        $this->userelatives = $userelatives;
        return $this;
    }

    #[\Override]
    public function export_for_template(renderer_base $output): array {
        $timestamp = $this->datetime->getTimestamp();
        $userdate = userdate($timestamp, get_string('strftimedayshort'));
        $relative = null;
        if ($this->userelatives) {
            $relative = $this->format_relative_date();
        }

        if ($this->timeonly) {
            $date = null;
        } else {
            $date = $relative ?? $userdate;
        }
        $data = [
            'timestamp' => $timestamp,
            'userdate' => $userdate,
            'date' => $date,
            'time' => $this->format_time(),
            'ispast' => $this->datetime < $this->clock->now(),
            'needtitle' => ($relative !== null || $this->timeonly),
            'link' => $this->link ? $this->link->out(false) : '',
        ];
        if ($this->is_near()) {
            $icon = new pix_icon(
                pix: 'i/warning',
                alt: get_string('warning'),
                component: 'moodle',
                attributes: ['class' => 'me-0 pb-1']
            );
            $data['isnear'] = true;
            $data['nearicon'] = $icon->export_for_template($output);
        }
        return $data;
    }

    /**
     * Checks if the date is near.
     *
     * @return bool Whether the date is near.
     */
    private function is_near(): bool {
        if ($this->near === null) {
            return false;
        }
        $due = $this->datetime->diff($this->clock->now());
        $intervalseconds = $this->interval_to_seconds($due);
        return $intervalseconds < $this->near && $intervalseconds > 0;
    }

    /**
     * Converts a DateInterval object to total seconds.
     *
     * @param \DateInterval $interval The interval to convert.
     * @return int The total number of seconds.
     */
    private function interval_to_seconds(DateInterval $interval): int {
        $reference = new DateTimeImmutable();
        $entime = $reference->add($interval);
        return $reference->getTimestamp() - $entime->getTimestamp();
    }

    /**
     * Formats the timestamp as a relative date string (e.g., "Today", "Yesterday", "Tomorrow").
     *
     * This method compares the given timestamp with the current date and returns a formatted
     * string representing the relative date. If the timestamp corresponds to today, yesterday,
     * or tomorrow, it returns the appropriate string. Otherwise, it returns null.
     *
     * @return string|null
     */
    private function format_relative_date(): ?string {
        $usertimestamp = $this->get_user_date($this->datetime->getTimestamp());
        if ($usertimestamp == $this->get_user_date($this->clock->now()->getTimestamp())) {
            $format = get_string(
                'timerelativetoday',
                'calendar',
                get_string('strftimedateshort', 'langconfig')
            );
        } else if ($usertimestamp == $this->get_user_date(strtotime('yesterday', $this->clock->now()->getTimestamp()))) {
            $format = get_string(
                'timerelativeyesterday',
                'calendar',
                get_string('strftimedateshort', 'langconfig')
            );
        } else if ($usertimestamp == $this->get_user_date(strtotime('tomorrow', $this->clock->now()->getTimestamp()))) {
            $format = get_string(
                'timerelativetomorrow',
                'calendar',
                get_string('strftimedateshort', 'langconfig')
            );
        } else {
            return null;
        }

        return userdate($this->datetime->getTimestamp(), $format);
    }

    /**
     * Formats the timestamp as a human readable time.
     *
     * @param int $timestamp The timestamp to format.
     * @param string $format The format to use.
     * @return string The formatted date.
     */
    private function get_user_date(int $timestamp, string $format = '%Y-%m-%d'): string {
        $calendartype = \core_calendar\type_factory::get_calendar_instance();
        $timezone = \core_date::get_user_timezone_object();
        return $calendartype->timestamp_to_date_string(
            time: $timestamp,
            format: $format,
            timezone: $timezone->getName(),
            fixday: true,
            fixhour: true,
        );
    }

    /**
     * Formats the timestamp as a human readable time.
     *
     * This method compares the given timestamp with the current date and returns a formatted
     * string representing the time.
     *
     * @return string
     */
    private function format_time(): string {
        global $CFG;
        // Ensure calendar constants are loaded.
        require_once($CFG->dirroot . '/calendar/lib.php');

        $timeformat = get_user_preferences('calendar_timeformat');
        if (empty($timeformat)) {
            $timeformat = get_config(null, 'calendar_site_timeformat');
        }

        // Allow language customization of selected time format.
        if ($timeformat === CALENDAR_TF_12) {
            $timeformat = get_string('strftimetime12', 'langconfig');
        } else if ($timeformat === CALENDAR_TF_24) {
            $timeformat = get_string('strftimetime24', 'langconfig');
        }

        if ($timeformat) {
            return userdate($this->datetime->getTimestamp(), $timeformat);
        }

        // Let's use default format.
        if ($this->langtimeformat === null) {
            $langtimeformat = get_string('strftimetime');
        }

        return userdate($this->datetime->getTimestamp(), $langtimeformat);
    }
}
