-- BagTags: marks equipment-set and BoE items in your bags and bank.
--## Author: zmudzinski ## Version: 1.0.0 ## IconTexture: Interface\Icons\INV_Misc_Bag_08
-- Configuration ------------------------------------------------------------
local LABEL_BOE = GetLocale() == "zhCN" and "装绑" or "BOE"
local LABEL_WOA = GetLocale() == "zhCN" and "战团" or "WOA"
local LABEL_SET = GetLocale() == "zhCN" and "方案" or "SET"
local SET_COLOR = { r = 0.40, g = 0.80, b = 1.00 } -- light blue: equipment set
local BOE_COLOR = { r = 0.20, g = 1.00, b = 0.30 } -- green: bind-on-equip
local WOA_COLOR = { r = 1.00, g = 0.50, b = 0.10 } -- orange: warbound on account

local BORDER_THICKNESS = 2

-- Equipment-set membership for a slot. The query moved into C_Container; fall
-- back to the legacy global if this client still exposes it there.
local GetSlotEquipmentSetInfo = (C_Container and C_Container.GetContainerItemEquipmentSetInfo)
    or GetContainerItemEquipmentSetInfo

-- Overlay (text + border) --------------------------------------------------

local function CreateBorder(button)
    local border = {}

    local function CreateEdge()
        local edge = button:CreateTexture(nil, "OVERLAY", nil, 7)
        edge:SetColorTexture(1, 1, 1, 1)
        return edge
    end

    border.top = CreateEdge()
    border.top:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0)
    border.top:SetPoint("TOPRIGHT", button, "TOPRIGHT", 0, 0)
    border.top:SetHeight(BORDER_THICKNESS)

    border.bottom = CreateEdge()
    border.bottom:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", 0, 0)
    border.bottom:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 0, 0)
    border.bottom:SetHeight(BORDER_THICKNESS)

    border.left = CreateEdge()
    border.left:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0)
    border.left:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", 0, 0)
    border.left:SetWidth(BORDER_THICKNESS)

    border.right = CreateEdge()
    border.right:SetPoint("TOPRIGHT", button, "TOPRIGHT", 0, 0)
    border.right:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 0, 0)
    border.right:SetWidth(BORDER_THICKNESS)

    function border:SetColor(r, g, b)
        for _, key in ipairs({ "top", "bottom", "left", "right" }) do
            self[key]:SetVertexColor(r, g, b)
            self[key]:Show()
        end
    end

    function border:Hide()
        self.top:Hide()
        self.bottom:Hide()
        self.left:Hide()
        self.right:Hide()
    end

    return border
end

local function GetOverlay(button)
    if not button.BagTagsText then
        local text = button:CreateFontString(nil, "OVERLAY")
        text:SetDrawLayer("OVERLAY", 7)
        text:SetFont(STANDARD_TEXT_FONT, 10, "OUTLINE")
        text:SetPoint("TOPRIGHT", button, "TOPRIGHT", 0, -2)
        text:SetJustifyH("RIGHT")
        button.BagTagsText = text
        button.BagTagsBorder = CreateBorder(button)
    end
    return button.BagTagsText, button.BagTagsBorder
end

local function ShowMark(button, label, color)
    local text, border = GetOverlay(button)
    text:SetText(label)
    text:SetTextColor(color.r, color.g, color.b)
    text:Show()
    border:SetColor(color.r, color.g, color.b)
end

local function HideMark(button)
    if button.BagTagsText then
        button.BagTagsText:Hide()
        button.BagTagsBorder:Hide()
    end
end

-- Detection ----------------------------------------------------------------

local function GetSetName(bag, slot)
    if not GetSlotEquipmentSetInfo then
        return nil
    end
    local inSet, setNames = GetSlotEquipmentSetInfo(bag, slot)
    if not inSet then
        return nil
    end
    -- setNames is a comma/newline-separated list; keep the corner tag compact.
    local name = (setNames and setNames:match("^[^,\n]+")) or LABEL_SET
    if #name > 6 then
        name = name:sub(1, 6)
    end
    return name
end

-- An item is BoE only if its base bind type is on-equip AND this specific
-- instance is still unbound (an equipped-then-removed item reports isBound).
local function IsBoE(slotInfo)
    if not slotInfo or slotInfo.isBound or not slotInfo.hyperlink then
        return false
    end
    local bindType = select(14, C_Item.GetItemInfo(slotInfo.hyperlink))
    return bindType == Enum.ItemBind.OnEquip
end

-- An item is Warbound on Account when it can move across the warband: either
-- still warbound-until-equipped (not yet bound), or already account-bound
-- (bound, and permitted in the account/warband bank as opposed to soulbound).
local function IsWarbound(bag, slot, slotInfo)
    if not ItemLocation then
        return false
    end
    local location = ItemLocation:CreateFromBagAndSlot(bag, slot)
    if not location or not location:IsValid() then
        return false
    end

    if C_Item.IsBoundToAccountUntilEquip and C_Item.IsBoundToAccountUntilEquip(location) then
        return true
    end

    if slotInfo.isBound and C_Bank and C_Bank.IsItemAllowedInBankType and Enum.BankType then
        return C_Bank.IsItemAllowedInBankType(Enum.BankType.Account, location) or false
    end

    return false
end

-- Update flow --------------------------------------------------------------

