--[[--------------------------------------------------------------------
    BrightQoL – BrightChat
    BrightQoL chat skin module.
    
    Lightweight, no dock system interference.
    Only skins, hooks, and cosmetic changes.
----------------------------------------------------------------------]]
local addonName, ns = ...
if not ns then return end

local BC = {}
ns.BrightChat = BC

---------------------------------------------------------------------------
-- Helper: nil-safe frame killer (hides + zeroes, no method replacement)
---------------------------------------------------------------------------
local function Kill(obj)
    if not obj then return end
    obj:SetAlpha(0)
    if obj.SetTexture then obj:SetTexture(nil) end
    if obj.Hide then obj:Hide() end
end

---------------------------------------------------------------------------
-- DB accessor
---------------------------------------------------------------------------
local function db() return BrightQoL and BrightQoL.brightChat end

---------------------------------------------------------------------------
-- Active frame tracking
---------------------------------------------------------------------------
local activeFrame = nil

local function GetActive()
    return activeFrame or ChatFrame1
end

---------------------------------------------------------------------------
-- Combat lockdown guard
---------------------------------------------------------------------------
local isRequestingRefresh = false

local function SafeCall(fn, ...)
    if InCombatLockdown() then
        if not isRequestingRefresh then
            isRequestingRefresh = true
            local args = {...}
            C_Timer.After(2, function()
                isRequestingRefresh = false
                fn(unpack(args))
            end)
        end
        return false
    end
    return true
end

---------------------------------------------------------------------------
-- Tab skinning
---------------------------------------------------------------------------

local function UpdateTabVisuals(tab)
    if not tab or not tab.BC_Skin then return end
    local skin  = tab.BC_Skin
    local frame = tab.chatFrame
    local d     = db()
    if not d then return end

    local isSelected = (GetActive() == frame)
    local isHover    = tab.BC_Hover

    -- Stop alerting when selected
    if isSelected and frame then
        frame.BC_Alerting = false
        if frame.alerting and FCF_StopAlertFlash then
            frame.alerting = false
            FCF_StopAlertFlash(frame)
        end
    end

    local isAlerting = (frame and (frame.BC_Alerting or frame.alerting)) and d.tabGlow

    local r = d.tabColorR or 0
    local g = d.tabColorG or 207/255
    local b = d.tabColorB or 1.0
    if d.tabUseClassColor then
        local cc = ns.Widgets and ns.Widgets.GetPlayerClassColor and ns.Widgets.GetPlayerClassColor()
        if cc then r, g, b = cc.r, cc.g, cc.b end
    end

    skin.line:SetShown(isSelected or isAlerting)

    -- Selector style: decorate active tab text
    local style  = d.tabSelectorStyle or "NONE"
    local STYLES = {
        NONE   = "%s",
        ARROW  = ">%s<",
        ARROW2 = ">>%s<<",
        BOX    = "[%s]",
        CURLY  = "{%s}",
        CURVE  = "(%s)",
    }
    local fmt = STYLES[style] or "%s"

    -- Selector symbols colour (only meaningful for non-NONE styles)
    local sr = d.tabSelectorColorR or r
    local sg = d.tabSelectorColorG or g
    local sb = d.tabSelectorColorB or b

    -- Inactive tab colour
    local ir = d.tabInactiveColorR or 0.75
    local ig = d.tabInactiveColorG or 0.75
    local ib = d.tabInactiveColorB or 0.75

    if isSelected then
        -- Apply selector style: wrap the raw tab name
        local rawText = skin.rawText or skin.text:GetText() or ""
        if style ~= "NONE" then
            local sHex  = string.format("|cff%02x%02x%02x", sr*255, sg*255, sb*255)
            local tHex  = string.format("|cff%02x%02x%02x", r*255,  g*255,  b*255)
            -- Build coloured selector: sHex > tHex Name |r sHex < |r
            local open_, close_ = fmt:match("^(.-)%%s(.-)$")
            skin.text:SetText(sHex..(open_ or "").."|r"..tHex..rawText.."|r"..sHex..(close_ or "").."|r")
        else
            skin.text:SetText(rawText)
        end
        skin.text:SetTextColor(r, g, b, 1)
        skin.line:SetVertexColor(r, g, b, 1)
        if skin.pulse then skin.pulse:Stop() end
        skin.text:SetAlpha(1)
        skin.line:SetAlpha(1)
    elseif isAlerting then
        skin.text:SetTextColor(r, g, b, 1)
        skin.line:SetVertexColor(r, g, b, 1)
        if skin.pulse and not skin.pulse:IsPlaying() then skin.pulse:Play() end
    else
        if isHover then
            skin.text:SetTextColor(math.min(ir+0.2,1), math.min(ig+0.2,1), math.min(ib+0.2,1), 1)
        else
            skin.text:SetTextColor(ir, ig, ib, 1)
        end
        skin.line:SetAlpha(0)
        if skin.pulse then skin.pulse:Stop() end
        skin.text:SetAlpha(d.tabInactiveAlpha or 0.7)
    end
end

