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

use DateTimeInterface;
use InvalidArgumentException;

/**
 * Class Delay: une durée jusqu'à un moment destination. le moment destination
 * est calculé à la création de l'objet en fonction du moment courant.
 *
 * La durée peut-être:
 * - un nombre, qui est le nombre de secondes jusqu'au moment destination
 * - une chaine de la forme "x[WDHMS]y" où x et y sont des nombres et la lettre
 * est l'unité de temps: W représente une semaine, D une journée, H une heure,
 * M une minute et S une seconde.
 * - la chaine "INF" qui représente une durée infinie
 *
 * Dans cette dernière forme, le timestamp destination est calculé en ajoutant x
 * unités. puis l'unité inférieure est ramenée à (0 + y)
 * par exemple,
 * - "1D5" signifie le lendemain à 5h (+1 jour, puis ramené à 5h)
 * - "2H0" signifie au plus deux heures plus tard
 * - "0W5" signifie à 5 le dimanche de la semaine en cours
 *
 * NB: la valeur y pour l'unité S est ignorée
 */
class Delay {
  static function with($delay, ?DateTimeInterface $from=null): self {
    if ($delay instanceof static) return $delay;
    else return new static($delay, $from);
  }

  /** valeurs par défaut de x et y pour les unités supportées */
  const DEFAULTS = [
    "w" => [0, 5],
    "d" => [1, 5],
    "h" => [1, 0],
    "m" => [1, 0],
    "s" => [1, 0],
  ];

  static function compute_dest(int $x, string $u, ?int $y, DateTime $from): array {
    $dest = DateTime::clone($from);
    $yu = null;
    switch ($u) {
    case "w":
      if ($x > 0) {
        $x *= 7;
        $dest->add(new \DateInterval("P${x}D"));
      }
      $w = 7 - intval($dest->format("w"));
      $dest->add(new \DateInterval("P${w}D"));
      $yu = "h";
      break;
    case "d":
      $dest->add(new \DateInterval("P${x}D"));
      $yu = "h";
      break;
    case "h":
      $dest->add(new \DateInterval("PT${x}H"));
      $yu = "m";
      break;
    case "m":
      $dest->add(new \DateInterval("PT${x}M"));
      $yu = "s";
      break;
    case "s":
      $dest->add(new \DateInterval("PT${x}S"));
      break;
    }
    if ($y !== null && $yu !== null) {
      $h = intval($dest->format("H"));
      $m = intval($dest->format("i"));
      switch ($yu) {
      case "h":
        $dest->setTime($y, 0, 0, 0);
        break;
      case "m":
        $dest->setTime($h, $y, 0, 0);
        break;
      case "s":
        $dest->setTime($h, $m, $y, 0);
        break;
      }
    }
    $u = strtoupper($u);
    $repr = $y !== null? "$x$u$y": "$x";
    return [$dest, $repr];
  }

  function __construct($delay, ?DateTimeInterface $from=null) {
    if ($from === null) $from = new DateTime();
    if ($delay === "INF") {
      $dest = DateTime::clone($from);
      $dest->add(new DateInterval("P9999Y"));
      $repr = "INF";
    } elseif (is_int($delay)) {
      [$dest, $repr] = self::compute_dest($delay, "s", null, $from);
    } elseif (is_string($delay) && preg_match('/^\d+$/', $delay)) {
      $x = intval($delay);
      [$dest, $repr] = self::compute_dest($x, "s", null, $from);
    } elseif (is_string($delay) && preg_match('/^(\d*)([wdhms])(\d*)$/i', $delay, $ms)) {
      [$x, $u, $y] = [$ms[1], $ms[2], $ms[3]];
      $u = strtolower($u);
      $default = self::DEFAULTS[$u];
      if ($x === "") $x = $default[0];
      else $x = intval($x);
      if ($y === "") $y = $default[1];
      else $y = intval($y);
      [$dest, $repr] = self::compute_dest($x, $u, $y, $from);
    } else {
      throw new InvalidArgumentException("invalid delay");
    }
    $this->dest = $dest;
    $this->repr = $repr;
  }

  function __serialize(): array {
    return [$this->dest, $this->repr];
  }
  function __unserialize(array $data): void {
    [$this->dest, $this->repr] = $data;
  }

  /** @var DateTime */
  protected $dest;

  function getDest(): DateTime {
    return $this->dest;
  }

  function addDuration($duration) {
    if (is_int($duration) && $duration < 0) {
      $this->dest->sub(DateInterval::with(-$duration));
    } else {
      $this->dest->add(DateInterval::with($duration));
    }
  }

  function subDuration($duration) {
    if (is_int($duration) && $duration < 0) {
      $this->dest->add(DateInterval::with(-$duration));
    } else {
      $this->dest->sub(DateInterval::with($duration));
    }
  }

  /** @var string */
  protected $repr;

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

  protected function _getDiff(?DateTimeInterface $now=null): \DateInterval {
    if ($now === null) $now = new DateTime();
    return $this->dest->diff($now);
  }

  /** retourner true si le délai imparti est écoulé */
  function isElapsed(?DateTimeInterface $now=null): bool {
    if ($this->repr === "INF") return false;
    else return $this->_getDiff($now)->invert == 0;
  }

  /**
   * retourner l'intervalle entre le moment courant et la destination.
   *
   * l'intervalle est négatif si le délai n'est pas écoulé, positif sinon
   */
  function getDiff(?DateTimeInterface $now=null): DateInterval {
    return new DateInterval($this->_getDiff($now));
  }
}
