-- TOG Tools — Name Prefix module (engine)
-- Prepends a configurable nickname prefix to outgoing chat messages.
--
-- Hook strategy:
--   Retail 12.0+  : EventRegistry "ChatFrame.OnEditBoxPreSendText".
--   Classic/older : per-editbox HookScript("OnKeyDown", ...) that fires
--                   on Enter / NumpadEnter and applies the prefix BEFORE
--                   the editbox's untouched OnEnterPressed handler runs.
--
-- Why every other entry point was rejected:
--   - `SendChatMessage` (global wrap)              : on BCC/Anniversary
--     1.15.x the chat editbox mixin captures it as a localized upvalue;
--     reassigning the global never fires for user-typed chat. Verified
--     empirically — the wrap installed but ModifyMessage never entered.
--   - `ChatEdit_SendText` (global wrap)            : same problem on
--     Anniversary. ChatFrameEditBoxMixin:OnEnterPressed dispatches via
--     `self:SendText()` (mixin method), not the global function.
--     Verified empirically too — install printed but no fire on /say.
--   - `ChatFrameEditBoxMixin.SendText` (method replace) : breaks OPie.
--     OPie's Rewire dispatches macrotext ring slots via
--     `securecall(box.SendText, box, false)` using synthetic editboxes
--     that share the mixin. Patching the mixin method breaks OPie macros.
--   - SetScript on OnEnterPressed WITHOUT securecall around origScript :
--     the editbox handler runs as addon Lua; calling origScript directly
--     keeps the taint flag set for the entire downstream chain. ParseText
--     dispatches /logout from inside the tainted chain and the protected
--     Logout() trips ADDON_ACTION_FORBIDDEN.
--   - SetScript on OnEnterPressed WITH securecall around origScript     :
--     v0.3.3 shipped this. securecall clears taint at the call boundary,
--     but on TBC / Anniversary 1.15.x the slash-dispatch chain
--     (ParseText -> slash-handler -> Logout()) still trips
--     ADDON_ACTION_FORBIDDEN for /camp /exit /logout — confirmed in the
--     field after v0.3.3 shipped. The replaced OnEnterPressed script slot
--     itself becomes addon-owned, and the taint check at the C level
--     blocks Logout() despite the securecall boundary.
--
-- HookScript("OnKeyDown", ...) avoids the problem entirely:
--   * OnKeyDown fires in its own C dispatch frame, BEFORE OnEnterPressed
--     is dispatched as a separate C-level call. Our hook returns to C
--     before OnEnterPressed begins, so addon Lua is never on the call
--     stack when SendText / ParseText / Logout() run.
--   * HookScript chains alongside any existing OnKeyDown handler instead
--     of claiming the script slot. Chat editboxes have no default
--     OnKeyDown binding, so our hook is the only one — and the
--     OnEnterPressed slot stays as the original Blizzard handler, so the
--     entire slash-dispatch chain executes in a fully secure context.
--   * For slash commands ApplyPrefix early-exits on the leading `/`, so
--     the editbox text is never touched anyway.
--
-- Per-editbox HookScript only touches user-visible ChatFrameN editboxes;
-- OPie's synthetic Rewire editboxes share the mixin via metatable but
-- have no instance-level OnKeyDown handler, so they are unaffected.

local _, addon = ...
local gv = addon.gameVersion

local NamePrefix = {}
addon.NamePrefix = NamePrefix

-- ---------------------------------------------------------------------------
-- Internal state
-- ---------------------------------------------------------------------------
local _hooked = false   -- set true once the hook is installed

