local E, C, L = select(2, ...):unpack()

------------------------------------------------------------------------
-- Chat Frame
------------------------------------------------------------------------

local module = E:Module("Chat")
module:SetConfigKey("chat")

local gsub, strfind, format = gsub, strfind, string.format
local strlower, BetterDate, time = strlower, BetterDate, time

local cfg = C.chat
local isScaling = false

------------------------------------------------------------------------
-- Global Chat Strings
------------------------------------------------------------------------

_G.CHAT_INSTANCE_CHAT_GET = "|Hchannel:INSTANCE_CHAT|h[" .. L.CHAT_INSTANCE_CHAT .. "]|h %s:\32"
_G.CHAT_INSTANCE_CHAT_LEADER_GET = "|Hchannel:INSTANCE_CHAT|h[" .. L.CHAT_INSTANCE_CHAT_LEADER .. "]|h %s:\32"
_G.CHAT_BN_WHISPER_GET = L.CHAT_BN_WHISPER .. " %s:\32"
_G.CHAT_GUILD_GET = "|Hchannel:GUILD|h[" .. L.CHAT_GUILD .. "]|h %s:\32"
_G.CHAT_OFFICER_GET = "|Hchannel:OFFICER|h[" .. L.CHAT_OFFICER .. "]|h %s:\32"
_G.CHAT_PARTY_GET = "|Hchannel:PARTY|h[" .. L.CHAT_PARTY .. "]|h %s:\32"
_G.CHAT_PARTY_LEADER_GET = "|Hchannel:PARTY|h[" .. L.CHAT_PARTY_LEADER .. "]|h %s:\32"
_G.CHAT_PARTY_GUIDE_GET = _G.CHAT_PARTY_LEADER_GET
_G.CHAT_RAID_GET = "|Hchannel:RAID|h[" .. L.CHAT_RAID .. "]|h %s:\32"
_G.CHAT_RAID_LEADER_GET = "|Hchannel:RAID|h[" .. L.CHAT_RAID_LEADER .. "]|h %s:\32"
_G.CHAT_RAID_WARNING_GET = "[" .. L.CHAT_RAID_WARNING .. "] %s:\32"
_G.CHAT_PET_BATTLE_COMBAT_LOG_GET = "|Hchannel:PET_BATTLE_COMBAT_LOG|h[" .. L.CHAT_PET_BATTLE .. "]|h:\32"
_G.CHAT_PET_BATTLE_INFO_GET = "|Hchannel:PET_BATTLE_INFO|h[" .. L.CHAT_PET_BATTLE .. "]|h:\32"
_G.CHAT_SAY_GET = "%s:\32"
_G.CHAT_WHISPER_GET = L.CHAT_WHISPER .. " %s:\32"
_G.CHAT_YELL_GET = "%s:\32"
_G.CHAT_FLAG_AFK = "|cffE7E716" .. L.CHAT_AFK .. "|r "
_G.CHAT_FLAG_DND = "|cffFF0000" .. L.CHAT_DND .. "|r "
_G.CHAT_FLAG_GM = "|cff4154F5" .. L.CHAT_GM .. "|r "
_G.ERR_FRIEND_ONLINE_SS = "|Hplayer:%s|h[%s]|h " .. L.CHAT_COME_ONLINE
_G.ERR_FRIEND_OFFLINE_S = "[%s] " .. L.CHAT_GONE_OFFLINE

------------------------------------------------------------------------
-- AddMessage Filter (channel number strip + realm name strip)
------------------------------------------------------------------------

local function strip(info, name) return format("|Hplayer:%s|h[%s]|h", info, name:gsub("%-[^|]+", "")) end

local function formatMessageFilter(_, _, text, ...)
    if type(text) ~= "string" or not canaccessvalue(text) then return end
    local modified = text:gsub("|h%[(%d+)%. .-%]|h", "|h[%1]|h")
    modified = modified:gsub("|Hplayer:(.-)|h%[(.-)%]|h", strip)
    if modified ~= text then return false, modified, ... end
end

------------------------------------------------------------------------
-- Chat Style
------------------------------------------------------------------------

