<?PHP
/**
 * Describes a time after splitting / transfer object.
 *
 * @author Joshua Ramon Enslin <joshua@museum-digital.de>
 */
declare(strict_types = 1);

/**
 * Describes a time after splitting / transfer object.
 */
final class NodaSplitTime {

    public const DEFAULT_DATE = '0001-01-01';

    public readonly string $start_year;
    public readonly string $end_year;

    public readonly string $counting_time_month;
    public readonly string $counting_time_day;

    public readonly NodaCountingTimeIndicator $counting_time_indicator;
    public readonly NodaTimeBeforeAfterIndicator $before_after_indicator;

    public string $start_date;
    public string $end_date;

    /**
     * Returns a single, exact date.
     *
     * @param string                       $year                   Year.
     * @param string                       $month                  Month.
     * @param string                       $day                    Day.
     * @param NodaTimeBeforeAfterIndicator $before_after_indicator Determines if the time is exact or before / after.
     *
     * @return NodaSplitTime
     */
    public static function genExactDate(string $year, string $month, string $day, NodaTimeBeforeAfterIndicator $before_after_indicator = NodaTimeBeforeAfterIndicator::none):NodaSplitTime {

        $start_year = $end_year = $year;
        $start_date = $end_date = $year . '-' . $month . '-' . $day;

        if ($before_after_indicator === NodaTimeBeforeAfterIndicator::before
            || $before_after_indicator === NodaTimeBeforeAfterIndicator::until
        ) {
            $start_year = $start_date = '?';
        }
        if ($before_after_indicator === NodaTimeBeforeAfterIndicator::after
            || $before_after_indicator === NodaTimeBeforeAfterIndicator::since
        ) {
            $end_year = $end_date = '?';
        }

        return new NodaSplitTime($start_year, $end_year, $month, $day, start_date: $start_date, end_date: $end_date);

    }

    /**
     * Validates the entered start year.
     *
     * @param string $input Input to validate.
     *
     * @return void
     */
    private function _validateStartYear(string $input):void {

        if (strlen($input) > 6) {
            throw new MDgenericInvalidInputsException("Time statement longer than 6 characters is impossible");
        }

        if (($this->before_after_indicator === NodaTimeBeforeAfterIndicator::before
            || $this->before_after_indicator === NodaTimeBeforeAfterIndicator::until)
            && $input !== '?'
        ) {
            throw new MDgenericInvalidInputsException("Times with no certain start need to have a question mark (?) entered as a start date");
        }

    }

    /**
     * Validates the entered end year.
     *
     * @param string $input Input to validate.
     *
     * @return void
     */
    private function _validateEndYear(string $input):void {

        if (strlen($input) > 6) {
            throw new MDgenericInvalidInputsException("Time statement longer than 6 characters is impossible");
        }

        if (($this->before_after_indicator === NodaTimeBeforeAfterIndicator::after
            || $this->before_after_indicator === NodaTimeBeforeAfterIndicator::since)
            && $input !== '?'
        ) {
            throw new MDgenericInvalidInputsException("Times with no certain end need to have a question mark (?) entered as a end date");
        }

    }

    /**
     * Pads to four digits. E.g. 2 > 02.
     *
     * @param string $input Input string.
     *
     * @return string
     */
    public static function pad_to_two(string $input):string {

        return \substr("00" . $input, -2);

    }

    /**
     * Validates the entered month string.
     *
     * @param string $input Input to validate.
     *
     * @return string
     */
    private function _validateCountingTimeMonth(string $input):string {

        if (strlen($input) > 2) throw new MDgenericInvalidInputsException("Input too long");

        if ($input === '00') {
            return $input;
        }

        if (($parsedInt = filter_var(ltrim($input, "0"), FILTER_VALIDATE_INT)) === false) {
            throw new MDgenericInvalidInputsException("Input value is not numeric");
        }
        if ($parsedInt > 12) {
            throw new MDgenericInvalidInputsException("Attempted to set a month number beyond 12");
        }

        return self::pad_to_two($input);

    }

