<?php

namespace Plainwork\Http;

class Router
{
    private array $tree = [];
    private string $separator = '/';
    private array $shortcut = [];
    // private const FOR_ME = 0;
    // private const FOR_CHILDREN = 1;
    private const FOR_ME = '/';
    private const FOR_CHILDREN = '/*';
    private string $lastRoute = '';

/* registration methods */
    public function perm(string $route, $handler, int $pos = 1): self
    {
        if (($pos < 1) or ($pos > 100)) {
            throw new \InvalidArgumentException(__METHOD__ . ': pos should be > 0 and < 100, input was: ' . $pos);
        }
        return $this->registerInTree($route, 'perm', [$pos, $handler]);
    }

    public function any(string $route, $handler): self
    {
        return $this->registerInTree($route, 'http', [100, $handler]);
    }

    public function get(string $route, $handler): self
    {
        return $this->registerInTree($route, 'get', [100, $handler]);
    }

    public function post(string $route, $handler): self
    {
        return $this->registerInTree($route, 'post', [100, $handler]);
    }

    public function put(string $route, $handler): self
    {
        return $this->registerInTree($route, 'put', [100, $handler]);
    }

    public function patch(string $route, $handler): self
    {
        return $this->registerInTree($route, 'patch', [100, $handler]);
    }

    public function delete(string $route, $handler): self
    {
        return $this->registerInTree($route, 'delete', [100, $handler], true);
    }

    public function middleware(string $route, $handler, int $pos): self
    {
        if (($pos < 1) or ($pos > 100)) {
            throw new \InvalidArgumentException(__METHOD__ . ': pos should be > 0 and < 100, input was: ' . $pos);
        }
        return $this->registerInTree($route, 'http', [$pos, $handler]);
    }

    public function meta(string $route, string $metaName, $what)
    {
        return $this->registerInTree($route, $metaName, [100, $what]);
    }

    private function registerInTree(string $route, string $context, $what): self
    {
        $index = self::FOR_ME;
        if ('*' === substr($route, -1)) {
            $index = self::FOR_CHILDREN;
            $route = substr($route, 0, -1);
        }

        $routeArray = strlen($route) ? explode($this->separator, $route) : [];

        $tree = &$this->tree;
        while ($routeArray) {
            $part = array_shift($routeArray);
            $tree[$part] ??= [];

            $tree = &$tree[$part];
        }

        $tree[$index] ??= [];
        $tree[$index][$context] ??= [];
        $tree[$index][$context][] = $what;

        $this->lastRoute = $route;

        return $this;
    }

    public function queryParamInt(string $route, string $param): self
    {
        return $this->queryParamRule($route, $param, 'int');
    }

    public function paramInt(string $param): self
    {
        return $this->queryParamInt($this->lastRoute, $param);
    }

    public function queryParamIntList(string $route, string $param): self
    {
        return $this->queryParamRule($route, $param, 'intlist');
    }

    public function paramIntList(string $param): self
    {
        return $this->queryParamIntList($this->lastRoute, $param);
    }

    public function queryParamFloat(string $route, string $param): self
    {
        return $this->queryParamRule($route, $param, 'float');
    }

    public function paramFloat(string $param): self
    {
        return $this->queryParamFloat($this->lastRoute, $param);
    }

    public function queryParamAlphaNumeric(string $route, string $param): self
    {
        return $this->queryParamRule($route, $param, 'alphanumeric');
    }

    public function paramAlphaNumeric(string $param): self
    {
        return $this->queryParamAlphaNumeric($this->lastRoute, $param);
    }

    private function queryParamRule(string $route, string $param, string $rule): self
    {
        $routeArray = strlen($route) ? explode($this->separator, $route) : [];

        $tree = &$this->tree;
        while ($routeArray) {
            $part = array_shift($routeArray);

        // gotcha
            if ('{' . $param . '}' === $part) {
                $tree[$part][self::FOR_ME]['rule'] = $rule;
                break;
            }

            $tree = &$tree[$part];
        }

        $tree[self::FOR_ME]['paramrule'] ??= [];
        $tree[self::FOR_ME]['paramrule'][$param] = $rule;

        return $this;
    }

    public function shortcut(string $fromRoute, string $toRoute): self
    {
        $this->shortcut[$fromRoute] = $toRoute;

        return $this;
    }

    public function isShortcut(string $route)
    {
        return $this->shortcut[$route] ?? false;
    }

/* get what was registered before */
    public function find(string $route, string $context): array
    {
        $ret = [];
// _print_r($this->tree);

        $contexts = [$context];
        if (in_array($context, ['get', 'post', 'put', 'patch', 'delete'])) {
            array_unshift($contexts, 'http');
        }

        $routeArray = strlen($route) ? explode($this->separator, $route) : [];
        $tree = $this->tree;
        while (true) {
            foreach ($contexts as $context_) {
            // is last?
                if (!$routeArray) {
                    $ret = array_merge($ret, $tree[self::FOR_ME][$context_] ?? []);
                }
                $ret = array_merge($ret, $tree[self::FOR_CHILDREN][$context_] ?? []);
            }

            if (!$routeArray) {
                break;
            }

            $part = array_shift($routeArray);
            if (!isset($tree[$part])) {
                $ret = [];
                break;
            }
            $tree = $tree[$part];
        }

        usort($ret, fn($a, $b) => ($b[0] - $a[0]));
        $ret = array_column($ret, 1);

        return $ret;
    }

/**
 * Example: alias(admin/customer, customer)
 * admin/customer/12/edit => customer/12/edit
 *
 * @param string, string
 */
    public function alias(string $newRoute, string $existingRoute): self
    {
    // find target
        $existingRouteArray = explode($this->separator, $existingRoute);
        $targetTree = &$this->tree;
        while ($existingRouteArray) {
            $part = array_shift($existingRouteArray);
            $targetTree = &$targetTree[$part];
        }

    // set ref link to existing route
        $newRouteArray = explode($this->separator, $newRoute);
        $tree = &$this->tree;
        while ($newRouteArray) {
            $part = array_shift($newRouteArray);
            $tree[$part] ??= [];
            if ($newRouteArray) {
                $tree = &$tree[$part];
            }
        }

        $tree[$part] = &$targetTree;

        return $this;
    }

