<?php
/**
 * @copyright (c) Kufer Software GmbH, Loisachstrasse 4, D-84453 Mühldorf
 */

declare(strict_types = 1);

namespace Kufer\Web\Classes\Dbfilter;

use Kufer\Web\Classes\Dbfilter\RegisteredCall\IRegisteredCall;
use Kufer\Web\Classes\Identifier\Identifier;
use SelectQueryBuilder;


abstract class AbstractSQLFilter implements ISQLFilter
{
  
  private $inverseCondition = false;

  
  private $stackFilterOptionMap = array(array());

  public const NAME_METHOD_SET = 'Set';
  public const NAME_METHOD_OR = 'Or';
  public const NAME_METHOD_AND = 'And';

  
  private $registeredCalls = array();

  
  public const TYPE_STRING = 0;

  
  public const TYPE_INTEGER = 1;

  
  public const TYPE_BOOLEAN_W_F = 2;

  
  public const TYPE_DATE = 3;

  
  public const TYPE_DATETIME = 4;

  
  public const TYPE_BOOLEAN_J_N = 5;

  
  public const TYPE_BOOLEAN_NOT_W_F = 6;

  
  public const TYPE_TIME = 7;

  
  public const TYPE_IDENTIFIER = 8;

  
  public const TYPE_FOREIGN_DB = 9;

  
  public const TYPE_BOOLEAN_1_MINUS_1 = 10;

  
  public const TYPE_BOOLEAN_1_0 = 11;

  
  public const TYPE_BOOLEAN_NOT_1_0 = 12;

  
  public const TYPE_IDENTIFIER_NO_GUID = 13;

  
  public const TYPE_TIME_NO_DATETIME = 14;

  
  public const TYPE_WO_TAGE = 15;

  
  public const TYPE_BOOLEAN_HALF_DEFAULT_TRUE = 16;

  
  public const FILTER_OPTION_GLOBAL = 'global';

  
  private $currentFilter;

  
  private $immutable = false;

  
  protected function __construct()
  {
    $this->currentFilter = static::NAME_METHOD_AND;
  }

  
  public static function create()
  {
    static $cache = array();
    if (!array_key_exists(static::class, $cache)) {
      $cache[static::class] = new static();
      $cache[static::class]->makeImmutable();
    }

    return (clone $cache[static::class]);
  }

  
  final public function makeImmutable(): void
  {
    $this->immutable = true;
  }

  
  final public function checkMutable(): void
  {
    if ($this->immutable) {
      throw new FilterImmutableException();
    }
  }

  
  private function getDefaultValueForType(int $type)
  {
    switch ($type) {
    case static::TYPE_STRING:
    case static::TYPE_WO_TAGE:
      return '';
    case static::TYPE_INTEGER:
      return 0;
    case static::TYPE_BOOLEAN_W_F:
    case static::TYPE_BOOLEAN_NOT_W_F:
    case static::TYPE_BOOLEAN_J_N:
    case static::TYPE_BOOLEAN_1_MINUS_1:
    case static::TYPE_BOOLEAN_1_0:
    case static::TYPE_BOOLEAN_NOT_1_0:
      return false;
    case static::TYPE_BOOLEAN_HALF_DEFAULT_TRUE:
      return true;
    case static::TYPE_DATE:
    case static::TYPE_DATETIME:
      return DEFAULTDATE;
    case static::TYPE_TIME:
    case static::TYPE_TIME_NO_DATETIME:
      return DEFAULTTIME;
    case static::TYPE_IDENTIFIER:
    case static::TYPE_IDENTIFIER_NO_GUID:
      return null;
    default:
      throw new \InvalidArgumentException("Unerwarteter Typ $type");
    }
  }

  
  final public function registerDb(string $name, string $column, int $type, $default = null, string $praeffix = ':table:'): void
  {
    $registerCall = null;
    if (\is_null($default)) {
      $default = $this->getDefaultValueForType($type);
    }

    switch ($type) {
    case static::TYPE_STRING:
      $registerCall = new RegisteredCall\RegisteredDBCallString($name, $praeffix, $column, $default);
      break;
    case static::TYPE_INTEGER:
      $registerCall = new RegisteredCall\RegisteredDBCallInteger($name, $praeffix, $column, $default);
      break;
    case static::TYPE_BOOLEAN_W_F:
      $registerCall = new RegisteredCall\RegisteredDBCallBooleanWF($name, $praeffix, $column, $default);
      break;
    case static::TYPE_BOOLEAN_NOT_W_F:
      $registerCall = new RegisteredCall\RegisteredDBCallBooleanNotWF($name, $praeffix, $column, $default);
      break;
    case static::TYPE_BOOLEAN_J_N:
      $registerCall = new RegisteredCall\RegisteredDBCallBooleanJN($name, $praeffix, $column, $default);
      break;
    case static::TYPE_DATE:
      $registerCall = new RegisteredCall\RegisteredDBCallDate($name, $praeffix, $column, $default);
      break;
    case static::TYPE_DATETIME:
      $registerCall = new RegisteredCall\RegisteredDBCallDateTime($name, $praeffix, $column, $default);
      break;
    case static::TYPE_TIME:
      $registerCall = new RegisteredCall\RegisteredDBCallTime($name, $praeffix, $column, $default);
      break;
    case static::TYPE_TIME_NO_DATETIME:
        $registerCall = new RegisteredCall\RegisteredDBCallTimeNoDateTime($name, $praeffix, $column, $default);
        break;
    case static::TYPE_WO_TAGE:
      $registerCall = new RegisteredCall\RegisteredDBCallWoTage($name, $praeffix, $column, $default);
      break;
      case static::TYPE_BOOLEAN_1_0:
      $registerCall = new RegisteredCall\RegisteredDBCallBoolean10($name, $praeffix, $column, $default);
      break;
    case static::TYPE_BOOLEAN_NOT_1_0:
      $registerCall = new RegisteredCall\RegisteredDBCallBooleanNot10($name, $praeffix, $column, $default);
      break;
    case static::TYPE_BOOLEAN_HALF_DEFAULT_TRUE:
      $registerCall = new RegisteredCall\RegisteredDBCallBooleanHalfDefaultTrue($name, $praeffix, $column, $default);
      break;
    case static::TYPE_IDENTIFIER:
      $columns = \StringOperations::explodeAndTrim($column);
      assert(count($columns) === 2, 'Identifier und GUID Spalte im Format (id,guid) erwartet!');
      $registerCall = new RegisteredCall\RegisteredDBCallIdentifier($name, $praeffix, $columns[0], $columns[1]);
      break;
   case static::TYPE_IDENTIFIER_NO_GUID:
     $registerCall = new RegisteredCall\RegisteredDBCallIdentifierNoGuid($name, $praeffix, $column, null);
     break;
    case static::TYPE_BOOLEAN_1_MINUS_1:
      $registerCall = new RegisteredCall\RegisteredDBCallBoolean1Minus1($name, $praeffix, $column, $default);
      break;
    default:
      throw new \InvalidArgumentException("Unerwarteter Typ $type");
    }

    $this->register($registerCall);
  }

  
  final public function registerForeignDb(string $name, $fnAddToBuilder, $builderFn, $builderFnAny = null): void
  {
    $registeredCall = new RegisteredCall\RegisteredForeignDBCall($name, $fnAddToBuilder, $builderFn, $builderFnAny);
    $this->register($registeredCall);
  }

