<?php

declare(strict_types=1);

final class Autoloader
{
	public const PATH_CLASSES = 'backend/classes';
	public const PATH_CONTROLLERS = self::PATH_CLASSES . '/controller';
	public 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);
	}
}