<?php
/**
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @copyright     Copyright (c) Brian Nesbitt <brian@nesbot.com>
 * @link          http://cakephp.org CakePHP(tm) Project
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\Chronos\Traits;

use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;

/**
 * Provides a number of datetime related factory methods.
 */
trait FactoryTrait
{
    /**
     * Holds the last error generated by createFromFormat
     *
     * @var array
     */
    protected static $_lastErrors = [];

    /**
     * Create a ChronosInterface instance from a DateTimeInterface one
     *
     * @param \DateTimeInterface $dt The datetime instance to convert.
     * @return static
     */
    public static function instance(DateTimeInterface $dt)
    {
        if ($dt instanceof static) {
            return clone $dt;
        }

        return new static($dt->format('Y-m-d H:i:s.u'), $dt->getTimezone());
    }

    /**
     * Create a ChronosInterface instance from a string.  This is an alias for the
     * constructor that allows better fluent syntax as it allows you to do
     * ChronosInterface::parse('Monday next week')->fn() rather than
     * (new Chronos('Monday next week'))->fn()
     *
     * @param \DateTimeInterface|string $time The strtotime compatible string to parse
     * @param \DateTimeZone|string|null $tz The DateTimeZone object or timezone name.
     * @return static
     */
    public static function parse($time = 'now', $tz = null)
    {
        return new static($time, $tz);
    }

    /**
     * Get a ChronosInterface instance for the current date and time
     *
     * @param \DateTimeZone|string|null $tz The DateTimeZone object or timezone name.
     * @return static
     */
    public static function now($tz = null)
    {
        return new static('now', $tz);
    }

    /**
     * Create a ChronosInterface instance for today
     *
     * @param \DateTimeZone|string|null $tz The timezone to use.
     * @return static
     */
    public static function today($tz = null)
    {
        return new static('midnight', $tz);
    }

    /**
     * Create a ChronosInterface instance for tomorrow
     *
     * @param \DateTimeZone|string|null $tz The DateTimeZone object or timezone name the new instance should use.
     * @return static
     */
    public static function tomorrow($tz = null)
    {
        return new static('tomorrow, midnight', $tz);
    }

    /**
     * Create a ChronosInterface instance for yesterday
     *
     * @param \DateTimeZone|string|null $tz The DateTimeZone object or timezone name the new instance should use.
     * @return static
     */
    public static function yesterday($tz = null)
    {
        return new static('yesterday, midnight', $tz);
    }

    /**
     * Create a ChronosInterface instance for the greatest supported date.
     *
     * @return \Cake\Chronos\ChronosInterface
     */
    public static function maxValue()
    {
        return static::createFromTimestampUTC(PHP_INT_MAX);
    }

    /**
     * Create a ChronosInterface instance for the lowest supported date.
     *
     * @return \Cake\Chronos\ChronosInterface
     */
    public static function minValue()
    {
        $max = PHP_INT_SIZE === 4 ? PHP_INT_MAX : PHP_INT_MAX / 10;

        return static::createFromTimestampUTC(~$max);
    }

    /**
     * Create a new ChronosInterface instance from a specific date and time.
     *
     * If any of $year, $month or $day are set to null their now() values
     * will be used.
     *
     * If $hour is null it will be set to its now() value and the default values
     * for $minute and $second will be their now() values.
     * If $hour is not null then the default values for $minute and $second
     * will be 0.
     *
     * @param int|null $year The year to create an instance with.
     * @param int|null $month The month to create an instance with.
     * @param int|null $day The day to create an instance with.
     * @param int|null $hour The hour to create an instance with.
     * @param int|null $minute The minute to create an instance with.
     * @param int|null $second The second to create an instance with.
     * @param \DateTimeZone|string|null $tz The DateTimeZone object or timezone name the new instance should use.
     * @return static
     */
    public static function create($year = null, $month = null, $day = null, $hour = null, $minute = null, $second = null, $tz = null)
    {
        $year = ($year === null) ? date('Y') : $year;
        $month = ($month === null) ? date('n') : $month;
        $day = ($day === null) ? date('j') : $day;

        if ($hour === null) {
            $hour = date('G');
            $minute = ($minute === null) ? date('i') : $minute;
            $second = ($second === null) ? date('s') : $second;
        } else {
            $minute = ($minute === null) ? 0 : $minute;
            $second = ($second === null) ? 0 : $second;
        }

        $instance = static::createFromFormat('Y-n-j G:i:s', sprintf('%s-%s-%s %s:%02s:%02s', 0, $month, $day, $hour, $minute, $second), $tz);

        return $instance->addYears($year);
    }