  private function register(RegisteredCall\IRegisteredCall $registeredCall): void
  {
    $this->registeredCalls[$registeredCall->getName()] = $registeredCall;
  }

  
  final public function applyFilterCurrent(string $key, $fn): void
  {
    $this->applyFilter($key, $fn, $this->currentFilter);
  }

  final public function applyFilter(string $key, $fn, string $method): void
  {
    switch ($method) {
      case static::NAME_METHOD_SET:
        $this->applyFilterSet($key, $fn);
        break;
      case static::NAME_METHOD_OR:
        $this->applyFilterOr($key, $fn);
        break;
      case static::NAME_METHOD_AND:
        $this->applyFilterAnd($key, $fn);
        break;
      default:
        throw new \RuntimeException("Unerwartete Filtermethode '{$this->currentFilter}'. Wenn leer, dann wurde eventuell der Konstruktur von " . self::class . ' nicht aufgerufen.');
    }
  }

  
  private function getFilter(string $key)
  {
    return $this->stackFilterOptionMap[\count($this->stackFilterOptionMap) - 1][$key] ?? array();
  }

  
  private function setFilter(string $key, $fn): void
  {
    $this->checkMutable(); 

    $this->stackFilterOptionMap[\count($this->stackFilterOptionMap) - 1][$key] = $fn;
  }

  
  final public function notAll($fn): IFilter
  {
    $this->not(function(self $filter) use ($fn): void {
      $this->all($fn);
    });

    return $this;
  }

  
  final public function notAny($fn): IFilter
  {
    $this->not(function(self $filter) use ($fn): void {
      $this->any($fn);
    });

    return $this;
  }

  
  final public function not($fn): IFilter
  {
    $cpyCondition = $this->inverseCondition;
    try {
      $this->inverseCondition = true;
      \call_user_func($fn, $this);
    } finally {
      $this->inverseCondition = $cpyCondition;
    }

    return $this;
  }

  
  private function or($fn): IFilter
  {
    return $this->applyFilterMethod(static::NAME_METHOD_OR, $fn);
  }

  
  private function and($fn): IFilter
  {
    return $this->applyFilterMethod(static::NAME_METHOD_AND, $fn);
  }

  
  final public function any($fn): IFilter
  {
    
    if ($this->isInverse()) {
      return $this->and($fn);
    } else {
      return $this->or($fn);
    }
  }

  
  final public function all($fn): IFilter
  {
    
    if ($this->isInverse()) {
      return $this->or($fn);
    } else {
      return $this->and($fn);
    }
  }

  
  private function applyFilterMethod(string $method, $fn)
  {
    $cpyFilter = $this->currentFilter;
    $this->pushFilterMethod();
    try {
      $this->currentFilter = $method;
      \call_user_func($fn, $this);
    } finally {
      
      $this->currentFilter = $cpyFilter;
      $this->popFilterMethod($method);
    }

    return $this;
  }

  
  private function pushFilterMethod(): void
  {
    $this->checkMutable();

    \array_push($this->stackFilterOptionMap, array());
  }

  
  private function popFilterMethod(string $oldMethod): void
  {
    $this->checkMutable();

    $cpyFilter = $this->currentFilter;
    $cpyInverse = $this->inverseCondition;
    if ($this->currentFilter === static::NAME_METHOD_SET) {
      $this->currentFilter = static::NAME_METHOD_AND;
    }
    $this->inverseCondition = false;

    $filterOptionMap = \array_pop($this->stackFilterOptionMap);
    
    
    
    if (!empty($filterOptionMap)) {
      $this->applyFilterCurrent(static::FILTER_OPTION_GLOBAL, $this->combine($filterOptionMap, $oldMethod));
    }

    $this->currentFilter = $cpyFilter;
    $this->inverseCondition = $cpyInverse;
  }

  
  private function isFilterSet(string $key): bool
  {
    return !empty(\ArrayOperations::lastValue($this->stackFilterOptionMap)[$key] ?? array());
  }

  
  private function applyFilterSet(string $key, $fn, bool $allowOtherOptions = true): void
  {
    if (!$allowOtherOptions && $this->isFilterSet($key)) {
      throw new FilterAlreadyExistsException(self::class, $key, static::NAME_METHOD_SET);
    }

    $fn = $this->applyIfInverse($fn);
    $this->setFilter($key, array($fn));
  }

  
  private function applyFilterOr(string $key, $fn, bool $allowOtherOptions = true): void
  {
    if (!$allowOtherOptions && $this->isFilterSet($key)) {
      throw new FilterAlreadyExistsException(self::class, $key, static::NAME_METHOD_OR);
    }

    $this->addFunctionToFilter($key, $fn);
  }

  
  private function applyFilterAnd(string $key, $fn, bool $allowOtherOptions = true): void
  {
    if (!$allowOtherOptions && $this->isFilterSet($key)) {
      throw new FilterAlreadyExistsException(self::class, $key, static::NAME_METHOD_AND);
    }

    $this->addFunctionToFilter($key, $fn);
  }

  
  private function addFunctionToFilter(string $key, $fn): void
  {
    $fn = $this->applyIfInverse($fn);
    $fns = $this->getFilter($key);
    array_push($fns, $fn);

    $this->setFilter($key, $fns);
  }

  
  private function applyIfInverse($fn)
  {
    if ($this->isInverse()) {
      return function(\SelectQueryBuilder $builder) use ($fn): \SelectQueryBuilder {
        return $builder->notExpr($fn);
      };
    } else {
      return $fn;
    }
  }

  
  public function reset(): void
  {
    $this->checkMutable();

    $this->stackFilterOptionMap[\count($this->stackFilterOptionMap) - 1] = array();
  }

