<?php
namespace nur\io\csv;

use nur\A;
use nur\str;

/**
 * Class Assoc2CsvHelper: outils pour écrire un flux au format CSV
 */
class Assoc2CsvHelper {
  private static function is_different(array $h1, array $h2): bool {
    sort($h1);
    sort($h2);
    return $h1 != $h2;
  }

  private function computeHeaders(array $row): array {
    return array_keys($row);
  }

  /** @var bool les flux avec plusieurs schémas sont-ils supportés? */
  protected $multiSchema = false;

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

  /**
   * @var array liste et ordre des champs en sortie. 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 bool faut-il afficher les en-têtes en sortie? */
  protected $outputHeaders = true;

  function setOutputHeaders(bool $outputHeaders): void {
    $this->outputHeaders = $outputHeaders;
  }

  /** @var ?array mappings des clés du tableau vers les colonne en sortie */
  protected $headerMappings;

  /**
   * $headerMappings peut être
   * - un tableau de la forme [include, add => null, source => dest]
   * - ou une chaine de la forme "include,=add,source=dest"
   * source est le nom de la clé dans le tableau en entrée, dest est le nom de
   * la colonne dans le flux CSV en sortie
   *
   * - 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 '=add' permettent d'ajouter dans la sortie une colonne avec
   * une valeur vide
   * - 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) {
          $source = substr($mapping, 0, $index);
          $dest = substr($mapping, $index + 1);
          if ($source && $dest) $headerMappings[$source] = $dest;
          elseif ($source) $headerMappings[$source] = null;
          else $headerMappings[$dest] = null;
        } else {
          $headerMappings[] = $mapping;
        }
      }
    }
    $this->headerMappings = $headerMappings;
  }

  function checkHeaders(?array $row): array {
    $skipLine = false;
    if ($row === null) {
      $outputHeaders = $this->outputHeaders && $this->headers !== null;
      return [$skipLine, $outputHeaders, $this->headers];
    }
    $prevHeaders = $this->headers;
    if ($this->multiSchema) {
      # vérifier si le schéma a changé
      $headers = $this->computeHeaders($row);
      if ($prevHeaders === null) $prevHeaders = $headers;
      if (self::is_different($prevHeaders, $headers)) {
        $skipLine = true;
        $this->outputHeaders = true;
      } else {
        $headers = $prevHeaders;
      }
    } else {
      $headers = $prevHeaders;
      if ($headers === null) $headers = $this->computeHeaders($row);
    }
    return [$skipLine, $this->outputHeaders, $headers];
  }

  function resetOutputHeaders(array $headers): void {
    $this->headers = $headers;
    $this->outputHeaders = false;
  }

  function cookHeaders(array $headers): array {
    return ut::writer_map_headers($headers, $this->headerMappings);
  }

  function cookValues(array $headers, array $row): array {
    $values = [];
    foreach ($headers as $header) {
      $values[$header] = A::get($row, $header);
    }
    $values = ut::writer_map_keys($values, $this->headerMappings);
    return array_values($values);
  }

  function getLine(array $values, string $flavour, bool $stripNl=true): string {
    [$separator, $enclosure, $escape] = flavours::get_params($flavour);
    $tmpf = fopen("php://memory", "w+b");
    fputcsv($tmpf, $values, $separator, $enclosure, $escape);
    rewind($tmpf);
    $line = stream_get_contents($tmpf);
    if ($stripNl) $line = str::strip_nl($line);
    fclose($tmpf);
    return $line;
  }
}
