<?php

namespace App\Services;

class BladeUiTranslationService
{
    private const ATTRIBUTE_NAMES = 'placeholder|title|aria-label|aria-description|alt|label|description|heading|empty-message|confirm-message|data-confirm|data-message';

    public function key(string $text): string
    {
        return 'ui.auto.'.substr(hash('sha256', $this->normalize($text)), 0, 24);
    }

    public function normalize(string $text): string
    {
        $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');

        return trim((string) preg_replace('/\s+/u', ' ', $text));
    }

    /** @return array<string,string> */
    public function extract(string $blade): array
    {
        $catalog = [];
        $masked = $this->maskIgnoredBlocks($blade);

        preg_match_all('/@section\(\s*[\'\"]title[\'\"]\s*,\s*([\'\"])(.*?)\1\s*\)/su', $masked, $sections, PREG_SET_ORDER);
        foreach ($sections as $match) {
            $this->add($catalog, $match[2]);
        }

        preg_match_all('/\b(?:'.self::ATTRIBUTE_NAMES.')\s*=\s*([\'\"])(.*?)\1/su', $masked, $attributes, PREG_SET_ORDER);
        foreach ($attributes as $match) {
            $this->add($catalog, $match[2]);
        }

        preg_match_all('/([\'\"](?:label|description|title|placeholder|heading|message)[\'\"]\s*=>\s*)([\'\"])(.*?)\2/su', $masked, $arrayValues, PREG_SET_ORDER);
        foreach ($arrayValues as $match) {
            $this->add($catalog, $match[3]);
        }

        preg_match_all('/>([^<>]+)</su', $masked, $textNodes, PREG_SET_ORDER);
        foreach ($textNodes as $match) {
            $this->add($catalog, $match[1]);
        }

        return $catalog;
    }

    public function compile(string $blade): string
    {
        [$masked, $blocks] = $this->maskBlocks($blade);

        $masked = preg_replace_callback(
            '/@section\(\s*([\'\"]title[\'\"])\s*,\s*([\'\"])(.*?)\2\s*\)/su',
            function (array $match): string {
                if (! $this->isTranslatable($match[3])) {
                    return $match[0];
                }

                return '@section('.$match[1].', '.$this->expression($match[3]).')';
            },
            $masked,
        ) ?? $masked;

        $masked = preg_replace_callback(
            '/(([\'\"])(?:label|description|title|placeholder|heading|message)\2\s*=>\s*)([\'\"])(.*?)\3/su',
            function (array $match): string {
                if (! $this->isTranslatable($match[4])) {
                    return $match[0];
                }

                return $match[1].$this->expression($match[4]);
            },
            $masked,
        ) ?? $masked;

        $masked = preg_replace_callback(
            '/\b('.self::ATTRIBUTE_NAMES.')\s*=\s*([\'\"])(.*?)\2/su',
            function (array $match): string {
                if (! $this->isTranslatable($match[3])) {
                    return $match[0];
                }

                return $match[1].'="{{ '.$this->expression($match[3]).' }}"';
            },
            $masked,
        ) ?? $masked;

        $masked = preg_replace_callback('/>([^<>]+)</su', function (array $match): string {
            if (! $this->isTranslatable($match[1])) {
                return $match[0];
            }

            preg_match('/^\s*/u', $match[1], $leading);
            preg_match('/\s*$/u', $match[1], $trailing);
            $text = $this->normalize($match[1]);

            return '>'.($leading[0] ?? '').'{{ '.$this->expression($text).' }}'.($trailing[0] ?? '').'<';
        }, $masked) ?? $masked;

        return strtr($masked, $blocks);
    }

    private function expression(string $text): string
    {
        $normalized = $this->normalize($text);

        return 'app(\\App\\Services\\LocalizationService::class)->ui('.var_export($this->key($normalized), true).', '.var_export($normalized, true).')';
    }

    /** @param array<string,string> $catalog */
    private function add(array &$catalog, string $text): void
    {
        if (! $this->isTranslatable($text)) {
            return;
        }

        $normalized = $this->normalize($text);
        $catalog[$this->key($normalized)] = $normalized;
    }

    private function isTranslatable(string $text): bool
    {
        $text = $this->normalize($text);

        if ($text === '' || mb_strlen($text) < 2 || ! preg_match('/\p{L}/u', $text)) {
            return false;
        }

        foreach (['{{', '}}', '{!!', '!!}', '<?', '@', '$', '->', '::', '\\App\\', '__(', '@lang', '___MF_UI_BLOCK_'] as $token) {
            if (str_contains($text, $token)) {
                return false;
            }
        }

        return true;
    }

    private function maskIgnoredBlocks(string $blade): string
    {
        return $this->maskBlocks($blade)[0];
    }

    /** @return array{0:string,1:array<string,string>} */
    private function maskBlocks(string $blade): array
    {
        $blocks = [];
        $patterns = [
            '/<script\b[^>]*>.*?<\/script>/isu',
            '/<style\b[^>]*>.*?<\/style>/isu',
            '/<pre\b[^>]*>.*?<\/pre>/isu',
            '/<code\b[^>]*>.*?<\/code>/isu',
            '/@verbatim.*?@endverbatim/su',
        ];

        foreach ($patterns as $pattern) {
            $blade = preg_replace_callback($pattern, function (array $match) use (&$blocks): string {
                $token = '___MF_UI_BLOCK_'.count($blocks).'___';
                $blocks[$token] = $match[0];

                return $token;
            }, $blade) ?? $blade;
        }

        return [$blade, $blocks];
    }
}