  public function createBuilder(\DBOperation $dbOperation): SelectQueryBuilder
  {
    $builder = new \SelectQueryBuilder($dbOperation);
    $builder = $builder->paramRaw('table', $this->getTablePraeffix());

    return $builder->andExpr($this->combineTopOfStack());
  }

  
  private function combineTopOfStack(): callable
  {
    $filterOptionMap = \ArrayOperations::lastValue($this->stackFilterOptionMap);

    return $this->combine($filterOptionMap, $this->currentFilter);
  }

  
  private function combine(array $functions, string $method): callable
  {
    return function(\SelectQueryBuilder $builder) use ($functions, $method) {
      $fns = (new \Kufer\Web\Classes\Stream\ArrayStream($functions))
        ->reduce('\\array_merge', array());

      if (count($fns) > 1) {
        foreach ($fns as $filterFn) {
          if (!is_callable($filterFn)) {
            throw new \InvalidArgumentException('$filter muss callable sein.');
          }

          switch ($method) {
          case static::NAME_METHOD_SET:
          case static::NAME_METHOD_AND:
            $builder = $builder->andExpr($filterFn);
            break;
          case static::NAME_METHOD_OR:
            $builder = $builder->orExpr($filterFn);
            break;
          }
        }
      } elseif (count($fns) === 1) {
        $filterFn = \ArrayOperations::firstValue($fns);
        \call_user_func($filterFn, $builder);
      }

      return $builder;
    };
  }

  
  abstract public function getTablePraeffix(): string;

