<?php
namespace nur\sery\file\csv;

use DateTimeInterface;
use nur\sery\cl;
use nur\sery\file\TempStream;
use nur\sery\os\path;
use nur\sery\php\nur_func;
use nur\sery\php\time\DateTime;
use nur\sery\web\http;

abstract class AbstractBuilder extends TempStream implements IBuilder {
  /** @var ?array schéma des données à écrire */
  const SCHEMA = null;

  /** @var ?array liste des colonnes à écrire */
  const HEADERS = null;

  /**
   * @var bool faut-il écrire les en-têtes, soit celles qui sont spécifiées,
   * soit celles qui sont calculées à partir des clés de la première ligne
   */
  const USE_HEADERS = true;

  /** @var ?string nom du fichier téléchargé */
  const OUTPUT = null;

  function __construct(?string $output, ?array $params=null) {
    if ($output !== null) $params["output"] = $output;
    $this->schema = $params["schema"] ?? static::SCHEMA;
    $this->headers = $params["headers"] ?? static::HEADERS;
    $this->useHeaders = $params["use_headers"] ?? static::USE_HEADERS;
    $rows = $params["rows"] ?? null;
    if (is_callable($rows)) $rows = $rows();
    $this->rows = $rows;
    $cookFunc = $params["cook_func"] ?? null;
    $cookCtx = $cookArgs = null;
    if ($cookFunc !== null) {
      nur_func::ensure_func($cookFunc, $this, $cookArgs);
      $cookCtx = nur_func::_prepare($cookFunc);
    }
    $this->cookCtx = $cookCtx;
    $this->cookArgs = $cookArgs;
    $this->output = $params["output"] ?? static::OUTPUT;
    $maxMemory = $params["max_memory"] ?? null;
    $throwOnError = $params["throw_on_error"] ?? null;
    parent::__construct($maxMemory, $throwOnError);
  }

  protected ?array $schema;

  protected ?array $headers;

  protected bool $useHeaders;

  protected ?iterable $rows;

  protected ?string $output;

  protected ?array $cookCtx;

  protected ?array $cookArgs;

  protected function ensureHeaders(?array $row=null): void {
    if ($this->headers !== null || !$this->useHeaders) return;
    if ($this->schema === null) $headers = null;
    else $headers = array_keys($this->schema);
    if ($headers === null && $row !== null) $headers = array_keys($row);
    $this->headers = $headers;
  }

  protected abstract function _write(array $row): void;

  protected bool $wroteHeaders = false;

  function writeHeaders(?array $headers=null): void {
    if ($this->wroteHeaders) return;
    if ($this->useHeaders) {
      if ($headers !== null) $this->headers = $headers;
      else $this->ensureHeaders();
      if ($this->headers !== null) $this->_write($this->headers);
    }
    $this->wroteHeaders = true;
  }

  protected function cookRow(?array $row): ?array {
    if ($this->cookCtx !== null) {
      $args = cl::merge([$row], $this->cookArgs);
      $row = nur_func::_call($this->cookCtx, $args);
    }
    if ($row !== null) {
      foreach ($row as &$value) {
        # formatter les dates
        if ($value instanceof DateTime) {
          $value = $value->format();
        } elseif ($value instanceof DateTimeInterface) {
          $value = DateTime::with($value)->format();
        }
      }; unset($value);
    }
    return $row;
  }

  function write(?array $row): void {
    $row = $this->cookRow($row);
    if ($row === null) return;
    $this->writeHeaders(array_keys($row));
    $this->_write($row);
  }

  function writeAll(?iterable $rows=null): void {
    $unsetRows = false;
    if ($rows === null) {
      $rows = $this->rows;
      $unsetRows = true;
    }
    if ($rows !== null) {
      foreach ($rows as $row) {
        $this->write(cl::with($row));
      }
    }
    if ($unsetRows) $this->rows = null;
  }

  abstract protected function _sendContentType(): void;

  protected bool $sentHeaders = false;

  function sendHeaders(): void {
    if ($this->sentHeaders) return;
    $this->_sendContentType();
    $output = $this->output;
    if ($output !== null) {
      http::download_as(path::filename($output));
    }
    $this->sentHeaders = true;
  }

  protected function _build(?iterable $rows=null): void {
    $this->writeAll($rows);
    $this->writeHeaders();
  }

  abstract protected function _checkOk(): bool;

  protected bool $built = false, $closed = false;

  function build(?iterable $rows=null, bool $close=true): bool {
    $ok = true;
    if (!$this->built) {
      $this->_build($rows);
      $this->built = true;
    }
    if ($close && !$this->closed) {
      $ok = $this->_checkOk();
      $this->closed = true;
    }
    return $ok;
  }

  function sendFile(?iterable $rows=null): int {
    if (!$this->built) {
      $this->_build($rows);
      $this->built = true;
    }
    if (!$this->closed) {
      if (!$this->_checkOk()) return 0;
      $this->closed = true;
    }
    $this->sendHeaders();
    return $this->fpassthru();
  }
}
