<?php

namespace AppBundle\History;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RouterInterface;

/**
 * Manage an history of Request objects visible to the screen.
 * 
 * @author Bastien Gatellier <contact@bgatellier.fr>
 */
class ScreenHistory
{
    const ROUTES_BLACKLIST = [
        // Symfony development routes
        '_profiler',
        '_twig_error_test',
        // Route used for going back
        'step_back',
    ];
    const SESSION_NAME = 'history';

    /**
     * @var RouterInterface
     */
    private $router;

    /**
     * @var SessionInterface
     */
    private $session;


    public function __construct(RouterInterface $router, SessionInterface $session)
    {
        $this->router = $router;
        $this->session = $session;
    }

    /**
     * Decide what to do with a Request / Response couple:
     * - Reset the current history
     * - Add the Request to the history
     *
     * @param Request $request
     * @param Response $response
     * @return void
     */
    public function handle(Request $request, Response $response): void
    {
        $request->query->has('_reset_history')
            ? $this->reset()
            : $this->push($request, $response)
        ;
    }

    /**
     * Pops the previous Request
     * 
     * @return Request
     */
    public function pop(): Request
    {
        $history = $this->session->get(self::SESSION_NAME, []);

        $request = $history ? array_pop($history) : Request::createFromGlobals();

        // Pops again, because the last Request of the history is always the current one.
        // And we need to retrieve the previous one.
        if ($history) {
            $request = array_pop($history);
        }

        $this->session->set(self::SESSION_NAME, $history);

        return $request;
    }
    
    /**
     * Push a Request to the stack if the conditions are met.
     *
     * @param Request $request
     * @param Response $response
     * @return void
     */
    private function push(Request $request, Response $response): void
    {
        $history = $this->session->get(self::SESSION_NAME, []);

        if ($this->isRequestAllowed($request, $response, $history)) {
            $history[] = $request;

            $this->session->set(self::SESSION_NAME, $history);
        }
    }

    /**
     * Reset the history stack.
     *
     * @return void
     */
    private function reset(): void
    {
        $this->session->set(self::SESSION_NAME, []);
    }

    /**
     * Checks if the request could be added to the history:
     * - response is successful (HTTP code 2xx)
     * - request matches a route which is not blacklisted
     * - response is an HTML page
     * - request is not asynchronous
     * - request query does not contain the 'iframe' argument
     * - request pathinfo is different from the previous one
     *
     * @param Request $request
     * @param Response $response
     * @param array $history
     * @return boolean
     */
    private function isRequestAllowed(Request $request, Response $response, array $history): bool
    {
        // The response is successful (HTTP code 2xx)
        $isAllowed = $response->isSuccessful();

        // The request matches a route which is not blacklisted
        if ($isAllowed) {
            try {
                $parameters = $this->router->matchRequest($request);
                
                $isAllowed = !in_array($parameters['_route'], self::ROUTES_BLACKLIST);
            } catch (ResourceNotFoundException | MethodNotAllowedException $e) {

            }
        }

        // The response is an HTML page
        if ($isAllowed) {
            $contentType = $response->headers->get('Content-Type');
            
            // Exclude every Response subclasses like Json or BinaryFile.
            // Assume that if the Content-Type header has not yet been set, it is an HTML page
            $isAllowed = !is_subclass_of($response, Response::class)
                && (null === $contentType || 1 === preg_match('/^text\/html/', $contentType));
        }

        // The request is not asynchronous
        if ($isAllowed) {
            $isAllowed = !$request->isXmlHttpRequest();
        }

        // The request query does not contain the 'iframe' argument
        if ($isAllowed) {
            $isAllowed = null === $request->query->get('iframe');
        }

        // The request pathinfo is different from the previous one
        if ($isAllowed) {
            $isAllowed = !$history || $history[count($history) - 1]->getPathInfo() !== $request->getPathInfo();
        }

        return $isAllowed;
    }
}