  final public function createSql(\DBOperation $dboperation): string
  {
    return $this->createBuilder($dboperation)
      ->getSQLCondition();
  }

  final protected function getRegisteredCallByName(string $name): ?RegisteredCall\IRegisteredCall
  {
    return $this->registeredCalls[$name] ?? null;
  }

  final protected function createRegisteredCallFunctionCall(RegisteredCall\IRegisteredCall $registeredCall, $method, array $values, $isDefault, $defaultValue): callable
  {
    switch ($method) {
    case 'equals':
      return !$isDefault ? $registeredCall->equals($this, $values[0]) : $registeredCall->equalsDefault($this, $values[0], $defaultValue);
    case 'in':
      return !$isDefault ? $registeredCall->in($this, $values[0]) : $registeredCall->inDefault($this, $values[0], $defaultValue);
    case 'contains':
      return $registeredCall->contains($this, $values[0]);
    case 'like':
      return $registeredCall->like($this, $values[0]);
    case 'less':
      return !$isDefault ? $registeredCall->less($this, $values[0]) : $registeredCall->lessDefault($this, $values[0], $defaultValue);
    case 'lessEquals':
      $fnCallEquals = $this->createRegisteredCallFunctionCall($registeredCall, 'equals', array($values[0]), $isDefault, $defaultValue);
      $fnCallLess = $this->createRegisteredCallFunctionCall($registeredCall, 'less', array($values[0]), $isDefault, $defaultValue);

      return \FunctionOperations::combine(
        $fnCallLess,
        static function(\SelectQueryBuilder $builder) use ($fnCallEquals): \SelectQueryBuilder {
          return $builder->orExpr($fnCallEquals);
        }
      );
    case 'greater':
      return !$isDefault ? $registeredCall->greater($this, $values[0]) : $registeredCall->greaterDefault($this, $values[0], $defaultValue);
    case 'greaterEquals':
      $fnCallEquals = $this->createRegisteredCallFunctionCall($registeredCall, 'equals', array($values[0]), $isDefault, $defaultValue);
      $fnCallGreater = $this->createRegisteredCallFunctionCall($registeredCall, 'greater', array($values[0]), $isDefault, $defaultValue);

      return \FunctionOperations::combine(
        $fnCallGreater,
        static function(\SelectQueryBuilder $builder) use ($fnCallEquals): \SelectQueryBuilder {
          return $builder->orExpr($fnCallEquals);
        }
      );
    case 'between':
      if ($values[0] === $values[1]) {
        return $this->createRegisteredCallFunctionCall($registeredCall, 'equals', array($values[0]), $isDefault, $defaultValue);
      }
      if ($values[0] > $values[1]) {
        return SQLFilterUtils::filterFalse();
      }

      $fnCallLessEquals = $this->createRegisteredCallFunctionCall($registeredCall, 'lessEquals', array($values[1]), $isDefault, $defaultValue);
      $fnCallGreaterEquals = $this->createRegisteredCallFunctionCall($registeredCall, 'greaterEquals', array($values[0]), $isDefault, $defaultValue);

      return \FunctionOperations::combine(
        static function(\SelectQueryBuilder $builder) use ($fnCallLessEquals): \SelectQueryBuilder {
          return $builder->andExpr($fnCallLessEquals);
        },
        static function(\SelectQueryBuilder $builder) use ($fnCallGreaterEquals): \SelectQueryBuilder {
          return $builder->andExpr($fnCallGreaterEquals);
        }
      );
    case 'is':
      return !$isDefault ? $registeredCall->is($this) : $registeredCall->isDefault($this, $defaultValue);
    case 'has':
      return $registeredCall->has($this, $values[0]);
    case 'hasAny':
      return $registeredCall->hasAny($this, $values[0]);
    default:
      throw new RegisteredCall\CallNotSupportedException("Der Aufruf von $method von " . $registeredCall->getName() . ' wird nicht unterstützt.');
    }
  }

  
  final protected function applyRegisteredCall(RegisteredCall\IRegisteredCall $registeredCall, $method, array $values, $isDefault, $defaultValue): void
  {
    $fnCall = $this->createRegisteredCallFunctionCall($registeredCall, $method, $values, $isDefault, $defaultValue);
    $this->applyFilterCurrent($registeredCall->getName(), $fnCall);
  }

  
  final public function applyFalse()
  {
    $this->applyFilterCurrent('false', SQLFilterUtils::filterFalse());
  }

  
  private function checkMethodExists(string $callee, string $fnname, string ...$fnnames): void
  {
    if (!method_exists($this, $fnname)
        || \ArrayOperations::any($fnnames, \FunctionOperations::args('\\method_exists', $this))) {
      throw new \RuntimeException('Erwartete Methode '
        . implode(', ', \array_merge(array($fnname), $fnnames))
        . " existiert nicht (erwartet von $callee).");
    }
  }

