<?php
namespace nur\sery\php;

use Closure;
use Exception;
use nur\sery\A;
use nur\sery\cv;
use nur\sery\StateException;
use nur\sery\ValueException;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;

/**
 * Class func: outils pour appeler fonctions et méthodes dynamiquement
 *
 * les types de fonctions supportés sont:
 * - fonctions (globales ou dans un namespace)
 * - classes (l'appel de cette "fonction" provoque l'instanciation de la classe)
 * - méthodes (statiques ou non, liables à une classe ou un objet)
 * - Closure
 */
class func {
  private static function _is_invalid(?string $f): bool {
    return $f === null || $f === "" || $f === "::" || $f === "->";
  }

  private static function _is_nfunction(?string $f): bool {
    return strpos($f, "\\") !== false;
  }

  private static function _parse_static(?string &$m): bool {
    $pos = strpos($m, "::");
    if ($pos === false) return false;
    $m = substr($m, $pos + 2);
    return true;
  }

  private static function _parse_method(?string &$m): bool {
    $pos = strpos($m, "->");
    if ($pos === false) return false;
    $m = substr($m, $pos + 2);
    return true;
  }

  #############################################################################
  # Fonctions

  /**
   * vérifier que $func est une fonction et la normaliser le cas échéant.
   * retourner true si c'est une fonction, false sinon
   *
   * les formes suivantes sont supportées:
   * - "function" si une classe du même nom n'existe pas déjà
   * - [false, "function", ...$args] c'est la forme normalisée
   * 
   * @param bool $strict vérifier l'inexistence de la classe et l'existence de
   * la fonction (ne pas uniquement faire une vérification syntaxique)
   */
  static function verifix_function(&$func, bool $strict=true, ?string &$reason=null): bool {
    if ($strict) {
      $msg = var_export($func, true);
      $reason = null;
    }
    if ($func instanceof ReflectionFunction) return true;
    if (is_string($func)) {
      $c = false;
      $f = $func;
    } elseif (is_array($func)) {
      if (!array_key_exists(0, $func)) return false;
      $c = $func[0];
      if (!array_key_exists(1, $func)) return false;
      $f = $func[1];
    } else {
      return false;
    }
    if ($c !== false) return false;
    if (!is_string($f)) return false;
    if (self::_is_invalid($f)) return false;
    if (self::_parse_static($f)) return false;
    if (self::_parse_method($f)) return false;
    if ($strict) {
      $reason = null;
      if (class_exists($f)) {
        $reason = "$msg: is a class";
        return false;
      }
      if (!function_exists($f)) {
        $reason = "$msg: function not found";
        return false;
      }
    }
    $func = [false, $f];
    return true;
  }

  /**
   * vérifier que $func est une fonction avec les règles de
   * {@link self::verifix_function()}
   */
  static function is_function($func, bool $strict=true, ?string &$reason=null): bool {
    return self::verifix_function($func, $strict, $reason);
  }

  #############################################################################
  # Classes

  /**
   * vérifier que $func est une classe et la normaliser le cas échéant.
   * retourner true si c'est une classe, false sinon
   *
   * les formes suivantes sont supportées:
   * - "class"
   * - ["class", false, ...$args] c'est la forme normalisée
   *
   * @param bool $strict vérifier l'existence de la classe (ne pas uniquement
   * faire une vérification syntaxique)
   */
  static function verifix_class(&$func, bool $strict=true, ?string &$reason=null): bool {
    if ($strict) {
      $msg = var_export($func, true);
      $reason = null;
    }
    if ($func instanceof ReflectionClass) return true;
    if (is_string($func)) {
      $c = $func;
      $f = false;
    } elseif (is_array($func)) {
      if (!array_key_exists(0, $func)) return false;
      $c = $func[0];
      if (!array_key_exists(1, $func)) return false;
      $f = $func[1];
    } else {
      return false;
    }
    if (!is_string($c)) return false;
    if (self::_is_invalid($c)) return false;
    if (self::_parse_static($c)) return false;
    if (self::_parse_method($c)) return false;
    if ($f !== false) return false;
    if ($strict) {
      if (!class_exists($c)) {
        $reason = "$msg: class not found";
        return false;
      }
    }
    $func = [$c, false];
    return true;
  }

