<?php

declare(strict_types=1);

class Autoloader
{
    private const PATH_CLASSES = 'backend/classes';
    private const PATH_CONTROLLERS = self::PATH_CLASSES . '/controller';
    private const PATH_CACHE = 'backend/cache/';

    public function __construct(string $cachePath = self::PATH_CACHE)
    {
    	if ($cachePath !== self::PATH_CACHE) {
    		$cachePath = substr($cachePath, -1) === '/' ? $cachePath : $cachePath . '/';
		}

        $routesFound = @include($cachePath . 'routes.php');
        $classesFound = @include($cachePath . 'classes.php');

        if (!$routesFound || !$classesFound) {
            throw new Exception(
                sprintf(
                    'Autoloader cache not found! Please generate it with %s::BuildCache() at first!',
                    self::class
                )
            );
        }

        spl_autoload_register(
            function (string $className) {
                if (!$this->loadClass($className)) {
                    throw new Exception(sprintf('Class %s couldn\'t be loaded!', $className));
                }
            }
        );
    }

    public static function BuildCache(): void
    {
        self::BuildClassCache();
        self::BuildRouteCache();
    }

    public static function BuildClassCache(): void
    {
        $classesResult = self::scanForClasses();

        $cacheContent = '';

        foreach ($classesResult as $className => $path) {
            $cacheContent .= sprintf("\t\t'%s' => '%s',\n", $className, $path);
        }

        $cacheContent .= "\t]\n);";

        self::buildCacheFile($cacheContent, 'classes');
    }

    private function loadClass(string $className): bool
    {
        if (!isset(CLASSES[$className]) || !@include(CLASSES[$className])) {
            return false;
        }

        return true;
    }

    public static function BuildRouteCache(): void
    {
        $controllersResult = self::scanForControllers();
        $controllerMethods = [
            'GET' => [],
            'POST' => [],
            'PUT' => [],
			'DELETE' => [],
        ];

        foreach ($controllersResult as $className => $path) {
            $file = fopen($path, 'r');
            $content = fread($file, filesize($path));
            fclose($file);

            preg_match_all('/(?<=private )\w+ \$\w+(?=;)/', $content, $matches);

            $params = [];

            foreach ($matches[0] as $match) {
                $parts = explode(' ', $match);
                $params[] = [
                    'type' => $parts[0],
                    'name' => $parts[1],
                ];
            }

            preg_match('/(?<=protected string \$route = \').*(?=\';)/', $content, $matches);
            $route = $matches[0];

            preg_match('/[A-Z][a-z]+(?=Controller)/', $className, $matches);
            $method = strtoupper($matches[0]);

            $controllerMethods[$method][$route] = [
                'name' => $className,
                'params' => $params,
            ];
        }

        $cacheContent = '';

        foreach ($controllerMethods as $method => $controllers) {
            $cacheContent .= self::createRoutesForMethod($method, $controllers);
        }

        $cacheContent .= "\t]\n);";

        self::buildCacheFile($cacheContent, 'routes');
    }

    private static function createRoutesForMethod(string $method, array $routes): string
    {
        krsort($routes);
        $stringRoutes = '';

        foreach ($routes as $route => $params) {
            $stringRoutes .= sprintf(
                "'%s' => [
                    'controller' => %s::class,
                    'params' => [
                        %s
                    ],
                ],
                ",
                $route,
                $params['name'],
                self::createRouteParams($params['params'])
            );
        }

        return sprintf(
            "
                '%s' => [
                    %s
                ],
            ",
            $method,
            $stringRoutes,
        );
    }

    private static function createRouteParams(array $params): string
    {
        $string = '';

        foreach ($params as $param) {
            $string .= sprintf(
                "
                    '%s' => [
                        'type' => '%s',
                    ],
                ",
                str_replace('$', '', $param['name']),
                $param['type']
            );
        }

        return $string;
    }

    private static function reformatCacheFileContent(string $content): string
    {
        $depth = 0;
        $reformatted = '';
        $replace = '';

        // Removing indents
        foreach (explode("\n", $content) as $line) {
            $trim = trim($line);

            if ($trim !== '') {
                $replace .= $trim . "\n";
            }
        }

        for ($i = 0; $i < strlen($replace); $i++) {
            if (in_array($replace[$i], [')', ']'])) {
                $depth--;
            }

            if ($replace[$i - 1] === "\n") {
                $reformatted .= str_repeat("\t", $depth);
            }

            $reformatted .= $replace[$i];

            if (in_array($replace[$i], ['(', '['])) {
                $depth++;
            }
        }

        return $reformatted;
    }

    private static function buildCacheFile(string $content, string $cacheName): void
    {
        $cacheContent = sprintf(
            "<?php\n\n/*\n * This file was auto generated on %s\n */\n\ndefine(\n\t'%s',\n\t[\n",
            (new DateTime())->format('Y-m-d H:i:s'),
            strtoupper($cacheName)
        );

        $cacheContent .= $content;

        $file = fopen(getcwd() . '/' . self::PATH_CACHE . $cacheName . '.php', 'w');
        fwrite($file, self::reformatCacheFileContent($cacheContent));
        fclose($file);
    }

    private static function scanForFiles(string $folder): array
    {
        $folder = substr($folder, -1) === '/' ? substr($folder, 0, -1) : $folder;
        $files = [];
        $handler = opendir($folder);

        while ($file = readdir($handler)) {
            $path = $folder . '/' . $file;

            if (is_dir($path) && $file !== '.' && $file !== '..') {
                $files = array_merge($files, self::scanForFiles($path));
            } elseif (is_file($path) && substr($path, -4) === '.php') {
                $className = substr($file, 0, -4);
                $files[$className] = $path;
            }
        }

        return $files;
    }

    private static function scanForClasses(): array
    {
        return self::scanForFiles(getcwd() . '/' . self::PATH_CLASSES);
    }

    private static function scanForControllers(): array
    {
        return self::scanForFiles(getcwd() . '/' . self::PATH_CONTROLLERS);
    }
}