  private function callRegisteredCall(string $name, $args, RegisteredCall\IRegisteredCall $registeredCall): void
  {
    $isDefault = \StringOperations::endsWith($name, 'Default');
    $matches = array();
    \preg_match('&^[a-z]+(Any|Equals)?&', $name, $matches);
    $method = $matches[0];

    $values = array();
    $defaultValue = null;

    $argsCount = count($args);
    if ($method === 'is') {
      if ($isDefault && $argsCount !== 1) {
        throw new \InvalidArgumentException("Expected one argument for $name");
      } elseif (!$isDefault && $argsCount !== 0) {
        throw new \InvalidArgumentException("Expected no arguments for $name");
      }

      $defaultValue = $isDefault ? $args[0] : null;
    } elseif ($method === 'between') {
      if ($isDefault && $argsCount !== 3) {
        throw new \InvalidArgumentException("Expected two arguments for $name");
      } elseif (!$isDefault && $argsCount !== 2) {
        throw new \InvalidArgumentException("Expected one argument for $name");
      }

      $values = array($args[0], $args[1]);
      $defaultValue = $isDefault ? $args[2] : null;
    } else {
      if ($isDefault && $argsCount !== 2) {
        throw new \InvalidArgumentException("Expected two arguments for $name");
      } elseif (!$isDefault && $argsCount !== 1) {
        throw new \InvalidArgumentException("Expected one argument for $name");
      }

      $values = array($args[0]);
      $defaultValue = $isDefault ? $args[1] : null;
    }

    $this->applyRegisteredCall($registeredCall, $method, $values, $isDefault, $defaultValue);
  }

