<?php
namespace nur\mapper\item;

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

/**
 * Class AbstractStringMapper: un mapper qui fait des opérations sur des chaines
 * (cette classe factorise les méthodes communes entre {@link StringMapper} et
 * {@link TextMapper})
 */
abstract class AbstractStringMapper extends Mapper {
  use Tparametrable;

  # si l'élément est un tableau, ces actions sont faites sur chaque valeur
  const ACTION_SET_VALUE = 1;
  const ACTION_ADD_PREFIX = 2;
  const ACTION_ADD_SUFFIX = 3;
  const ACTION_DEL_PREFIX = 4;
  const ACTION_DEL_SUFFIX = 5;
  const ACTION_LEFT = 6;
  const ACTION_RIGHT = 7;
  const ACTION_SUBSTR = 8;
  const ACTION_REPLACE = 9;
  const ACTION_SPLIT = 10;
  const ACTION_JOIN = 11;
  const ACTION_TRUNC = 12;
  const ACTION_TRIM = 13;
  const ACTION_LTRIM = 14;
  const ACTION_RTRIM = 15;
  const ACTION_LOWER = 16;
  const ACTION_LOWER1 = 17;
  const ACTION_UPPER = 18;
  const ACTION_UPPER1 = 19;
  const ACTION_UPPERW = 20;
  # si l'élément est un tableau, ces actions sont faites sur l'élément en entier
  const ACTION_WHOLE_ITEM = 100;
  const ACTION_SPLIT_ALL = 101;
  const ACTION_JOIN_ALL = 102;

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

