<?php

namespace YOOtheme;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class Kernel implements \ArrayAccess
{
    use ContainerTrait;

    /**
     * @var \SplStack
     */
    protected $stack;

    /**
     * @var boolean
     */
    protected $locked;

    /**
     * @var \ArrayAccess|array
     */
    protected $container;

    /**
     * Constructor.
     *
     * @param \ArrayAccess|array $container
     */
    public function __construct($container)
    {
        $this->stack = new \SplStack();
        $this->stack->push($this);
        $this->container = $container;
    }

    /**
     * Dispatches router with request and response.
     *
     * @param  ServerRequestInterface $request
     * @param  ResponseInterface      $response
     * @return ResponseInterface
     */
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response)
    {
        try {
            $response = $this['router']->dispatch($request, $response);
        } catch (\Exception $e) {
            $response = $this->handleException($e, $request, $response);
        }

        return $response;
    }

    /**
     * Handles a request and converts it to a response.
     *
     * @param  ServerRequestInterface $request
     * @param  ResponseInterface      $response
     * @param  boolean                $send
     * @return ResponseInterface
     */
    public function handle(ServerRequestInterface $request, ResponseInterface $response, $send = true)
    {
        try {

            $start = $this->stack->top();
            $this->locked = true;
            $request->setAttribute('foundHandler', [$this, 'handleFound']);
            $request->setAttribute('callableResolver', [$this, 'resolveCallable']);
            $response = $start($request, $response);
            $this->locked = false;

        } catch (\Exception $e) {
            $response = $this->handleException($e, $request, $response);
        }

        return $send ? $response->send() : $response;
    }

    /**
     * Handles a found route.
     *
     * @param  callable               $callable
     * @param  ServerRequestInterface $request
     * @param  ResponseInterface      $response
     * @param  array                  $params
     * @return ResponseInterface
     */
    public function handleFound(callable $callable, ServerRequestInterface $request, ResponseInterface $response, array $params)
    {
        $args = [];
        $params = array_replace(compact('request', 'response'), $params);

        foreach ($this->resolveParameters($callable) as $param) {

            $name = $param->getName();

            if (isset($params[$name])) {
                $args[] = $params[$name];
                continue;
            }

            if ($request->getParam($name) !== null) {
                $args[] = $request->getParam($name);
                continue;
            }

            if ($param->isDefaultValueAvailable()) {
                $args[] = $param->getDefaultValue();
                continue;
            }

            if (is_array($callable)) {
                $handler = sprintf('%s::%s()', get_class($callable[0]), $callable[1]);
            } elseif (is_object($callable)) {
                $handler = get_class($callable);
            } else {
                $handler = $callable;
            }

            throw new \RuntimeException("Handler '{$handler}' requires that you provide a value for the '{$name}' argument");
        }

        return call_user_func_array($callable, $args);
    }

    /**
     * Handles an exception.
     *
     * @param  \Exception             $e
     * @param  ServerRequestInterface $request
     * @param  ResponseInterface      $response
     * @return Response
     */
    protected function handleException(\Exception $e, ServerRequestInterface $request, ResponseInterface $response)
    {
        $event = $this['events']->trigger('error', [$request, $response, $e]);

        if ($event->result instanceof ResponseInterface) {
            return $event->result;
        }

        if ($e instanceof Http\Exception) {
            return $response->withStatus($e->getCode(), $e->getMessage());
        }

        return $response->withStatus(500, $e->getMessage());
    }

    /**
     * Adds a middleware callable.
     *
     * @param  callable $callable
     * @return self
     */
    public function addMiddleware(callable $callable)
    {
        if ($this->locked) {
            throw new \RuntimeException('Middleware can’t be added once the stack is dequeuing');
        }

        $next = $this->stack->top();

        $this->stack->push(function (ServerRequestInterface $request, ResponseInterface $response) use ($callable, $next) {

            $result = $callable($request, $response, $next);

            if (!$result instanceof ResponseInterface) {
                throw new \UnexpectedValueException('Middleware must return instance of Psr\Http\Message\ResponseInterface');
            }

            return $result;
        });

        return $this;
    }

    /**
     * Resolves a string to a callable that can be dispatched.
     *
     * @param  callable|string $callable
     * @return callable
     */
    public function resolveCallable($callable)
    {
        $pattern = '!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!';

        if (is_callable($callable)) {
            return $callable;
        }

        if (is_string($callable) && preg_match($pattern, $callable, $matches)) {

            list(, $class, $method) = $matches;

            if (isset($this[$class])) {
                return [$this[$class], $method];
            }

            if (class_exists($class)) {
                return [new $class, $method];
            }
        }

        throw new \RuntimeException("Route callable is not resolvable");
    }

    /**
     * Resolves method parameters from a callable.
     *
     * @param  callable $callable
     * @return array
     */
    public function resolveParameters(callable $callable)
    {
        if (is_array($callable)) {
            $refl = new \ReflectionMethod($callable[0], $callable[1]);
        } elseif (is_object($callable) && !$callable instanceof \Closure) {
            $refl = (new \ReflectionObject($callable))->getMethod('__invoke');
        } else {
            $refl = new \ReflectionFunction($callable);
        }

        return $refl->getParameters();
    }
}