  private function callIn(string $name, string $inArray, array $args): void
  {
    if (!is_iterable($args[0] ?? null)) {
      throw new \InvalidArgumentException('Iterable erwartet.');
    }
    $equalsFnName = 'equals' . \StringOperations::substr($name, \StringOperations::strlen($inArray));
    $this->checkMethodExists($name, $equalsFnName);

    $equalsFn = array($this, $equalsFnName);
    $this->any(function(self $filter) use ($args, $equalsFn): void {
      foreach ($args[0] as $value) {
        \call_user_func($equalsFn, $value);
      }
    });
  }

  private function callEquals(string $name, string $equals, array $args): void
  {
    if (!isset($args[0])) {
      throw new \InvalidArgumentException('Argument erwartet.');
    }

    $inFnName = 'in' . \StringOperations::substr($name, \StringOperations::strlen($equals));
    $this->checkMethodExists($name, $inFnName);

    $inFn = array($this, $inFnName);
    \call_user_func($inFn, array($args[0]));
  }

  private function callContains(string $name, string $contains, array $args): void
  {
    if (!\is_string($args[0] ?? null)) {
      throw new \InvalidArgumentException('string erwartet.');
    }
    $likeFnName = 'like' . \StringOperations::substr($name, \StringOperations::strlen($contains));
    $this->checkMethodExists($name, $likeFnName);

    $likeFn = array($this, $likeFnName);
    \call_user_func($likeFn, '%' . $args[0] . '%');
  }

  private function callHasAny(string $name, string $hasAny, array $args): void
  {
    if (!\is_iterable($args[0] ?? null)) {
      throw new \InvalidArgumentException('Iterable erwartet.');
    }

    $hasFnName = 'has' . \StringOperations::substr($name, \StringOperations::strlen($hasAny));
    $this->checkMethodExists($name, $hasFnName);

    $hasFn = array($this, $hasFnName);
    $this->any(function(self $filter) use ($args, $hasFn) {
      foreach ($args[0] as $value) {
        \call_user_func($hasFn, $value);
      }
    });
  }

  private function callHas(string $name, string $has, array $args): void
  {
    if (!isset($args[0])) {
      throw new \InvalidArgumentException('Argument erwartet.');
    }

    $hasAnyFnName = 'hasAny' . \StringOperations::substr($name, \StringOperations::strlen($has));
    $this->checkMethodExists($name, $hasAnyFnName);

    $hasAnyFn = array($this, $hasAnyFnName);
    \call_user_func($hasAnyFn, array($args[0]));
  }

