local addonName, ns = ...
local L = ns.L

local Visuals = {}
ns.Visuals = Visuals

local WHITE = "Interface\\Buttons\\WHITE8X8"
local TOOLTIP_BG = "Interface\\Tooltips\\UI-Tooltip-Background"
local TOOLTIP_BORDER = "Interface\\Tooltips\\UI-Tooltip-Border"
local GLOW_TEXTURE = "Interface\\AddOns\\ChatFloater\\Media\\Float\\softglow.tga"

local ICON_NAMES = {
    Say = "say",
    Yell = "yell",
    Whisper = "whisper",
    Party = "party",
    Raid = "raid",
    Instance = "instance",
    Guild = "guild",
    Dice = "dice",
    Macro = "macro",
    ReadyCheck = "ready",
    LeaveReset = "leave",
    Reload = "reload",
}

local ICON_RESOLUTIONS = { 16, 32, 64, 128, 256, 512 }

local CHANNEL_BUTTONS = {
    Say = true,
    Yell = true,
    Whisper = true,
    Party = true,
    Raid = true,
    Instance = true,
    Guild = true,
}

local function Clamp(value, minValue, maxValue)
    value = tonumber(value) or minValue
    if value < minValue then return minValue end
    if value > maxValue then return maxValue end
    return value
end

local function GetScale(db)
    return Clamp(db and db.chatUIScale or 1, 0.65, 1.60)
end

local function GetPanelAlpha(db)
    return Clamp(db and db.chatPanelOpacity or 0.82, 0, 1)
end

local function GetGlowStrength(db)
    return Clamp(db and db.chatGlowStrength or 0.75, 0, 1)
end

local function ReadableChannelColor(r, g, b, multiplier)
    multiplier = multiplier or 1
    return math.min(1, (0.22 + r * 0.78) * multiplier),
           math.min(1, (0.22 + g * 0.78) * multiplier),
           math.min(1, (0.22 + b * 0.78) * multiplier)
end

local function GetIconTexture(id, renderedSize)
    local name = ICON_NAMES[id]
    if not name then return nil end

    local px = math.max(1, tonumber(renderedSize) or 16)
    local selected = 512

    for _, size in ipairs(ICON_RESOLUTIONS) do
        if px <= size then
            selected = size
            break
        end
    end

    return "Interface\\AddOns\\ChatFloater\\Media\\Icons\\" ..
           tostring(selected) .. "\\" .. name .. ".tga"
end

function Visuals.IsChannelButton(id)
    return CHANNEL_BUTTONS[id] == true
end

function Visuals.GetButtonMetrics(def, db)
    local scale = GetScale(db)
    local width = CHANNEL_BUTTONS[def.id] and 43 or 31
    if def.id == "Whisper" then width = 47 end
    if def.id == "Instance" then width = 45 end

    return math.floor(width * scale + 0.5),
           math.floor(32 * scale + 0.5)
end

function Visuals.GetPanelPadding(db)
    local scale = GetScale(db)
    return math.floor(3 * scale + 0.5),
           math.floor(2 * scale + 0.5)
end

local function EnsureTexture(button, key, layer, sublevel)
    if not button[key] then
        button[key] = button:CreateTexture(nil, layer, nil, sublevel)
    end
    return button[key]
end

local function EnsureFont(button, key)
    if not button[key] then
        button[key] = button:CreateFontString(nil, "OVERLAY")
    end
    return button[key]
end

local function EnsureElements(button)
    local glow = EnsureTexture(button, "floatGlow", "ARTWORK", -7)
    glow:SetTexture(GLOW_TEXTURE)
    glow:SetBlendMode("ADD")
    glow:ClearAllPoints()
    glow:SetPoint("TOPLEFT", button, "TOPLEFT", -8, 8)
    glow:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 8, -8)
    glow:Hide()

    local icon = EnsureTexture(button, "floatIcon", "ARTWORK", 1)
    icon:SetTexCoord(0, 1, 0, 1)

    local label = EnsureFont(button, "floatLabel")
    label:SetJustifyH("CENTER")
    label:SetJustifyV("MIDDLE")

    local activeGlow = EnsureTexture(button, "floatActiveGlow", "ARTWORK", 4)
    activeGlow:SetTexture(GLOW_TEXTURE)
    activeGlow:SetBlendMode("ADD")
    activeGlow:Hide()

    local active = EnsureTexture(button, "floatActive", "OVERLAY", 5)
    active:SetTexture(WHITE)
    active:SetHeight(3)
    active:Hide()

    local hover = EnsureTexture(button, "floatHover", "ARTWORK", -2)
    hover:SetTexture(WHITE)
    hover:ClearAllPoints()
    hover:SetAllPoints(button)
    hover:Hide()

    local separator = EnsureTexture(button, "floatSeparator", "ARTWORK", 0)
    separator:SetTexture(WHITE)
    separator:Hide()