  /**
   * vérifier que $func est une classe avec les règles de
   * {@link self::verifix_class()}
   */
  static function is_class($func, bool $strict=true, ?string &$reason=null): bool {
    return self::verifix_class($func, $strict, $reason);
  }

  #############################################################################
  # Méthodes statiques

  private static function _parse_class_s(?string $cs, ?string &$c, ?string &$s): bool {
    if (self::_is_invalid($cs) || self::_parse_method($cs)) return false;
    $pos = strpos($cs, "::");
    if ($pos === false) return false;
    if ($pos === 0) return false;
    $tmpc = substr($cs, 0, $pos);
    $cs = substr($cs, $pos + 2);
    if (self::_is_nfunction($cs)) return false;
    [$c, $s] = [$tmpc, cv::vn($cs)];
    return true;
  }

  private static function _parse_c_static(?string $cs, ?string &$c, ?string &$s, ?bool &$bound): bool {
    if (self::_is_invalid($cs) || self::_parse_method($cs)) return false;
    $pos = strpos($cs, "::");
    if ($pos === false) return false;
    if ($pos == strlen($cs) - 2) return false;
    if ($pos > 0) {
      $tmpc = substr($cs, 0, $pos);
      $bound = true;
    } else {
      $tmpc = null;
      $bound = false;
    }
    $cs = substr($cs, $pos + 2);
    if (self::_is_nfunction($cs)) return false;
    [$c, $s] = [$tmpc, cv::vn($cs)];
    return true;
  }

