<?php
/**
 * Utility class for date time operations.
 *
 * @package Ealab\Deployer
 */

namespace EaLab\WP_Deployer\vendor\Ealab\Deployer\Utility;

use DateTime;
use DateTimeZone;
use Exception;

/**
 * Utility class for date time operations.
 */
class Date {
	/**
	 * Format for bundle label fallbacks.
	 */
	public const AS_BUNDLE_LABEL = 'Y-m-d H:i:s';

	/**
	 * Format for inserting datetime with milliseconds in the database.
	 */
	public const DB_DATETIME_FORMAT_MILLISECONDS = 'Y-m-d H:i:s.u';

	/**
	 * The ISO date time format.
	 */
	public const ISO = 'Y-m-d\TH:i:s';

	/**
	 * The ISO date time format without any separator.
	 */
	public const ISO_COMPACT = 'YmdHisu';

	/**
	 * The ISO date time format, with microseconds.
	 */
	public const ISO_MILLISECONDS = 'Y-m-d\TH:i:s.u';

	/**
	 * The UTC time zone.
	 *
	 * Use the getUtcTimeZone() method instead of this property.
	 *
	 * @var DateTimeZone
	 */
	private static DateTimeZone $utc_time_zone;

	/**
	 * The "zero" date. It's the earliest date that can be saved in the DB.
	 *
	 * This date is used as the track date for items tracked the first time
	 * upon their tracker activation.
	 *
	 * @var DateTime
	 */
	private static DateTime $zero_date;

	/**
	 * Gets the elapsed time since the given datetime object.
	 *
	 * The result is internationalized.
	 *
	 * Inspired by https://stackoverflow.com/a/18602474 (Glavić and others).
	 *
	 * @param \DateTimeInterface $datetime The datetime object acting as the "since" date.
	 * @param bool               $full Set to true to get the full elapsed time.
	 *     When false, the result will be only the first valid part.
	 *
	 * @return string|null The elapsed time. If the date is invalid, this will return "never".
	 */
	public static function elapsedTimeSince( \DateTimeInterface $datetime, $full = false ) {
		$now  = new DateTime();
		$diff = $now->diff( $datetime );

		$string = array(
			// translators: %s is the count.
			'y' => array( _dgettext( 'deployer-core', '%s year' ), _dgettext( 'deployer-core', '%s years' ) ),
			// translators: %s is the count.
			'm' => array( _dgettext( 'deployer-core', '%s month' ), _dgettext( 'deployer-core', '%s months' ) ),
			// translators: %s is the count.
			'd' => array( _dgettext( 'deployer-core', '%s day' ), _dgettext( 'deployer-core', '%s days' ) ),
			// translators: %s is the count.
			'h' => array( _dgettext( 'deployer-core', '%s hour' ), _dgettext( 'deployer-core', '%s hours' ) ),
			// translators: %s is the count.
			'i' => array( _dgettext( 'deployer-core', '%s minute' ), _dgettext( 'deployer-core', '%s minutes' ) ),
			// translators: %s is the count.
			's' => array( _dgettext( 'deployer-core', '%s second' ), _dgettext( 'deployer-core', '%s seconds' ) ),
		);

		$stop = false;

		foreach ( $string as $k => &$v ) {
			if ( ! $stop && $diff->$k ) {
				$v = sprintf( $diff->$k > 1 ? $v[1] : $v[0], $diff->$k );

				if ( ! $full ) {
					// No need to go further.
					$stop = true;
				}
			} else {
				unset( $string[ $k ] );
			}
		}

		// translators: Should give something like "2 hours ago", "il y a 2 heures".
		return $string ? sprintf( _dgettext( 'deployer-core', '%s ago' ), implode( ', ', $string ) ) : _dgettext( 'deployer-core', 'just now' );
	}

	/**
	 * Gets the date and time representation set on this site.
	 *
	 * @return string The format string.
	 */
	public static function getSiteDateTimeFormat(): string {
		return get_option( 'date_format', 'Y-m-d' ) . ' - ' . get_option( 'time_format', 'H:i:s' );
	}

	/**
	 * Gets the current date time.
	 *
	 * @param DateTimeZone|null $timezone The timezone to use for the DateTime instance. If null, UTC is used.
	 *
	 * @return DateTime The current date time, microseconds included.
	 *
	 * @throws Exception If the date time cannot be created.
	 */
	public static function getCurrentDateTime( DateTimeZone $timezone = null ): DateTime {
		// Initialize the date time using microtime.
		// Such initialisation is needed to get the microseconds.
		// Unix epoch time values are always in UTC.
		// See https://stackoverflow.com/questions/17909871/getting-date-format-m-d-y-his-u-from-milliseconds
		// for more information.
		$datetime = DateTime::createFromFormat( 'U.u', number_format( microtime( true ), 6, '.', '' ) );

		if ( $timezone ) {
			$datetime->setTimezone( $timezone );
		}

		return $datetime;
	}

	/**
	 * Gets a shared UTC time zone object.
	 *
	 * @return DateTimeZone UTC time zone object.
	 */
	public static function getUtcTimeZone(): DateTimeZone {
		if ( empty( static::$utc_time_zone ) ) {
			static::$utc_time_zone = new DateTimeZone( 'UTC' );
		}

		return static::$utc_time_zone;
	}

	/**
	 * Gets the "zero" date. It's the earliest date that can be saved in the DB.
	 *
	 * This date is used as the track date for items tracked the first time
	 * upon their tracker activation.
	 *
	 * @return DateTime The datetime representing the zero date. Initialized in the UTC timezone.
	 *
	 * @throws Exception DateTime exception.
	 */
	public static function getZeroDate(): DateTime {
		if ( empty( static::$zero_date ) ) {
			static::$zero_date = new \DateTime( '1000-01-01T00:00:00.000000', new \DateTimeZone( 'UTC' ) );
		}

		return static::$zero_date;
	}

	/**
	 * Converts a date time source to a DateTime instance.
	 *
	 * @param mixed             $datetime_source The date time source.
	 * @param DateTimeZone|null $timezone The timezone, if the date time source is a string. If the date time source
	 *   is numeric, the timezone is ignored.
	 *
	 * @return DateTime|null The date time object, or null if the date time source is invalid.
	 */
	public static function toDateTime( $datetime_source, DateTimeZone $timezone = null ): ?DateTime {
		try {
			$datetime = new DateTime( $datetime_source, $timezone );
		} catch ( Exception $e ) {
			return null;
		}

		return $datetime;
	}

	/**
	 * Converts a date time source to a DateTime instance.
	 *
	 * @param string            $format The format of the date time source.
	 * @param string            $datetime_source The date time source.
	 * @param DateTimeZone|null $timezone The timezone, if the date time source is a string. If the date time source
	 *   is numeric, the timezone is ignored.
	 *
	 * @return DateTime|null The date time object, or null if the date time source is invalid.
	 */
	public static function toDateTimeWithFormat( string $format, string $datetime_source, DateTimeZone $timezone = null ): ?DateTime {
		$result = date_create_from_format( $format, $datetime_source, $timezone ?? static::getUtcTimeZone() );

		return $result instanceof DateTime ? $result : null;
	}
}
