<?php
namespace Kant\Routing;

use Closure;
use Kant\Kant;
use Kant\Support\Str;
use Kant\Http\Request;
use InvalidArgumentException;
use Kant\Routing\UrlRoutable;

class UrlGenerator
{

    /**
     * The route collection.
     *
     * @var \Kant\Routing\RouteCollection
     */
    protected $routes;

    /**
     * The request instance.
     *
     * @var \Kant\Http\Request
     */
    protected $request;

    /**
     * The forced URL root.
     *
     * @var string
     */
    protected $forcedRoot;

    /**
     * The forced schema for URLs.
     *
     * @var string
     */
    protected $forceScheme;

    /**
     * A cached copy of the URL root for the current request.
     *
     * @var string|null
     */
    protected $cachedRoot;

    /**
     * A cached copy of the URL schema for the current request.
     *
     * @var string|null
     */
    protected $cachedSchema;

    /**
     * The root namespace being applied to controller actions.
     *
     * @var string
     */
    protected $rootNamespace;

    /**
     * The session resolver callable.
     *
     * @var callable
     */
    protected $sessionResolver;

    /**
     * The callback to use to format hosts.
     *
     * @var \Closure
     */
    protected $formatHostUsing;

    /**
     * The callback to use to format paths.
     *
     * @var \Closure
     */
    protected $formatPathUsing;

    /**
     * The route URL generator instance.
     *
     * @var \Kant\Routing\RouteUrlGenerator
     */
    protected $routeGenerator;

    /**
     * Create a new URL Generator instance.
     *
     * @param \Kant\Routing\RouteCollection $routes            
     * @param \Kant\Http\Request $request            
     * @return void
     */
    public function __construct(RouteCollection $routes, Request $request)
    {
        $this->routes = $routes;
        
        $this->setRequest($request);
    }

    /**
     * Get the full URL for the current request.
     *
     * @return string
     */
    public function full()
    {
        return $this->request->fullUrl();
    }

    /**
     * Get the current URL for the request.
     *
     * @return string
     */
    public function current($params = [])
    {
        return $this->to($this->request->getPathInfo(), $params);
    }

    /**
     * Get the URL for the previous request.
     *
     * @param mixed $fallback            
     * @return string
     */
    public function previous($fallback = false)
    {
        $referrer = $this->request->headers->get('referer');
        
        $url = $referrer ? $this->to($referrer) : $this->getPreviousUrlFromSession();
        
        if ($url) {
            return $url;
        } elseif ($fallback) {
            return $this->to($fallback);
        } else {
            return $this->to('/');
        }
    }

    /**
     * Get the previous URL from the session if possible.
     *
     * @return string|null
     */
    protected function getPreviousUrlFromSession()
    {
        $session = $this->getSession();
        
        return $session ? $session->previousUrl() : null;
    }

    /**
     * Generate an absolute URL to the given path.
     *
     * @param string $path            
     * @param mixed $extra            
     * @param bool|null $secure            
     * @return string
     */
    public function to($path = "", $extra = [], $secure = null)
    {
        if (is_array($path)) {
            list ($name, $parameters) = $this->parsePathQuery($path);
            return $this->route($name, $parameters);
        }
        
        if ($path === '') {
            $path = Kant::$app->getRequest()->getPathInfo();
        }
        
        // First we will check if the URL is already a valid URL. If it is we will not
        // try to generate a new one but will simply return the URL as is, which is
        // convenient since developers do not always have to check if it's valid.
        if ($this->isValidUrl($path)) {
            return $path;
        }
        
        $tail = @array_map('trim', (array) $this->formatParameters($extra));
        
        // Once we have the scheme we will compile the "tail" by collapsing the values
        // into a single string delimited by slashes. This just makes it convenient
        // for passing the array of parameters to this URL as a list of segments.
        $root = $this->formatRoot($this->formatScheme($secure));
        
        list ($path, $query) = $this->extractQueryString($path);
        
        return $this->format($root, '/' . trim($path, '/')) . $this->httpBluidQuery($tail, $query);
    }
	
    /**
     * Generate a secure, absolute URL to the given path.
     *
     * @param string $path            
     * @param array $parameters            
     * @return string
     */
    public function secure($path, $parameters = [])
    {
        return $this->to($path, $parameters, true);
    }

    /**
     * Generate the URL to an application asset.
     *
     * @param string $path            
     * @param bool|null $secure            
     * @return string
     */
    public function asset($path, $secure = null)
    {
        if ($this->isValidUrl($path)) {
            return $path;
        }
        
        // Once we get the root URL, we will check to see if it contains an index.php
        // file in the paths. If it does, we will remove it since it is not needed
        // for asset paths, but only for routes to endpoints in the application.
        $root = $this->formatRoot($this->formatScheme($secure));
        
        return $this->removeIndex($root) . '/' . trim($path, '/');
    }