  /**
   * vérifier que $func est une méthode statique, et la normaliser le cas
   * échéant. retourner true si c'est une méthode statique, false sinon
   *
   * les formes suivantes sont supportées (XXX étant null ou n'importe quelle
   * valeur scalaire de n'importe quel type sauf false)
   * - "XXX::function"
   * - ["XXX::function", ...$args]
   * - [XXX, "::function", ...$args]
   * - [XXX, "function", ...$args] c'est la forme normalisée
   *
   * Si XXX est une classe, la méthode statique est liée. sinon, elle doit être
   * liée à une classe avant d'être utilisée
   *
   * @param bool $strict vérifier l'existence de la classe et de la méthode si
   * la méthode est liée (ne pas uniquement faire une vérification syntaxique)
   */
  static function verifix_static(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool {
    if ($strict) {
      $msg = var_export($func, true);
      $reason = null;
    }
    if ($func instanceof ReflectionMethod) {
      $bound = false;
      return true;
    }
    if (is_string($func)) {
      if (!self::_parse_c_static($func, $c, $f, $bound)) return false;
      $cf = [$c, $f];
    } elseif (is_array($func)) {
      $cf = $func;
      if (!array_key_exists(0, $cf)) return false;
      $c = $cf[0];
      if ($c === false) return false;
      if (is_object($c)) $c = get_class($c);
      if (is_string($c)) {
        if (self::_is_invalid($c)) return false;
        if (self::_parse_class_s($c, $c, $f)) {
          $cf[0] = $c;
          if ($f !== null) {
            # ["class::method"] --> ["class", "method"]
            array_splice($cf, 1, 0, [$f]);
          }
          $bound = true;
        } elseif (self::_parse_c_static($c, $c, $f, $bound)) {
            # ["::method"] --> [null, "method"]
            array_splice($cf, 0, 0, [null]);
            $cf[1] = $f;
        } else {
          $cf[0] = $c;
          $bound = is_string($c);
        }
      } else {
        $cf[0] = null;
        $bound = false;
      }
      #
      if (!array_key_exists(1, $cf)) return false;
      $f = $cf[1];
      if (!is_string($f)) return false;
      if (self::_parse_c_static($f, $rc, $f, $rbound)) {
        if ($rc !== null && $c === null) {
          $c = $rc;
          $bound = $rbound;
        }
      } else {
        if (self::_is_invalid($f)) return false;
        if (self::_is_nfunction($f)) return false;
        if (self::_parse_method($f)) return false;
        self::_parse_static($f);
      }
      $cf[1] = $f;
    } else {
      return false;
    }
    if ($strict) {
      $reason = null;
      if ($bound) {
        if (!class_exists($c)) {
          $reason = "$msg: class not found";
          return false;
        }
        if (!method_exists($c, $f)) {
          $reason = "$msg: method not found";
          return false;
        }
      } else {
        $reason = "$msg: not bound";
      }
    }
    $func = $cf;
    return true;
  }

  /**
   * vérifier que $func est une méthode statique avec les règles de
   * {@link self::verifix_static()}
   */
  static function is_static($func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool {
    return self::verifix_static($func, $strict, $bound, $reason);
  }

  #############################################################################
  # Méthodes non statiques

  private static function _parse_class_m(?string $cm, ?string &$c, ?string &$m): bool {
    if (self::_is_invalid($cm) || self::_parse_static($cm)) return false;
    $pos = strpos($cm, "->");
    if ($pos === false) return false;
    if ($pos === 0) return false;
    $tmpc = substr($cm, 0, $pos);
    $cm = substr($cm, $pos + 2);
    if (self::_is_nfunction($cm)) return false;
    [$c, $m] = [$tmpc, cv::vn($cm)];
    return true;
  }

  private static function _parse_c_method(?string $cm, ?string &$c, ?string &$m, ?bool &$bound): bool {
    if (self::_is_invalid($cm) || self::_parse_static($cm)) return false;
    $pos = strpos($cm, "->");
    if ($pos === false) return false;
    if ($pos == strlen($cm) - 2) return false;
    if ($pos > 0) {
      $tmpc = substr($cm, 0, $pos);
      $bound = true;
    } else {
      $tmpc = null;
      $bound = false;
    }
    $cm = substr($cm, $pos + 2);
    if (self::_is_nfunction($cm)) return false;
    [$c, $m] = [$tmpc, cv::vn($cm)];
    return true;
  }

  /**
   * vérifier que $func est une méthode non statique, et la normaliser le cas
   * échéant. retourner true si c'est une méthode non statique, false sinon
   *
   * les formes suivantes sont supportées (XXX étant null ou n'importe quelle
   * valeur scalaire de n'importe quel type sauf false)
   * - "XXX->function"
   * - ["XXX->function", ...$args]
   * - [XXX, "->function", ...$args]
   * - [XXX, "function", ...$args] c'est la forme normalisée
   *
   * Si XXX est une classe ou un objet, la méthode est liée. dans tous les cas,
   * elle doit être liée à un objet avant d'être utilisée
   *
   * @param bool $strict vérifier l'existence de la classe et de la méthode si
   * la méthode est liée (ne pas uniquement faire une vérification syntaxique)
   */
  static function verifix_method(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool {
    if ($strict) {
      $msg = var_export($func, true);
      $reason = null;
    }
    if ($func instanceof ReflectionMethod) {
      $bound = false;
      return true;
    }
    if (is_string($func)) {
      if (!self::_parse_c_method($func, $c, $f, $bound)) return false;
      $cf = [$c, $f];
    } elseif (is_array($func)) {
      $cf = $func;
      if (!array_key_exists(0, $cf)) return false;
      $c = $cf[0];
      if ($c === false) return false;
      if (is_object($c)) {
        $bound = true;
      } elseif (is_string($c)) {
        if (self::_is_invalid($c)) return false;
        if (self::_parse_class_m($c, $c, $f)) {
          $cf[0] = $c;
          if ($f !== null) {
            # ["class->method"] --> ["class", "method"]
            array_splice($cf, 1, 0, [$f]);
          }
          $bound = true;
        } elseif (self::_parse_c_method($c, $c, $f, $bound)) {
          # ["->method"] --> [null, "method"]
          array_splice($cf, 0, 0, [null]);
          $cf[1] = $f;
        } else {
          $cf[0] = $c;
          $bound = is_string($c);
        }
      } else {
        $cf[0] = null;
        $bound = false;
      }
      #
      if (!array_key_exists(1, $cf)) return false;
      $f = $cf[1];
      if (!is_string($f)) return false;
      if (self::_parse_c_method($f, $rc, $f, $rbound)) {
        if ($rc !== null && $c === null) {
          $c = $rc;
          $bound = $rbound;
        }
      } else {
        if (self::_is_invalid($f)) return false;
        if (self::_is_nfunction($f)) return false;
        if (self::_parse_static($f)) return false;
        self::_parse_method($f);
      }
      $cf[1] = $f;
    } else {
      return false;
    }
    if ($strict) {
      $reason = null;
      if ($bound) {
        if (!is_object($c) && !class_exists($c)) {
          $reason = "$msg: class not found";
          return false;
        }
        if (!method_exists($c, $f)) {
          $reason = "$msg: method not found";
          return false;
        }
      } else {
        $reason = "$msg: not bound";
      }
    }
    $func = $cf;
    return true;
  }

  /**
   * vérifier que $func est une méthode non statique avec les règles de
   * {@link self::verifix_method()}
   */
  static function is_method($func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool {
    return self::verifix_method($func, $strict, $bound, $reason);
  }

  #############################################################################
  # func

  const TYPE_MASK = 0b11;

  const FLAG_STATIC = 0b100;

  const TYPE_CLOSURE = 0, TYPE_FUNCTION = 1, TYPE_CLASS = 2, TYPE_METHOD = 3;

  const TYPE_STATIC = self::TYPE_METHOD | self::FLAG_STATIC;

  protected static function not_a_callable($func, ?string $reason) {
    if ($reason === null) {
      $msg = var_export($func, true);
      $reason = "$msg: not a callable";
    }
    return new ValueException($reason);
  }

  static function with($func, ?array $args=null, bool $strict=true): self {
    if (!is_array($func)) {
      if ($func instanceof Closure) {
        return new self(self::TYPE_CLOSURE, $func, $args);
      } elseif ($func instanceof ReflectionFunction) {
        return new self(self::TYPE_FUNCTION, $func, $args);
      } elseif ($func instanceof ReflectionClass) {
        return new self(self::TYPE_CLASS, $func, $args);
      } elseif ($func instanceof ReflectionMethod) {
        return new self(self::TYPE_METHOD, $func, $args, false);
      }
    }
    if (self::verifix_function($func, $strict, $reason)) {
      return new self(self::TYPE_FUNCTION, $func, $args, false, $reason);
    } elseif (self::verifix_class($func, $strict, $reason)) {
      return new self(self::TYPE_CLASS, $func, $args, false, $reason);
    } elseif (self::verifix_method($func, $strict, $bound, $reason)) {
      return new self(self::TYPE_METHOD, $func, $args, $bound, $reason);
    } elseif (self::verifix_static($func, $strict, $bound, $reason)) {
      return new self(self::TYPE_STATIC, $func, $args, $bound, $reason);
    }
    throw self::not_a_callable($func, $reason);
  }

  static function ensure($func, ?array $args=null, bool $strict=true): self {
    $func = self::with($func, $args, $strict);
    if (!$func->isBound()) {
      throw self::not_a_callable($func->func, $func->reason);
    }
    return $func;
  }

  static function check($func, ?array $args=null, bool $strict=true): bool {
    try {
      self::ensure($func, $args, $strict);
      return true;
    } catch (Exception $e) {
      return false;
    }
  }

  static function call($func, ...$args) {
    return self::with($func)->invoke($args);
  }

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

  protected function __construct(int $type, $func, ?array $args=null, bool $bound=false, ?string $reason=null) {
    $flags = $type & ~self::TYPE_MASK;
    $type = $type & self::TYPE_MASK;
    $object = null;
    $prefixArgs = [];
    if (!is_array($func)) {
      $reflection = $func;
      $func = null;
    } else {
      if (count($func) > 2) {
        $prefixArgs = array_slice($func, 2);
        $func = array_slice($func, 0, 2);
      }
      [$c, $f] = $func;
      switch ($type) {
      case self::TYPE_FUNCTION:
        $reflection = new ReflectionFunction($f);
        break;
      case self::TYPE_CLASS:
        $reflection = new ReflectionClass($c);
        break;
      case self::TYPE_METHOD:
        if ($c === null) {
          $reflection = null;
        } else {
          $reflection = new ReflectionMethod($c, $f);
          if (is_object($c)) $object = $c;
        }
        break;
      default:
        throw StateException::unexpected_state();
      }
    }
    A::merge($prefixArgs, $args);

    $this->type = $type;
    $this->flags = $flags;
    $this->func = $func;
    $this->bound = $bound;
    $this->reason = $reason;
    $this->object = $object;
    $this->prefixArgs = $prefixArgs;
    $this->updateReflection($reflection);
  }

  protected int $type;

  protected int $flags;

  protected ?array $func;

  protected bool $bound;

  protected ?string $reason;

  protected ?object $object;

  protected array $prefixArgs;

  /** @var Closure|ReflectionFunction|ReflectionMethod|ReflectionClass */
  protected $reflection;

  protected bool $variadic;

  protected int $minArgs;

  protected int $maxArgs;

  protected function updateReflection($reflection): void {
    $variadic = false;
    $minArgs = $maxArgs = 0;
    if ($reflection instanceof Closure) {
      $r = new ReflectionFunction($reflection);
      $variadic = $r->isVariadic();
      $minArgs = $r->getNumberOfRequiredParameters();
      $maxArgs = $r->getNumberOfParameters();
    } elseif ($reflection instanceof ReflectionClass) {
      $r = $reflection->getConstructor();
      if ($r === null) {
        $variadic = false;
        $minArgs = $maxArgs = 0;
      } else {
        $variadic = $r->isVariadic();
        $minArgs = $r->getNumberOfRequiredParameters();
        $maxArgs = $r->getNumberOfParameters();
      }
    } elseif ($reflection !== null) {
      $variadic = $reflection->isVariadic();
      $minArgs = $reflection->getNumberOfRequiredParameters();
      $maxArgs = $reflection->getNumberOfParameters();
    }
    $this->reflection = $reflection;
    $this->variadic = $variadic;
    $this->minArgs = $minArgs;
    $this->maxArgs = $maxArgs;
  }

  function isBound(): bool {
    if ($this->type !== self::TYPE_METHOD) return true;
    if ($this->flags & self::FLAG_STATIC) return $this->bound;
    else return $this->bound && $this->object !== null;
  }

  function bind($object): self {
    if ($this->type !== self::TYPE_METHOD) return $this;

    [$c, $f] = $this->func;
    if ($this->reflection === null) {
      $this->func[0] = $c = $object;
      $this->updateReflection(new ReflectionMethod($c, $f));
    }
    if (is_object($object) && !($this->flags & self::FLAG_STATIC)) {
      if (is_object($c)) $c = get_class($c);
      if (is_string($c) && !($object instanceof $c)) {
        throw ValueException::invalid_type($object, $c);
      }
      $this->object = $object;
      $this->bound = true;
    }
    return $this;
  }

  function invoke(?array $args=null) {
    $args = array_merge($this->prefixArgs, $args ?? []);
    if (!$this->variadic) $args = array_slice($args, 0, $this->maxArgs);
    $minArgs = $this->minArgs;
    while (count($args) < $minArgs) $args[] = null;

    switch ($this->type) {
    case self::TYPE_CLOSURE:
      /** @var Closure $closure */
      $closure = $this->reflection;
      return $closure(...$args);
    case self::TYPE_FUNCTION:
      /** @var ReflectionFunction $function */
      $function = $this->reflection;
      return $function->invoke(...$args);
    case self::TYPE_METHOD:
      /** @var ReflectionMethod $method */
      $method = $this->reflection;
      if ($method === null) throw self::not_a_callable($this->func, $this->reason);
      return $method->invoke($this->object, ...$args);
    case self::TYPE_CLASS:
      /** @var ReflectionClass $class */
      $class = $this->reflection;
      return $class->newInstance(...$args);
    default:
      throw StateException::unexpected_state();
    }
  }
}
