<?php
namespace nur\mapper\item;

use nur\A;
use nur\b\IllegalAccessException;
use nur\b\params\Tparametrable;
use nur\b\ValueException;
use nur\mapper\base\Mapper;
use nur\mapper\base\mapper_utils;
use nur\mapper\base\mark_utils;
use nur\str;

/**
 * Class ItemFilterMapper: filtrage des élements d'un flux
 *
 * --autogen-properties-and-methods--
 * @method int getSkipItems()
 * @method int getMaxCount()
 * @method setAction($value)
 * @method int setSkipItems(int $value)
 * @method int setMaxCount(int $value)
 * @method array|null setConditions(?array $value)
 */
class ItemFilterMapper extends Mapper {
  use Tparametrable;

  const ACTION_FILTER = 1;
  const ACTION_KEEP = 2;
  const ACTION_MARK = 3;

  const CONDITION_EQ = 1;
  const CONDITION_NE = 2;
  const CONDITION_LT = 3;
  const CONDITION_LE = 4;
  const CONDITION_GT = 5;
  const CONDITION_GE = 6;
  const CONDITION_Z = 7;
  const CONDITION_NZ = 8;
  const CONDITION_N = 9;
  const CONDITION_NN = 10;
  const CONDITION_TRUE = 11;
  const CONDITION_FALSE = 12;

  const PARAMETRABLE_PARAMS_SCHEMA = [
    "action" => ["key", self::ACTION_KEEP, "action à effectuer si l'élément correspondant"],
    "skip_items" => ["int", null, "nombre d'éléments à ignorer au début du flux"],
    "max_count" => ["int", null, "nombre d'éléments maximum à retourner"],
    "conditions" => ["?array", null, "conditions que doivent remplir l'élément pour être sélectionné"],
  ];

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

  function pp_setAction($action): void {
    switch ($action) {
    case self::ACTION_FILTER:
    case self::ACTION_KEEP:
    case self::ACTION_MARK:
      break;
    case "filter":
    case "x":
      $action = self::ACTION_FILTER;
      break;
    case "keep":
    case "k":
      $action = self::ACTION_KEEP;
      break;
    case "mark":
    case "m":
      $action = self::ACTION_MARK;
      break;
    default:
      throw ValueException::invalid_value($action, "action");
    }
    if ($action == self::ACTION_MARK) mark_utils::set_use_marks($this);
    $this->ppAction = $action;
  }

  function setActionFilter(): self {
    $this->pp_setAction(self::ACTION_FILTER);
    return $this;
  }

  function setActionKeep(): self {
    $this->pp_setAction(self::ACTION_KEEP);
    return $this;
  }

  function setActionMark(): self {
    $this->pp_setAction(self::ACTION_MARK);
    return $this;
  }

  /** @var int nombre d'éléments à ignorer au début du flux */
  protected $ppSkipItems = 0;

  /** @var int nombre d'éléments maximum à retourner */
  protected $ppMaxCount = -1;

  /** @var array */
  protected $ppConditions = [];

  function pp_setConditions(?array $conditions): void {
    if ($conditions !== null) {
      foreach ($conditions as $condition) {
        $this->addCondition($condition);
      }
    }
  }

  function addCondition(?string $condition): self {
    if (preg_match('/^([A-Za-z0-9_]*)=(.*)$/', $condition, $vs)) {
      $key = A::get($vs, 1);
      $condition = A::get($vs, 2);
    } else {
      $key = null;
    }
    if (mapper_utils::check_prefix($condition, $value, "eq:", "=")) {
      $this->ppConditions[] = [self::CONDITION_EQ, $key, $value];
    } elseif (mapper_utils::check_prefix($condition, $value, "ne:", "!=")) {
      $this->ppConditions[] = [self::CONDITION_NE, $key, $value];
    } elseif (mapper_utils::check_prefix($condition, $value, "lt:", "<")) {
      $this->ppConditions[] = [self::CONDITION_LT, $key, $value];
    } elseif (mapper_utils::check_prefix($condition, $value, "le:", "<=")) {
      $this->ppConditions[] = [self::CONDITION_LE, $key, $value];
    } elseif (mapper_utils::check_prefix($condition, $value, "gt:", ">")) {
      $this->ppConditions[] = [self::CONDITION_GT, $key, $value];
    } elseif (mapper_utils::check_prefix($condition, $value, "ge:", ">=")) {
      $this->ppConditions[] = [self::CONDITION_GE, $key, $value];
    } elseif (mapper_utils::check_prefix($condition, $value, "nz")) {
      $this->ppConditions[] = [self::CONDITION_NZ, $key];
    } elseif (mapper_utils::check_prefix($condition, $value, "z")) {
      $this->ppConditions[] = [self::CONDITION_Z, $key];
    } elseif (mapper_utils::check_prefix($condition, $value, "notnull", "nn")) {
      $this->ppConditions[] = [self::CONDITION_NN, $key];
    } elseif (mapper_utils::check_prefix($condition, $value, "null", "n")) {
      $this->ppConditions[] = [self::CONDITION_N, $key];
    } elseif (mapper_utils::check_prefix($condition, $value, "true", "t")) {
      $this->ppConditions[] = [self::CONDITION_TRUE, $key];
    } elseif (mapper_utils::check_prefix($condition, $value, "false", "f")) {
      $this->ppConditions[] = [self::CONDITION_FALSE, $key];
    } else {
      # par défaut, tester l'égalité
      $this->ppConditions[] = [self::CONDITION_EQ, $key, $condition];
    }
    return $this;
  }

