local _, addon = ...
local HLY = addon.HLY
local P = addon.P
local pairs = pairs
local pcall = pcall
local tonumber = tonumber
local type = type
local _G = _G

local LibStub = _G.LibStub
local AceDB = LibStub("AceDB-3.0")

local BACKDROP_STYLE = {
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Buttons\\WHITE8x8",
    edgeSize = 1,
    insets = { left = 0, right = 0, top = 0, bottom = 0 },
}

local FONT_FALLBACKS = {
    Homespun = "Fonts\\ARIALN.TTF",
    PTSansNarrow = "Fonts\\ARIALN.TTF",
}

function HLY:PLAYER_EQUIPMENT_CHANGED()
    self:RefreshCharacterSlots()
    self:RefreshCompareStats()
end

function HLY:INSPECT_READY()
    self:RefreshInspectSlots()
    self:RefreshCompareStats()
end

function HLY:CopyTable(source)
    if type(source) ~= "table" then
        return source
    end

    local result = {}
    for key, value in pairs(source) do
        result[key] = self:CopyTable(value)
    end
    return result
end

function HLY:GetFontPath(fontName)
    if addon.LSM then
        local path = addon.LSM:Fetch("font", fontName, true)
        if path then
            return path
        end
    end

    return FONT_FALLBACKS[fontName] or STANDARD_TEXT_FONT
end

function HLY:GetFontValues()
    if addon.LSM then
        return addon.LSM:HashTable("font")
    end

    return {
        Homespun = "Homespun",
        Default = "Default",
    }
end

function HLY:GetFontList()
    if addon.LSM and addon.LSM.List then
        local values = {}
        for _, fontName in ipairs(addon.LSM:List("font")) do
            values[fontName] = fontName
        end
        return values
    end

    return self:GetFontValues()
end

function HLY:ApplyFont(text, fontName, fontSize, fontOutline)
    if not text then
        return
    end

    local flags = fontOutline
    if flags == "NONE" then
        flags = ""
    elseif flags == "SHADOWOUTLINE" then
        flags = "OUTLINE"
    end

    local fontPath = self:GetFontPath(fontName)
    local applied = fontPath and text:SetFont(fontPath, fontSize, flags)

    if not applied then
        applied = text:SetFont("Fonts\\FRIZQT__.TTF", fontSize, flags)
    end

    if not applied then
        local defaultFontObject = _G.GameFontNormal
        if defaultFontObject then
            local defaultPath, defaultSize, defaultFlags = defaultFontObject:GetFont()
            applied = defaultPath and text:SetFont(defaultPath, fontSize or defaultSize or 12, flags ~= "" and flags or defaultFlags)
        end
    end

    if not applied then
        text:SetFontObject(_G.GameFontNormal)
    end

    if fontOutline == "SHADOWOUTLINE" then
        text:SetShadowColor(0, 0, 0, 1)
        text:SetShadowOffset(1, -1)
    else
        text:SetShadowColor(0, 0, 0, 0)
        text:SetShadowOffset(0, 0)
    end
end

function HLY:SetPoint(object, point, xOffset, yOffset)
    if not object then
        return
    end

    object:ClearAllPoints()
    object:SetPoint(point or "CENTER", xOffset or 0, yOffset or 0)
end

function HLY:ApplyBackdrop(frame)
    if not frame or frame.hlyBackdropApplied or not frame.SetBackdrop then
        return
    end

    frame:SetBackdrop(BACKDROP_STYLE)
    frame:SetBackdropColor(0.05, 0.05, 0.05, 0.92)
    frame:SetBackdropBorderColor(0.35, 0.35, 0.35, 1)
    frame.hlyBackdropApplied = true
end

function HLY:ShortValue(value, decimals)
    value = tonumber(value) or 0
    decimals = decimals or 0

    local absValue = math.abs(value)
    if absValue >= 1000000 then
        return string.format("%." .. decimals .. "fm", value / 1000000)
    end
    if absValue >= 1000 then
        return string.format("%." .. decimals .. "fk", value / 1000)
    end
    if decimals > 0 and value ~= math.floor(value) then
        return string.format("%." .. decimals .. "f", value)
    end

    return tostring(math.floor(value + 0.5))