local function InitTab(frame)
    local name     = frame:GetName()
    local blizzTab = _G[name.."Tab"]
    if not blizzTab or blizzTab.BC_Skin then return end

    -- Create overlay skin frame
    local skin = CreateFrame("Frame", nil, blizzTab)
    skin:SetAllPoints()
    blizzTab.BC_Skin   = skin
    blizzTab.chatFrame = frame

    -- Fix alpha and prevent WoW from changing it
    blizzTab:SetAlpha(1)
    hooksecurefunc(blizzTab, "SetAlpha", function(self, alpha)
        if alpha < 0.5 then self:SetAlpha(1) end
    end)

    -- Strip Blizzard tab textures
    if blizzTab.NineSlice  then blizzTab.NineSlice:SetAlpha(0)  end
    if blizzTab.Background then blizzTab.Background:SetAlpha(0) end
    for i = 1, blizzTab:GetNumRegions() do
        local r = select(i, blizzTab:GetRegions())
        if r and r:IsObjectType("Texture") then Kill(r) end
    end
    if blizzTab.Text then blizzTab.Text:SetAlpha(0) end

    -- Custom text
    local d    = db()
    local font = (d and d.tabFont) or (ns.Theme and ns.Theme.GetFont and ns.Theme.GetFont()) or "Fonts\\FRIZQT__.TTF"
    local LSM  = LibStub and LibStub("LibSharedMedia-3.0", true)
    if LSM and d and d.tabFont then font = LSM:Fetch("font", d.tabFont, true) or font end
    skin.text = skin:CreateFontString(nil, "OVERLAY")
    skin.text:SetFont(font, d and d.tabFontSize or 11, d and d.tabFontOutline or "OUTLINE")
    skin.text:SetJustifyH("CENTER")
    skin.text:SetJustifyV("MIDDLE")
    skin.text:SetPoint("CENTER", 0, 3)
    blizzTab:SetHeight(d and d.tabHeight or 22)

    -- Active indicator line (2px, bottom of tab)
    skin.line = skin:CreateTexture(nil, "OVERLAY")
    skin.line:SetTexture("Interface\\Buttons\\WHITE8x8")
    skin.line:SetHeight(2)
    skin.line:SetPoint("BOTTOMLEFT",  4, 2)
    skin.line:SetPoint("BOTTOMRIGHT", -4, 2)

    -- Pulse animation for unread alerts
    skin.pulse = skin:CreateAnimationGroup()
    local a1 = skin.pulse:CreateAnimation("Alpha"); a1:SetTarget(skin.text); a1:SetFromAlpha(0.3); a1:SetToAlpha(1);   a1:SetDuration(0.6); a1:SetOrder(1)
    local a2 = skin.pulse:CreateAnimation("Alpha"); a2:SetTarget(skin.text); a2:SetFromAlpha(1);   a2:SetToAlpha(0.3); a2:SetDuration(0.6); a2:SetOrder(2)
    local a3 = skin.pulse:CreateAnimation("Alpha"); a3:SetTarget(skin.line); a3:SetFromAlpha(0.3); a3:SetToAlpha(1);   a3:SetDuration(0.6); a3:SetOrder(1)
    local a4 = skin.pulse:CreateAnimation("Alpha"); a4:SetTarget(skin.line); a4:SetFromAlpha(1);   a4:SetToAlpha(0.3); a4:SetDuration(0.6); a4:SetOrder(2)
    skin.pulse:SetLooping("REPEAT")

    blizzTab:HookScript("OnEnter", function(self) self.BC_Hover = true;  UpdateTabVisuals(self) end)
    blizzTab:HookScript("OnLeave", function(self) self.BC_Hover = false; UpdateTabVisuals(self) end)
    blizzTab:HookScript("OnShow",  function(self) UpdateTabVisuals(self) end)

    UpdateTabVisuals(blizzTab)
end

local function PackTabs()
    for i = 1, NUM_CHAT_WINDOWS or 10 do
        local f   = _G["ChatFrame"..i]
        local tab = _G["ChatFrame"..i.."Tab"]
        if f and tab and tab.BC_Skin then
            local text = (tab.Text and tab.Text:GetText()) or GetChatWindowInfo(i) or ("Chat "..i)
            if text == "" then text = "Chat "..i end
            tab.BC_Skin.rawText = text
            -- Only set plain text if not selected (selected applies style in UpdateTabVisuals)
            if GetActive() ~= f then
                tab.BC_Skin.text:SetText(text)
            end
            -- Resize native tab frame to give text room + padding
            local d = db()
            local extraPad = 20 -- base padding per side
            local styleExtra = 0
            local style = d and d.tabSelectorStyle or "NONE"
            if style == "ARROW"  then styleExtra = 20
            elseif style == "ARROW2" then styleExtra = 36
            elseif style == "BOX" or style == "CURLY" or style == "CURVE" then styleExtra = 16
            end
            local textW = tab.BC_Skin.text:GetStringWidth()
            local newW  = math.max(40, textW + extraPad + styleExtra)
            tab:SetWidth(newW)
            tab.BC_Skin.text:SetPoint("CENTER", 0, 3)
            UpdateTabVisuals(tab)
        end
    end
end
BC.PackTabs = PackTabs

---------------------------------------------------------------------------
-- Chat frame skinning
---------------------------------------------------------------------------