local function setChatStyle(frame)
    local id = frame:GetID()
    local chat = frame:GetName()

    _G[chat]:SetFrameLevel(5)
    _G[chat]:SetClampedToScreen(false)
    _G[chat]:SetFading(false)

    _G[chat .. "EditBox"]:ClearAllPoints()
    _G[chat .. "EditBox"]:SetPoint("BOTTOMLEFT", ChatFrame1, "TOPLEFT", -10, 23)
    _G[chat .. "EditBox"]:SetPoint("BOTTOMRIGHT", ChatFrame1, "TOPRIGHT", 11, 23)

    for j = 1, #CHAT_FRAME_TEXTURES do
        _G[chat .. CHAT_FRAME_TEXTURES[j]]:SetTexture(nil)
    end

    local tab = _G[format("ChatFrame%sTab", id)]
    tab.Left:Kill()
    tab.Middle:Kill()
    tab.Right:Kill()
    tab.ActiveLeft:Kill()
    tab.ActiveMiddle:Kill()
    tab.ActiveRight:Kill()
    tab.HighlightLeft:Kill()
    tab.HighlightMiddle:Kill()
    tab.HighlightRight:Kill()

    local bf = frame.buttonFrame
    if bf then bf:SetGhost() end

    _G[format("ChatFrame%sTabGlow", id)]:Kill()
    frame.ScrollBar:Kill()

    local editLeft = _G[format("ChatFrame%sEditBoxLeft", id)]
    if editLeft then editLeft:Kill() end
    local editMid = _G[format("ChatFrame%sEditBoxMid", id)]
    if editMid then editMid:Kill() end
    local editRight = _G[format("ChatFrame%sEditBoxRight", id)]
    if editRight then editRight:Kill() end

    _G[chat .. "EditBox"]:StripTextures(2)

    if frame.ScrollToBottomButton then
        frame.ScrollToBottomButton:ClearAllPoints()
        frame.ScrollToBottomButton:SetPoint("BOTTOMRIGHT", frame, -2, 22)
        frame:HookScript("OnUpdate", function(self)
            if not self:AtBottom() then
                frame.ScrollToBottomButton:Show()
            else
                frame.ScrollToBottomButton:Hide()
            end
        end)
    end

    local tab_convo = _G[chat .. "Tab"].conversationIcon
    if tab_convo then tab_convo:Kill() end

    local eb = _G[chat .. "EditBox"]
    eb:SetAltArrowKeyMode(false)
    eb:SetClampedToScreen(true)

    local lang = _G[chat .. "EditBoxLanguage"]
    if lang then
        lang:GetRegions():SetAlpha(0)
        lang:SetPoint("TOPLEFT", eb, "TOPRIGHT", 5, 0)
        lang:SetPoint("BOTTOMRIGHT", eb, "BOTTOMRIGHT", 29, 0)
        E:StyleFrame(lang, true)
    end

    if cfg.editbox_color == true then
        local editBoxBg = CreateFrame("Frame", nil, _G[chat .. "EditBox"])
        editBoxBg:SetPoint("TOPLEFT", _G[chat .. "EditBox"], "TOPLEFT", 7, -5)
        editBoxBg:SetPoint("BOTTOMRIGHT", _G[chat .. "EditBox"], "BOTTOMRIGHT", -7, 4)
        editBoxBg:SetFrameStrata("LOW")
        editBoxBg:SetFrameLevel(1)
        editBoxBg:SetTemplate("default")

        local function colorize(r, g, b)
            if editBoxBg then editBoxBg:SetBackdropBorderColor(r, g, b) end
        end

        local eb = _G[chat .. "EditBox"]
        if eb.UpdateHeader then
            hooksecurefunc(eb, "UpdateHeader", function(self)
                local chatType = self:GetAttribute("chatType")
                if not chatType then return end

                local chanTarget = self:GetAttribute("channelTarget")
                local chanName = chanTarget and GetChannelName(chanTarget)
                if chanName and chatType == "CHANNEL" then
                    if chanName == 0 then
                        colorize(unpack(C.media.border_color))
                    else
                        colorize(ChatTypeInfo[chatType .. chanName].r, ChatTypeInfo[chatType .. chanName].g, ChatTypeInfo[chatType .. chanName].b)
                    end
                else
                    colorize(ChatTypeInfo[chatType].r, ChatTypeInfo[chatType].g, ChatTypeInfo[chatType].b)
                end
            end)
        end
    end

    if _G[chat] == _G["ChatFrame2"] then
        if CombatLogQuickButtonFrame_Custom then
            CombatLogQuickButtonFrame_Custom:StripTextures()
            CombatLogQuickButtonFrame_Custom:CreateBackdrop()
            CombatLogQuickButtonFrame_Custom.__backdrop:SetPoint("TOPLEFT", 1, -4)
            CombatLogQuickButtonFrame_Custom.__backdrop:SetPoint("BOTTOMRIGHT", -22, 0)

            E:StyleCloseButton(CombatLogQuickButtonFrame_CustomAdditionalFilterButton, CombatLogQuickButtonFrame_Custom.__backdrop)
            CombatLogQuickButtonFrame_CustomAdditionalFilterButton:SetSize(12, 12)
            CombatLogQuickButtonFrame_CustomAdditionalFilterButton:SetHitRectInsets(0, 0, 0, 0)

            CombatLogQuickButtonFrame_CustomProgressBar:ClearAllPoints()
            CombatLogQuickButtonFrame_CustomProgressBar:SetPoint("TOPLEFT", CombatLogQuickButtonFrame_Custom.__backdrop, 2, -2)
            CombatLogQuickButtonFrame_CustomProgressBar:SetPoint("BOTTOMRIGHT", CombatLogQuickButtonFrame_Custom.__backdrop, -2, 2)
            CombatLogQuickButtonFrame_CustomProgressBar:SetStatusBarTexture(C.media.texture.status_f)

            CombatLogQuickButtonFrameButton1:SetPoint("BOTTOM", 0, 0)
        end
    end

    frame.skinned = true
