<?php
namespace nur\mapper\base;

use ArrayAccess;
use Countable;
use nur\b\coll\IArray;
use nur\b\coll\TBaseArray;
use nur\b\coll\TGenericArray;
use nur\b\ICloseable;
use nur\b\params\Parametrable;
use nur\b\params\Tparametrable;
use nur\b\ValueException;
use nur\func;
use nur\mapper\base\oobd\IOobdManager;
use nur\mapper\base\oobd\TOobdManager;

/**
 * Class Consumer: une classe qui applique des mappers sur les données d'un
 * producer.
 */
class Consumer extends Parametrable implements ArrayAccess, Countable, IArray, ICloseable, IOobdManager {
  use TBaseArray, TGenericArray, Tparametrable, TOobdManager;

  function __construct($producer=null, ...$mappers) {
    parent::__construct();
    $this->data = [];
    if ($producer !== null) $this->setProducer($producer);
    $this->addAll($mappers);
  }

  function _haveMethod(string $method): bool {
    return method_exists($this, $method);
  }

  /** @var iterable */
  private $producer;

  /** @var PushProducer */
  private $pushProducer;

  function setProducer($producer, ...$args): self {
    $producer = producer_utils::ensure_producer($producer, $args);
    $this->producer = $producer;
    $this->pushProducer = $producer instanceof PushProducer? $producer: null;
    return $this;
  }

  function set($key, $mapper): self { return $this->_set($key, mapper_utils::ensure_mapper_class($mapper)); }
  function add($mapper): self { return $this->_set(null, mapper_utils::ensure_mapper_class($mapper)); }

  /** @var bool */
  private $setup = false;

  private function ensureSetup(): bool {
    if (!$this->setup) {
      $this->setup();
      $this->setup = true;
      return true;
    }
    return false;
  }

  protected function setup(): void {
  }

  /** @var iterable */
  private $pushIterator;

  private function _ensureSharedOobdManager(iterable $iterator): void {
    if ($iterator instanceof IOobdManager) {
      # récupérer le gestionnaire partagé le cas échéant
      $sharedOobdManager = $iterator->getSharedOobdManager();
      if ($sharedOobdManager !== null) $this->setSharedOobdManager($sharedOobdManager);
    }
  }

  private function buildIterator(bool $ensurePushable=false): array {
    $this->ensureSetup();
    if ($this->pushIterator === null) {
      if (!$ensurePushable) {
        $iterator = $this->producer;
        if ($iterator === null) {
          throw new ValueException("a producer is required");
        }
        if ($this->pushProducer === null) {
          if ($iterator instanceof IOobdManager) {
            # s'assurer qu'il y a toujours un gestionnaire partagé
            $iterator->ensureSharedOobdManager();
          }
          $iterator = mapper_utils::assemble_mappers($this->data, $iterator);
          $this->_ensureSharedOobdManager($iterator);
          return [true, $iterator];
        }
      }
      $iterator = $this->pushProducer;
      if ($iterator === null)  $iterator = new PushProducer();
      # s'assurer qu'il y a toujours un gestionnaire partagé
      $iterator->ensureSharedOobdManager();
      $this->pushProducer = $iterator;
      $this->pushIterator = mapper_utils::assemble_mappers($this->data, $iterator);
      $this->_ensureSharedOobdManager($this->pushIterator);
    }
    return [false, $this->pushIterator];
  }

  private $iterator;

  function hasOvalue(string $name): bool {
    if ($this->_hasOobdValue($name)) {
      return true;
    } elseif ($this->sharedOobdManager !== null
      && $this->sharedOobdManager->hasOvalue($name)) {
      return true;
    } elseif ($this->iterator instanceof IOobdManager) {
      return $this->iterator->hasOvalue($name);
    } elseif ($this->pushIterator instanceof IOobdManager) {
      return $this->pushIterator->hasOvalue($name);
    }
    return false;
  }

  /**
   * c'est la méthode {@link consume()} qui construit $this->iterator. l'accès
   * aux données OOB de la chaine de mapper n'est donc valide que dans les
   * méthodes {@link _consume()} et {@link cook()}
   *
   * De même, en mode push, ce n'est qu'après l'appel d'une des méthodes
   * {@link ensurePushable()}, {@link push()} ou {@link pushAll()} que les
   * données OOB de la chaine de mapper sont disponibles
   */
  function getOvalue(string $name, $default=null) {
    if ($this->_hasOobdValue($name)) {
      return $this->_getOobdValue($name, $default);
    } elseif ($this->sharedOobdManager !== null
      && $this->sharedOobdManager->hasOvalue($name)) {
      return $this->sharedOobdManager->getOvalue($name, $default);
    } elseif ($this->iterator instanceof IOobdManager) {
      return $this->iterator->getOvalue($name, $default);
    } elseif ($this->pushIterator instanceof IOobdManager) {
      return $this->pushIterator->getOvalue($name, $default);
    }
    return null;
  }

  function cook($item) {
  }

  protected function _consume(iterable $items): void {
    $cookFunc = func::_prepare([$this, "cook"]);
    foreach ($items as $key => $item) {
      func::_call($cookFunc, [$item, $key]);
    }
  }

  /**
   * consommer toutes les valeurs du producer
   *
   * les méthodes {@link setup()} et {@link teardown()} sont appelées
   * automatiquement
   */
  function consume($producer=null, ...$mappers): void {
    if ($producer !== null) $this->setProducer($producer);
    if ($mappers) $this->resetAll($mappers);

    $close = $this->ensureSetup();
    try {
      [$close, $iterator] = $this->buildIterator();
      $this->iterator = $iterator;
      $this->_consume($iterator);
    } finally {
      if ($close) $this->close();
    }
  }

  protected function teardown(): void {
    if ($this->iterator instanceof ICloseable) $this->iterator->close();
    $this->iterator = null;
    if ($this->pushIterator !== null) {
    if ($this->pushIterator instanceof ICloseable) $this->pushIterator->close();
      $this->pushProducer = null;
      $this->pushIterator = null;
    }
  }

  /** appeler la méthode {@link teardown()} si nécessaire */
  function close(): void {
    if ($this->setup) {
      $this->teardown();
      $this->setup = false;
    }
  }

  /**
   * s'assurer que les données OOB sont disponibles avant l'utilisation de
   * {@link push()} ou {@link pushAll()}. cette méthode n'a pas d'autre utilité
   */
  function ensurePushable(): void {
    $this->buildIterator(true);
  }

  /**
   * insérer une valeur, comme si elle provenait du producer.
   *
   * la méthodes {@link setup()} est appelée automatiquement si nécessaire, mais
   * pas {@link teardown()}. il faut donc penser à appeler {@link close()} quand
   * on a terminé
   */
  function push($item, $key=null): void {
    $this->buildIterator(true);
    $this->pushProducer->push($item, $key);
    $this->_consume($this->pushIterator);
  }

  /**
   * insérer des valeurs, comme si elles provenaient du producer
   */
  function pushAll(iterable $items): void {
    $this->buildIterator(true);
    $this->pushProducer->pushAll($items);
    $this->_consume($this->pushIterator);
  }
}