<?php

namespace PhpApi;

use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\SimpleAnnotationReader;
use Saxulum\AnnotationManager\Manager\AnnotationManager;
use PhpApi\Annotations\Api;
use PhpApi\Annotations\HttpMethod;

/**
 * This classe resolves URL routing by using annotations on methods. 
 * It specifies a pattern that, when matched, triggers that operation.
 * 
 * @package PhpApi
 * @copyright (c) 2017, Federal Institute of Rondonia
 * @license http://gnu.org/licenses/lgpl.txt LGPL-3.0+
 * @author Natanael Simoes <natanael.simoes@ifro.edu.br>
 * @since Release 0.1.0
 * @link https://github.com/ifroariquemes/PHP-API Github repository
 */
class PhpApi
{

    use \FlorianWolters\Component\Util\Singleton\SingletonTrait;

    /**
     * The file path where the @Api annotation is setup
     * @var string
     */
    const API_ANNOTATION_PATH = '/Annotations/Api.php';

    /**
     * The file path where the @Method annotation is setup
     * @var string
     */
    const METHOD_ANNOTATION_PATH = '/Annotations/HttpMethod.php';

    /**
     * The directory this class will iterate looking for @Api annotation
     * @var string
     */
    private $sourceDir;

    /**
     * The request string captured from URL
     * @var string
     */
    private $request;

    /**
     * The request method (GET, PUT, DELETE...)
     * @var string 
     */
    private $requestMethod;

    /**
     * Holds all @Api routes.
     * It stores the <i>pattern</i> that triggers the route, <i>class</i> and
     * <i>function</i> that will be executed, moreover the specific HTTP 
     * <i>method</i> used for that function
     * @var Route[]
     */
    private $routes = array();

    /**
     * Stores only the patterns that triggers routes (to improve search speed)
     * @var array
     */
    private $patterns = array();

    /**
     * Initilizes the object loading routes and patterns. Do not instantiate
     * this directly. Instead, use the 
     * @param string $sourceDir The directory this class will iterate looking for @Api annotation
     * @uses PhpApi::generateRoutes
     */
    protected function __construct($sourceDir)
    {
        Token::authenticate();
        if (!file_exists($sourceDir)) {
            throw new \Exception('Source directory does not exists.');
        }
        $this->sourceDir = $sourceDir;
        $serverPHPSelf = filter_input(INPUT_SERVER, 'PHP_SELF');
        $serverPHPRequest = filter_input(INPUT_SERVER, 'REQUEST_URI');
        $this->requestMethod = filter_input(INPUT_SERVER, 'REQUEST_METHOD');
        $selfPart = substr($serverPHPSelf, 0, strrpos($serverPHPSelf, '/') + 1);
        $requestUri = str_replace($selfPart, '', $serverPHPRequest);
        $request = ($pos = strpos($requestUri, '?')) ?
                substr($requestUri, 0, $pos) : $requestUri;
        $this->request = (substr($request, -1) === '/') ?
                substr($request, 0, -1) : $request;
        $this->generateRoutes();
    }

    /**
     * Start the PhpApi library and evaluates the actual URL
     * @param string $sourceDir The directory this class will iterate looking for @Api annotation
     * @uses PhpApi::getInstance
     * @uses PhpApi::evaluateURL
     */
    public static function start($sourceDir)
    {
        self::getInstance($sourceDir)->evaluateURL();
    }

    /**
     * Generates routes that triggers specific class methods
     * @uses PhpApi::getAllClasses
     * @uses \Saxulum\AnnotationManager\Helper\ClassInfo::getMethodInfos
     * @uses AnnotationReader::getMethodAnnotation
     * @uses PhpApi::addRoute
     */
    private function generateRoutes()
    {
        AnnotationRegistry::registerFile(__DIR__ . self::API_ANNOTATION_PATH);
        AnnotationRegistry::registerFile(__DIR__ . self::METHOD_ANNOTATION_PATH);
        $annotationReader = new AnnotationReader();
        foreach ($this->getAllClasses() as $class) {
            foreach ($class->getMethodInfos() as $method) {
                $method = new \ReflectionMethod($class->getName(), $method->getName());
                $apiPattern = $annotationReader->getMethodAnnotation($method, new Api);
                $httpMethod = $annotationReader->getMethodAnnotation($method, new HttpMethod);
                if (!is_null($apiPattern)) {
                    $this->addRoute($method, $apiPattern, $httpMethod);
                }
            }
        }
    }

    /**
     * Adds the route and pattern for a given $method using a specific
     * HTTP request method if available
     * @param \ReflectionMethod $method The class method that will be triggered
     * @param Api $api The $method @Api annotation
     * @param HttpMethod $httpMethod The $method @HttpMethod Annotation
     */
    private function addRoute(\ReflectionMethod &$method, Api &$api, HttpMethod &$httpMethod = null)
    {
        if (!is_null($api) && self::validatePattern($api->pattern)) {
            $patterns = explode(';', $api->pattern);
            foreach ($patterns as $pattern) {
                array_push($this->routes, new Route($pattern, $method->class, $method->name, $httpMethod->name ?? null));
                array_push($this->patterns, $pattern);
            }
        }
    }