  function addConditionEq($value, ?string $key=null): self {
    $this->ppConditions[] = [self::CONDITION_EQ, $key, $value];
    return $this;
  }

  function addConditionNe($value, ?string $key=null): self {
    $this->ppConditions[] = [self::CONDITION_NE, $key, $value];
    return $this;
  }

  function addConditionLt($value, ?string $key=null): self {
    $this->ppConditions[] = [self::CONDITION_LT, $key, $value];
    return $this;
  }

  function addConditionLe($value, ?string $key=null): self {
    $this->ppConditions[] = [self::CONDITION_LE, $key, $value];
    return $this;
  }

  function addConditionGt($value, ?string $key=null): self {
    $this->ppConditions[] = [self::CONDITION_GT, $key, $value];
    return $this;
  }

  function addConditionGe($value, ?string $key=null): self {
    $this->ppConditions[] = [self::CONDITION_GE, $key, $value];
    return $this;
  }

  function addConditionZ(?string $key=null): self {
    $this->ppConditions[] = [self::CONDITION_Z, $key];
    return $this;
  }

  function addConditionNz(?string $key=null): self {
    $this->ppConditions[] = [self::CONDITION_NZ, $key];
    return $this;
  }

  function addConditionN(?string $key=null): self {
    $this->ppConditions[] = [self::CONDITION_N, $key];
    return $this;
  }

  function addConditionNn(?string $key=null): self {
    $this->ppConditions[] = [self::CONDITION_NN, $key];
    return $this;
  }

  function addConditionTrue(?string $key=null): self {
    $this->ppConditions[] = [self::CONDITION_TRUE, $key];
    return $this;
  }

  function addConditionFalse(?string $key=null): self {
    $this->ppConditions[] = [self::CONDITION_FALSE, $key];
    return $this;
  }

  #############################################################################

  protected static function check(&$item, ?string $forKey, callable $func, array $args): bool {
    if (is_array($item)) {
      [$forKey, $subKey] = str::split_pair($forKey, ".");
      $result = false;
      foreach ($item as $key => &$value) {
        if ($forKey !== null & $key != $forKey) continue;
        $result = $result || self::check($value, $subKey, $func, $args);
      }
      return $result;
    } else {
      return boolval($func($item, ...$args));
    }
  }

  protected static function check_eq($value, $other): bool {
    return $value == $other;
  }

  protected static function check_ne($value, $other): bool {
    return $value != $other;
  }

  protected static function check_lt($value, $other): bool {
    return $value < $other;
  }

  protected static function check_le($value, $other): bool {
    return $value <= $other;
  }

  protected static function check_gt($value, $other): bool {
    return $value > $other;
  }

  protected static function check_ge($value, $other): bool {
    return $value >= $other;
  }

  protected static function check_z($value): bool {
    return $value === null || $value === false;
  }

  protected static function check_nz($value): bool {
    return $value !== null && $value !== false;
  }

  protected static function check_n($value): bool {
    return $value === null;
  }

  protected static function check_nn($value): bool {
    return $value !== null;
  }

  protected static function check_true($value): bool {
    return $value === true;
  }

  protected static function check_false($value): bool {
    return $value === false;
  }

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

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