    /**
     * Validates the entered day string.
     *
     * @param string $input Input to validate.
     *
     * @return string
     */
    private function _validateCountingTimeDay(string $input):string {

        if (strlen($input) > 2) throw new MDgenericInvalidInputsException("Input too long");

        if ($input === '00') {
            return $input;
        }

        if (($parsedInt = filter_var(ltrim($input, "0"), FILTER_VALIDATE_INT)) === false) {
            throw new MDgenericInvalidInputsException("Input value is not numeric");
        }
        if ($parsedInt > 31) {
            throw new MDgenericInvalidInputsException("Attempted to set a day number beyond 31");
        }

        return self::pad_to_two($input);

    }

    /**
     * Returns a date time for the start of the time.
     *
     * @return DateTime
     */
    public function startToDateTime():DateTime {

        if ($this->counting_time_indicator === NodaCountingTimeIndicator::bce) {
            return new DateTime('-' . $this->start_date);
        }
        else {
            return new DateTime($this->start_date);
        }

    }

    /**
     * Returns a date time for the end of the time.
     *
     * @return DateTime
     */
    public function endToDateTime():DateTime {

        if ($this->counting_time_indicator === NodaCountingTimeIndicator::bce) {
            return new DateTime('-' . $this->end_date);
        }
        else {
            return new DateTime($this->end_date);
        }

    }

    /**
     * Generates a time name based on the current entry.
     *
     * @return string
     */
    public function toTimeName():string {

        $prefix = $this->before_after_indicator->toString();
        if (!empty($prefix)) $prefix .= ' ';

        // Determine start and end for display
        if ($this->start_year === '?') {
            $start = (int)$this->end_year;
        }
        else $start = (int)$this->start_year;

        if ($this->end_year === '?') {
            $end = (int)$this->start_year;
        }
        else $end = (int)$this->end_year;

        if ($this->before_after_indicator === NodaTimeBeforeAfterIndicator::before && $this->counting_time_month === '00') {
            $start++;
            $end++;
        }
        else if ($this->before_after_indicator === NodaTimeBeforeAfterIndicator::after && $this->counting_time_month === '00') {
            $start--;
            $end--;
        }

        // Determine suffix
        if ($start < 0 && $end < 0) {
            $suffix = " v. Chr.";
        }
        else if ($end < 1000) {
            $suffix = " n. Chr.";
        }
        else $suffix = "";

        $start = \abs($start);
        $end = \abs($end);

        if ($start !== $end) {
            return "{$prefix}{$start}-{$end}{$suffix}";
        }

        // A single day of a given month of a given (single) year
        else if (\intval($this->counting_time_month) !== 0 and \intval($this->counting_time_day) !== 0) {
            return "{$prefix}{$this->counting_time_day}.{$this->counting_time_month}.{$start}{$suffix}";
        }

        // A single year
        else if ($start === $end && trim((string)$this->counting_time_month, " 0") === "" && trim((string)$this->counting_time_day, " 0") === "") {
            return "{$prefix}{$start}{$suffix}";
        }

        // Single month of a given year
        else if ($start === $end && trim((string)$this->counting_time_month, " 0") !== "" && trim((string)$this->counting_time_day, " 0") === "") {

            $fmt = new IntlDateFormatter(
                'de-DE',
                IntlDateFormatter::FULL,
                IntlDateFormatter::FULL,
                null,
                IntlDateFormatter::GREGORIAN,
                'MMMM Y'
            );

            try {
                return $prefix . $fmt->format(MD_STD::strtotime("{$start}-{$this->counting_time_month}-15 01:01:01")) . $suffix;
            }
            catch (MDInvalidInputDate $e) {
                return "";
            }
        }

        return "";

    }

    /**
     * Returns an array in the old time splitter format (array with sex values).
     *
     * @return array<string>
     */
    public function toOldFormat():array {

        return [
            $this->start_year,
            $this->end_year,
            $this->counting_time_month,
            $this->counting_time_day,
            $this->counting_time_indicator->toString(),
            $this->before_after_indicator->toString(),
        ];

    }