  private function callSearch(string $name, string $search, array $args): void
  {
    if (!isset($args[0])) {
      throw new \InvalidArgumentException('Argument erwartet.');
    }

    $containsFnName = 'contains' . \StringOperations::substr($name, \StringOperations::strlen($search));
    $containsFn = array($this, $containsFnName);
    $this->all(function() use ($containsFn, $args) {
      (new \Kufer\Web\Classes\Stream\IterableStream(\StringOperations::explodeGenerator(' ', $args[0])))
        ->map('\\trim')
        ->filter(\FunctionOperations::not(\FunctionOperations::isEmpty()))
        ->each($containsFn);
    });
  }

  private function callStartsWith(string $name, string $startsWith, array $args): void
  {
    if (!\is_string($args[0] ?? null)) {
      throw new \invalidargumentexception('string erwartet.');
    }
    $likefnname = 'like' . \StringOperations::substr($name, \StringOperations::strlen($startsWith));

    $likefn = array($this, $likefnname);
    \call_user_func($likefn, $args[0] . '%');
  }

  private function callEndsWith(string $name, string $endsWith, array $args): void
  {
    if (!\is_string($args[0] ?? null)) {
      throw new \invalidargumentexception('string erwartet.');
    }
    $likefnname = 'like' . \StringOperations::substr($name, \StringOperations::strlen($endsWith));

    $likefn = array($this, $likefnname);
    \call_user_func($likefn, '%' . $args[0]);
  }

  
  final public function isInverse(): bool
  {
    return $this->inverseCondition;
  }

  
  public function isEmpty(): bool
  {
    return count($this->stackFilterOptionMap) === 1 && empty($this->stackFilterOptionMap[0]);
  }

  public function __call(string $name, $args)
  {
    
    $search = 'search';
    if ($name !== $search && \StringOperations::startsWith($name, $search)) {
      $this->callSearch($name, $search, $args);

      return $this;
    }

    
    $startsWith = 'startsWith';
    if ($name !== $startsWith && \StringOperations::startsWith($name, $startsWith)) {
      $this->callStartsWith($name, $startsWith, $args);

      return $this;
    }

    
    $endsWith = 'endsWith';
    if ($name !== $endsWith && \StringOperations::startsWith($name, $endsWith)) {
      $this->callEndsWith($name, $endsWith, $args);

      return $this;
    }

    
    $registeredCallName = \preg_replace(array('&^[a-z]+(Any|Equals)?&', '&Default$&'), '', $name);
    $registeredCall = $this->registeredCalls[$registeredCallName] ?? null;
    if (!\is_null($registeredCall)) {
      $this->callRegisteredCall($name, $args, $registeredCall);

      return $this;
    }

    
    $inArray = 'in';
    if ($name !== $inArray && \StringOperations::startsWith($name, $inArray)) {
      $this->callIn($name, $inArray, $args);

      return $this;
    }
    
    $equals = 'equals';
    if ($name !== $equals && \StringOperations::startsWith($name, $equals)) {
      $this->callEquals($name, $equals, $args);

      return $this;
    }
    
    $contains = 'contains';
    if ($name !== $contains && \StringOperations::startsWith($name, $contains)) {
      $this->callContains($name, $contains, $args);

      return $this;
    }
    
    $hasAny = 'hasAny';
    if ($name !== $hasAny && \StringOperations::startsWith($name, $hasAny)) {
      $this->callHasAny($name, $hasAny, $args);

      return $this;
    }
    
    $has = 'has';
    if ($name !== $has && \StringOperations::startsWith($name, $has)) {
      $this->callHas($name, $has, $args);

      return $this;
    }

    throw new \InvalidArgumentException("Funktion $name existiert nicht hier " . self::class);
  }

  public function __clone()
  {
    $this->immutable = false;
    $this->stackFilterOptionMap = (new \ArrayObject($this->stackFilterOptionMap))->getArrayCopy();
    foreach ($this->stackFilterOptionMap as &$filterOptionMap) {
      $filterOptionMap = (new \ArrayObject($filterOptionMap))->getArrayCopy();
      foreach ($filterOptionMap as &$filterOptions) {
        $filterOptions = (new \ArrayObject($filterOptions))->getArrayCopy();
      }
    }
  }

