<?php

namespace Snaju\Inception\Http\Request;

use Closure;
use Snaju\Inception\Exception\InvalidArgumentException;
use Snaju\Inception\Event\Types\OnRouteEnd;
use Snaju\Inception\Event\Types\OnRouteFallback;
use Snaju\Inception\Event\Types\OnRouteProcess;
use Snaju\Inception\Http\Middleware\MiddlewareRegistry;
use Snaju\Inception\InceptionCore;
use Snaju\Inception\Route\RouteManager;

/**
 * Convenience class for organizing blocks of code according to the request
 * method that should trigger the scoped block of code.
 *
 * @method get(\Closure $func, array $middleware = []) Handle GET requests from a client.
 * @method post(\Closure $func, array $middleware = []) Handle POST requests from a client.
 * @method put(\Closure $func, array $middleware = []) Handle PUT requests from a client.
 * @method patch(\Closure $func, array $middleware = []) Handle PATCH requests from a client.
 * @method delete(\Closure $func, array $middleware = []) Handle DELETE requests from a client.
 * @method copy(\Closure $func, array $middleware = []) Handle COPY requests from a client.
 * @method head(\Closure $func, array $middleware = []) Handle HEAD requests from a client.
 * @method options(\Closure $func, array $middleware = []) Handle OPTIONS requests from a client.
 * @method link(\Closure $func, array $middleware = []) Handle LINK requests from a client.
 * @method unlink(\Closure $func, array $middleware = []) Handle UNLINK requests from a client.
 * @method purge(\Closure $func, array $middleware = []) Handle PURGE requests from a client.
 * @method lock(\Closure $func, array $middleware = []) Handle LOCK requests from a client.
 * @method unlock(\Closure $func, array $middleware = []) Handle UNLOCK requests from a client.
 * @method propfind(\Closure $func, array $middleware = []) Handle PROPFIND requests from a client.
 * @method view(\Closure $func, array $middleware = []) Handle VIEW requests from a client.
 * @package Snaju\Inception\Http\Request
 */
class Controller
{

    private static ?Controller $instance = null;

    private MiddlewareRegistry $middlewareRegistry;

    private WebRequest $request;

    /**
     * The middleware that shoud be ran on every method for the route.
     * @var array
     */
    public array $globalRouteMiddleware = [];

    /**
     * The methods that the Controller has a registered callable for and won't run the fallback method for.
     * @var array
     */
    public array $registeredMethods = [];

    /**
     * Returns the Controller instance (Singleton)
     *
     * @param array $middleware
     * @return Controller
     */
    public static function getInstance($middleware = []): Controller
    {
        if (self::$instance === null) {
            self::$instance = new Controller($middleware);
        }
        return self::$instance;
    }

    /**
     * Controller constructor.
     * @param array $middleware
     */
    public function __construct($middleware = [])
    {
        $this->request = InceptionCore::getRequest();
        $this->middlewareRegistry = InceptionCore::$middleware;
        $this->globalRouteMiddleware = $middleware;
    }

    public function __call($method, $args)
    {
        if (! $args[0] instanceof Closure) {
            throw new InvalidArgumentException('Class RequestHandler expects a closure to be passed as an agruments when registering a route');
        }

        // "Register" the method as an implemented method for the endpoint
        $requestMethod = strtolower($this->request->getMethod());
        $this->registeredMethods[] = strtolower($method);

        if (strtolower($method) === $requestMethod) {
            $routeMiddleware = $this->globalRouteMiddleware;

            // Check if the route has any registered middleware and register it (if applicable)
            if (isset($args[1]) && gettype($args[1]) === 'array') {
                $routeMiddleware = array_merge($routeMiddleware, $args[1]);
            }

            // Run the call to execute all middleware that should run before the
            // core of the application. Then run the core, followed by any middleware
            // that should be ran after the request lifestyle
            $this->middlewareRegistry->init($routeMiddleware)->process($this->request, function(WebRequest $request) use ($args) {
                // Get the last part of the uri path and get the matched route from the RouteManager.
                // Determine if a model id was specified in the uri and call the
                // user-defined handler with the WebRequest and model id (if applicable)
                $modelId = null;
                $pathSegments = explode('/', $this->request->getUri()->getPath());
                $partPos = array_search(RouteManager::$lastMatchedRoutePart, $pathSegments);

                if (! empty($partPos) && gettype($partPos) !== 'boolean') {
                    if ($partPos < count($pathSegments) - 1) {
                        $modelId = (string)$pathSegments[count($pathSegments) - 1];
                    }
                }

                // Execute the specified closure passing the web request and model id (if applicable)
                call_user_func($args[0], $request, $modelId);

                (new OnRouteProcess($this->request->getUri()->getPath()))->call();

                return $request;
            });

            (new OnRouteEnd($this->request->getUri()->getPath()))->call();
        }
    }

    /**
     * Convenience method that checks if the controller has a registered
     * callable for the specified request method.
     *
     * @param string $method
     * @return bool
     */
    public function acceptsMethod(string $method): bool
    {
        return (in_array(strtolower($method), $this->registeredMethods));
    }

    /**
     * Process the code starting with execution of "before" middleware,
     * the core (i.e.: route), followed by any "after" middleware.
     *
     * @param Closure $func
     */
    public function execute(Closure $func): void
    {
        // Process the middlewareRegistry, core, followed by any after middlewareRegistry
        $this->middlewareRegistry->init()
            ->process($this->request, function($request, $func) {
                call_user_func($func);
                // Return the request so that any after middlewareRegistry can be called
                return $request;
            });
    }

    /**
     * Accepts and executes a closure when a request uses a method that
     * hasn't been "registered," will not get triggered if a request method
     * matches __call magic method.
     *
     * @param Closure $func
     */
    public function fallback(Closure $func): void
    {
        $requestMethod = strtolower($this->request->getMethod());
        if (! $this->acceptsMethod($requestMethod)) {
            call_user_func($func, $this->request);

            // Trigger the OnRouteFallback and OnRouteEnd events
            (new OnRouteFallback($this->request->getUri()->getPath()))->call();
            (new OnRouteEnd($this->request->getUri()->getPath()))->call();
        }
    }

}