    /**
     * Generate the URL to a secure asset.
     *
     * @param string $path            
     * @return string
     */
    public function secureAsset($path)
    {
        return $this->asset($path, true);
    }

    /**
     * Generate the URL to an asset from a custom root domain such as CDN, etc.
     *
     * @param string $root            
     * @param string $path            
     * @param bool|null $secure            
     * @return string
     */
    public function assetFrom($root, $path, $secure = null)
    {
        // Once we get the root URL, we will check to see if it contains an index.php
        // file in the paths. If it does, we will remove it since it is not needed
        // for asset paths, but only for routes to endpoints in the application.
        $root = $this->formatRoot($this->formatScheme($secure), $root);
        
        return $this->removeIndex($root) . '/' . trim($path, '/');
    }

    /**
     * Remove the index.php file from a path.
     *
     * @param string $root            
     * @return string
     */
    protected function removeIndex($root)
    {
        $i = 'index.php';
        
        return Str::contains($root, $i) ? str_replace('/' . $i, '', $root) : $root;
    }

    /**
     * Get the default scheme for a raw URL.
     *
     * @param bool|null $secure            
     * @return string
     */
    public function formatScheme($secure)
    {
        if (!is_null($secure)) {
            return $secure ? 'https://' : 'http://';
        }
        
        if (is_null($this->cachedSchema)) {
            $this->cachedSchema = $this->forceScheme ?  : $this->request->getScheme() . '://';
        }
        
        return $this->cachedSchema;
    }

    /**
     * Get the URL to a named route.
     *
     * @param string $name            
     * @param mixed $parameters            
     * @param bool $absolute            
     * @return string
     *
     * @throws \InvalidArgumentException
     */
    public function route($name, $parameters = [], $absolute = true)
    {
        if (!is_null($route = $this->routes->getByName($name))) {
            return $this->parseToRoute($route, $parameters, $absolute);
        }
        
        throw new InvalidArgumentException("Route [{$name}] not defined.");
    }

    /**
     * Get the URL for a given route instance.
     *
     * @param \Kant\Routing\Route $route            
     * @param mixed $parameters            
     * @param bool $absolute            
     * @return string
     *
     * @throws \Kant\Exception\UrlGenerationException
     */
    protected function parseToRoute($route, $parameters, $absolute)
    {
        return $this->routeUrl()->to($route, $this->formatParameters($parameters), $absolute);
    }

    /**
     * Get the URL to a controller action.
     *
     * @param string $action            
     * @param mixed $parameters            
     * @param bool $absolute            
     * @return string
     *
     * @throws \InvalidArgumentException
     */
    public function action($action, $parameters = [], $absolute = true)
    {
        if (is_null($route = $this->routes->getByAction($action = $this->formatAction($action)))) {
            throw new InvalidArgumentException("Action {$action} not defined.");
        }
        
        return $this->parseToRoute($route, $parameters, $absolute);
    }

    /**
     * Format the given controller action.
     *
     * @param string $action            
     * @return string
     */
    protected function formatAction($action)
    {
        if ($this->rootNamespace && ! (strpos($action, '\\') === 0)) {
            return $this->rootNamespace . '\\' . $action;
        } else {
            return trim($action, '\\');
        }
    }

    /**
     * Format the array of URL parameters.
     *
     * @param mixed|array $parameters            
     * @return array
     */
    public function formatParameters($parameters)
    {
        $parameters = array_wrap($parameters);
        foreach ($parameters as $key => $parameter) {
            if ($parameter instanceof UrlRoutable) {
                $parameters[$key] = $parameter->getRouteKey();
            }
        }
        
        return $parameters;
    }

    /**
     * Extract the query string from the given path.
     *
     * @param string $path            
     * @return array
     */
    protected function extractQueryString($path)
    {
        if (($queryPosition = strpos($path, '?')) !== false) {
            return [
                substr($path, 0, $queryPosition),
                substr($path, $queryPosition)
            ];
        }
        
        return [
            $path,
            ''
        ];
    }

    /**
     * Get the base URL for the request.
     *
     * @param string $scheme            
     * @param string $root            
     * @return string
     */
    public function formatRoot($scheme, $root = null)
    {
        if (is_null($root)) {
            if (is_null($this->cachedRoot)) {
                $this->cachedRoot = $this->forcedRoot ?  : $this->request->root();
            }
            
            $root = $this->cachedRoot;
        }
        
        $start = Str::startsWith($root, 'http://') ? 'http://' : 'https://';
        
        return preg_replace('~' . $start . '~', $scheme, $root, 1);
    }

