<?php

declare(strict_types=1);

namespace Loupe\Loupe\Internal\LanguageDetection;

use Nitotm\Eld\EldDataFile;
use Nitotm\Eld\EldMode;
use Nitotm\Eld\EldScheme;
use Nitotm\Eld\LanguageDetector;

class NitotmLanguageDetector implements LanguageDetectorInterface
{
    private LanguageDetector|null $languageDetector = null;

    /**
     * @param array<string> $languages
     */
    public function __construct(private readonly array $languages)
    {
    }

    /**
     * @param array<string, string> $document
     */
    public function detectForDocument(array $document): DocumentResult
    {
        $bestScoresPerLanguage = [];
        $languagePerAttribute = [];

        foreach ($document as $attribute => $value) {
            if ('' === $value) {
                continue;
            }

            $languageResult = $this->getLanguageDetector()->detect($value);
            $weight = log(mb_strlen($value) + 1);

            // Store the best score per language
            foreach ($languageResult->scores() as $lang => $score) {
                // Weigh longer texts higher because scores will be more precise
                $score *= $weight;

                if (isset($bestScoresPerLanguage[$lang])) {
                    $bestScoresPerLanguage[$lang] = max($bestScoresPerLanguage[$lang], $score);
                } else {
                    $bestScoresPerLanguage[$lang] = $score;
                }
            }

            // If the language detection was reliable, we use this language for that attribute
            if ($languageResult->isReliable()) {
                $languagePerAttribute[$attribute] = $languageResult->language;
            }
        }

        // The overall highest score is the best language for the entire document (if any)
        $bestLanguage = null;
        if ([] !== $bestScoresPerLanguage) {
            /** @var string $bestLanguage */
            $bestLanguage = array_keys($bestScoresPerLanguage, max($bestScoresPerLanguage), true)[0];
        }

        return new DocumentResult($languagePerAttribute, $bestLanguage);
    }

    public function detectForQuery(string $query): string|null
    {
        $languageResult = $this->getLanguageDetector()->detect($query);

        if ('und' !== $languageResult->language) {
            return $languageResult->language;
        }

        return null;
    }

    public function detectForString(string $string): string|null
    {
        $language = null;
        $languageResult = $this->getLanguageDetector()->detect($string);

        // For one simple string we have to check if the language result is reliable. There's not enough data for
        // something like "Star Wars". It might be detected as nonsense, and we get weird stemming results.
        if ($languageResult->isReliable()) {
            $language = $languageResult->language;
        }

        return $language;
    }

    /**
     * Lazy initialize the LanguageDetector because it does all sorts of stuff in the constructor.
     * That way we can prevent loading useless stuff into memory if e.g. you only call $engine->countDocuments()
     * or anything else that does not need language detection.
     */
    private function getLanguageDetector(): LanguageDetector
    {
        if (null === $this->languageDetector) {
            $this->languageDetector = new LanguageDetector(
                EldDataFile::LARGE, // Large is actually faster than small, see https://github.com/nitotm/efficient-language-detector/issues/15
                EldScheme::ISO639_1,
                EldMode::MODE_BYTES, // Use bytes mode which requires less memory but is still fast enough for our use case
            );
            $this->languageDetector->enableTextCleanup(true); // Clean stuff like URLs, domains etc. to improve language detection
            $this->languageDetector->langSubset($this->languages);
        }

        return $this->languageDetector;
    }
}