local function RefreshChatFrame(frame)
    if not frame then return end
    local name = frame:GetName()
    local d    = db()
    if not d then return end

    -- Kill standard Blizzard decorations
    if frame.Background then Kill(frame.Background) end
    if frame.NineSlice  then frame.NineSlice:SetAlpha(0) end
    if frame.ButtonFrame then
        Kill(frame.ButtonFrame)
        if frame.ButtonFrame.Background then Kill(frame.ButtonFrame.Background) end
    end

    local subNames = {
        "RightTexture","LeftTexture","TopTexture","BottomTexture",
        "ButtonFrameBackground","ButtonFrameLeftTexture","ButtonFrameRightTexture",
        "ButtonFrameTopTexture","ButtonFrameBottomTexture",
    }
    for _, sn in ipairs(subNames) do
        Kill(_G[name..sn])
    end

    -- Kill scrollbar + scroll-to-bottom
    local scrollBar = frame.ScrollBar or _G[name.."ScrollBar"]
    if scrollBar then
        scrollBar:Hide()
        scrollBar:SetScript("OnShow", scrollBar.Hide)
    end
    local scrollBtn = frame.ScrollToBottomButton or _G[name.."ScrollToBottomButton"]
    if scrollBtn then
        scrollBtn:Hide()
        scrollBtn:SetScript("OnShow", scrollBtn.Hide)
    end
    -- Kill minimize button
    Kill(_G[name.."ButtonFrameMinimizeButton"])

    -- Background texture
    if not frame.BC_BG then
        frame.BC_BG = frame:CreateTexture(nil, "BACKGROUND", nil, -8)
        frame.BC_BG:SetTexture("Interface\\Buttons\\WHITE8x8")
    end
    local pr, pg, pb = d.bgColorR or 0.08, d.bgColorG or 0.08, d.bgColorB or 0.10
    local pa         = d.bgAlpha  or 0.8
    frame.BC_BG:SetVertexColor(pr, pg, pb, pa)
    frame.BC_BG:SetPoint("TOPLEFT",     frame, "TOPLEFT",     -(d.padLeft or 4),  (d.padTop    or 4))
    frame.BC_BG:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT",  (d.padRight or 4), -(d.padBottom or 4))

    -- Border (always created, shown based on setting)
    if not frame.BC_Border then
        frame.BC_Border = CreateFrame("Frame", nil, frame, "BackdropTemplate")
        frame.BC_Border:SetBackdrop({edgeFile="Interface\\Buttons\\WHITE8x8", edgeSize=1})
        frame.BC_Border:SetBackdropBorderColor(0.2, 0.2, 0.2, 0.8)
        frame.BC_Border:SetFrameLevel(frame:GetFrameLevel() + 1)
        frame.BC_Border:SetPoint("TOPLEFT",     frame.BC_BG, "TOPLEFT",     -1,  1)
        frame.BC_Border:SetPoint("BOTTOMRIGHT", frame.BC_BG, "BOTTOMRIGHT",  1, -1)
        frame.BC_Border:EnableMouse(false)
    end
    frame.BC_Border:SetShown(d.showBorder or false)

    -- Editbox
    if not InCombatLockdown() then
        local eb = _G[name.."EditBox"]
        if eb then
            -- Strip editbox textures
            if not eb.BC_Stripped then
                for i = 1, eb:GetNumRegions() do
                    local r = select(i, eb:GetRegions())
                    if r and r:IsObjectType("Texture") then r:SetAlpha(0) end
                end
                local fl = _G[name.."EditBoxFocusLeft"]
                local fm = _G[name.."EditBoxFocusMid"]
                local fr = _G[name.."EditBoxFocusRight"]
                if fl then fl:SetTexture(nil) end
                if fm then fm:SetTexture(nil) end
                if fr then fr:SetTexture(nil) end
                eb.BC_Stripped = true
            end

            -- Background
            if not eb.BC_BG then
                eb.BC_BG = eb:CreateTexture(nil, "BACKGROUND", nil, -8)
                eb.BC_BG:SetTexture("Interface\\Buttons\\WHITE8x8")
                eb.BC_BG:SetAllPoints(eb)
            end
            eb.BC_BG:SetVertexColor(pr, pg, pb, pa)

            -- Border on editbox
            if not eb.BC_Border then
                eb.BC_Border = CreateFrame("Frame", nil, eb, "BackdropTemplate")
                eb.BC_Border:SetBackdrop({edgeFile="Interface\\Buttons\\WHITE8x8", edgeSize=1})
                eb.BC_Border:SetBackdropBorderColor(0.2, 0.2, 0.2, 0.8)
                eb.BC_Border:SetPoint("TOPLEFT",     eb, "TOPLEFT",    -1,  1)
                eb.BC_Border:SetPoint("BOTTOMRIGHT", eb, "BOTTOMRIGHT", 1, -1)
            end

            -- Focus border highlight
            if not eb.BC_HooksDone then
                eb:HookScript("OnEditFocusGained", function(self)
                    if self.BC_Border then
                        local ar, ag, ab = ns.Theme.GetAccent()
                        self.BC_Border:SetBackdropBorderColor(ar, ag, ab, 0.9)
                    end
                end)
                eb:HookScript("OnEditFocusLost", function(self)
                    if self.BC_Border then
                        self.BC_Border:SetBackdropBorderColor(0.2, 0.2, 0.2, 0.8)
                    end
                end)
                eb.BC_HooksDone = true
            end

            -- Position
            eb:ClearAllPoints()
            if d.editBoxTop then
                eb:SetPoint("BOTTOMLEFT",  frame.BC_BG, "TOPLEFT",  0,  4)
                eb:SetPoint("BOTTOMRIGHT", frame.BC_BG, "TOPRIGHT", 0,  4)
            else
                eb:SetPoint("TOPLEFT",  frame.BC_BG, "BOTTOMLEFT",  0, -4)
                eb:SetPoint("TOPRIGHT", frame.BC_BG, "BOTTOMRIGHT", 0, -4)
            end
        end
    end