end

function HLY:GetClassColor(classFile)
    local color = classFile and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile]
    return color or NORMAL_FONT_COLOR
end

-- 12.1 ("secret values"): unit/aura APIs may return secrets during combat,
-- encounters, M+ and PvP. type() is safe on them, but comparing a secret to a
-- string errors. Strip secrets to nil at the boundary so callers can compare.
function HLY:PlainValue(value)
    if value == nil then
        return nil
    end
    if issecretvalue and issecretvalue(value) then
        return nil
    end
    return value
end

-- Role lookup that is safe on both live (12.0.x) and 12.1+.
function HLY:GetUnitRole(unit)
    return self:PlainValue(self:SafeCall(UnitGroupRolesAssigned, unit))
end

function HLY:SafeCall(func, ...)
    if not func then
        return nil
    end

    local ok, result = pcall(func, ...)
    if ok then
        return result
    end

    return nil
end

function HLY:SafeNumber(value, fallback)
    local ok, numberValue = pcall(tonumber, value)
    if ok and numberValue then
        local usable = pcall(function()
            return numberValue >= 0 or numberValue < 0
        end)
        if usable then
            return numberValue
        end
    end

    return fallback or 0
end

function HLY:GetEquippedItemLevel(unit)
    local total, count = 0, 0
    for slot = 1, 17 do
        local link = self:SafeCall(GetInventoryItemLink, unit, slot)
        if link then
            local detailedItemLevel = self:SafeCall(GetDetailedItemLevelInfo, link)
            local itemLevel = self:SafeNumber(detailedItemLevel)
            if itemLevel <= 0 then
                local ok, itemName, itemLink, itemQuality, fallbackItemLevel = pcall(GetItemInfo, link)
                itemLevel = ok and self:SafeNumber(fallbackItemLevel) or 0
            end
            if itemLevel and itemLevel > 0 then
                total = total + itemLevel
                count = count + 1
            end
        end
    end

    if count == 0 then
        return 0
    end

    return total / count
end

function HLY:GetUnitItemLevel(unit)
    if unit == "player" and C_PaperDollInfo and C_PaperDollInfo.GetItemLevel then
        local ok, overall, equipped = pcall(C_PaperDollInfo.GetItemLevel)
        local itemLevel = 0
        if ok then
            itemLevel = self:SafeNumber(equipped)
            if itemLevel <= 0 then
                itemLevel = self:SafeNumber(overall)
            end
        end
        if itemLevel and itemLevel > 0 then
            return itemLevel
        end
    elseif unit ~= "player" and C_PaperDollInfo and C_PaperDollInfo.GetInspectItemLevel then
        local itemLevel = self:SafeNumber(self:SafeCall(C_PaperDollInfo.GetInspectItemLevel, unit))
        if itemLevel and itemLevel > 0 then
            return itemLevel
        end
    end

    return self:GetEquippedItemLevel(unit)
end

function HLY:GetBagItemLink(bagID, slotID)
    bagID = tonumber(bagID)
    slotID = tonumber(slotID)
    if not bagID or not slotID or slotID < 1 then
        return nil
    end

    if C_Container and C_Container.GetContainerItemLink then
        local ok, itemLink = pcall(C_Container.GetContainerItemLink, bagID, slotID)
        if ok then
            return itemLink
        end
        return nil
    end
    if GetContainerItemLink then
        local ok, itemLink = pcall(GetContainerItemLink, bagID, slotID)
        if ok then
            return itemLink
        end
        return nil
    end

    return nil
end

function HLY:GetElvUIBags()
    if self.elvuiBags ~= nil then
        return self.elvuiBags or nil
    end

    if type(_G.ElvUI) == "table" then
        local ok, engine = pcall(unpack, _G.ElvUI)
        if ok and engine and engine.GetModule then
            local found, module = pcall(engine.GetModule, engine, "Bags", true)
            if found and module then
                self.elvuiBags = module
                return module
            end
        end
    end

    self.elvuiBags = false
    return nil
end