end

local function ClearLegacy(button)
    if button.bllText then button.bllText:Hide() end
    if button.bllBG then button.bllBG:Hide() end
end

local function ApplyFloat(button, def, db)
    local scale = GetScale(db)
    local r, g, b = def.color[1], def.color[2], def.color[3]
    local isChannel = CHANNEL_BUTTONS[def.id] == true
    local iconSize = math.floor((isChannel and 21 or 22) * scale + 0.5)

    button.floatIcon:ClearAllPoints()
    if isChannel then
        button.floatIcon:SetPoint("LEFT", button, "LEFT", 2 * scale, 0)
    else
        button.floatIcon:SetPoint("CENTER", button, "CENTER", 0, 0)
    end
    button.floatIcon:SetSize(iconSize, iconSize)
    button.floatIcon:SetTexture(GetIconTexture(def.id, iconSize))
    button.floatIcon:SetVertexColor(1, 1, 1, 1)
    button.floatIcon:Show()

    button.floatLabel:ClearAllPoints()
    if isChannel then
        button.floatLabel:SetPoint("LEFT", button.floatIcon, "RIGHT", 2 * scale, 0)
        button.floatLabel:SetPoint("RIGHT", button, "RIGHT", -1 * scale, 0)
        button.floatLabel:SetFont(STANDARD_TEXT_FONT, math.max(13, math.floor(13 * scale + 0.5)), "OUTLINE")
        button.floatLabel:SetText(def.shortName or "")
        local lr, lg, lb = ReadableChannelColor(r, g, b, 1)
        button.floatLabel:SetTextColor(lr, lg, lb, 1)
        button.floatLabel:Show()
    else
        button.floatLabel:Hide()
    end

    button.floatActive:ClearAllPoints()
    button.floatActive:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", 4 * scale, 0)
    button.floatActive:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -4 * scale, 0)
    button.floatActive:SetHeight(math.max(2, math.floor(2 * scale + 0.5)))
    button.floatActive:SetVertexColor(r, g, b, 1)

    button.floatActiveGlow:ClearAllPoints()
    button.floatActiveGlow:SetPoint("BOTTOMLEFT", button.floatActive, "BOTTOMLEFT", -5 * scale, -5 * scale)
    button.floatActiveGlow:SetPoint("TOPRIGHT", button.floatActive, "TOPRIGHT", 5 * scale, 5 * scale)
    button.floatActiveGlow:SetVertexColor(r, g, b, 1)

    button.floatSeparator:ClearAllPoints()
    if (db.chatLayoutDirection or "HORIZONTAL") == "VERTICAL" then
        button.floatSeparator:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", 4 * scale, 0)
        button.floatSeparator:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -4 * scale, 0)
        button.floatSeparator:SetHeight(1)
    else
        button.floatSeparator:SetPoint("RIGHT", button, "RIGHT", 0, 0)
        button.floatSeparator:SetSize(1, math.max(12, math.floor(15 * scale + 0.5)))
    end
    button.floatSeparator:SetVertexColor(0.35, 0.40, 0.47, 0.14)
    button.floatSeparator:Show()
end

function Visuals.ApplyButton(button, def, db)
    EnsureElements(button)
    ClearLegacy(button)

    button.floatDef = def
    button.floatIsHover = false
    button.floatIsPressed = false

    ApplyFloat(button, def, db)
    Visuals.UpdateState(button, db)
end

function Visuals.UpdateState(button, db)
    if not button or not button.floatDef then return end

    local def = button.floatDef
    local r, g, b = def.color[1], def.color[2], def.color[3]
    local hover = button.floatIsHover == true
    local pressed = button.floatIsPressed == true
    local active = button.floatIsActive == true and
                   (not db or db.chatShowActiveChannel ~= false)
    local glowStrength = GetGlowStrength(db)

    local dim = pressed and 0.55 or (hover and 1.0 or 0.92)
    button.floatIcon:SetVertexColor(dim, dim, dim, 1)

    if CHANNEL_BUTTONS[def.id] then
        local lr, lg, lb = ReadableChannelColor(r, g, b, dim)
        button.floatLabel:SetTextColor(lr, lg, lb, 1)
    end

    button.floatHover:SetVertexColor(r, g, b, 0.05)
    if hover then button.floatHover:Show() else button.floatHover:Hide() end

    if active then
        button.floatActive:Show()
        button.floatActiveGlow:SetAlpha(0.70 * glowStrength)
        button.floatActiveGlow:Show()
    else
        button.floatActive:Hide()
        button.floatActiveGlow:Hide()
    end

    if hover or active then
        button.floatGlow:SetVertexColor(r, g, b, 1)
        button.floatGlow:SetAlpha((active and 0.62 or 0.24) * glowStrength)
        button.floatGlow:Show()
    else
        button.floatGlow:Hide()
    end