  function addAction(?string $action): self {
    if (preg_match('/^([A-Za-z0-9_]*)=(.*)$/', $action, $vs)) {
      $key = A::get($vs, 1);
      $action = A::get($vs, 2);
    } else {
      $key = null;
    }
    if (mapper_utils::check_prefix($action, $value, "set:", "=")) {
      $this->actions[] = [self::ACTION_SET_VALUE, $key, $value];
    } elseif (mapper_utils::check_prefix($action, $value, "addp:", "+#")) {
      $this->actions[] = [self::ACTION_ADD_PREFIX, $key, $value];
    } elseif (mapper_utils::check_prefix($action, $value, "adds:", "+%", "+")) {
      $this->actions[] = [self::ACTION_ADD_SUFFIX, $key, $value];
    } elseif (mapper_utils::check_prefix($action, $value, "delp:", "-#", "#")) {
      $this->actions[] = [self::ACTION_DEL_PREFIX, $key, $value];
    } elseif (mapper_utils::check_prefix($action, $value, "dels:", "-%", "%", "-")) {
      $this->actions[] = [self::ACTION_DEL_SUFFIX, $key, $value];
    } elseif (mapper_utils::check_prefix($action, $value, "left:", "l:")) {
      $length = intval($value);
      $this->actions[] = [self::ACTION_LEFT, $key, $length];
    } elseif (mapper_utils::check_prefix($action, $value, "right:", "r:")) {
      $length = intval($value);
      $this->actions[] = [self::ACTION_RIGHT, $key, $length];
    } elseif (mapper_utils::check_prefix($action, $value, "substr:", "mid:", "m:")) {
      preg_match('/^([^:]*)(?::(.*))?$/', $value, $vs);
      $offset = $vs[1];
      $offset = $offset? intval($offset): 0;
      $length = A::get($vs, 2);
      if ($length !== null) $length = intval($length);
      $this->actions[] = [self::ACTION_SUBSTR, $key, $offset, $length];
    } elseif (mapper_utils::check_prefix($action, $value, "replace:", "repl:", "r//", "r/")) {
      if (str::_starts_with("r//", $action)) {
        $sep = "/";
        $limit = -1;
      } elseif (str::_starts_with("r/", $action)) {
        $sep = "/";
        $limit = 1;
      } else {
        $sep = ":";
        $limit = -1;
      }
      [$from, $to] = str::split_pair($value, $sep);
      if ($sep == "/") str::del_suffix($to, "/");
      $this->actions[] = [self::ACTION_REPLACE, $key, false, $from, $to, $limit];
    } elseif (mapper_utils::check_prefix($action, $value, "split:", "|", "split")) {
      if ($action === "split") $value = ",";
      $this->actions[] = [self::ACTION_SPLIT, $key, $value];
    } elseif (mapper_utils::check_prefix($action, $value, "join:", "§", "join")) {
      if ($action === "join") $value = ",";
      $this->actions[] = [self::ACTION_JOIN, $key, $value];
    } elseif (mapper_utils::check_prefix($action, $value, "trunc:")) {
      preg_match('/^([^:]*)(?::([^:]*)(?::([^:]*))?)?$/', $value, $vs);
      $length = $vs[1];
      $length = $length? intval($length): 0;
      $ellips = base::t2(A::get($vs, 2));
      $suffix = A::get($vs, 3);
      $this->actions[] = [self::ACTION_TRUNC, $key, $length, $ellips, $suffix];
    } elseif (mapper_utils::check_prefix($action, $value, "trim", "t")) {
      $this->actions[] = [self::ACTION_TRIM, $key];
    } elseif (mapper_utils::check_prefix($action, $value, "ltrim", "lt")) {
      $this->actions[] = [self::ACTION_LTRIM, $key];
    } elseif (mapper_utils::check_prefix($action, $value, "rtrim", "rt")) {
      $this->actions[] = [self::ACTION_RTRIM, $key];
    } elseif (mapper_utils::check_prefix($action, $value, "lower", "l")) {
      $this->actions[] = [self::ACTION_LOWER, $key];
    } elseif (mapper_utils::check_prefix($action, $value, "lower1", "l1")) {
      $this->actions[] = [self::ACTION_LOWER1, $key];
    } elseif (mapper_utils::check_prefix($action, $value, "upper", "u")) {
      $this->actions[] = [self::ACTION_UPPER, $key];
    } elseif (mapper_utils::check_prefix($action, $value, "upper1", "u1")) {
      $this->actions[] = [self::ACTION_UPPER1, $key];
    } elseif (mapper_utils::check_prefix($action, $value, "upperw", "uw")) {
      $this->actions[] = [self::ACTION_UPPERW, $key];
    } elseif (mapper_utils::check_prefix($action, $value, "split_all:", "split_all")) {
      if ($action === "split_all") $value = ",";
      $this->actions[] = [self::ACTION_SPLIT_ALL, $key, $value];
    } elseif (mapper_utils::check_prefix($action, $value, "join_all:", "join_all")) {
      if ($action === "join_all") $value = ",";
      $this->actions[] = [self::ACTION_JOIN_ALL, $key, $value];
    } else {
      # par défaut, spécifier la valeur
      $this->actions[] = [self::ACTION_SET_VALUE, $key, $action];
    }
    return $this;
  }

  function addActionSetValue(?string $value, ?string $key=null): self {
    $this->actions[] = [self::ACTION_SET_VALUE, $key, $value];
    return $this;
  }

  function addActionAddPrefix(?string $value, ?string $key=null): self {
    $this->actions[] = [self::ACTION_ADD_PREFIX, $key, $value];
    return $this;
  }

  function addActionAddSuffix(?string $value, ?string $key=null): self {
    $this->actions[] = [self::ACTION_ADD_SUFFIX, $key, $value];
    return $this;
  }

  function addActionDelPrefix(?string $value, ?string $key=null): self {
    $this->actions[] = [self::ACTION_DEL_PREFIX, $key, $value];
    return $this;
  }

  function addActionDelSuffix(?string $value, ?string $key=null): self {
    $this->actions[] = [self::ACTION_DEL_SUFFIX, $key, $value];
    return $this;
  }

  function addActionLeft(int $length, ?string $key=null): self {
    $this->actions[] = [self::ACTION_LEFT, $key, $length];
    return $this;
  }