end


---------------------------------------------------------------------------
-- Side buttons skinning (ChatFrameMenuButton, ChatFrameChannelButton)
---------------------------------------------------------------------------

local SIDE_BUTTONS = {
    { key = "ChatFrameMenuButton",    icon = "Interface\\AddOns\\BrightQoL\\Assets\\Chat\\menu.png" },
    { key = "ChatFrameChannelButton", icon = "Interface\\AddOns\\BrightQoL\\Assets\\Chat\\channel.png" },
    { key = "BrightQoL_ChatSettingsBtn", icon = "Interface\\AddOns\\BrightQoL\\Assets\\Chat\\cog.png",
      onCreate = function(btn)
          btn:SetScript("OnClick", function()
              if ns.MainFrame then
                  if ns.MainFrame:IsShown() and BrightQoL.config and BrightQoL.config.lastTab == "chat" then
                      ns.MainFrame:Hide()
                  else
                      ns.MainFrame:Show()
                      if ns.OpenTab then ns:OpenTab("chat") end
                  end
              end
          end)
      end,
    },
}

local function SnapButtons()
    if InCombatLockdown() then return end
    local d      = db()
    if not d then return end
    local anchor = ChatFrame1.BC_BG
    if not anchor then return end

    local hide      = d.hideButtons
    local sideOff   = -4

    -- QuickJoinToastButton: just hide it, we don't use it
    local qjb = _G.QuickJoinToastButton
    if qjb then qjb:Hide() end

    for _, data in ipairs(SIDE_BUTTONS) do
        -- Custom BrightQoL buttons: create if not in _G
        if not _G[data.key] and data.onCreate then
            local b = CreateFrame("Button", data.key, ChatFrame1)
            b:SetSize(22, 22)
            _G[data.key] = b
        end
        local btn = _G[data.key]
        if btn then
            if hide then
                btn:Hide()
            else
                btn:Show()

                -- Anchor to the left of BC_BG, stacked vertically
                if not btn.BC_Positioned then
                    btn:ClearAllPoints()
                    btn:SetSize(22, 22)
                    btn:SetPoint("TOPRIGHT", anchor, "TOPRIGHT", 26, sideOff)
                    btn.BC_Positioned = true
                end

                -- Skin once
                if not btn.BC_Initialized then
                    -- Strip original textures
                    for i = 1, btn:GetNumRegions() do
                        local r = select(i, btn:GetRegions())
                        if r and r:IsObjectType("Texture") then r:SetAlpha(0) end
                    end

                    -- Dark backdrop
                    local back = CreateFrame("Frame", nil, btn, "BackdropTemplate")
                    back:SetAllPoints()
                    back:SetFrameLevel(btn:GetFrameLevel() - 1)
                    back:SetBackdrop({
                        bgFile   = "Interface\\Buttons\\WHITE8x8",
                        edgeFile = "Interface\\Buttons\\WHITE8x8",
                        edgeSize = 1,
                    })
                    back:SetBackdropColor(0.06, 0.06, 0.08, 0.9)
                    back:SetBackdropBorderColor(0.18, 0.18, 0.22, 0.9)
                    btn.BC_Back = back

                    -- Label
                    if data.icon then
                        local ic = btn:CreateTexture(nil, "OVERLAY")
                        ic:SetPoint("TOPLEFT",     btn, "TOPLEFT",     2,  -2)
                        ic:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2,  2)
                        ic:SetTexture(data.icon)
                        btn.BC_Label = ic
                    elseif data.text then
                        local fs = btn:CreateFontString(nil, "OVERLAY")
                        fs:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
                        fs:SetJustifyH("CENTER")
                        fs:SetJustifyV("MIDDLE")
                        fs:SetPoint("CENTER")
                        fs:SetText(data.text)
                        fs:SetTextColor(0.7, 0.7, 0.7, 1)
                        btn.BC_Label = fs
                    end

                    -- Hover
                    btn:HookScript("OnEnter", function(self)
                        if self.BC_Back then
                            local ar, ag, ab = ns.Theme.GetAccent()
                            self.BC_Back:SetBackdropColor(ar * 0.2, ag * 0.2, ab * 0.2, 0.9)
                            self.BC_Back:SetBackdropBorderColor(ar, ag, ab, 0.6)
                        end
                        if self.BC_Label then
                            if self.BC_Label.SetTextColor then self.BC_Label:SetTextColor(1,1,1,1)
                            elseif self.BC_Label.SetVertexColor then self.BC_Label:SetVertexColor(1,1,1,1) end
                        end
                    end)
                    btn:HookScript("OnLeave", function(self)
                        if self.BC_Back then
                            self.BC_Back:SetBackdropColor(0.06, 0.06, 0.08, 0.9)
                            self.BC_Back:SetBackdropBorderColor(0.18, 0.18, 0.22, 0.9)
                        end
                        if self.BC_Label then
                            if self.BC_Label.SetTextColor then self.BC_Label:SetTextColor(0.7,0.7,0.7,1)
                            elseif self.BC_Label.SetVertexColor then self.BC_Label:SetVertexColor(0.8,0.8,0.8,1) end
                        end
                    end)

                    -- Custom action override
                    if data.onCreate then data.onCreate(btn) end
                    btn.BC_Initialized = true
                end

                sideOff = sideOff - 26
            end
        end
    end