-- ---------------------------------------------------------------------------
-- Core: apply the configured prefix to msg for chatType / channelArg.
-- Returns the (possibly prefixed) message, or msg unchanged if no prefix
-- should be applied.  Pure — no side effects.
-- ---------------------------------------------------------------------------
local function ApplyPrefix(msg, chatType, channelArg)
    local cfg = addon.DB and addon.DB.global.namePrefix
    if not (cfg and cfg.enabled) then return msg end

    local nick = cfg.nickname
    if not nick or nick == "" then return msg end

    -- Optionally suppress when nickname matches current character name
    if cfg.hideIfCharName then
        local charName = UnitName("player")
        if charName and nick == charName then return msg end
    end

    if not msg or msg == "" then return msg end

    -- Slash commands and macros are never prefixed
    if string.sub(msg, 1, 1) == "/" then return msg end

    -- Skip messages starting with "!" (addon commands like !keys)
    if cfg.skipExclamation and string.sub(msg, 1, 1) == "!" then return msg end

    local shouldPrefix = false
    if     chatType == "SAY"           and cfg.say      then shouldPrefix = true
    elseif chatType == "GUILD"         and cfg.guild    then shouldPrefix = true
    elseif chatType == "OFFICER"       and cfg.officer  then shouldPrefix = true
    elseif chatType == "PARTY"         and cfg.party    then shouldPrefix = true
    elseif chatType == "PARTY_LEADER"  and cfg.party    then shouldPrefix = true
    elseif chatType == "RAID"          and cfg.raid     then shouldPrefix = true
    elseif chatType == "RAID_WARNING"  and cfg.raid     then shouldPrefix = true
    elseif chatType == "INSTANCE_CHAT" and cfg.instance then shouldPrefix = true
    elseif chatType == "CHANNEL"       and cfg.channel and cfg.channel ~= "" then
        if channelArg then
            local _, chname = GetChannelName(channelArg)
            if chname and strupper(cfg.channel) == strupper(chname) then
                shouldPrefix = true
            end
        end
    end

    if not shouldPrefix then return msg end

    -- Build prefix from format string (safe substitution)
    local fmt = (cfg.format and cfg.format ~= "") and cfg.format or "(%s): "
    return string.format(fmt, nick) .. msg
end

-- Read editBox text, apply prefix in-place via SetText. Used by both the
-- Retail EventRegistry path and the Classic OnKeyDown hook.
-- The InCombatLockdown guard is kept because SetText on the editbox can
-- taint the SendChatMessage call that follows on some Retail builds.
local function ModifyMessage(editBox)
    if InCombatLockdown() then return end
    local msg = editBox:GetText()
    if not msg or msg == "" then return end
    local chatType = editBox:GetAttribute("chatType") or editBox.chatType or "SAY"
    local channelTarget = editBox:GetAttribute("channelTarget") or editBox.channelTarget
    local result = ApplyPrefix(msg, chatType, channelTarget)
    if result ~= msg then
        editBox:SetText(result)
    end
    addon:Debug(string.format("NamePrefix fire: chatType=%s prefixed=%s",
        tostring(chatType), tostring(result ~= msg)))
end

-- ---------------------------------------------------------------------------
-- Hook installation
-- ---------------------------------------------------------------------------
function NamePrefix:Install()
    if _hooked then return end

    if gv.isRetail120Plus and EventRegistry then
        -- Retail 12.0+: this event fires BEFORE SendText reads the editbox,
        -- so our SetText reaches the message that gets sent.
        EventRegistry:RegisterCallback(
            "ChatFrame.OnEditBoxPreSendText",
            function(_, editBox)
                local ok, err = pcall(ModifyMessage, editBox)
                if not ok and addon.DB and addon.DB.global.namePrefix.enabled then
                    print("|cffFF8000TOGTools|r NamePrefix error: " .. tostring(err))
                end
            end,
            NamePrefix
        )
        _hooked = true
        addon:Debug("NamePrefix: hook installed (EventRegistry)")
    else
        -- Classic / older Retail: per-editbox HookScript("OnKeyDown", ...)
        -- on each ChatFrameN.EditBox. On Enter / NumpadEnter we apply the
        -- prefix to the editbox text; the editbox's untouched OnEnterPressed
        -- handler then fires as a separate C-level dispatch and reads the
        -- modified text. Because OnEnterPressed runs in its own C frame
        -- with no addon Lua on the stack, the slash-dispatch chain
        -- (ParseText -> slash-handler -> Logout()) is fully secure, so
        -- /camp / /exit / /logout no longer trip ADDON_ACTION_FORBIDDEN.
        local wrapped = 0
        for i = 1, NUM_CHAT_WINDOWS or 10 do
            local eb = _G["ChatFrame" .. i .. "EditBox"]
            if eb and eb.HookScript then
                eb:HookScript("OnKeyDown", function(editBox, key)
                    if key == "ENTER" or key == "NUMPADENTER" then
                        pcall(ModifyMessage, editBox)
                    end
                end)
                wrapped = wrapped + 1
            end
        end
        _hooked = true
        addon:Debug(string.format(
            "NamePrefix: hook installed (OnKeyDown x%d)", wrapped))
    end
end

function NamePrefix:OnEnable()
    self:Install()
end

-- ---------------------------------------------------------------------------
-- Register as a module (engine only — no tab key; tab is in NamePrefixTab.lua)
-- ---------------------------------------------------------------------------
-- The tab registration lives in GUI/NamePrefixTab.lua which loads after this
-- file and merges its Draw/WINDOW_SIZE into addon.modules["nameprefix"].
addon.modules["nameprefix"] = NamePrefix