  function addActionRight(int $length, ?string $key=null): self {
    $this->actions[] = [self::ACTION_RIGHT, $key, $length];
    return $this;
  }

  function addActionSubstr(int $offset, ?int $length, ?string $key=null): self {
    $this->actions[] = [self::ACTION_SUBSTR, $key, $offset, $length];
    return $this;
  }

  function addActionReplace(string $from, ?string $to, int $limit=-1, ?string $key=null): self {
    $this->actions[] = [self::ACTION_REPLACE, $key, false, $from, $to, $limit];
    return $this;
  }

  function addActionSplit(?string $sep, ?string $key=null): self {
    $this->actions[] = [self::ACTION_SPLIT, $key, $sep];
    return $this;
  }

  function addActionJoin(?string $sep, ?string $key=null): self {
    $this->actions[] = [self::ACTION_JOIN, $key, $sep];
    return $this;
  }

  function addActionTrunc(int $length, bool $ellips=false, ?string $suffix=null, ?string $key=null): self {
    $this->actions[] = [self::ACTION_TRUNC, $key, $length, $ellips, $suffix];
    return $this;
  }

  function addActionTrim(?string $key=null): self {
    $this->actions[] = [self::ACTION_TRIM, $key];
    return $this;
  }

  function addActionLtrim(?string $key=null): self {
    $this->actions[] = [self::ACTION_LTRIM, $key];
    return $this;
  }

  function addActionRtrim(?string $key=null): self {
    $this->actions[] = [self::ACTION_RTRIM, $key];
    return $this;
  }

  function addActionLower(?string $key=null): self {
    $this->actions[] = [self::ACTION_LOWER, $key];
    return $this;
  }

  function addActionLower1(?string $key=null): self {
    $this->actions[] = [self::ACTION_LOWER1, $key];
    return $this;
  }

  function addActionUpper(?string $key=null): self {
    $this->actions[] = [self::ACTION_UPPER, $key];
    return $this;
  }

  function addActionUpper1(?string $key=null): self {
    $this->actions[] = [self::ACTION_UPPER1, $key];
    return $this;
  }

  function addActionUpperw(?string $key=null): self {
    $this->actions[] = [self::ACTION_UPPERW, $key];
    return $this;
  }

  const PARAMETRABLE_PARAMS_SCHEMA = [
    "actions" => ["?array", null, "actions à effectuer sur les chaines du flux"],
    "marked_only" => ["bool", null, "ne faire les actions que sur les éléments marqués"],
  ];

  function pp_setActions(?array $actions): void {
    if ($actions !== null) {
      foreach ($actions as $action) {
        $this->addAction($action);
      }
    }
  }

  /** @var bool */
  protected $markedOnly;

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

  protected static function apply(&$item, ?string $forKey, callable $func, array $args): void {
    if (is_array($item)) {
      foreach ($item as $key => &$value) {
        if ($forKey !== null & $key != $forKey) continue;
        self::apply($value, null, $func, $args);
      }
    } else {
      $item = $func($item, ...$args);
    }
  }

  protected static function set_value($value) {
    if (base::z($value)) return $value;
    return strval($value);
  }

  protected static function add_prefix($value, ?string $prefix) {
    return "$prefix$value";
  }

  protected static function add_suffix($value, ?string $suffix) {
    return "$value$suffix";
  }

  protected static function split($value, string $sep): array {
    if ($value === null) return [];
    return explode($sep, strval($value));
  }