end
BC.SnapButtons = SnapButtons


---------------------------------------------------------------------------
-- Drag-to-move (bypass Edit Mode)
-- Makes ChatFrame1 freely draggable via left-click on the background.
-- Position saved to db and restored on next login.
---------------------------------------------------------------------------

local function SavePosition(frame)
    local d = db()
    if not d then return end
    local point, _, relPoint, x, y = frame:GetPoint(1)
    if point then
        d.position = { point=point, relPoint=relPoint, x=x, y=y }
    end
    local w, h = frame:GetSize()
    d.chatSize = { w=w, h=h }
end

local function RestorePosition()
    local d = db()
    if not d then return end
    -- Only restore if user has previously dragged the chat
    if not d.position then return end
    local p = d.position
    -- SetUserPlaced must be false temporarily to allow SetPoint
    ChatFrame1:SetUserPlaced(false)
    ChatFrame1:SetMovable(true)
    ChatFrame1:ClearAllPoints()
    ChatFrame1:SetPoint(
        p.point    or "BOTTOMLEFT",
        UIParent,
        p.relPoint or "BOTTOMLEFT",
        p.x or 0,
        p.y or 0
    )
    if d.chatSize then
        ChatFrame1:SetSize(d.chatSize.w, d.chatSize.h)
    end
    ChatFrame1:SetUserPlaced(true)
end

-- After the user drags and we save position, mark as user-placed.
-- SetUserPlaced(true) tells WoW not to auto-reposition this frame.
-- We don't override SetPoint — that breaks initial placement.
local function LockPosition()
    -- Nothing to do at init; position is locked per-drag via SetUserPlaced
end

local function SetupDragOnBG()
    local frame = ChatFrame1
    if frame.BC_DragSetup then return end

    -- Allow the frame to be placed flush against screen edges
    frame:SetClampedToScreen(false)
    frame:SetClampRectInsets(0, 0, 0, 0)

    -- Create an invisible drag handle that covers the chat background
    -- It sits just above the background but below text
    local dragHandle = CreateFrame("Frame", "BrightQoL_ChatDragHandle", frame)
    dragHandle:SetPoint("TOPLEFT",     frame, "TOPLEFT",     0,  0)
    dragHandle:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 28) -- leave bottom for editbox
    dragHandle:SetFrameLevel(frame:GetFrameLevel() + 2)
    dragHandle:EnableMouse(true)
    dragHandle:RegisterForDrag("LeftButton")

    -- Drag tooltip hint
    dragHandle:SetScript("OnEnter", function(self)
        local d = db()
        if d and d.chatLocked then return end
        -- subtle cursor change only
    end)

    dragHandle:SetScript("OnDragStart", function(self)
        local d = db()
        if d and d.chatLocked then return end
        -- temporarily allow setpoint for StartMoving's internal tracking
        frame:SetMovable(true)
        frame:StartMoving()
    end)

    dragHandle:SetScript("OnDragStop", function(self)
        frame:StopMovingOrSizing()
        frame:SetUserPlaced(true)
        SavePosition(frame)
    end)

    -- Resize handle (bottom-right corner)
    if not frame.BC_ResizeHandle then
        frame:SetResizable(true)
        if frame.SetResizeBounds then
            frame:SetResizeBounds(200, 100)
        end

        local handle = CreateFrame("Frame", nil, frame)
        handle:SetSize(16, 16)
        handle:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
        handle:SetFrameLevel(frame:GetFrameLevel() + 20)
        handle:EnableMouse(true)

        local hBar = handle:CreateTexture(nil, "OVERLAY")
        hBar:SetSize(14, 2); hBar:SetPoint("BOTTOMRIGHT", 0, 0)
        hBar:SetTexture("Interface\\Buttons\\WHITE8x8")
        hBar:SetVertexColor(0, 207/255, 1.0, 0.5)

        local vBar = handle:CreateTexture(nil, "OVERLAY")
        vBar:SetSize(2, 14); vBar:SetPoint("BOTTOMRIGHT", 0, 0)
        vBar:SetTexture("Interface\\Buttons\\WHITE8x8")
        vBar:SetVertexColor(0, 207/255, 1.0, 0.5)

        handle:SetScript("OnEnter", function()
            hBar:SetVertexColor(0, 207/255, 1.0, 1.0)
            vBar:SetVertexColor(0, 207/255, 1.0, 1.0)
        end)
        handle:SetScript("OnLeave", function(self)
            if not self.isSizing then
                hBar:SetVertexColor(0, 207/255, 1.0, 0.5)
                vBar:SetVertexColor(0, 207/255, 1.0, 0.5)
            end
        end)
        handle:SetScript("OnMouseDown", function(self, btn)
            local d = db()
            if btn ~= "LeftButton" or (d and d.chatLocked) then return end
            frame:SetResizable(true)
            frame:StartSizing("BOTTOMRIGHT")
            self.isSizing = true
        end)
        handle:SetScript("OnMouseUp", function(self)
            if not self.isSizing then return end
            frame:StopMovingOrSizing()
            self.isSizing = false
            hBar:SetVertexColor(0, 207/255, 1.0, 0.5)
            vBar:SetVertexColor(0, 207/255, 1.0, 0.5)
            SavePosition(frame)
        end)

        frame.BC_ResizeHandle = handle
    end

    frame.BC_DragSetup  = true
    frame.BC_DragHandle = dragHandle
