<?php

namespace OCA\Talk\Vendor\CuyZ\Valinor\Utility\Reflection;

use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Exception\Template\DuplicatedTemplateName;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Lexer\TokensExtractor;
use ReflectionClass;
use ReflectionFunctionAbstract;
use ReflectionParameter;
use ReflectionProperty;

use function array_key_exists;
use function array_merge;
use function array_search;
use function array_shift;
use function array_splice;
use function assert;
use function end;
use function explode;
use function in_array;
use function preg_match;
use function preg_match_all;
use function str_replace;
use function str_split;
use function strrpos;
use function substr;
use function trim;

/** @internal */
final class DocParser
{
    public static function propertyType(ReflectionProperty $reflection): ?string
    {
        $doc = self::sanitizeDocComment($reflection->getDocComment());

        if ($doc === null) {
            return null;
        }

        return self::annotationType($doc, 'var');
    }

    public static function parameterType(ReflectionParameter $reflection): ?string
    {
        $doc = self::sanitizeDocComment($reflection->getDeclaringFunction()->getDocComment());

        if ($doc === null) {
            return null;
        }

        $parameters = [];

        $tokens = (new TokensExtractor($doc))->all();

        while (($token = array_shift($tokens)) !== null) {
            if (! in_array($token, ['@param', '@phpstan-param', '@psalm-param'], true)) {
                continue;
            }

            $dollarSignKey = (int)array_search('$', $tokens, true);
            $name = $tokens[$dollarSignKey + 1] ?? null;

            $parameters[$name][$token] = implode('', array_splice($tokens, 0, $dollarSignKey));
        }

        return $parameters[$reflection->name]['@phpstan-param']
            ?? $parameters[$reflection->name]['@psalm-param']
            ?? $parameters[$reflection->name]['@param']
            ?? null;
    }

    public static function functionReturnType(ReflectionFunctionAbstract $reflection): ?string
    {
        $doc = self::sanitizeDocComment($reflection->getDocComment());

        if ($doc === null) {
            return null;
        }

        return self::annotationType($doc, 'return');
    }

    /**
     * @param ReflectionClass<object> $reflection
     * @return array<string, string>
     */
    public static function localTypeAliases(ReflectionClass $reflection): array
    {
        $doc = self::sanitizeDocComment($reflection->getDocComment());

        if ($doc === null) {
            return [];
        }

        $cases = self::splitStringBy($doc, '@phpstan-type', '@psalm-type');

        $types = [];

        foreach ($cases as $case) {
            if (! preg_match('/\s*(?<name>[a-zA-Z]\w*)\s*=?\s*(?<type>.*)/s', $case, $matches)) {
                continue;
            }

            $types[$matches['name']] = self::findType($matches['type']);
        }

        return $types;
    }

    /**
     * @param ReflectionClass<object> $reflection
     * @return array<string, string[]>
     */
    public static function importedTypeAliases(ReflectionClass $reflection): array
    {
        $doc = self::sanitizeDocComment($reflection->getDocComment());

        if ($doc === null) {
            return [];
        }

        $cases = self::splitStringBy($doc, '@phpstan-import-type', '@psalm-import-type');

        $types = [];

        foreach ($cases as $case) {
            if (! preg_match('/\s*(?<name>[a-zA-Z]\w*)\s*from\s*(?<class>\w+)/', $case, $matches)) {
                continue;
            }

            $types[$matches['class']][] = $matches['name'];
        }

        return $types;
    }

    /**
     * @param ReflectionClass<object> $reflection
     * @return array<string>
     */
    public static function classExtendsTypes(ReflectionClass $reflection): array
    {
        $doc = self::sanitizeDocComment($reflection->getDocComment());

        if ($doc === null) {
            return [];
        }

        preg_match_all('/@(phpstan-|psalm-)?extends\s+(?<type>.+)/', $doc, $matches);

        return $matches['type'];
    }

    /**
     * @param ReflectionClass<object> $reflection
     * @return array<non-empty-string, non-empty-string|null>
     */
    public static function classTemplates(ReflectionClass $reflection): array
    {
        $doc = self::sanitizeDocComment($reflection->getDocComment());

        if ($doc === null) {
            return [];
        }

        $templates = [];

        preg_match_all("/@(phpstan-|psalm-)?template\s+(?<name>\w+)(\s+of\s+(?<type>.+))?/", $doc, $matches);

        foreach ($matches['name'] as $key => $name) {
            /** @var non-empty-string $name */
            if (array_key_exists($name, $templates)) {
                throw new DuplicatedTemplateName($reflection->name, $name);
            }

            $template = $matches['type'][$key];

            if ($template === '') {
                $templates[$name] = null;
            } else {
                $templates[$name] = self::findType($template);
            }
        }

        return $templates;
    }

    private static function annotationType(string $string, string $annotation): ?string
    {
        foreach (["@phpstan-$annotation", "@psalm-$annotation", "@$annotation"] as $case) {
            $pos = strrpos($string, $case);

            if ($pos !== false) {
                return self::findType(substr($string, $pos + strlen($case)));
            }
        }

        return null;
    }

    /**
     * @return non-empty-string
     */
    private static function findType(string $string): string
    {
        $operatorsMatrix = [
            '{' => '}',
            '<' => '>',
            '"' => '"',
            "'" => "'",
        ];

        $type = '';
        $operators = [];
        $expectExpression = true;

        $string = str_replace("\n", ' ', $string);
        $chars = str_split($string);

        foreach ($chars as $key => $char) {
            if ($operators === []) {
                if ($char === '|' || $char === '&') {
                    $expectExpression = true;
                } elseif (! $expectExpression && $chars[$key - 1] === ' ') {
                    break;
                } elseif ($char !== ' ') {
                    $expectExpression = false;
                }
            }

            if (isset($operatorsMatrix[$char])) {
                $operators[] = $operatorsMatrix[$char];
            } elseif ($operators !== [] && $char === end($operators)) {
                array_pop($operators);
            }

            $type .= $char;
        }

        $type = trim($type);

        assert($type !== '');

        return $type;
    }

    private static function sanitizeDocComment(string|false $doc): ?string
    {
        /** @infection-ignore-all mutating `$doc` to `true` makes no sense */
        if ($doc === false) {
            return null;
        }

        $doc = preg_replace('#^\s*/\*\*([^/]+)\*/\s*$#', '$1', $doc);

        return preg_replace('/^\s*\*\s*(\S*)/m', '$1', (string)$doc);
    }

    /**
     * @param non-empty-string ...$cases
     * @return list<string>
     */
    private static function splitStringBy(string $string, string ...$cases): array
    {
        $result = [$string];

        foreach ($cases as $case) {
            $previousResult = $result;
            $result = [];
            foreach ($previousResult as $value) {
                $result = array_merge($result, explode($case, $value));
            }
        }

        // Remove the first segment of the docs before the first `$cases` string
        array_shift($result);

        return $result;
    }
}
