-- ChatTranslateCore.lua
--
-- Übersetzt eingehenden Chat automatisch ins Deutsche (Standard-Zielsprache).
-- Mit /ct lang <code> auf eine andere Sprache umstellbar, falls mal nötig.
-- Drei Stufen, in dieser Reihenfolge:
--   1. Komplette Phrase im Wörterbuch (ChatTranslateDictionary.lua)        -> sofort, offline
--   2. Einzelne Wörter im Wörterbuch                                      -> sofort, offline, Teilübersetzung
--   3. Unbekannter Text wird in eine Warteschlange gelegt                  -> für das optionale
--      Begleitskript (companion/chattranslate_companion.py), das echte
--      maschinelle Übersetzung offline berechnet und das Ergebnis in
--      SavedVariables zurückschreibt. Sichtbar nach /reload.
--
-- Wichtig: WoW-Addons haben keinen Netzwerkzugriff (kein socket, kein http).
-- Stufe 3 ist deshalb keine "Live"-Übersetzung, sondern eine Brücke über die
-- SavedVariables-Datei, die nur beim Laden des Addons gelesen wird.

local ADDON_PREFIX = "|cff55ccffChatTranslate|r"

ChatTranslateDB = ChatTranslateDB or {
    enabled     = true,
    targetLang  = nil,   -- nil = automatisch Deutsch (siehe GetTargetLang)
    minStopwords = 2,    -- ab wie vielen Treffern eine Sprache als erkannt gilt
    dictHits    = 0,
    queue       = {},    -- [hash] = { text=, srcLang=, targetLang=, queuedAt= }
    translations = {},   -- [hash] = "übersetzter Text" (vom Begleitskript befüllt)
}

local function GetTargetLang()
    return ChatTranslateDB.targetLang or "de"
end