  protected function doActionItem(&$item, int $action, ?string $forKey, array $params): void {
    switch ($action) {
    case self::ACTION_SET_VALUE:
      if (is_array($item)) {
        foreach ($item as $key => &$value) {
          if ($forKey !== null & $key != $forKey) continue;
          $value = self::set_value(...$params);
        }
      } else {
        $item = self::set_value(...$params);
      }
      break;
    case self::ACTION_ADD_PREFIX:
      self::apply($item, $forKey, [static::class, "add_prefix"], $params);
      break;
    case self::ACTION_ADD_SUFFIX:
      self::apply($item, $forKey, [static::class, "add_suffix"], $params);
      break;
    case self::ACTION_DEL_PREFIX:
      self::apply($item, $forKey, [static::class, "del_prefix"], $params);
      break;
    case self::ACTION_DEL_SUFFIX:
      self::apply($item, $forKey, [static::class, "del_suffix"], $params);
      break;
    case self::ACTION_LEFT:
      self::apply($item, $forKey, [static::class, "left"], $params);
      break;
    case self::ACTION_RIGHT:
      self::apply($item, $forKey, [static::class, "right"], $params);
      break;
    case self::ACTION_SUBSTR:
      self::apply($item, $forKey, [static::class, "substr"], $params);
      break;
    case self::ACTION_REPLACE:
      $regexp = $params[0];
      $params = array_slice($params, 1);
      if ($regexp) {
        #XXX à implémenter
        #self::apply($item, $forKey, [static::class, "preg_replace"], $params);
      } else {
        self::apply($item, $forKey, [static::class, "str_replace"], $params);
      }
      break;
    case self::ACTION_SPLIT:
      self::apply($item, $forKey, [static::class, "split"], $params);
      break;
    case self::ACTION_JOIN:
      self::apply($item, $forKey, [static::class, "join"], $params);
      break;
    case self::ACTION_TRUNC:
      self::apply($item, $forKey, [static::class, "trunc"], $params);
      break;
    case self::ACTION_TRIM:
      self::apply($item, $forKey, [static::class, "trim"], $params);
      break;
    case self::ACTION_LTRIM:
      self::apply($item, $forKey, [static::class, "ltrim"], $params);
      break;
    case self::ACTION_RTRIM:
      self::apply($item, $forKey, [static::class, "rtrim"], $params);
      break;
    case self::ACTION_LOWER:
      self::apply($item, $forKey, [static::class, "lower"], $params);
      break;
    case self::ACTION_LOWER1:
      self::apply($item, $forKey, [static::class, "lower1"], $params);
      break;
    case self::ACTION_UPPER:
      self::apply($item, $forKey, [static::class, "upper"], $params);
      break;
    case self::ACTION_UPPER1:
      self::apply($item, $forKey, [static::class, "upper1"], $params);
      break;
    case self::ACTION_UPPERW:
      self::apply($item, $forKey, [static::class, "upperw"], $params);
      break;
    default:
      throw IllegalAccessException::not_implemented("action $action");
    }
  }

  protected static function split_all($item, string $sep): array {
    if (base::z($item)) return $item;
    if (is_array($item)) $item = self::join_all($item, "");
    return explode($sep, strval($item));
  }

  protected static function join_all($item, string $sep): string {
    if (base::z($item)) return $item;
    return implode($sep, A::with($item));
  }

  protected function doActionWhole(&$item, int $action, ?string $forKey, array $params): void {
    switch ($action) {
    case self::ACTION_SPLIT_ALL:
      $item = static::split_all($item, ...$params);
      break;
    case self::ACTION_JOIN_ALL:
      $item = static::join_all($item, ...$params);
      break;
    default:
      throw IllegalAccessException::not_implemented("action $action");
    }
  }

  protected function setup(): void {
    parent::setup();
    if ($this->markedOnly === null) {
      $this->markedOnly = mark_utils::is_use_marks($this);
    }
  }

  function mapper($item) {
    if (!$this->markedOnly || $this->isItemMarked($item)) {
      foreach ($this->actions as $actionParams) {
        [$action, $forKey] = array_slice($actionParams, 0, 2);
        $params = array_slice($actionParams, 2);
        if ($action < self::ACTION_WHOLE_ITEM) {
          $this->doActionItem($item, $action, $forKey, $params);
        } else {
          $this->doActionWhole($item, $action, $forKey, $params);
        }
      }
    }
    return $item;
  }
}
