<?php

declare( strict_types=1 );

namespace DocketConnector\Common\Utilities;

use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use Exception;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Abort if this file is called directly.
}

if ( ! class_exists( Timing::class ) ) {
	class Timing {

		/**
		 * Get the time zone string from WordPress General Settings, with America/Chicago as the fallback.
		 *
		 * @link https://www.php.net/manual/timezones.php Ideally, your WordPress time zone is already one of these.
		 *                                                If not, at least make sure that your fallback is.
		 *
		 * @see  timezone_identifiers_list()
		 *
		 * @param string $fallback The valid PHP time zone string to use if WordPress is not set to a valid one.
		 *
		 * @return string
		 */
		public function get_php_time_zone_string_from_wp( string $fallback = 'America/Chicago' ): string {
			$time_zone = get_option( 'timezone_string' ); // could return NULL

			if ( ! in_array( $time_zone, timezone_identifiers_list() ) ) {
				$time_zone = $fallback;
			}

			return $time_zone;
		}

		/**
		 * Get the PHP DateTimeImmutable() object for the current time in the time zone from WordPress settings.
		 *
		 * If WordPress setting is not a valid PHP time zone, fallback to Chicago (Central Time).
		 *
		 * @return DateTimeImmutable|false
		 */
		public function get_current_time_wp_tz_date_object() {
			$time_zone = $this->get_php_time_zone_string_from_wp();

			try {
				$now = new DateTimeImmutable( 'now', new DateTimeZone( $time_zone ) );
			}
			catch ( Exception $exception ) {
				$now = false;
			}

			return $now;
		}

		/**
		 * Given a timestamp from UTC time (such as from Toolset Types), get its DateTimeImmutable(),
		 * then convert it to our site's time zone, then return it in the specified format (string),
		 * or as the DateTimeImmutable() result if no format is specified.
		 *
		 * @link https://secure.php.net/manual/function.date.php Allowed date() formats.
		 *
		 * @param int    $utc_timestamp An integer timestamp in UTC time zone.
		 * @param string $format        A valid PHP date() format.
		 *
		 * @return DateTimeImmutable|string|bool
		 */
		public function get_datetime_from_utc_timestamp(
			int $utc_timestamp,
			string $format = ''
		) {
			if (
				empty( $utc_timestamp )
				|| ! is_int( $utc_timestamp )
			) {
				return '';
			}

			try {
				$utc_datetime = ( new DateTimeImmutable() )->setTimezone( new DateTimeZone( 'UTC' ) )->setTimestamp( $utc_timestamp );
			}
			catch ( Exception $e ) {
				return '';
			}

			$result = $utc_datetime->setTimezone( new DateTimeZone( $this->get_php_time_zone_string_from_wp() ) );

			if ( ! empty( $format ) ) {
				$result = $result->format( $format ); // could return false
			}

			return $result;
		}

		/**
		 * Get the PHP DateTimeImmutable() object for the current time in the time zone from WordPress settings.
		 *
		 * If WordPress setting is not a valid PHP time zone, fallback to Europe/Amsterdam.
		 *
		 * @return DateTimeImmutable|bool
		 */
		public function get_current_datetime_wp_tz() {
			try {
				return new DateTimeImmutable( 'now', new DateTimeZone( $this->get_php_time_zone_string_from_wp() ) );
			}
			catch ( Exception $e ) {
				return false;
			}
		}

		/**
		 * Get the start or end time of a DateTimeImmutable object, as a DateTimeImmutable object or in a given format.
		 *
		 * @param DateTimeImmutable $datetime
		 * @param bool              $start
		 * @param bool              $format
		 *
		 * @return bool|DateTimeImmutable|string
		 */
		public function get_start_end_of_day_from_datetime(
			DateTimeImmutable $datetime,
			bool $start = true,
			bool $format = false
		) {
			if ( ! $datetime instanceof DateTimeImmutable ) {
				return false;
			}

			$new = $datetime->format( 'Y-m-d' );

			if ( empty( $start ) ) {
				$new .= ' 23:59:59';
			} else {
				$new .= ' 00:00:00';
			}

			try {
				$result = new DateTimeImmutable( $new, $datetime->getTimezone() );
			}
			catch ( Exception $e ) {
				return false;
			}

			if ( ! empty( $format ) ) {
				$result = $result->format( $format );
			}

			return $result;
		}

		/**
		 * Get the difference (in minutes) that End DateTimeImmutable is after Start DateTimeImmutable.
		 *
		 * @param DateTimeImmutable $start_datetime
		 * @param DateTimeImmutable $end_datetime
		 *
		 * @return false|float|int Number of minutes (int|float). False if both are not DateTimeImmutable or if End is before Start.
		 */
		public function get_minutes_diff_between_two_datetimes(
			DateTimeImmutable $start_datetime,
			DateTimeImmutable $end_datetime
		) {
			if (
				! $start_datetime instanceof DateTimeImmutable
				|| ! $end_datetime instanceof DateTimeImmutable
				|| $start_datetime > $end_datetime
			) {
				return false;
			}

			$diff = $start_datetime->diff( $end_datetime );

			// There is always 60 seconds in a minute and 60 minutes in an hour, but there isn't always 24 hours in a day (due to DST).
			// https://stackoverflow.com/a/23675286/893907
			try {
				$total_seconds = ( new DateTimeImmutable() )->setTimeStamp( 0 )->add( $diff )->getTimeStamp();
			}
			catch ( Exception $e ) {
				return false;
			}

			$total_minutes = $total_seconds / 60;

			return $total_minutes;
		}

		/**
		 * Get the minutes duration difference between two strings in 24 hour time format (no date or seconds allowed).
		 *
		 * @param string $start_24_format
		 * @param string $end_24_format
		 *
		 * @return false|float|int Numeric amount. False if not valid 24 hour time format (##:##) or End is before Start.
		 */
		public function get_minutes_diff_between_two_times_same_day(
			string $start_24_format,
			string $end_24_format
		) {
			if (
				false === $this->is_valid_24_hour_format_time_string( $start_24_format )
				|| false === $this->is_valid_24_hour_format_time_string( $end_24_format )
			) {
				return false;
			}

			$time_zone = $this->get_php_time_zone_string_from_wp();
			$shared    = 'January 1, 1970 ';

			$start = $shared . $start_24_format;
			$end   = $shared . $end_24_format;

			try {
				$start_datetime = new DateTimeImmutable( $start, new DateTimeZone( $time_zone ) );
			}
			catch ( Exception $e ) {
				return false;
			}

			try {
				$end_datetime = new DateTimeImmutable( $end, new DateTimeZone( $time_zone ) );
			}
			catch ( Exception $e ) {
				return false;
			}

			$total_minutes = $this->get_minutes_diff_between_two_datetimes( $start_datetime, $end_datetime );

			if ( empty( $total_minutes ) ) {
				return false;
			}

			return $total_minutes;
		}

		/**
		 * Determine if a string is in a 24 hour time format, such as 00:00, 17:30, or 23:59.
		 *
		 * @link https://regex101.com/r/YPkRtl/1 Regex test for ##:## format.
		 * @link https://regex101.com/r/x4wE80/1 Regex test for ##:##:## format.
		 *
		 * @param string $time_string
		 * @param bool   $allow_seconds False matches ##:## but not ##:##:##. True allows the third ":##" as optional.
		 *
		 * @return bool
		 */
		public function is_valid_24_hour_format_time_string(
			string $time_string,
			bool $allow_seconds = false
		): bool {
			if ( ! is_string( $time_string ) ) {
				return false;
			}

			if ( empty( $allow_seconds ) ) {
				return (bool) preg_match( '/^(([0-1][0-9]|2[0-3]):[0-5][0-9])$/', $time_string );
			} else {
				return (bool) preg_match( '/^(([0-1][0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?)$/', $time_string );
			}
		}

		/**
		 * Whether a string is not just ISO 8601 compliant, as there are many valid variations, but that it is of the
		 * explicit '2005-08-15T15:52:01Z' or '2005-08-15T15:52:01+00:00' format, i.e. the extended variation,
		 * requiring colons but disallowing fractional units of time. See unit tests for valid and invalid.
		 * Also requires to be an actual/valid datetime, not allowing the 25th hour of the day, for example.
		 *
		 * @see https://wikipedia.org/wiki/ISO_8601 The many valid variations.
		 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
		 *      This method (or a new, separate one) could be changed to optionally allow fractional seconds so it
		 *      could be used with JavaScript's `Date.prototype.toISOString()`.
		 *
		 * @param string $datetime
		 *
		 * @return bool Whether string is a valid datetime formatted as extended ISO 8601 with colons, without fractions.
		 */
		public function is_fully_formatted_iso_8601_datetime_string( string $datetime ): bool {
			$dt = DateTimeImmutable::createFromFormat( DateTimeInterface::ATOM, $datetime );

			// If string was not of ISO 8601 format (even a less strict variant).
			if ( false === $dt ) {
				return false;
			}

			/**
			 * If a string ends with a capital 'Z', replace just that 'Z' with '+00:00' -- only for the comparison.
			 * Uses Positive Lookbehind regex to only replace the 'Z' if present at the end and preceded by a number.
			 */
			$zulu_to_zeros = preg_replace( '/(?<=\d)Z$/', '+00:00', $datetime );

			return $zulu_to_zeros === $dt->format( 'c' );
		}

		/**
		 * Get 'now' in UTC as an ISO 8601 string.
		 *
		 * @return string|null A full ISO 8601 string for right now in UTC. Null if some surprising error.
		 */
		public function get_now_utc_iso_8601_datetime_string(): ?string {
			try {
				$now_utc = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) );
			}
			catch ( Exception $exception ) {
				return null;
			}

			return $now_utc->format( 'c' );
		}
	}
}