function HLY:GetBagButtonInfo(button)
    if not button then
        return nil, nil
    end

    local objectType
    if button.GetObjectType then
        local ok, value = pcall(button.GetObjectType, button)
        if ok then
            objectType = value
        end
    end
    if objectType ~= "Button" and objectType ~= "CheckButton" and objectType ~= "ItemButton" then
        return nil, nil
    end

    local bagID = button.bagID or button.BagID or button.bag
    if bagID == nil and button.GetBagID then
        local ok, value = pcall(button.GetBagID, button)
        if ok then
            bagID = value
        end
    end
    if bagID == nil and button.GetBag then
        local ok, value = pcall(button.GetBag, button)
        if ok then
            bagID = value
        end
    end

    local slotID = button.slotID or button.SlotID or button.slot
    if slotID == nil and button.GetID then
        local ok, value = pcall(button.GetID, button)
        if ok then
            slotID = value
        end
    end
    if slotID == nil and button.GetSlot then
        local ok, value = pcall(button.GetSlot, button)
        if ok then
            slotID = value
        end
    end

    if bagID == nil then
        local parent = button:GetParent()
        if parent then
            bagID = parent.bagID or parent.BagID
            if bagID == nil and parent.GetID then
                local ok, value = pcall(parent.GetID, parent)
                if ok then
                    bagID = value
                end
            end
        end
    end

    if bagID == nil or slotID == nil then
        return nil, nil
    end

    bagID = tonumber(bagID)
    slotID = tonumber(slotID)
    if not bagID or not slotID or slotID < 1 then
        return nil, nil
    end

    return bagID, slotID
end

