<?php
namespace nur\io\csv;

use nur\A;

/**
 * Class Csv2AssocHelper: outils pour lire un flux au format CSV
 */
class Csv2AssocHelper {
  /** @var bool */
  protected $multiSchema = false;

  function setMultiSchema(bool $multiSchema): void {
    $this->multiSchema = $multiSchema;
  }

  /** @var int */
  protected $skipLines = 0;

  function setSkipLines(int $skipLines): void {
    $this->skipLines = $skipLines;
  }

  /**
   * @var ?bool faut-il analyser le premier élément du flux pour calculer la
   * liste des clés en entrée? null signifie que la valeur est dynamique: elle
   * vaut ($this->ppHeaders === null)
   *
   * Si ce champ est vrai, le premier élément est toujours consommé. cependant,
   * la liste des champs n'est analysée que si elle n'a pas été spécifiée au
   * préalable avec {@link setHeaders()}
   */
  protected $parseHeaders;

  function setParseHeaders(?bool $parseHeaders): void {
    $this->parseHeaders = $parseHeaders;
  }

  /**
   * @var array liste des champs en entrée dans l'ordre. si cette valeur n'est
   * pas spécifiée, elle est calculée à partir du premier élément du flux.
   */
  protected $headers;

  function setHeaders(?array $headers): void {
    $this->headers = $headers;
  }

  /** @var ?array mappings des colonnes vers les clés du tableau résultat */
  protected $headerMappings;

  /**
   * $headerMappings peut être
   * - un tableau de la forme [include, exclude => null, dest => source]
   * - ou une chaine de la forme "include,=exclude,dest=source"
   * source est le nom de la colonne du flux CSV, dest est la clé dans le
   * tableau retourné
   *
   * - les éléments 'include' permettent d'inclure le champ spécifié. si aucun
   * champ include n'est spécifié, *tous* les champs sont inclus (sauf ceux qui
   * sont exclus, bien entendu)
   * - les éléments '=exclude' permettent d'exclure le champ spécifié.
   * - les éléments 'dest=source' permettent de renommer les champs: le champ
   * source dans le flux CSV devient le champ dest dans le tableau associatif
   */
  function setHeaderMappings($headerMappings): void {
    if ($headerMappings !== null && !is_array($headerMappings)) {
      $mappings = explode(",", strval($headerMappings));
      $headerMappings = [];
      foreach ($mappings as $mapping) {
        if (($index = strpos($mapping, "=")) !== false) {
          $dest = substr($mapping, 0, $index);
          $source = substr($mapping, $index + 1);
          if ($dest && $source) $headerMappings[$dest] = $source;
          elseif ($dest) $headerMappings[$dest] = null;
          else $headerMappings[$source] = null;
        } else {
          $headerMappings[] = $mapping;
        }
      }
    }
    $this->headerMappings = $headerMappings;
  }

  /** @var bool */
  protected $shouldMapEmpty = false;

  /** @var mixed */
  protected $mapEmpty = null;

  function setMapEmpty($mapEmpty): void {
    $this->shouldMapEmpty = true;
    $this->mapEmpty = $mapEmpty;
  }

  function _parseLine(): bool {
    if ($this->skipLines > 0) {
      $this->skipLines--;
      return false;
    }
    return true;
  }

  function checkHeaders(array $values): bool {
    if ($this->multiSchema && count($values) === 1 && $values[0] === null) {
      # ligne vide, changer de schéma
      $this->headers = null;
      return true;
    }
    $parseHeaders = $this->parseHeaders;
    $setParseHeaders = $parseHeaders !== null;
    if ($parseHeaders === null) $parseHeaders = $this->headers === null;
    if ($parseHeaders) {
      if ($this->headers === null) $this->headers = $values;
      if ($setParseHeaders) $this->parseHeaders = false;
      return true;
    }
    return false;
  }

  function mapValues(array $values): array {
    $headers = $this->headers;
    if ($headers === null) return $values;
    $row = [];
    $index = 0;
    foreach ($headers as $header) {
      $row[$header] = A::get($values, $index++, false);
    }
    return $row;
  }

  function mapRow(array $values): array {
    $row = $this->mapValues($values);
    $row = ut::reader_map_keys($row, $this->headerMappings);
    foreach ($row as &$value) {
      if ($value === "" && $this->shouldMapEmpty) $value = $this->mapEmpty;
    }; unset($value);
    return $row;
  }
}