    /**
     * Format the given URL segments into a single URL.
     *
     * @param string $root            
     * @param string $path            
     * @return string
     */
    public function format($root, $path)
    {
        $path = '/' . trim($path, '/');
        
        if ($this->formatHostUsing) {
            $root = call_user_func($this->formatHostUsing, $root);
        }
        
        if ($this->formatPathUsing) {
            $path = call_user_func($this->formatPathUsing, $path);
        }
        
        return trim($root . $path, '/');
    }

    /**
     * Determine if the given path is a valid URL.
     *
     * @param string $path            
     * @return bool
     */
    public function isValidUrl($path)
    {
        if (!Str::startsWith($path, [
            '#',
            '//',
            'mailto:',
            'tel:',
            'http://',
            'https://'
        ])) {
            return filter_var($path, FILTER_VALIDATE_URL) !== false;
        }
        
        return true;
    }

    /**
     * Get the Route URL generator instance.
     *
     * @return \Kant\Routing\RouteUrlGenerator
     */
    protected function routeUrl()
    {
        if (!$this->routeGenerator) {
            $this->routeGenerator = new RouteUrlGenerator($this, $this->request);
        }
        
        return $this->routeGenerator;
    }

    /**
     * Set the default named parameters used by the URL generator.
     *
     * @param array $defaults            
     * @return void
     */
    public function defaults(array $defaults)
    {
        $this->routeUrl()->defaults($defaults);
    }

    /**
     * Force the scheme for URLs.
     *
     * @param string $schema            
     * @return void
     */
    public function forceScheme($schema)
    {
        $this->cachedSchema = null;
        
        $this->forceScheme = $schema . '://';
    }

    /**
     * Set the forced root URL.
     *
     * @param string $root            
     * @return void
     */
    public function forceRootUrl($root)
    {
        $this->forcedRoot = rtrim($root, '/');
        
        $this->cachedRoot = null;
    }

    /**
     * Set a callback to be used to format the host of generated URLs.
     *
     * @param \Closure $callback            
     * @return $this
     */
    public function formatHostUsing(Closure $callback)
    {
        $this->formatHostUsing = $callback;
        
        return $this;
    }

    /**
     * Set a callback to be used to format the path of generated URLs.
     *
     * @param \Closure $callback            
     * @return $this
     */
    public function formatPathUsing(Closure $callback)
    {
        $this->formatPathUsing = $callback;
        
        return $this;
    }

    /**
     * Get the path formatter being used by the URL generator.
     *
     * @return \Closure
     */
    public function pathFormatter()
    {
        return $this->formatPathUsing ?  : function ($path) {
            return $path;
        };
    }

    /**
     * Get the request instance.
     *
     * @return \Kant\Http\Request
     */
    public function getRequest()
    {
        return $this->request;
    }

    /**
     * Set the current request instance.
     *
     * @param \Kant\Http\Request $request            
     * @return void
     */
    public function setRequest(Request $request)
    {
        $this->request = $request;
        
        $this->cachedRoot = null;
        $this->cachedSchema = null;
        $this->routeGenerator = null;
    }

    /**
     * Set the route collection.
     *
     * @param \Kant\Routing\RouteCollection $routes            
     * @return $this
     */
    public function setRoutes(RouteCollection $routes)
    {
        $this->routes = $routes;
        
        return $this;
    }

    /**
     * Get the session implementation from the resolver.
     *
     * @return \Kant\Session\Store|null
     */
    protected function getSession()
    {
        if ($this->sessionResolver) {
            return call_user_func($this->sessionResolver);
        }
    }

    /**
     * Set the session resolver for the generator.
     *
     * @param callable $sessionResolver            
     * @return $this
     */
    public function setSessionResolver(callable $sessionResolver)
    {
        $this->sessionResolver = $sessionResolver;
        
        return $this;
    }

    /**
     * Set the root controller namespace.
     *
     * @param string $rootNamespace            
     * @return $this
     */
    public function setRootControllerNamespace($rootNamespace)
    {
        $this->rootNamespace = $rootNamespace;
        
        return $this;
    }

    /**
     * Shift the first item from $path array, the left are to build parameters
     *
     * @param type $path            
     * @return type
     */
    protected function parsePathQuery($path)
    {
        return [
            array_shift($path),
            $path
        ];
    }

    /**
     * Implode tail and query from the static::to methd
     *
     * @param array $tail            
     * @param string $query            
     * @return string
     */
    protected function httpBluidQuery(array $tail, $query = NULL)
    {
        if (!empty($query)) {
            parse_str(ltrim($query, "?"), $params);
            return "?" . http_build_query(array_merge($params, $tail));
        }
        if (!empty($tail)) {
            return "?" . http_build_query($tail);
        }
    }
}
