<?php
namespace nur\sery\php\time;

use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;

/**
 * Class DateTime: une date et une heure
 *
 * @property-read int $year
 * @property-read int $month
 * @property-read int $day
 * @property-read int $hour
 * @property-read int $minute
 * @property-read int $second
 * @property-read int $wday
 * @property-read int $wnum
 * @property-read string $timezone
 * @property-read string $datetime
 * @property-read string $date
 * @property-read string $Ymd
 * @property-read string $YmdHMS
 * @property-read string $YmdHMSZ
 */
class DateTime extends \DateTime {
  static function with($datetime): self {
    if ($datetime instanceof static) return $datetime;
    else return new static($datetime);
  }

  const DMY_PATTERN = '/^(\d+)\/(\d+)(?:\/(\d+))?$/';
  const YMD_PATTERN = '/^((?:\d{2})?\d{2})(\d{2})(\d{2})$/';
  const DMYHIS_PATTERN = '/^(\d+)\/(\d+)(?:\/(\d+))? +(\d+)[h:.](\d+)(?:[:.](\d+))?$/';
  const YMDHISZ_PATTERN = '/^((?:\d{2})?\d{2})(\d{2})(\d{2})[tT](\d{2})(\d{2})(\d{2})?([zZ])?$/';

  static function isa($datetime): bool {
    if ($datetime === null) return false;
    if ($datetime instanceof DateTimeInterface) return true;
    if (!is_int($datetime) && !is_string($datetime)) return false;
    return preg_match(self::DMY_PATTERN, $datetime) ||
      preg_match(self::YMD_PATTERN, $datetime) ||
      preg_match(self::DMYHIS_PATTERN, $datetime) ||
      preg_match(self::YMDHISZ_PATTERN, $datetime);
  }

  static function isa_datetime($datetime, bool $frOnly=false): bool {
    if ($datetime === null) return false;
    if ($datetime instanceof DateTimeInterface) return true;
    if (!is_int($datetime) && !is_string($datetime)) return false;
    return preg_match(self::DMYHIS_PATTERN, $datetime) ||
      (!$frOnly && preg_match(self::YMDHISZ_PATTERN, $datetime));
  }

  static function isa_date($date, bool $frOnly=false): bool {
    if ($date === null) return false;
    if ($date instanceof DateTimeInterface) return true;
    if (!is_int($date) && !is_string($date)) return false;
    return preg_match(self::DMY_PATTERN, $date) ||
      (!$frOnly && preg_match(self::YMD_PATTERN, $date));
  }

  static function _YmdHMSZ_format(\DateTime $datetime): string {
    $YmdHMS = $datetime->format("Ymd\\THis");
    $Z = $datetime->format("P");
    if ($Z === "+00:00") $Z = "Z";
    return "$YmdHMS$Z";
  }

  const DEFAULT_FORMAT = "d/m/Y H:i:s";
  const INT_FORMATS = [
    "year" => "Y",
    "month" => "m",
    "day" => "d",
    "hour" => "H",
    "minute" => "i",
    "second" => "s",
    "wday" => "N",
    "wnum" => "W",
  ];
  const STRING_FORMATS = [
    "timezone" => "P",
    "datetime" => "d/m/Y H:i:s",
    "date" => "d/m/Y",
    "Ymd" => "Ymd",
    "YmdHMS" => "Ymd\\THis",
    "YmdHMSZ" => [self::class, "_YmdHMSZ_format"],
  ];

  static function clone(DateTimeInterface $dateTime): self {
    if ($dateTime instanceof static) return clone $dateTime;
    $clone = new static();
    $clone->setTimestamp($dateTime->getTimestamp());
    $clone->setTimezone($dateTime->getTimezone());
    return $clone;
  }

  /**
   * corriger une année à deux chiffres qui est située dans le passé et
   * retourner l'année à 4 chiffres.
   *
   * par exemple, si l'année courante est 2019, alors:
   * - fix_past_year('18') === '2018'
   * - fix_past_year('19') === '1919'
   * - fix_past_year('20') === '1920'
   */
  static function fix_past_year(int $year): int {
    if ($year < 100) {
      $y = getdate(); $y = $y["year"];
      $r = $y % 100;
      $c = $y - $r;
      if ($year >= $r) $year += $c - 100;
      else $year += $c;
    }
    return $year;
  }

  /**
   * corriger une année à deux chiffres et retourner l'année à 4 chiffres.
   * l'année charnière entre année passée et année future est 70
   *
   * par exemple, si l'année courante est 2019, alors:
   * - fix_past_year('18') === '2018'
   * - fix_past_year('19') === '2019'
   * - fix_past_year('20') === '2020'
   * - fix_past_year('69') === '2069'
   * - fix_past_year('70') === '1970'
   * - fix_past_year('71') === '1971'
   */
  static function fix_any_year(int $year): int {
    if ($year < 100) {
      $y = intval(date("Y"));
      $r = $y % 100;
      $c = $y - $r;
      if ($year >= 70) $year += $c - 100;
      else $year += $c;
    }
    return $year;
  }