  public function generateDocumentationForRegisteredCalls(): string
  {
    $result = '';
    $fnCreateLine = static function($str) {
      return "@method self $str\n";
    };

    foreach ($this->registeredCalls as $key => $registeredCall) {
      $type = '';
      switch ($registeredCall->getType()) {
      case static::TYPE_BOOLEAN_J_N:
      case static::TYPE_BOOLEAN_NOT_W_F:
      case static::TYPE_BOOLEAN_W_F:
      case static::TYPE_BOOLEAN_1_MINUS_1:
      case static::TYPE_BOOLEAN_HALF_DEFAULT_TRUE:
        $type = 'bool';
        break;
      case static::TYPE_DATETIME:
      case static::TYPE_DATE:
      case static::TYPE_TIME:
      case static::TYPE_TIME_NO_DATETIME:
      case static::TYPE_STRING:
        $type = 'string';
        break;
      case static::TYPE_WO_TAGE:
        $type = 'string|array';
        break;
      case static::TYPE_INTEGER:
        $type = 'int';
        break;
      case static::TYPE_IDENTIFIER_NO_GUID:
      case static::TYPE_IDENTIFIER:
        $type = '\\' . Identifier::class . '|int';
        break;
      }

      $caps = $registeredCall->getCapabilities();
      if ($caps & IRegisteredCall::SUPPORTS_EQUALS) {
        $result .= $fnCreateLine("equals${key}(${type} \$obj)");
      }
      if ($caps & IRegisteredCall::SUPPORTS_EQUALS_DEFAULT) {
        $result .= $fnCreateLine("equals${key}Default(${type} \$obj, \$default)");
      }
      if ($caps & IRegisteredCall::SUPPORTS_LIKE) {
        $result .= $fnCreateLine("like${key}(${type} \$obj)");
        $result .= $fnCreateLine("contains${key}(${type} \$obj)");
      }
      if ($caps & IRegisteredCall::SUPPORTS_GREATER) {
        $result .= $fnCreateLine("greater${key}(${type} \$obj)");
        $result .= $fnCreateLine("greaterEquals${key}(${type} \$obj)");
      }
      if ($caps & IRegisteredCall::SUPPORTS_GREATER_DEFAULT) {
        $result .= $fnCreateLine("greater${key}Default(${type} \$obj)");
        $result .= $fnCreateLine("greaterEquals${key}Default(${type} \$obj, \$default)");
      }
      if ($caps & IRegisteredCall::SUPPORTS_LESS) {
        $result .= $fnCreateLine("less${key}(${type} \$obj)");
        $result .= $fnCreateLine("lessEquals${key}(${type} \$obj)");
      }
      if ($caps & IRegisteredCall::SUPPORTS_LESS_DEFAULT) {
        $result .= $fnCreateLine("less${key}Default(${type} \$obj)");
        $result .= $fnCreateLine("lessEquals${key}Default(${type} \$obj, \$default)");
      }
      if ($caps & IRegisteredCall::SUPPORTS_IN) {
        $result .= $fnCreateLine("in${key}(${type}[] \$ary)");
      }
      if ($caps & IRegisteredCall::SUPPORTS_IN_DEFAULT) {
        $result .= $fnCreateLine("in${key}Default(${type}[] \$ary)");
      }
      if ($caps & IRegisteredCall::SUPPORTS_IS) {
        $result .= $fnCreateLine("is${key}()");
      }
      if ($caps & IRegisteredCall::SUPPORTS_IS_DEFAULT) {
        $result .= $fnCreateLine("is${key}Default(\$default)");
      }
      if ($caps & IRegisteredCall::SUPPORTS_HAS) {
        $result .= $fnCreateLine("has${key}(mixed \$obj)");
      }
      if ($caps & IRegisteredCall::SUPPORTS_HAS_ANY) {
        $result .= $fnCreateLine("hasAny${key}(array \$obj)");
      }
    }

    return $result;
  }
}