-- Bag item buttons expose the container via GetBagID()/GetID(); reworked bank
-- item buttons expose it as bankTabID/containerSlotID (with a GetBankTabID
-- accessor). Resolve both so one updater serves bags and bank.
local function GetButtonLocation(button)
    local bag = button.bankTabID
    if bag == nil and button.GetBankTabID then
        bag = button:GetBankTabID()
    end
    if bag == nil and button.GetBagID then
        bag = button:GetBagID()
    end

    local slot = button.containerSlotID
    if slot == nil and button.GetID then
        slot = button:GetID()
    end

    return bag, slot
end

local function UpdateButton(button)
    local bag, slot = GetButtonLocation(button)
    if bag == nil or slot == nil then
        HideMark(button)
        return
    end

    local slotInfo = C_Container.GetContainerItemInfo(bag, slot)
    if not slotInfo then
        HideMark(button)
        return
    end

    local setName = GetSetName(bag, slot)
    if setName then
        ShowMark(button, setName, SET_COLOR)
    elseif IsWarbound(bag, slot, slotInfo) then
        ShowMark(button, LABEL_WOA, WOA_COLOR)
    elseif IsBoE(slotInfo) then
        ShowMark(button, LABEL_BOE, BOE_COLOR)
    else
        HideMark(button)
    end
end

local function ForEachItemButton(frame, fn)
    if frame.EnumerateValidItems then
        for _, button in frame:EnumerateValidItems() do
            fn(button)
        end
    elseif frame.Items then
        for _, button in ipairs(frame.Items) do
            if button.GetBagID then
                fn(button)
            end
        end
    end
end

local function UpdateContainerFrame(frame)
    if not frame then
        return
    end
    ForEachItemButton(frame, UpdateButton)
end

local function ForEachContainerFrame(fn)
    if ContainerFrameCombinedBags then
        fn(ContainerFrameCombinedBags)
    end
    for i = 1, (NUM_CONTAINER_FRAMES or 13) do
        local frame = _G["ContainerFrame" .. i]
        if frame then
            fn(frame)
        end
    end
end

-- Coalesce bursts of triggers (events firing together, layout settling after
-- a bag opens) into a single deferred pass over the shown container frames.
local refreshScheduled = false
local function ScheduleRefresh()
    if refreshScheduled then
        return
    end
    refreshScheduled = true
    C_Timer.After(0, function()
        refreshScheduled = false
        ForEachContainerFrame(function(frame)
            if frame:IsShown() then
                UpdateContainerFrame(frame)
            end
        end)
    end)
end

-- Hooking ------------------------------------------------------------------

-- The redraw entry point was renamed across client versions, so hook whichever
-- of the known layout methods this client actually exposes. Hooking the mixin
-- covers every frame/button that uses it.
local function TryHookMixinMethod(mixin, method, callback)
    if mixin and type(rawget(mixin, method)) == "function" then
        hooksecurefunc(mixin, method, callback)
        return true
    end
    return false
end

-- The bank UI is load-on-demand, so its mixin may not exist until the player
-- first opens a bank. Hooking the per-item-button refresh marks each bank slot
-- whenever Blizzard lays it out (open, tab switch, deposit/withdraw).
local bankHooksInstalled = false
local function InstallBankHooks()
    if bankHooksInstalled then
        return
    end
    local mixin = BankPanelItemButtonMixin
    if not mixin then
        return
    end
    local hooked = false
    hooked = TryHookMixinMethod(mixin, "Refresh", UpdateButton) or hooked
    hooked = TryHookMixinMethod(mixin, "Init", UpdateButton) or hooked
    hooked = TryHookMixinMethod(mixin, "Update", UpdateButton) or hooked
    bankHooksInstalled = hooked
end

-- Wiring -------------------------------------------------------------------

local function OnEvent(_, event)
    if C_AddOns.IsAddOnLoaded("Baganator") then return end
    if event == "ADDON_LOADED" then
        InstallBankHooks()
        return
    end
    if event == "BANKFRAME_OPENED" then
        InstallBankHooks()
    end
    ScheduleRefresh()
end

local events = CreateFrame("Frame")
local function SafeRegisterEvent(event)
    pcall(events.RegisterEvent, events, event)
end
for _, event in ipairs({
    "PLAYER_LOGIN",
    "ADDON_LOADED",
    "EQUIPMENT_SETS_CHANGED",
    "BAG_UPDATE_DELAYED",
    "BANKFRAME_OPENED",
    "PLAYERBANKSLOTS_CHANGED",
    "PLAYERREAGENTBANKSLOTS_CHANGED",
    "ITEM_LOCK_CHANGED",
}) do
    SafeRegisterEvent(event)
end
events:SetScript("OnEvent", OnEvent)

-- Bag frames (individual and combined) lay out through the container mixin.
TryHookMixinMethod(ContainerFrameMixin, "Update", UpdateContainerFrame)
TryHookMixinMethod(ContainerFrameMixin, "UpdateItems", UpdateContainerFrame)
TryHookMixinMethod(ContainerFrameCombinedBagsMixin, "Update", UpdateContainerFrame)
TryHookMixinMethod(ContainerFrameCombinedBagsMixin, "UpdateItems", UpdateContainerFrame)

-- Fallback / defense in depth: also repaint when bags are opened, in case the
-- layout method above could not be hooked on this client.
for _, name in ipairs({ "OpenAllBags", "ToggleAllBags", "OpenBackpack", "ToggleBackpack", "OpenBag", "ToggleBag" }) do
    if type(_G[name]) == "function" then
        hooksecurefunc(name, ScheduleRefresh)
    end
end

-- In case the bank UI is already loaded when this addon initializes.
InstallBankHooks()