  function __construct($datetime="now", DateTimeZone $timezone=null) {
    $datetime ??= "now";
    if ($datetime instanceof \DateTimeInterface) {
      if ($timezone === null) $timezone = $datetime->getTimezone();
      parent::__construct();
      $this->setTimestamp($datetime->getTimestamp());
      $this->setTimezone($timezone);
    } elseif (is_int($datetime)) {
      parent::__construct("now", $timezone);
      $this->setTimestamp($datetime);
    } elseif (!is_string($datetime)) {
      throw new InvalidArgumentException("datetime must be a string or an instance of DateTimeInterface");
    } else {
      $Y = $H = $Z = null;
      if (preg_match(self::DMY_PATTERN, $datetime, $ms)) {
        $Y = $ms[3] ?? null;
        if ($Y !== null) $Y = self::fix_any_year(intval($Y));
        else $Y = intval(date("Y"));
        $m = intval($ms[2]);
        $d = intval($ms[1]);
      } elseif (preg_match(self::YMD_PATTERN, $datetime, $ms)) {
        $Y = $ms[1];
        if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1]));
        else $Y = intval($Y);
        $m = intval($ms[2]);
        $d = intval($ms[3]);
      } elseif (preg_match(self::DMYHIS_PATTERN, $datetime, $ms)) {
        $Y = $ms[3];
        if ($Y !== null) $Y = self::fix_any_year(intval($Y));
        else $Y = intval(date("Y"));
        $m = intval($ms[2]);
        $d = intval($ms[1]);
        $H = intval($ms[4]);
        $M = intval($ms[5]);
        $S = intval($ms[6] ?? 0);
      } elseif (preg_match(self::YMDHISZ_PATTERN, $datetime, $ms)) {
        $Y = $ms[1];
        if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1]));
        else $Y = intval($Y);
        $m = intval($ms[2]);
        $d = intval($ms[3]);
        $H = intval($ms[4]);
        $M = intval($ms[5]);
        $S = intval($ms[6] ?? 0);
        $Z = $ms[7] ?? null;
      }
      if ($Y !== null) {
        if ($H === null) $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d);
        else $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H, $M, $S);
        if ($Z !== null) $timezone = new DateTimeZone("UTC");
      }
      parent::__construct($datetime, $timezone);
    }
  }

  function diff($target, $absolute=false): DateInterval {
    return new DateInterval(parent::diff($target, $absolute));
  }

  function format($format=self::DEFAULT_FORMAT): string {
    return \DateTime::format($format);
  }

  /**
   * modifier cet objet pour que l'heure soit à 00:00:00 ce qui le rend propice
   * à l'utilisation comme borne inférieure d'une période
   */
  function wrapStartOfDay(): self {
    $this->setTime(0, 0);
    return $this;
  }

  /**
   * modifier cet objet pour que l'heure soit à 23:59:59.999999 ce qui le rend
   * propice à l'utilisation comme borne supérieure d'une période
   */
  function wrapEndOfDay(): self {
    $this->setTime(23, 59, 59, 999999);
    return $this;
  }

  function getPrevDay(int $nbDays=1, bool $skipWeekend=false): self {
    if ($nbDays == 1 && $skipWeekend && $this->wday == 1) {
      $nbdays = 3;
    }
    return static::with($this->sub(new \DateInterval("P${nbDays}D")));
  }

  function getNextDay(int $nbDays=1, bool $skipWeekend=false): self {
    if ($nbDays == 1 && $skipWeekend) {
      $wday = $this->wday;
      if ($wday > 5) $nbDays = 8 - $this->wday;
    }
    return static::with($this->add(new \DateInterval("P${nbDays}D")));
  }

  function getElapsedAt(?DateTime $now=null, ?int $resolution=null): string {
    return Elapsed::format_at($this, $now, $resolution);
  }

  function getElapsedSince(?DateTime $now=null, ?int $resolution=null): string {
    return Elapsed::format_since($this, $now, $resolution);
  }

  function getElapsedDelay(?DateTime $now=null, ?int $resolution=null): string {
    return Elapsed::format_delay($this, $now, $resolution);
  }

  function __toString(): string {
    return $this->format();
  }

  function __get($name) {
    if (array_key_exists($name, self::INT_FORMATS)) {
      $format = self::INT_FORMATS[$name];
      if (is_callable($format)) return $format($this);
      else return intval($this->format($format));
    } elseif (array_key_exists($name, self::STRING_FORMATS)) {
      $format = self::STRING_FORMATS[$name];
      if (is_callable($format)) return $format($this);
      else return $this->format($format);
    }
    throw new InvalidArgumentException("Unknown property $name");
  }
}