    /**
     * Constructor.
     *
     * @param string                       $start_year              Year.
     * @param string                       $end_year                Year.
     * @param string                       $counting_time_month     Month.
     * @param string                       $counting_time_day       Day.
     * @param NodaCountingTimeIndicator    $counting_time_indicator Determines if the time is BCE or CCE.
     * @param NodaTimeBeforeAfterIndicator $before_after_indicator  Determines if the time is inexact to one direction.
     * @param false|string                 $start_date              Start date.
     * @param false|string                 $end_date                End date.
     *
     * @return void
     */
    public function __construct(string $start_year, string $end_year,
        string $counting_time_month = "00", string $counting_time_day = "00",
        NodaCountingTimeIndicator $counting_time_indicator = NodaCountingTimeIndicator::ce,
        NodaTimeBeforeAfterIndicator $before_after_indicator = NodaTimeBeforeAfterIndicator::none,
        false|string $start_date = false,
        false|string $end_date = false,
    ) {

        if (substr($start_year, 0, 1) === '-') {
            if (strlen($start_year) > 5) $start_year = '-' . str_pad(trim($start_year, '-'), 4, '-', STR_PAD_LEFT);
        }
        if ($start_date !== false && str_starts_with($start_date, '-')) {
            $parts = explode('-', trim($start_date, '-'));
            $parts[0] = str_pad($parts[0], 4, '0', STR_PAD_LEFT);
            $start_date = '-' . implode('-', $parts);
        }
        if (substr($end_year, 0, 1) === '-') {
            if (strlen($end_year) > 5) $end_year = '-' . str_pad(trim($end_year, '-'), 4, '-', STR_PAD_LEFT);
        }
        if ($end_date !== false && str_starts_with($end_date, '-')) {
            $parts = explode('-', trim($end_date, '-'));
            $parts[0] = str_pad($parts[0], 4, '0', STR_PAD_LEFT);
            $end_date = '-' . implode('-', $parts);
        }

        $this->counting_time_indicator = $counting_time_indicator;
        $this->before_after_indicator = $before_after_indicator;

        $this->_validateStartYear($start_year);
        $this->start_year = $start_year;

        $this->_validateEndYear($end_year);
        $this->end_year = $end_year;

        $this->counting_time_month = $this->_validateCountingTimeMonth($counting_time_month);
        $this->counting_time_day = $this->_validateCountingTimeDay($counting_time_day);

        // Calculate start date and end date
        if (($this->before_after_indicator === NodaTimeBeforeAfterIndicator::before
            || $this->before_after_indicator === NodaTimeBeforeAfterIndicator::until)
            || $start_date === '?'
        ) {
            $this->start_date = '-9999-12-31';
        }
        if (($this->before_after_indicator === NodaTimeBeforeAfterIndicator::after
            || $this->before_after_indicator === NodaTimeBeforeAfterIndicator::since)
            || $end_date === '?'
        ) {
            $this->end_date = '9999-12-31';
        }

        if (!isset($this->start_date) && false !== $start_date) {
            $this->start_date = date("Y-m-d", MD_STD::strtotime($start_date));
        }
        if (!isset($this->end_date) && false !== $end_date) {
            $this->end_date = date("Y-m-d", MD_STD::strtotime($end_date));
        }

        if (!isset($this->start_date)) {
            if ($this->counting_time_month === "00") {
                $this->start_date = $this->start_year . '-01-01';
            }
            else if ($this->counting_time_day === "00") {
                $this->start_date = $this->start_year . '-' . $this->counting_time_month . '-01';
            }
            else {
                throw new MDgenericInvalidInputsException("Cannot identify start date automatically");
            }
        }
        if (!isset($this->end_date)) {
            if ($this->counting_time_month === "00") {
                $this->end_date = $this->end_year . '-12-31';
            }
            else if ($this->counting_time_day === "00") {
                $this->end_date = $this->end_year . '-' . $this->counting_time_month . '-31';
            }
            else {
                throw new MDgenericInvalidInputsException("Cannot identify end date automatically");
            }
        }

        // Validate
        $startDateTime = MD_STD::strtotime("2000-" . substr($this->start_date, -5));
        if (checkdate((int)date('m', $startDateTime), (int)date('d', $startDateTime), (int)date('Y', $startDateTime)) === false) {
            throw new MDgenericInvalidInputsException("Invalid start date: " . $this->start_date);
        }

        if (!empty((int)$this->counting_time_day)) {
            // The year 2000 is used here as it is a leap year and lots of years accepted in md are not accepted
            // by checkdate.
            if (checkdate((int)$this->counting_time_month, (int)$this->counting_time_day, 2000) === false) {
                throw new MDgenericInvalidInputsException("Invalid date formed by counting time: " . $this->counting_time_month . ' -- ' . $this->counting_time_day);
            }
        }

    }
}