-- ============================================================
-- UTF-8 Dekodierung (nur für Skripterkennung, keine volle Validierung)
-- ============================================================
local function UTF8Codepoints(str)
    local codepoints = {}
    local i, len = 1, #str
    while i <= len do
        local b1 = string.byte(str, i)
        local cp, size
        if b1 < 0x80 then
            cp, size = b1, 1
        elseif b1 >= 0xF0 then
            cp, size = b1 - 0xF0, 4
        elseif b1 >= 0xE0 then
            cp, size = b1 - 0xE0, 3
        elseif b1 >= 0xC0 then
            cp, size = b1 - 0xC0, 2
        else
            cp, size = nil, 1 -- ungültiges Leadbyte, ein Byte überspringen
        end
        if cp then
            for j = 1, size - 1 do
                local nb = string.byte(str, i + j)
                if not nb or nb < 0x80 then break end
                cp = cp * 64 + (nb - 0x80)
            end
            codepoints[#codepoints + 1] = cp
        end
        i = i + size
    end
    return codepoints
end

-- Unicode-Bereiche, die eindeutig auf eine Sprache hindeuten (kein Latein).
local SCRIPT_RANGES = {
    { lang = "ru", min = 0x0400, max = 0x04FF }, -- Kyrillisch
    { lang = "ko", min = 0xAC00, max = 0xD7A3 }, -- Hangul-Silben
    { lang = "ja", min = 0x3040, max = 0x30FF }, -- Hiragana/Katakana
    { lang = "zh", min = 0x4E00, max = 0x9FFF }, -- CJK Unified Ideographs
}

local function DetectScript(str)
    local counts = {}
    for _, cp in ipairs(UTF8Codepoints(str)) do
        for _, range in ipairs(SCRIPT_RANGES) do
            if cp >= range.min and cp <= range.max then
                counts[range.lang] = (counts[range.lang] or 0) + 1
            end
        end
    end
    local best, bestCount = nil, 0
    for lang, count in pairs(counts) do
        if count > bestCount then
            best, bestCount = lang, count
        end
    end
    if bestCount >= 2 then
        return best
    end
    return nil
end

-- ============================================================
-- Tokenizer: behandelt UTF-8-Mehrbyte-Folgen als Wortzeichen, damit
-- Wörter wie "está" oder "für" nicht an den Akzenten zerbrechen.
-- ============================================================
local function Tokenize(str)
    local words, current = {}, {}
    for i = 1, #str do
        local byte = string.byte(str, i)
        local isWordByte = (byte >= 65 and byte <= 90) or (byte >= 97 and byte <= 122) or byte >= 0x80
        if isWordByte then
            current[#current + 1] = string.char(byte)
        elseif #current > 0 then
            words[#words + 1] = table.concat(current)
            current = {}
        end
    end
    if #current > 0 then
        words[#words + 1] = table.concat(current)
    end
    return words
end

local function DetectLatinLanguage(words)
    local scores = {}
    for _, word in ipairs(words) do
        local lw = word:lower()
        for lang, list in pairs(CT_Dictionary.stopwords) do
            for _, sw in ipairs(list) do
                if lw == sw then
                    scores[lang] = (scores[lang] or 0) + 1
                    break
                end
            end
        end
    end
    local best, bestScore = nil, 0
    for lang, score in pairs(scores) do
        if score > bestScore then
            best, bestScore = lang, score
        end
    end
    if bestScore >= ChatTranslateDB.minStopwords then
        return best
    end
    return nil
end

-- ============================================================
-- Übersetzungs-Warteschlange für das Begleitskript
-- ============================================================
local function HashMessage(msg)
    local hash = 5381
    for i = 1, #msg do
        hash = (hash * 33 + string.byte(msg, i)) % 4294967296
    end
    return tostring(hash)
end

local function QueueForExternalTranslation(msg, srcLang, targetLang)
    local key = HashMessage(msg)
    local cached = ChatTranslateDB.translations[key]
    if cached then
        return cached
    end
    if not ChatTranslateDB.queue[key] then
        ChatTranslateDB.queue[key] = {
            text = msg,
            srcLang = srcLang or "auto",
            targetLang = targetLang,
            queuedAt = time(),
        }
    end
    return nil
end

-- ============================================================
-- Kernlogik: liefert (übersetzterText, quelle) oder nil
-- quelle in {"dict", "partial", "external", "queued"}
-- ============================================================
local function TranslateMessage(msg)
    if not ChatTranslateDB.enabled or not msg or msg == "" then
        return nil
    end

    local targetLang = GetTargetLang()
    local lower = msg:lower()

    -- Stufe 1: komplette Phrase
    local phraseIdx = CT_Dictionary.phraseIndex[targetLang]
    if phraseIdx and phraseIdx[lower] then
        ChatTranslateDB.dictHits = ChatTranslateDB.dictHits + 1
        return phraseIdx[lower], "dict"
    end

    -- Stufe 1b: hat das Begleitskript für GENAU diese Nachricht schon eine
    -- fertige Übersetzung abgeliefert? Die ist immer besser als der
    -- Wort-für-Wort-Fallback aus Stufe 2, hat also Vorrang, sobald sie da ist.
    local hash = HashMessage(msg)
    local externalCached = ChatTranslateDB.translations[hash]
    if externalCached then
        ChatTranslateDB.dictHits = ChatTranslateDB.dictHits + 1
        return externalCached, "external"
    end

    -- Stufe 2: Wort-für-Wort-Fallback. Der Wort-Index ist quellsprachen-
    -- unabhängig aufgebaut (jedes Wort aus jeder Sprache zeigt direkt auf
    -- seine targetLang-Übersetzung), läuft deshalb VOR der Spracherkennung.
    -- Sonst killt eine zu schwache Stoppwort-Erkennung kurze Sätze, obwohl
    -- das Wörterbuch längst ein Wort darin kennt.
    local words = Tokenize(msg)
    local wordIdx = CT_Dictionary.wordIndex[targetLang]
    local partialResult, partialOk = nil, false
    if wordIdx then
        local outWords, hitCount = {}, 0
        for _, w in ipairs(words) do
            local translated = wordIdx[w:lower()]
            if translated then
                outWords[#outWords + 1] = translated
                hitCount = hitCount + 1
            else
                outWords[#outWords + 1] = w
            end
        end
        if hitCount >= 2 or (hitCount >= 1 and #words <= 2) then
            partialResult, partialOk = table.concat(outWords, " "), true
        end
    end

    -- Stufe 3: Quellsprache bestimmen (Skript zuerst, dann Stoppwort-Heuristik).
    local srcLang = DetectScript(msg) or DetectLatinLanguage(words)

    -- Stufe 4: ans Begleitskript vormerken - UNABHÄNGIG davon, ob Stufe 2
    -- schon etwas geliefert hat. Der Wort-Fallback ist nur eine Soforthilfe
    -- fürs aktuelle /reload; die echte Übersetzung soll trotzdem nachkommen
    -- und beim nächsten /reload über Stufe 1b automatisch die bessere
    -- Version liefern, statt für immer beim Wort-Salat zu bleiben.
    if srcLang and srcLang ~= targetLang then
        QueueForExternalTranslation(msg, srcLang, targetLang)
    end

    if partialOk then
        ChatTranslateDB.dictHits = ChatTranslateDB.dictHits + 1
        return partialResult, "partial"
    end

    return nil
end

-- ============================================================
-- Chat-Hook: hängt die Übersetzung an die Originalnachricht an,
-- statt sie zu ersetzen. "false" als erster Rückgabewert bedeutet
-- "nicht unterdrücken", die folgenden Werte ersetzen msg/author/...
-- ============================================================
local TAG_BY_SOURCE = {
    dict     = "Ü",
    partial  = "Ü~",
    external = "Ü",
}

local function OnChatMessage(self, event, msg, author, ...)
    local translated, source = TranslateMessage(msg)
    if translated and source ~= "queued" then
        local tag = TAG_BY_SOURCE[source] or "Ü"
        local newMsg = msg .. "  |cff55ccff[" .. tag .. ": " .. translated .. "]|r"
        return false, newMsg, author, ...
    end
    return false
end

local CHAT_EVENTS = {
    "CHAT_MSG_SAY", "CHAT_MSG_YELL",
    "CHAT_MSG_PARTY", "CHAT_MSG_PARTY_LEADER",
    "CHAT_MSG_RAID", "CHAT_MSG_RAID_LEADER", "CHAT_MSG_RAID_WARNING",
    "CHAT_MSG_GUILD", "CHAT_MSG_OFFICER",
    "CHAT_MSG_WHISPER",
    "CHAT_MSG_CHANNEL",
    "CHAT_MSG_INSTANCE_CHAT", "CHAT_MSG_INSTANCE_CHAT_LEADER",
}

for _, evt in ipairs(CHAT_EVENTS) do
    ChatFrame_AddMessageEventFilter(evt, OnChatMessage)
end

-- ============================================================
-- Slash-Befehle
-- ============================================================
local function PrintMsg(text)
    DEFAULT_CHAT_FRAME:AddMessage(ADDON_PREFIX .. ": " .. text)
end

local function CountTable(t)
    local n = 0
    for _ in pairs(t) do n = n + 1 end
    return n
end

SLASH_CHATTRANSLATE1 = "/ct"
SLASH_CHATTRANSLATE2 = "/chattranslate"
SlashCmdList["CHATTRANSLATE"] = function(msg)
    local cmd, rest = msg:match("^(%S*)%s*(.*)$")
    cmd = cmd:lower()

    if cmd == "on" then
        ChatTranslateDB.enabled = true
        PrintMsg("aktiviert.")
    elseif cmd == "off" then
        ChatTranslateDB.enabled = false
        PrintMsg("deaktiviert.")
    elseif cmd == "lang" then
        if rest ~= "" then
            ChatTranslateDB.targetLang = rest:lower()
            PrintMsg("Zielsprache fest auf '" .. rest:lower() .. "' gesetzt.")
        else
            PrintMsg("aktuelle Zielsprache: " .. GetTargetLang())
        end
    elseif cmd == "auto" then
        ChatTranslateDB.targetLang = nil
        PrintMsg("Zielsprache zurückgesetzt auf Standard (" .. GetTargetLang() .. ").")
    elseif cmd == "status" then
        PrintMsg(("%d Wörterbuch-Treffer | %d wartend auf das Begleitskript | %d bereits übersetzt"):format(
            ChatTranslateDB.dictHits, CountTable(ChatTranslateDB.queue), CountTable(ChatTranslateDB.translations)))
    elseif cmd == "clearqueue" then
        ChatTranslateDB.queue = {}
        PrintMsg("Warteschlange geleert.")
    else
        PrintMsg("Befehle: /ct on, /ct off, /ct lang <code>, /ct auto, /ct status, /ct clearqueue")
    end
end