end

------------------------------------------------------------------------
-- Tab Channel Switch
------------------------------------------------------------------------

local cycles = {
    {
        chatType = "SAY",
        use = function() return 1 end,
    },
    {
        chatType = "PARTY",
        use = function() return not IsInRaid() and IsInGroup(LE_PARTY_CATEGORY_HOME) end,
    },
    {
        chatType = "RAID",
        use = function() return IsInRaid(LE_PARTY_CATEGORY_HOME) end,
    },
    {
        chatType = "INSTANCE_CHAT",
        use = function() return IsPartyLFG() end,
    },
    {
        chatType = "GUILD",
        use = function() return IsInGuild() end,
    },
    {
        chatType = "OFFICER",
        use = function() return C_GuildInfo.IsGuildOfficer() end,
    },
    {
        chatType = "SAY",
        use = function() return 1 end,
    },
}

local function updateTabChannelSwitch(self)
    if strsub(tostring(self:GetText()), 1, 1) == "/" then return end
    local currChatType = self:GetAttribute("chatType")

    if IsShiftKeyDown() and (currChatType == "WHISPER" or currChatType == "BN_WHISPER") then
        self:SetAttribute("chatType", "SAY")
        ChatEdit_UpdateHeader(self)
        return
    end

    for i, curr in ipairs(cycles) do
        if curr.chatType == currChatType then
            local h, r, step = i + 1, #cycles, 1
            if IsShiftKeyDown() then
                h, r, step = i - 1, 1, -1
            end
            for j = h, r, step do
                if cycles[j]:use(self, currChatType) then
                    self:SetAttribute("chatType", cycles[j].chatType)
                    ChatEdit_UpdateHeader(self)
                    return
                end
            end
        end
    end
end

------------------------------------------------------------------------
-- Loot Icons
------------------------------------------------------------------------

local function addLootIcons(_, _, message, ...)
    if issecretvalue(message) then return end
    local function icon(link)
        local texture = C_Item.GetItemIconByID(link)
        return "\124T" .. texture .. ":12:12:0:0:64:64:5:59:5:59\124t" .. link
    end
    message = message:gsub("(\124c%x+\124Hitem:.-\124h\124r)", icon)
    return false, message, ...
end

------------------------------------------------------------------------
-- Remove Realm Name
------------------------------------------------------------------------

local function removeRealmName(_, _, msg, author, ...)
    if issecretvalue(msg) then return end
    local realm = gsub(E.realm, " ", "")
    if msg:find("-" .. realm) then return false, gsub(msg, "%-" .. realm, ""), author, ... end
end

------------------------------------------------------------------------
-- Typo History
------------------------------------------------------------------------

local function typoHistoryPosthook(chat, text)
    if text and canaccessvalue(text) and strfind(text, HELP_TEXT_SIMPLE) then ChatEdit_AddHistory(chat.editBox) end
end

------------------------------------------------------------------------
-- Timestamp
--
-- Prepend a unified timestamp to every line. This is done by replacing
-- ChatFrame.AddMessage (the only place that sees the final rendered text
-- for all message types, including addon prints), mirroring ElvUI.
------------------------------------------------------------------------

