#!/usr/bin/env php
<?php

declare(strict_types=1);

if (
	!(is_file($file = __DIR__ . '/../vendor/autoload.php') && include $file) &&
	!(is_file($file = __DIR__ . '/../../../autoload.php') && include $file)
) {
	fwrite(STDERR, "Install packages using Composer.\n");
	exit(1);
}


echo '
NEON linter
-----------
';

if ($argc < 2) {
	echo "Usage: neon-lint [--debug] <path>\n";
	exit(1);
}

$debug = in_array('--debug', $argv, true);
if ($debug) {
	echo "Debug mode\n";
}

$path = end($argv);

try {
	$linter = new NeonLinter(debug: $debug);
	$ok = $linter->scanDirectory($path);
	exit($ok ? 0 : 1);

} catch (Throwable $e) {
	fwrite(STDERR, $debug ? "\n$e\n" : "\nError: {$e->getMessage()}\n");
	exit(2);
}


class NeonLinter
{
	/** @var string[] */
	public array $excludedDirs = ['.*', '*.tmp', 'temp', 'vendor', 'node_modules'];


	public function __construct(
		private readonly bool $debug = false,
	) {
	}


	public function scanDirectory(string $path): bool
	{
		$this->initialize();
		echo "Scanning $path\n";
		$counter = 0;
		$errors = 0;
		foreach ($this->getFiles($path) as $file) {
			$file = (string) $file;
			echo preg_replace('~\.?[/\\\]~A', '', $file), "\x0D";
			$errors += $this->lintFile($file) ? 0 : 1;
			echo str_pad('...', strlen($file)), "\x0D";
			$counter++;
		}

		echo "Done (checked $counter files, found errors in $errors)\n";
		return !$errors;
	}


	public function lintFile(string $file): bool
	{
		if ($this->debug) {
			echo $file, "\n";
		}

		$s = file_get_contents($file);
		if (str_starts_with($s, "\xEF\xBB\xBF")) {
			$this->writeError('WARNING', $file, 'contains BOM');
			$s = substr($s, 3);
		}

		try {
			Nette\Neon\Neon::decode($s);
			return true;

		} catch (Nette\Neon\Exception $e) {
			if ($this->debug) {
				echo $e;
			}
			$this->writeError('ERROR', $file, $e->getMessage());
			return false;
		}
	}


	private function initialize(): void
	{
		if (function_exists('pcntl_signal')) {
			pcntl_signal(SIGINT, function (): never {
				pcntl_signal(SIGINT, SIG_DFL);
				echo "Terminated\n";
				exit(1);
			});
		} elseif (function_exists('sapi_windows_set_ctrl_handler')) {
			sapi_windows_set_ctrl_handler(function (): never {
				echo "Terminated\n";
				exit(1);
			});
		}

		set_time_limit(0);
	}


	private function getFiles(string $path): Iterator
	{
		$it = match (true) {
			is_file($path) => new ArrayIterator([$path]),
			is_dir($path) => $this->findNeonFiles($path),
			(bool) preg_match('~[*?]~', $path) => new GlobIterator($path),
			default => throw new InvalidArgumentException("File or directory '$path' not found."),
		};
		return new CallbackFilterIterator($it, fn($file) => is_file((string) $file));
	}


	private function findNeonFiles(string $dir): Generator
	{
		foreach (scandir($dir) as $name) {
			$path = ($dir === '.' ? '' : $dir . DIRECTORY_SEPARATOR) . $name;
			if ($name !== '.' && $name !== '..' && is_dir($path)) {
				foreach ($this->excludedDirs as $pattern) {
					if (fnmatch($pattern, $name)) {
						continue 2;
					}
				}
				yield from $this->findNeonFiles($path);

			} elseif (str_ends_with($name, '.neon')) {
				yield $path;
			}
		}
	}


	private function writeError(string $label, string $file, string $message): void
	{
		fwrite(STDERR, str_pad("[$label]", 13) . ' ' . $file . '    ' . $message . "\n");
	}
}