    /**
     * Returns all classes within the source directory
     * @return \Saxulum\AnnotationManager\Helper\ClassInfo[]
     * @uses AnnotationManager::buildClassInfosBasedOnPath
     */
    private function getAllClasses(): array
    {
        $annotationReader = new SimpleAnnotationReader();
        $annotationManager = new AnnotationManager($annotationReader);
        return $annotationManager->buildClassInfosBasedOnPath($this->sourceDir);
    }

    /**
     * Validates @Api pattern string
     * @param string $pattern The pattern
     * @return boolean If the pattern is valid
     */
    private static function validatePattern(string $pattern): bool
    {
        if (substr($pattern, -1) === '/' ||
                strpos('\\', $pattern) !== false ||
                strpos(' ', $pattern) !== false) {
            $patternRules = <<<EOT
Route pattern $pattern not following routing rules: patterns cannot end with /
    and have any whitespaces or \\
EOT;
            http_response_code(400);
            echo json_encode(['message' => $patternRules]);
            exit;
        }
        return true;
    }

    /**
     * Returns the route for a given pattern
     * @param string $pattern The pattern
     * @return Route The route
     * @uses Route::acceptPatternAndHttpMethod
     */
    private function getRoute(string $pattern): Route
    {
        $onlyRoute = false;
        foreach ($this->routes as $route) {
            if ($route->acceptPattern($pattern) && $route->acceptHttpMethod($this->requestMethod)) {
                return $route;
            }
        }
        http_response_code(501); // If reaches here, the real route does not accept the HTTP method requested
        echo json_encode(['message' => "The method $this->requestMethod is not implemented for this request."]);
        exit;
    }

    /**
     * Evaluates URL looking for a route/pattern that matches the request
     * @uses PhpApi::getUniquePatterns
     * @uses PhpApi::getRoute
     * @uses PhpApi::searchRequestPattern
     * @uses PhpApi::processRequestPattern
     */
    private function evaluateURL()
    {
        if (empty($this->request)) { //index
            echo str_replace('\\', '', json_encode($this->getUniquePatterns()));
            exit;
        } elseif (in_array($this->request, $this->patterns)) {
            $this->getRoute($this->request)->execute();
        } else {
            $pattern = $this->searchRequestPattern();
            $this->processRequestPattern($pattern);
        }
    }

    /**
     * Returns all patterns used in @Api. This method returns just one ocurrence
     * of the pattern even if it is used in multiple locations (with distinctive
     * HTTP request methods)
     * @return array
     */
    private function getUniquePatterns(): array
    {
        $pM = array();
        foreach ($this->patterns as $pattern) {
            if (!in_array($pattern, $pM)) {
                array_push($pM, $pattern);
            }
        }
        return $pM;
    }

    /**
     * Executes the class method for the given pattern 
     * @param string $pattern
     * @uses PhpApi::explodePattern
     * @uses PhpApi::getRoute
     * @uses Route::getMethodParams
     * @uses Route::execute
     */
    private function processRequestPattern(string $pattern)
    {
        $arRequest = $this->explodePattern($this->request);
        $arPattern = $this->explodePattern($pattern);
        $route = $this->getRoute($pattern);
        $refParams = $route->getMethodParams();
        $params = array();
        for ($i = 0, $max = count($arPattern); $i < $max; $i++) {
            if (substr($arPattern[$i], 0, 1) === '$') {
                $index = array_search(substr($arPattern[$i], 1), $refParams);
                $params[$index] = $arRequest[$i];
            }
        }
        array_multisort($params);
        $route->execute($params);
    }

    /**
     * Search for patterns that looks like the original request
     * @return string The pattern
     * @uses PhpApi::explodePattern
     */
    private function searchRequestPattern(): string
    {
        $arRequest = $this->explodePattern($this->request);
        $countRequest = count($arRequest);
        $exPatterns = array_map('PhpApi\PhpApi::explodePattern', $this->patterns); // explode all patterns
        $coPatterns = array_filter($exPatterns, function ($elem) use ($countRequest) { // filter where count is equal to request
            return count($elem) === $countRequest;
        });
        $index = 0;
        do {
            $coPatterns = array_filter($coPatterns, function ($elem) use ($arRequest, $index) { // filter where all position are equal, ignores when expecting variable
                return (substr($elem[$index], 0, 1) === '$') ? true : $elem[$index] === $arRequest[$index];
            });
            $index++;
        } while (array_key_exists($index, $arRequest));
        if (count($coPatterns) === 0) { // found no patterns as requested
            http_response_code(404);
            echo json_encode(['message' => 'Resource not found.']);
            exit;
        } else {
            return $this->patterns[array_keys($coPatterns)[0]];
        }
    }

    /**
     * Explodes a pattern or route into a array using a slash as delimiter
     * @param string $pattern The pattern or route
     * @return array The exploded string
     */
    private static function explodePattern(string $pattern): array
    {
        return explode('/', $pattern);
    }

}