  protected function setup(): void {
    parent::setup();
    $this->skipped = 0;
    $this->count = 0;
  }

  protected function select($item, bool $selected, ?int $action=null) {
    if ($action === null) $action = $this->ppAction;
    switch ($action) {
    case self::ACTION_FILTER:
      if ($selected) return $this->mapToNone();
      else return $item;
    case self::ACTION_KEEP:
      if ($selected) return $item;
      else return $this->mapToNone();
    case self::ACTION_MARK:
      if ($selected) $this->setItemMarked($item);
      return $item;
    }
    throw IllegalAccessException::not_implemented("action $this->ppAction");
  }

  function mapper($item) {
    if ($this->skipped < $this->ppSkipItems) {
      $this->skipped++;
      return $this->select($item, false, self::ACTION_KEEP);
    }
    if ($this->ppMaxCount < 0 || $this->count < $this->ppMaxCount) {
      $this->count++;
      $selected = true;
      foreach ($this->ppConditions as $conditionParams) {
        [$condition, $forKey] = array_slice($conditionParams, 0, 2);
        $params = array_slice($conditionParams, 2);
        switch ($condition) {
        case self::CONDITION_EQ:
          $selected = $selected && self::check($item, $forKey, [self::class, "check_eq"], $params);
          break;
        case self::CONDITION_NE:
          $selected = $selected && self::check($item, $forKey, [self::class, "check_ne"], $params);
          break;
        case self::CONDITION_LT:
          $selected = $selected && self::check($item, $forKey, [self::class, "check_lt"], $params);
          break;
        case self::CONDITION_LE:
          $selected = $selected && self::check($item, $forKey, [self::class, "check_le"], $params);
          break;
        case self::CONDITION_GT:
          $selected = $selected && self::check($item, $forKey, [self::class, "check_gt"], $params);
          break;
        case self::CONDITION_GE:
          $selected = $selected && self::check($item, $forKey, [self::class, "check_ge"], $params);
          break;
        case self::CONDITION_Z:
          $selected = $selected && self::check($item, $forKey, [self::class, "check_z"], $params);
          break;
        case self::CONDITION_NZ:
          $selected = $selected && self::check($item, $forKey, [self::class, "check_nz"], $params);
          break;
        case self::CONDITION_N:
          $selected = $selected && self::check($item, $forKey, [self::class, "check_n"], $params);
          break;
        case self::CONDITION_NN:
          $selected = $selected && self::check($item, $forKey, [self::class, "check_nn"], $params);
          break;
        case self::CONDITION_TRUE:
          $selected = $selected && self::check($item, $forKey, [self::class, "check_true"], $params);
          break;
        case self::CONDITION_FALSE:
          $selected = $selected && self::check($item, $forKey, [self::class, "check_false"], $params);
          break;
        default:
          throw IllegalAccessException::not_implemented("condition $condition");
        }
      }
      return $this->select($item, $selected);
    }
    return $this->select($item, false, self::ACTION_KEEP);
  }

  #############################################################################
  const _AUTOGEN_CONSTS = [
    "" => [self::class, "_autogen_consts"],
  ];
  const _AUTOGEN_LITERALS = /*autogen*/[
    [
      \nur\b\params\parametrable_utils::class,
      '\\nur\\b\\params\\parametrable_utils::class',
    ],
    [
      self::PARAMETRABLE_PARAMS_SCHEMA,
      'self::PARAMETRABLE_PARAMS_SCHEMA',
    ],
  ];
  const _AUTOGEN_METHODS = /*autogen*/[
    [
      \nur\b\params\parametrable_utils::class,
      '_autogen_methods_getters',
      self::PARAMETRABLE_PARAMS_SCHEMA,
      null,
    ],
    [
      \nur\b\params\parametrable_utils::class,
      '_autogen_methods_setters',
      self::PARAMETRABLE_PARAMS_SCHEMA,
      null,
    ],
  ];
  const _AUTO_GETTERS = /*autogen*/[
    'getAction' => 'action',
    'getSkipItems' => 'skip_items',
    'getMaxCount' => 'max_count',
    'getConditions' => 'conditions',
  ];
  const _AUTO_SETTERS = /*autogen*/[
    'setAction' => 'action',
    'setSkipItems' => 'skip_items',
    'setMaxCount' => 'max_count',
    'setConditions' => 'conditions',
  ];
  #--autogen-dynamic--
}