    public function hasPath(string $path): bool
    {
        $ret = false;

        list($route,) = $this->parsePath($path);
        if (null === $route) {
            return $ret;
        }

        $routeArray = strlen($route) ? explode($this->separator, $route) : [];
        if (!$routeArray) {
            $ret = true;

            return $ret;
        }

        $tree = $this->tree;
        while (true) {
            if (!$routeArray) {
                break;
            }

            $part = array_shift($routeArray);
            if (!isset($tree[$part])) {
                return $ret;
            }

            $tree = $tree[$part];
        }

        if (isset($tree[self::FOR_ME])) {
            $ret = true;
        }

        return $ret;
    }

/**
 * Finds the route pattern with array of parameters from an input route.
 * Example:
 * customer/12/edit => [customer/{id}/edit, [id => 12]]
 *
 * @param string
 * @return array(?string, array)
 */
    public function parsePath(string $path): array
    {
        $route = $params = [];

        if ('' === $path) {
            return ['', []];
        }

        $pathArray = explode($this->separator, $path);

        $ok = false;
        $tree = $this->tree;
        // while ($part = array_shift($pathArray)) {
        while ($pathArray) {
            $part = array_shift($pathArray);
            $ok = false;
            $nextPart = $part;

        // scalar path
            if (isset($tree[$part])) {
            // last one?
                if (!$pathArray) {
                    if (!isset($tree[$part][self::FOR_ME])) {
                        break;
                    }
                }

                $route[] = $part;
                $ok = true;
        // parametrized part
            } else {
                foreach (array_keys($tree) as $test) {
                    if ('{' !== substr($test, 0, 1)) {
                        continue;
                    }

                    $ok = true;
                    $pname = substr($test, 1, -1);
                    $pvalue = $part;

                // have rules for param?
                    if (isset($tree[$test][self::FOR_ME]['rule'])) {
                        $pvalue = $this->applyRuleToParam($pvalue, $tree[$test][self::FOR_ME]['rule']);
                        if ((null === $pvalue) or ([] === $pvalue)) {
                            $ok = false;
                            break;
                        }
                    }

                    $route[] = $test;
                    $params[$pname] = $pvalue;

                    $nextPart = $test;
                    break;
                }
            }

            if (!$ok) {
                break;
            }

            $tree = $tree[$nextPart];
        }

        $ret = $ok ? [join($this->separator, $route), $params] : [null, []];

        return $ret;
    }

    public function filterParam(string $route, string $paramName, $paramValue)
    {
        $ret = null;

        // [$route, $params] = $this->parsePath($path);
        // if (null === $route) {
            // return $ret;
        // }

        $routeArray = explode($this->separator, $route);
        $rule = null;

        $tree = $this->tree;
        while ($part = array_shift($routeArray)) {
            if (isset($tree[$part])) {
                $tree = $tree[$part];
                $rule = $tree[self::FOR_ME]['paramrule'][$paramName] ?? $rule;
            } else {
                $tree = null;
                break;
            }
        }

        if (null === $tree) {
            return $ret;
        }

        $ret = $paramValue;

        $rule = $tree[self::FOR_ME]['paramrule'][$paramName] ?? $rule;
        if (null === $rule) {
            return $ret;
        }

        $ret = $this->applyRuleToParam($paramValue, $rule);

        return $ret;
    }

    private function applyRuleToParam($paramValue, string $rule)
    {
        $ret = null;

        if (('intlist' === $rule) && is_array($paramValue)) {
            $ret = array_map('intval', $paramValue);

            return $ret;
        }

        if (is_array($paramValue)) {
            return $ret;
        }

        $paramValue = trim($paramValue);

        if ('int' === $rule) {
            if (preg_match('/^[0-9]*$/', $paramValue)) {
                $ret = (int) $paramValue;
            }
        } elseif ('float' === $rule) {
            if (preg_match('/^[0-9\.\-]*$/', $paramValue)) {
                $ret = (float) $paramValue;
            }
        } elseif ('intlist' === $rule) {
            $ret = [];
            if (preg_match('/^[0-9\_]*$/', $paramValue)) {
                $ret = explode('_', $paramValue);
                $ret = array_map('intval', $ret);
            }
        } elseif ('alphanumeric' === $rule) {
            if (preg_match('/^[0-9a-z\-\_]*$/', $paramValue)) {
                $ret = $paramValue;
            }
        }

        return $ret;
    }
}