    /**
     * Create a ChronosInterface instance from just a date. The time portion is set to now.
     *
     * @param int $year The year to create an instance with.
     * @param int $month The month to create an instance with.
     * @param int $day The day to create an instance with.
     * @param \DateTimeZone|string|null $tz The DateTimeZone object or timezone name the new instance should use.
     * @return static
     */
    public static function createFromDate($year = null, $month = null, $day = null, $tz = null)
    {
        return static::create($year, $month, $day, null, null, null, $tz);
    }

    /**
     * Create a ChronosInterface instance from just a time. The date portion is set to today.
     *
     * @param int|null $hour The hour to create an instance with.
     * @param int|null $minute The minute to create an instance with.
     * @param int|null $second The second to create an instance with.
     * @param \DateTimeZone|string|null $tz The DateTimeZone object or timezone name the new instance should use.
     * @return static
     */
    public static function createFromTime($hour = null, $minute = null, $second = null, $tz = null)
    {
        return static::create(null, null, null, $hour, $minute, $second, $tz);
    }

    /**
     * Create a ChronosInterface instance from a specific format
     *
     * @param string $format The date() compatible format string.
     * @param string $time The formatted date string to interpret.
     * @param \DateTimeZone|string|null $tz The DateTimeZone object or timezone name the new instance should use.
     * @return static
     * @throws \InvalidArgumentException
     */
    #[\ReturnTypeWillChange]
    public static function createFromFormat($format, $time, $tz = null)
    {
        if ($tz !== null) {
            $dt = parent::createFromFormat($format, $time, static::safeCreateDateTimeZone($tz));
        } else {
            $dt = parent::createFromFormat($format, $time);
        }

        $errors = parent::getLastErrors();
        if ($dt == false) {
            throw new InvalidArgumentException(implode(PHP_EOL, $errors['errors']));
        }

        $dt = static::instance($dt);
        static::$_lastErrors = $errors;

        return $dt;
    }

    /**
     * Creates a ChronosInterface instance from an array of date and time values.
     *
     * The 'year', 'month' and 'day' values must all be set for a date. The time
     * values all default to 0.
     *
     * The 'timezone' value can be any format supported by `\DateTimeZone`.
     *
     * Allowed values:
     *  - year
     *  - month
     *  - day
     *  - hour
     *  - minute
     *  - second
     *  - microsecond
     *  - meridian ('am' or 'pm')
     *  - timezone
     *
     * @param (int|string)[] $values Array of date and time values.
     * @return static
     */
    public static function createFromArray($values)
    {
        $values += ['hour' => 0, 'minute' => 0, 'second' => 0, 'microsecond' => 0, 'timezone' => null];

        $formatted = '';
        if (
            isset($values['year'], $values['month'], $values['day']) &&
            (
                is_numeric($values['year']) &&
                is_numeric($values['month']) &&
                is_numeric($values['day'])
            )
        ) {
            $formatted .= sprintf('%04d-%02d-%02d ', $values['year'], $values['month'], $values['day']);
        }

        if (isset($values['meridian']) && (int)$values['hour'] === 12) {
            $values['hour'] = 0;
        }
        if (isset($values['meridian'])) {
            $values['hour'] = strtolower($values['meridian']) === 'am' ? $values['hour'] : $values['hour'] + 12;
        }
        $formatted .= sprintf(
            '%02d:%02d:%02d.%06d',
            $values['hour'],
            $values['minute'],
            $values['second'],
            $values['microsecond']
        );

        return static::parse($formatted, $values['timezone']);
    }

    /**
     * Create a ChronosInterface instance from a timestamp
     *
     * @param int $timestamp The timestamp to create an instance from.
     * @param \DateTimeZone|string|null $tz The DateTimeZone object or timezone name the new instance should use.
     * @return static
     */
    public static function createFromTimestamp($timestamp, $tz = null)
    {
        return static::now($tz)->setTimestamp($timestamp);
    }

    /**
     * Create a ChronosInterface instance from an UTC timestamp
     *
     * @param int $timestamp The UTC timestamp to create an instance from.
     * @return static
     */
    public static function createFromTimestampUTC($timestamp)
    {
        return new static('@' . $timestamp);
    }

    /**
     * Creates a DateTimeZone from a string or a DateTimeZone
     *
     * @param \DateTimeZone|string|null $object The value to convert.
     * @return \DateTimeZone
     * @throws \InvalidArgumentException
     */
    protected static function safeCreateDateTimeZone($object)
    {
        if ($object === null) {
            return new DateTimeZone(date_default_timezone_get());
        }

        if ($object instanceof DateTimeZone) {
            return $object;
        }

        return new DateTimeZone($object);
    }

    /**
     * Returns any errors or warnings that were found during the parsing
     * of the last object created by this class.
     *
     * @return array
     */
    #[\ReturnTypeWillChange]
    public static function getLastErrors()
    {
        if (empty(static::$_lastErrors)) {
            return parent::getLastErrors();
        }

        return static::$_lastErrors;
    }
}
