<?php
namespace nur\b\io;

use nur\b\ICloseable;
use nur\os;

/**
 * Class SharedFile: un fichier accédé par plusieurs processus en même temps.
 * le verrouillage est automatique en fonction de l'accès en lecture ou en
 * écriture.
 */
class SharedFile implements ICloseable {
  function __construct(string $file) {
    $this->file = $file;
  }

  /** @var string chemin du fichier */
  protected $file;

  /** @var resource descripteur de fichier */
  protected $fd;

  /** @var int */
  protected $serial;

  /**
   * @throws IOException
   * @return resource
   */
  protected function open() {
    if ($this->fd === null) {
      IOException::ensure_not_false(os::mkdirof($this->file));
      $this->fd = IOException::ensure_not_false(fopen($this->file, "c+b"));
      $this->serial = 0;
    }
    return $this->fd;
  }

  protected function lock(int $operation, ?int &$wouldBlock=null): bool {
    $locked = flock($this->open(), $operation, $wouldBlock);
    if ($locked) return true;
    if ($operation & LOCK_NB) return false;
    else throw IOException::error();
  }

  protected function unlock(bool $close=false): void {
    if ($this->fd !== null) {
      flock($this->fd, LOCK_UN);
      if ($close) $this->close();
    }
  }

  function size(): int {
    $file = $this->file;
    if (!file_exists($file)) return 0;
    else return filesize($file);
  }

  /** @throws IOException */
  function tell(): int {
    return IOException::ensure_not_false(ftell($this->fd));
  }

  /**
   * @return int la position après avoir déplacé le pointeur
   * @throws IOException
   */
  function seek(int $offset, int $whence=SEEK_SET): int {
    $r = fseek($this->fd, $offset, $whence);
    if ($r == -1) throw IOException::error();
    return $this->tell();
  }

  /**
   * essayer de verrouiller le fichier en lecture. retourner true si l'opération
   * réussit. dans ce cas, il faut appeler {@link getReader()} avec l'argument
   * true
   */
  function canRead(): bool {
    return $this->lock(LOCK_SH + LOCK_NB);
  }

  /**
   * verrouiller en mode partagé puis retourner un objet permettant de lire le
   * fichier.
   */
  function getReader(bool $alreadyLockedByCanRead=false): IReader {
    if (!$alreadyLockedByCanRead) $this->lock(LOCK_SH);
    return new class($this->fd, ++$this->serial, $this) extends StreamReader {
      function __construct($fd, int $serial, SharedFile $sf) {
        $this->sf = $sf;
        $this->serial = $serial;
        parent::__construct($fd);
      }

      /** @var SharedFile */
      private $sf;

      /** @var int */
      private $serial;

      function close(bool $close=true): void {
        if ($this->sf !== null && $close) {
          $this->sf->close($this->serial);
          $this->fd = null;
          $this->sf = null;
        }
      }
    };
  }

  /**
   * essayer de verrouiller le fichier en écriture. retourner true si l'opération
   * réussit. dans ce cas, il faut appeler {@link getWriter()} avec l'argument
   * true
   */
  function canWrite(): bool {
    return $this->lock(LOCK_EX + LOCK_NB);
  }

  /**
   * verrouiller en mode exclusif puis retourner un objet permettant d'écrire
   * dans le fichier
   */
  function getWriter(bool $alreadyLockedByCanWrite=false): IWriter {
    if (!$alreadyLockedByCanWrite) $this->lock(LOCK_EX);
    return new class($this->fd, ++$this->serial, $this) extends StreamWriter {
      function __construct($fd, int $serial, SharedFile $sf) {
        $this->sf = $sf;
        $this->serial = $serial;
        parent::__construct($fd);
      }

      /** @var SharedFile */
      private $sf;

      /** @var int */
      private $serial;

      function close(bool $close=true): void {
        if ($this->sf !== null && $close) {
          $this->sf->close($this->serial);
          $this->fd = null;
          $this->sf = null;
        }
      }
    };
  }

  function close(?int $serial=null): void {
    if ($this->fd !== null && ($serial === null || $this->serial === $serial)) {
      fclose($this->fd);
      $this->fd = null;
    }
  }

  /** retourner le contenu du fichier sous forme de chaine */
  function getContents(bool $alreadyLockedByCanRead=false): string {
    if (!$alreadyLockedByCanRead) $this->lock(LOCK_SH);
    try {
      return file_get_contents($this->file);
    } finally {
      $this->unlock(true);
    }
  }

  function unserialize(?array $options=null, bool $alreadyLockedByCanRead=false) {
    $args = [$this->getContents($alreadyLockedByCanRead)];
    if ($options !== null) $args[] = $options;
    return unserialize(...$args);
  }

  /** écrire le contenu spécifié dans le fichier */
  function putContents(string $contents, bool $alreadyLockedByCanWrite=false): void {
    if (!$alreadyLockedByCanWrite) $this->lock(LOCK_EX);
    try {
      file_put_contents($this->file, $contents);
    } finally {
      $this->unlock(true);
    }
  }

  function serialize($object, bool $alreadyLockedByCanWrite=false): void {
    $this->putContents(serialize($object), $alreadyLockedByCanWrite);
  }
}