local function addMessageTimestamp(frame, msg, ...)
    if type(msg) == "string" and canaccessvalue(msg) and msg ~= "" then
        msg = format("%s[%s]|r %s", E:RGBToHex(cfg.time_color), BetterDate(cfg.time_format or "%H:%M", time()), msg)
    end
    return frame.OldAddMessage(frame, msg, ...)
end

------------------------------------------------------------------------
-- Chat History Override
--
-- Replacing AddMessage write-taints the frame, so Blizzard's
-- MessageEventHandler runs tainted and crashes when it indexes the
-- protected ChatHistory accessID tables (HistoryKeeper.lua). Mirror ElvUI:
-- reimplement the ChatHistory_* globals with addon-owned tables (freely
-- indexable while tainted) and bail on secret tokens -- chat targets and
-- senders are secret in combat and cannot be used as table keys.
------------------------------------------------------------------------

local function setupChatHistory()
    local accessIDs = {}
    local accessIDToType = {}
    local accessIDToTarget = {}
    local accessIDToChanSender = {}
    local nextAccessID = 1

    local function getToken(chatType, chatTarget, chanSender) return format("%s;;%s;;%s", strlower(chatType), chatTarget or "", chanSender or "") end

    function ChatHistory_GetToken(chatType, chatTarget, chanSender) return getToken(chatType, chatTarget, chanSender) end

    function ChatHistory_GetAccessID(chatType, chatTarget, chanSender)
        local token = getToken(chatType, chatTarget, chanSender)
        if issecretvalue(token) then return end
        if not accessIDs[token] then
            accessIDs[token] = nextAccessID
            accessIDToType[nextAccessID] = chatType
            accessIDToTarget[nextAccessID] = chatTarget
            accessIDToChanSender[nextAccessID] = chanSender
            nextAccessID = nextAccessID + 1
        end
        return accessIDs[token]
    end

    function ChatHistory_GetChatType(accessID) return accessIDToType[accessID], accessIDToTarget[accessID], accessIDToChanSender[accessID] end

    function ChatHistory_GetAllAccessIDsByChanSender(chanSender)
        local results = {}
        if issecretvalue(chanSender) then return results end
        for accessID, sender in pairs(accessIDToChanSender) do
            if strlower(sender) == strlower(chanSender) then results[#results + 1] = accessID end
        end
        return results
    end
end

------------------------------------------------------------------------
-- Lifecycle
------------------------------------------------------------------------

function module:OnInit()
    if not cfg or not cfg.enable then return end

    C_CVar.SetCVar("chatStyle", "classic")
    C_CVar.SetCVar("chatMouseScroll", 1)

    GeneralDockManagerOverflowButton:SetPoint("BOTTOMRIGHT", ChatFrame1, "TOPRIGHT", 0, 5)
    hooksecurefunc(GeneralDockManagerScrollFrame, "SetPoint", function(self, point, anchor, attachTo, x, y)
        if anchor == GeneralDockManagerOverflowButton and x == 0 and y == 0 then self:SetPoint(point, anchor, attachTo, 0, -4) end
    end)

    local function setupChat()
        for i = 1, NUM_CHAT_WINDOWS do
            local frame = _G[format("ChatFrame%s", i)]
            if not frame.skinned then setChatStyle(frame) end
        end

        local var = cfg.sticky and 1 or 0
        ChatTypeInfo.SAY.sticky = var
        ChatTypeInfo.PARTY.sticky = var
        ChatTypeInfo.PARTY_LEADER.sticky = var
        ChatTypeInfo.GUILD.sticky = var
        ChatTypeInfo.OFFICER.sticky = var
        ChatTypeInfo.RAID.sticky = var
        ChatTypeInfo.RAID_WARNING.sticky = var
        ChatTypeInfo.INSTANCE_CHAT.sticky = var
        ChatTypeInfo.INSTANCE_CHAT_LEADER.sticky = var
        ChatTypeInfo.WHISPER.sticky = var
        ChatTypeInfo.BN_WHISPER.sticky = var
        ChatTypeInfo.CHANNEL.sticky = var
    end

    if C_AddOns.IsAddOnLoaded("Blizzard_CombatLog") then
        setupChat()
    else
        local function onAddonLoaded(_, _, addon)
            if addon == "Blizzard_CombatLog" then
                self:UnregisterEvent("ADDON_LOADED", onAddonLoaded)
                setupChat()
            end
        end
        self:RegisterEvent("ADDON_LOADED", onAddonLoaded)
    end

    hooksecurefunc("FCF_OpenTemporaryWindow", function()
        local frame = FCF_GetCurrentChatFrame()
        if not frame.skinned then setChatStyle(frame) end
        if frame ~= _G["ChatFrame2"] and not frame.OldAddMessage then
            frame.OldAddMessage = frame.AddMessage
            frame.AddMessage = addMessageTimestamp
        end
    end)

    hooksecurefunc("ChatEdit_CustomTabPressed", updateTabChannelSwitch)

    if cfg.loot_icons then ChatFrame_AddMessageEventFilter("CHAT_MSG_LOOT", addLootIcons) end

    ChatFrame_AddMessageEventFilter("CHAT_MSG_SYSTEM", removeRealmName)

    local formatEvents = {
        "CHAT_MSG_SAY",
        "CHAT_MSG_YELL",
        "CHAT_MSG_GUILD",
        "CHAT_MSG_OFFICER",
        "CHAT_MSG_PARTY",
        "CHAT_MSG_PARTY_LEADER",
        "CHAT_MSG_RAID",
        "CHAT_MSG_RAID_LEADER",
        "CHAT_MSG_INSTANCE_CHAT",
        "CHAT_MSG_INSTANCE_CHAT_LEADER",
        "CHAT_MSG_CHANNEL",
        "CHAT_MSG_WHISPER",
        "CHAT_MSG_WHISPER_INFORM",
        "CHAT_MSG_BN_WHISPER",
        "CHAT_MSG_BN_WHISPER_INFORM",
    }
    for _, event in ipairs(formatEvents) do
        ChatFrame_AddMessageEventFilter(event, formatMessageFilter)
    end

    for i = 1, NUM_CHAT_WINDOWS do
        if i ~= 2 then hooksecurefunc(_G["ChatFrame" .. i], "AddMessage", typoHistoryPosthook) end
    end

    -- Disable Blizzard's native timestamps; addMessageTimestamp prepends ours.
    C_CVar.SetCVar("showTimestamps", "none")

    -- Make Blizzard's chat history taint-safe before tainting the frames below.
    setupChatHistory()

    for i = 1, NUM_CHAT_WINDOWS do
        if i ~= 2 then
            local cf = _G["ChatFrame" .. i]
            if not cf.OldAddMessage then
                cf.OldAddMessage = cf.AddMessage
                cf.AddMessage = addMessageTimestamp
            end
        end
    end
end

function module:OnEnable()
    local function updateChatSize()
        if isScaling then return end
        isScaling = true

        ChatFrame1:ClearAllPoints()
        ChatFrame1:SetHeight(120)
        if cfg.editbox_color then
            ChatFrame1:SetPoint(cfg.pos[1], cfg.pos[2], cfg.pos[3], cfg.pos[4], cfg.pos[5] + 4)
        else
            ChatFrame1:SetPoint(cfg.pos[1], cfg.pos[2], cfg.pos[3], cfg.pos[4], cfg.pos[5])
        end
        FCF_SavePositionAndDimensions(ChatFrame1)

        isScaling = false
    end

    for i = 1, NUM_CHAT_WINDOWS do
        local chat = _G[format("ChatFrame%s", i)]
        local id = chat:GetID()
        local _, fontSize = FCF_GetChatWindowInfo(id)

        if fontSize < 11 then
            FCF_SetChatWindowFontSize(nil, chat, 11)
        else
            FCF_SetChatWindowFontSize(nil, chat, fontSize)
        end

        chat:SetFont(STANDARD_TEXT_FONT, fontSize, "")
        chat:SetShadowOffset(1, -1)

        if i == 1 then
            updateChatSize()
        elseif i == 2 then
            if not cfg.combatlog then
                FCF_DockFrame(chat)
                ChatFrame2Tab:EnableMouse(false)
                ChatFrame2TabText:Hide()
                ChatFrame2Tab:SetWidth(0.001)
                ChatFrame2Tab.SetWidth = E.Dummy
                FCF_DockUpdate()
            end
        end

        chat:SetScript("OnMouseWheel", FloatingChatFrame_OnMouseScroll)
    end

    self:RegisterEvent("UI_SCALE_CHANGED", updateChatSize)

    hooksecurefunc(ChatFrame1, "SetPoint", function(_, _, _, _, x)
        if isScaling then return end
        if x ~= cfg.pos[4] then updateChatSize() end
    end)
end