end

function Visuals.SetHover(button, state, db)
    if not button then return end
    button.floatIsHover = state and true or false
    Visuals.UpdateState(button, db)
end

function Visuals.SetPressed(button, state, db)
    if not button then return end
    button.floatIsPressed = state and true or false
    Visuals.UpdateState(button, db)
end

function Visuals.SetActive(button, state, db)
    if not button then return end
    button.floatIsActive = state and true or false
    Visuals.UpdateState(button, db)
end

function Visuals.ApplyPanel(frame, db)
    if not frame or not frame.SetBackdrop then return end

    local alpha = GetPanelAlpha(db)
    local showBackground = not db or db.chatPanelBackground ~= false

    -- Background and border are intentionally independent. Turning off
    -- "Panel Background" must never remove or dim the configured border.
    if showBackground then
        frame:SetBackdrop({
            bgFile = TOOLTIP_BG,
            tile = true,
            tileSize = 16,
            insets = { left = 3, right = 3, top = 3, bottom = 3 },
        })
        frame:SetBackdropColor(0.008, 0.014, 0.022, 0.55 * alpha)
    else
        frame:SetBackdrop(nil)
    end

    local borderFrame = frame.floatBorderFrame
    if not borderFrame then
        borderFrame = CreateFrame("Frame", nil, frame, "BackdropTemplate")
        borderFrame:EnableMouse(false)
        borderFrame:SetFrameLevel(frame:GetFrameLevel())
        frame.floatBorderFrame = borderFrame
    end

    local texture = db and db.chatPanelBorderTexture or "BLIZZARD_TOOLTIP"
    local borderOpacity = Clamp(db and db.chatPanelBorderOpacity or 0.90, 0, 1)
    if texture == "NONE" or borderOpacity <= 0 then
        borderFrame:SetBackdrop(nil)
        borderFrame:Hide()
        return
    end

    local borderSize = math.floor(Clamp(db and db.chatPanelBorderSize or 10, 1, 32) + 0.5)
    local borderOffset = math.floor(Clamp(db and db.chatPanelBorderOffset or 0, 0, 16) + 0.5)

    local color = db and db.chatPanelBorderColor
    local br = Clamp(color and color.r or 0.38, 0, 1)
    local bg = Clamp(color and color.g or 0.44, 0, 1)
    local bb = Clamp(color and color.b or 0.52, 0, 1)

    local edgeFile = TOOLTIP_BORDER
    if texture == "SOLID" then
        edgeFile = WHITE
    else
        local mediaName = type(texture) == "string" and texture:match("^LSM:(.+)$")
        if mediaName and ns.FetchSharedMediaBorder then
            edgeFile = ns.FetchSharedMediaBorder(mediaName) or TOOLTIP_BORDER
        end
    end

    borderFrame:ClearAllPoints()
    borderFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", -borderOffset, borderOffset)
    borderFrame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", borderOffset, -borderOffset)
    borderFrame:SetBackdrop({
        edgeFile = edgeFile,
        edgeSize = borderSize,
        insets = { left = 0, right = 0, top = 0, bottom = 0 },
    })
    -- Border opacity is independent from Panel opacity/background opacity.
    borderFrame:SetBackdropBorderColor(br, bg, bb, borderOpacity)
    borderFrame:Show()
end

local function AddTooltipLines(text)
    if type(text) ~= "string" then return end
    for line in text:gmatch("[^\n]+") do
        GameTooltip:AddLine(line, 0.88, 0.88, 0.88, true)
    end
end

function Visuals.ShowTooltip(button, def)
    if not GameTooltip or not button or not def then return end
    GameTooltip:SetOwner(button, "ANCHOR_TOP", 0, 8)
    GameTooltip:ClearLines()

    local r, g, b = def.color[1], def.color[2], def.color[3]
    GameTooltip:SetText(def.displayName or def.tooltip or def.shortName or "ChatFloater", r, g, b)
    if def.tooltip and def.tooltip ~= def.displayName then
        AddTooltipLines(def.tooltip)
    end

    GameTooltip:AddLine(" ")
    GameTooltip:AddLine("ChatFloater • /chatfloat", 0.48, 0.72, 1.0, true)
    GameTooltip:Show()
end

function Visuals.HideTooltip()
    if GameTooltip then GameTooltip:Hide() end
end