end

local function ApplyLock()
    local d      = db()
    local locked = d and d.chatLocked
    local dh     = ChatFrame1.BC_DragHandle
    if dh then dh:EnableMouse(not locked) end
    if ChatFrame1.BC_ResizeHandle then
        ChatFrame1.BC_ResizeHandle:SetShown(not locked)
    end
end
BC.ApplyLock = ApplyLock

function BC.SetupDrag()
    LockPosition()
    SetupDragOnBG()
    RestorePosition()
    ApplyLock()
end


---------------------------------------------------------------------------
-- Copy chat button (TOPRIGHT of chat frame)
---------------------------------------------------------------------------

local function SetupCopyButton()
    local frame = ChatFrame1
    if frame.BC_CopyBtn then return end

    local btn = CreateFrame("Frame", "BrightQoL_CopyChat", frame)
    btn:SetSize(20, 22)
    btn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, -4)
    btn:SetFrameLevel(frame:GetFrameLevel() + 10)
    btn:EnableMouse(true)
    btn:SetAlpha(0.35)

    local tex = btn:CreateTexture(nil, "OVERLAY")
    tex:SetAllPoints()
    tex:SetTexture("Interface\\AddOns\\BrightQoL\\Assets\\Chat\\Copy.tga")
    btn.texture = tex

    btn:SetScript("OnEnter", function(self)
        self:SetAlpha(1)
        GameTooltip:SetOwner(self, "ANCHOR_LEFT")
        GameTooltip:SetText("Copy Chat", 1, 1, 1)
        GameTooltip:Show()
    end)
    btn:SetScript("OnLeave", function(self)
        self:SetAlpha(0.35)
        GameTooltip:Hide()
    end)
    btn:SetScript("OnMouseUp", function(self, button)
        if button ~= "LeftButton" then return end
        local dock   = GENERAL_CHAT_DOCK
        local target = (dock and dock.selected) or ChatFrame1
        local lines  = {}
        local n      = target.GetNumMessages and target:GetNumMessages() or 0
        for i = 1, n do
            local msg = target.GetMessageInfo and target:GetMessageInfo(i)
            if msg then
                msg = msg:gsub("|c%x%x%x%x%x%x%x%x","")
                         :gsub("|r","")
                         :gsub("|H[^|]+|h([^|]*)|h","%1")
                         :gsub("|n","\\n")
                lines[#lines+1] = msg
            end
        end
        if #lines == 0 then return end

        if not BrightQoL_CopyChatPopup then
            local pop = CreateFrame("Frame","BrightQoL_CopyChatPopup",UIParent,"BackdropTemplate")
            pop:SetSize(500, 340)
            pop:SetPoint("CENTER")
            pop:SetFrameStrata("DIALOG")
            pop:SetMovable(true); pop:EnableMouse(true)
            pop:RegisterForDrag("LeftButton")
            pop:SetScript("OnDragStart", pop.StartMoving)
            pop:SetScript("OnDragStop",  pop.StopMovingOrSizing)
            pop:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8x8",edgeFile="Interface\\Buttons\\WHITE8x8",edgeSize=1})
            pop:SetBackdropColor(0.05,0.05,0.08,0.97)
            pop:SetBackdropBorderColor(0.2,0.2,0.25,0.9)
            tinsert(UISpecialFrames,"BrightQoL_CopyChatPopup")

            local title = pop:CreateFontString(nil,"OVERLAY","GameFontNormal")
            title:SetPoint("TOPLEFT",10,-8)
            title:SetText("Copy Chat  |cff888888(Ctrl+A, Ctrl+C)|r")
            title:SetTextColor(0,207/255,1,1)

            local x = CreateFrame("Button",nil,pop)
            x:SetSize(20,20); x:SetPoint("TOPRIGHT",-4,-4)
            local xt = x:CreateFontString(nil,"OVERLAY","GameFontNormal")
            xt:SetAllPoints(); xt:SetText("|cffff4444\195\151|r")
            x:SetScript("OnClick",function() pop:Hide() end)

            local scroll = CreateFrame("ScrollFrame",nil,pop,"UIPanelScrollFrameTemplate")
            scroll:SetPoint("TOPLEFT",pop,"TOPLEFT",8,-28)
            scroll:SetPoint("BOTTOMRIGHT",pop,"BOTTOMRIGHT",-28,8)

            local eb = CreateFrame("EditBox",nil,scroll)
            eb:SetMultiLine(true); eb:SetAutoFocus(false)
            eb:SetFontObject(ChatFontNormal)
            eb:SetWidth(scroll:GetWidth() or 450)
            eb:SetMaxLetters(0)
            eb:SetScript("OnEscapePressed",function() pop:Hide() end)
            scroll:SetScrollChild(eb)
            pop.editBox = eb
        end

        local pop = BrightQoL_CopyChatPopup
        pop.editBox:SetText(table.concat(lines,"\n"))
        pop:Show()
        pop.editBox:SetFocus()
        pop.editBox:HighlightText()
        pop.editBox:SetCursorPosition(0)
    end)

    frame.BC_CopyBtn = btn
end
BC.SetupCopyButton = SetupCopyButton


local function RefreshTabFont()
    local d = db()
    if not d then return end
    local font    = d.tabFont or (ns.Theme and ns.Theme.GetFont and ns.Theme.GetFont()) or "Fonts\\FRIZQT__.TTF"
    local LSM     = LibStub and LibStub("LibSharedMedia-3.0", true)
    if LSM and d.tabFont then font = LSM:Fetch("font", d.tabFont, true) or font end
    local size    = d.tabFontSize    or 11
    local outline = d.tabFontOutline or "OUTLINE"
    local height  = d.tabHeight      or 22
    for i = 1, NUM_CHAT_WINDOWS or 10 do
        local blizzTab = _G["ChatFrame"..i.."Tab"]
        if blizzTab and blizzTab.BC_Skin then
            blizzTab.BC_Skin.text:SetFont(font, size, outline)
            blizzTab:SetHeight(height)
        end
    end
    PackTabs()
end
BC.RefreshTabFont = RefreshTabFont


---------------------------------------------------------------------------
-- EditBox chat history
---------------------------------------------------------------------------

local HISTORY_MAX = 50

local function SetupEditBoxHistory(eb)
    if not eb or eb.BC_HistorySetup then return end

    -- Ensure the persistent table exists
    if not BrightQoLCharDB then BrightQoLCharDB = {} end
    if not BrightQoLCharDB.chatEditHistory then BrightQoLCharDB.chatEditHistory = {} end

    -- Point historyLines directly at the saved table (same reference = auto-persists)
    eb.historyLines  = BrightQoLCharDB.chatEditHistory
    eb.historyIndex  = 0
    eb.BC_HistorySaved = ""

    -- Hook AddHistoryLine to capture sent text into our persistent table
    if eb.AddHistoryLine then
        hooksecurefunc(eb, "AddHistoryLine", function(self, line)
            local lines = self.historyLines
            if not lines or not line then return end
            local msg = strtrim(line)
            if strlen(msg) == 0 then return end
            -- Block secure commands
            local cmd = msg:match("^(/%w+)")
            if cmd and IsSecureCmd and IsSecureCmd(cmd) then return end
            -- Remove duplicate
            for i, text in ipairs(lines) do
                if text == msg then tremove(lines, i); break end
            end
            tinsert(lines, msg)
            -- Trim to max
            while #lines > HISTORY_MAX do tremove(lines, 1) end
            -- Reset index so next UP starts from the newest
            self.historyIndex = 0
        end)
    end

    -- UP = older message, DOWN = newer message
    eb:HookScript("OnKeyDown", function(self, key)
        local lines = self.historyLines
        if not lines then return end
        local max = #lines
        if max == 0 then return end

        if key == "UP" then
            if self.historyIndex == 0 then
                self.BC_HistorySaved = self:GetText()
            end
            self.historyIndex = math.min(self.historyIndex + 1, max)
            local line = lines[max - (self.historyIndex - 1)]
            if line then self:SetText(line); self:SetCursorPosition(#line) end

        elseif key == "DOWN" then
            if self.historyIndex <= 0 then return end
            self.historyIndex = self.historyIndex - 1
            if self.historyIndex == 0 then
                local saved = self.BC_HistorySaved or ""
                self:SetText(saved)
                self:SetCursorPosition(#saved)
            else
                local line = lines[max - (self.historyIndex - 1)]
                if line then self:SetText(line); self:SetCursorPosition(#line) end
            end
        end
    end)

    -- Reset index on focus lost
    eb:HookScript("OnEditFocusLost", function(self)
        self.historyIndex  = 0
        self.BC_HistorySaved = ""
    end)

    -- Allow UP/DOWN arrows to be captured by OnKeyDown
    -- (by default WoW uses arrows to move cursor; this disables that)
    eb:SetAltArrowKeyMode(false)

    eb.BC_HistorySetup = true
end
BC.SetupEditBoxHistory = SetupEditBoxHistory


---------------------------------------------------------------------------
-- Class color player names
-- Sets ChatTypeInfo.colorNameByClass = true for all chat types.
-- WoW uses this flag natively to colour player names by class.
-- SAFEGUARD: skipped on Retail 12.x+ (secret values build) to avoid taint.
---------------------------------------------------------------------------

local function IsSecretValuesBuild()
    if WOW_PROJECT_ID ~= WOW_PROJECT_MAINLINE then return false end
    if type(GetBuildInfo) ~= "function" then return true end
    local ver = select(4, GetBuildInfo())
    return type(ver) == "number" and ver >= 120000
end

local function ApplyClassColorNames(enable)
    -- Never apply on secret values builds — would taint the chat pipeline
    if IsSecretValuesBuild() then return end
    for _, info in pairs(ChatTypeInfo) do
        if type(info) == "table" then
            info.colorNameByClass = enable or nil
        end
    end
end
BC.ApplyClassColorNames = ApplyClassColorNames

---------------------------------------------------------------------------
-- Refresh all frames
---------------------------------------------------------------------------

local function RefreshAll()
    if not SafeCall(RefreshAll) then return end
    for i = 1, NUM_CHAT_WINDOWS or 10 do
        local f = _G["ChatFrame"..i]
        if f then
            InitTab(f)
            RefreshChatFrame(f)
        end
    end
    PackTabs()
    SnapButtons()
    SetupCopyButton()
    -- Class colour names
    local _d = db()
    if _d then ApplyClassColorNames(_d.classColorNames) end
    -- Setup history on all editboxes
    for i = 1, NUM_CHAT_WINDOWS or 10 do
        local eb = _G["ChatFrame"..i.."EditBox"]
        if eb then SetupEditBoxHistory(eb) end
    end
    isRequestingRefresh = false
end
BC.RefreshAll = RefreshAll

---------------------------------------------------------------------------
-- Public API for Config panel
---------------------------------------------------------------------------

function BC.RefreshButtons()
    SnapButtons()
end

function BC.RefreshColors()
    for i = 1, NUM_CHAT_WINDOWS or 10 do
        local f = _G["ChatFrame"..i]
        if f then RefreshChatFrame(f) end
    end
end

function BC.RefreshTabs()
    PackTabs()
end

---------------------------------------------------------------------------
-- Initialization
---------------------------------------------------------------------------

local loader = CreateFrame("Frame")
loader:RegisterEvent("ADDON_LOADED")
loader:RegisterEvent("PLAYER_ENTERING_WORLD")
loader:RegisterEvent("PLAYER_REGEN_ENABLED")
loader:RegisterEvent("EDIT_MODE_LAYOUTS_UPDATED")
loader:SetScript("OnEvent", function(self, event, arg1)

    if event == "ADDON_LOADED" and arg1 == addonName then
        -- Hook alert flash to track alerting tabs
        hooksecurefunc("FCF_StartAlertFlash", function(f)
            f.BC_Alerting = true
            local tab = _G[f:GetName().."Tab"]
            if tab and tab.BC_Skin then UpdateTabVisuals(tab) end
        end)

        -- Refresh tab visuals on dock updates
        hooksecurefunc("FCF_DockUpdate", function()
            C_Timer.After(0.05, PackTabs)
        end)

        -- Track active frame
        hooksecurefunc("FCF_SelectDockFrame", function(f)
            activeFrame = f
            PackTabs()
        end)

        -- Temporary windows (whisper popups etc.)
        hooksecurefunc("FCF_OpenTemporaryWindow", function()
            C_Timer.After(0.1, RefreshAll)
        end)

        self:UnregisterEvent("ADDON_LOADED")

    elseif event == "PLAYER_ENTERING_WORLD" then
        local d = db()
        if not d or not d.enabled then return end

        activeFrame = (FCF_GetFocusedChatWindow and FCF_GetFocusedChatWindow()) or ChatFrame1
        RefreshAll()
        BC.SetupDrag()
        ApplyClassColorNames(db() and db().classColorNames)
        self:UnregisterEvent("PLAYER_ENTERING_WORLD")

    elseif event == "PLAYER_REGEN_ENABLED" then
        local d = db()
        if not d or not d.enabled then return end
        -- Retry any refresh deferred by combat lockdown
        C_Timer.After(0.1, RefreshAll)

    elseif event == "EDIT_MODE_LAYOUTS_UPDATED" then
        -- WoW repositions ChatFrame1 when Edit Mode saves a layout.
        -- Re-apply our saved position a tick later.
        local d = db()
        if not d or not d.enabled or not d.position then return end
        C_Timer.After(0.1, function()
            local dd = db()
            if dd and dd.position then
                ChatFrame1:SetUserPlaced(false)
                ChatFrame1:SetMovable(true)
                ChatFrame1:ClearAllPoints()
                ChatFrame1:SetPoint(
                    dd.position.point    or "BOTTOMLEFT",
                    UIParent,
                    dd.position.relPoint or "BOTTOMLEFT",
                    dd.position.x or 0,
                    dd.position.y or 0
                )
                if dd.chatSize then
                    ChatFrame1:SetSize(dd.chatSize.w, dd.chatSize.h)
                end
                ChatFrame1:SetUserPlaced(true)
                ChatFrame1:SetClampedToScreen(false)
                ChatFrame1:SetClampRectInsets(0, 0, 0, 0)
            end
        end)
    end
end)

-- Hook EditModeManagerFrame to re-apply position when Edit Mode panel closes
C_Timer.After(2, function()
    if EditModeManagerFrame and not EditModeManagerFrame.BC_Hooked then
        EditModeManagerFrame:HookScript("OnHide", function()
            local d = db()
            if not d or not d.enabled or not d.position then return end
            C_Timer.After(0.1, function()
                local dd = db()
                if dd and dd.position then
                    ChatFrame1:SetUserPlaced(false)
                    ChatFrame1:SetMovable(true)
                    ChatFrame1:ClearAllPoints()
                    ChatFrame1:SetPoint(
                        dd.position.point    or "BOTTOMLEFT",
                        UIParent,
                        dd.position.relPoint or "BOTTOMLEFT",
                        dd.position.x or 0,
                        dd.position.y or 0
                    )
                    if dd.chatSize then
                        ChatFrame1:SetSize(dd.chatSize.w, dd.chatSize.h)
                    end
                    ChatFrame1:SetUserPlaced(true)
                    ChatFrame1:SetClampedToScreen(false)
                    ChatFrame1:SetClampRectInsets(0, 0, 0, 0)
                end
            end)
        end)
        EditModeManagerFrame.BC_Hooked = true
    end
end)