function HLY:GetBagRoots()
    local roots, seen = {}, {}

    local function isFrameObject(frame)
        if type(frame) ~= "table" or not frame.GetObjectType then
            return false
        end

        local ok, objectType = pcall(frame.GetObjectType, frame)
        return ok and objectType ~= nil
    end

    local function addRoot(frame)
        if isFrameObject(frame) and not seen[frame] then
            seen[frame] = true
            roots[#roots + 1] = frame
        end
    end

    addRoot(_G.ContainerFrameCombinedBags)
    addRoot(_G.BankPanel)
    addRoot(_G.AccountBankPanel)
    for index = 1, 20 do
        addRoot(_G["ContainerFrame" .. index])
    end

    local elvuiBags = self:GetElvUIBags()
    if elvuiBags then
        addRoot(elvuiBags.BagFrame)
        addRoot(elvuiBags.BankFrame)
    end

    local bagnon = _G.Bagnon
    local bagnonFrames = bagnon and bagnon.Frames
    if bagnonFrames then
        for _, id in ipairs({ "inventory", "bank" }) do
            local frame = self:SafeCall(bagnonFrames.Get, bagnonFrames, id)
            addRoot(frame)
            addRoot(frame and frame.ItemGroup)
        end

        if bagnonFrames.Iterate then
            local ok, iterator, state, initial = pcall(bagnonFrames.Iterate, bagnonFrames)
            if ok and iterator then
                for _, frame in iterator, state, initial do
                    addRoot(frame)
                    addRoot(frame and frame.ItemGroup)
                end
            end
        end
    end

    addRoot(_G.BagnonInventory1)
    addRoot(_G.BagnonBank1)
    addRoot(_G.BagnonInventoryFrame)
    addRoot(_G.BagnonBankFrame)
    addRoot(_G.BagnonBackpackFrame)

    addRoot(_G.BaganatorBackpackViewFrame)
    addRoot(_G.BaganatorBankViewFrame)
    addRoot(_G.BaganatorSingleViewBackpackViewFrame)
    addRoot(_G.BaganatorSingleViewBankViewFrame)

    addRoot(_G.EUI_MainBagFrame)
    addRoot(_G.EUI_ReagentBagFrame)
    addRoot(_G.EUI_BagsWindowFrame)
    addRoot(_G.EUI_BankFrame)

    return roots
end

function HLY:CollectBagButtons(root, results, seenFrames)
    if not root or seenFrames[root] then
        return
    end

    seenFrames[root] = true

    local bagID, slotID = self:GetBagButtonInfo(root)
    if bagID ~= nil and slotID ~= nil then
        results[#results + 1] = { button = root, bagID = bagID, slotID = slotID }
    end

    local ok, children = pcall(function()
        return { root:GetChildren() }
    end)

    if not ok or not children then
        return
    end

    for _, child in pairs(children) do
        if child then
            self:CollectBagButtons(child, results, seenFrames)
        end
    end
end

function HLY:ForEachBagSlot(callback)
    local results, seenFrames, seenButtons = {}, {}, {}

    for _, root in ipairs(self:GetBagRoots()) do
        self:CollectBagButtons(root, results, seenFrames)
    end

    for _, entry in pairs(results) do
        if not seenButtons[entry.button] then
            seenButtons[entry.button] = true
            callback(entry.button, entry.bagID, entry.slotID)
        end
    end
end

function HLY:HandleBagRefresh()
    self:RefreshAllSlots()
    self:RefreshBindSlots()
end

function HLY:QueueBagRefresh()
    if self.bagRefreshPending then
        return
    end

    self.bagRefreshPending = true
    C_Timer.After(0.05, function()
        self.bagRefreshPending = nil
        self:HandleBagRefresh()
    end)
end

function HLY:GetBagCacheKey(bagID, slotID)
    return tostring(bagID) .. ":" .. tostring(slotID)
end

function HLY:SafeRegisterEvent(eventName, method)
    local ok = pcall(self.RegisterEvent, self, eventName, method)
    return ok
end

function HLY:HookBagFrameShow(frame)
    if type(frame) ~= "table" or frame.hlyBagHooked or not frame.GetObjectType or not frame.HookScript then
        return
    end

    local ok = pcall(frame.GetObjectType, frame)
    if not ok then
        return
    end

    ok = pcall(frame.HookScript, frame, "OnShow", function()
            self:QueueBagRefresh()
    end)
    if ok then
        frame.hlyBagHooked = true
    end
end

function HLY:HookKnownBagFrames()
    local roots = self:GetBagRoots()
    for _, frame in ipairs(roots) do
        self:HookBagFrameShow(frame)
    end
end

function HLY:SetupBagnonHooks()
    if self.bagnonHooksSet then
        return
    end

    local bagnon = _G.Bagnon
    local bagnonFrames = bagnon and bagnon.Frames
    if not bagnonFrames then
        return
    end

    if bagnonFrames.New then
        hooksecurefunc(bagnonFrames, "New", function()
            self:HookKnownBagFrames()
            self:QueueBagRefresh()
        end)
    end
    if bagnonFrames.Show then
        hooksecurefunc(bagnonFrames, "Show", function()
            self:HookKnownBagFrames()
            self:QueueBagRefresh()
        end)
    end
    if bagnonFrames.Update then
        hooksecurefunc(bagnonFrames, "Update", function()
            self:QueueBagRefresh()
        end)
    end

    self.bagnonHooksSet = true
    self.waitingForBagnonHooks = nil
    self:HookKnownBagFrames()
end

function HLY:SetupBagHooks()
    if self.bagHooksSet then
        return
    end

    self:SafeRegisterEvent("BAG_UPDATE_DELAYED", "QueueBagRefresh")
    self:SafeRegisterEvent("PLAYERBANKSLOTS_CHANGED", "QueueBagRefresh")
    self:SafeRegisterEvent("BANKFRAME_OPENED", "QueueBagRefresh")
    self:SafeRegisterEvent("PLAYERREAGENTBANKSLOTS_CHANGED", "QueueBagRefresh")
    self:SafeRegisterEvent("BAG_CLOSED", "QueueBagRefresh")
    self:SafeRegisterEvent("BAG_OPEN", "QueueBagRefresh")

    if _G.ContainerFrame_UpdateAll then
        hooksecurefunc("ContainerFrame_UpdateAll", function()
            self:QueueBagRefresh()
        end)
    end

    local elvuiBags = self:GetElvUIBags()
    if elvuiBags then
        hooksecurefunc(elvuiBags, "UpdateSlot", function(_, frame, bagID, slotID)
            local bag = frame and frame.Bags and frame.Bags[bagID]
            local slot = bag and bag[slotID]
            if slot then
                self:UpdateSlotUpgradeText(slot, bagID, slotID)
                self:UpdateSlotBindText(slot, bagID, slotID)
            end
        end)

        hooksecurefunc(elvuiBags, "UpdateAllSlots", function()
            self:QueueBagRefresh()
        end)
    end

    self:HookBagFrameShow(_G.ContainerFrameCombinedBags)
    self:HookBagFrameShow(_G.BankPanel)
    self:HookBagFrameShow(_G.AccountBankPanel)
    self:HookBagFrameShow(_G.BagnonInventory1)
    self:HookBagFrameShow(_G.BagnonBank1)
    self:HookBagFrameShow(_G.BagnonInventoryFrame)
    self:HookBagFrameShow(_G.BagnonBankFrame)
    self:HookBagFrameShow(_G.BagnonBackpackFrame)
    self:HookBagFrameShow(_G.BaganatorBackpackViewFrame)
    self:HookBagFrameShow(_G.BaganatorBankViewFrame)
    self:HookBagFrameShow(_G.BaganatorSingleViewBackpackViewFrame)
    self:HookBagFrameShow(_G.BaganatorSingleViewBankViewFrame)
    self:HookBagFrameShow(_G.EUI_MainBagFrame)
    self:HookBagFrameShow(_G.EUI_ReagentBagFrame)
    self:HookBagFrameShow(_G.EUI_BagsWindowFrame)
    self:HookBagFrameShow(_G.EUI_BankFrame)

    -- EllesmereUIBags re-renders (incl. bank tab switch to warband, which fires no
    -- game event) go through these methods — refresh our overlay after each.
    if _G.EUI_Bank and _G.EUI_Bank.RefreshBank and not self.euiBankHooked then
        hooksecurefunc(_G.EUI_Bank, "RefreshBank", function() self:QueueBagRefresh() end)
        self.euiBankHooked = true
    end
    if _G.EUI_Bags and _G.EUI_Bags.RefreshInventory and not self.euiBagsHooked then
        hooksecurefunc(_G.EUI_Bags, "RefreshInventory", function() self:QueueBagRefresh() end)
        self.euiBagsHooked = true
    end

    if elvuiBags then
        self:HookBagFrameShow(elvuiBags.BagFrame)
        self:HookBagFrameShow(elvuiBags.BankFrame)
    end

    for index = 1, 20 do
        self:HookBagFrameShow(_G["ContainerFrame" .. index])
    end

    if not self.bagnonHooksSet then
        self:SetupBagnonHooks()
        if not self.bagnonHooksSet then
            self.waitingForBagnonHooks = true
            self:RegisterEvent("ADDON_LOADED")
        end
    end

    self:HookKnownBagFrames()
    self.bagHooksSet = true
end

function HLY:OnInitialize()
    self.database = AceDB:New("HLYQoLDB", {
        profile = self:CopyTable(P.hlyqol),
    }, true)

    self.db = self.database.profile

    self:RegisterChatCommand("hlyqol", "OpenOptions")
    self:RegisterChatCommand("hlyqol2", "OpenOptions")
end

function HLY:OnEnable()
    self:SetupBagHooks()
    self:SetupUpgradeTrackHooks()
    self:SetupBindHooks()
    self:SetupInspectCompareHooks()
    self:SetupAutoAccept()
    self:SetupJoinReminder()
    self:SetupDeathAlert()
    self:SetupTrinketAlert()
    self:SetupLoot()
    self:SetupBonusRoll()
    self:SetupLustAlert()
    self:SetupGearCheck()
    self:SetupHLYstats()
    self:SetupMinimapButton()
    self:SetupFrenchConnection()
    self:SetupNotAgain()
    self:SetupMarker()
    self:SetupFishing()
    self:SetupStatistics()

    self:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
    self:RegisterEvent("INSPECT_READY")

    self:RefreshAllSlots()
    self:RefreshBindSlots()
    self:RefreshCharacterSlots()
    self:RefreshInspectSlots()
    self:RefreshCompareStats()
end

