--[[
TRANSMOGGIN SOFTWARE LICENSE
Copyright (c) 2026 Rooster_Roost
All Rights Reserved.

Transmoggin is proprietary software created for use with World of Warcraft.

PERMITTED USE

Permission is granted to download, install, and use Transmoggin for personal,
non-commercial gameplay purposes.

Users may create and share screenshots, videos, livestreams, guides, reviews,
and other media that feature Transmoggin during normal use.

RESTRICTIONS

Without prior written permission from Rooster_Roost, you may not:

1. Redistribute Transmoggin, in whole or in part.

2. Repackage, mirror, re-upload, or otherwise distribute Transmoggin through
   another website, addon service, application, repository, or distribution
   platform.

3. Modify Transmoggin and publicly distribute the modified version.

4. Fork, clone, rebrand, or publish a derivative version of Transmoggin.

5. Copy or reuse substantial portions of Transmoggin's source code in another
   addon, project, or product.

6. Extract, reproduce, redistribute, or incorporate Transmoggin's compiled or
   curated databases into another addon, project, or service.

7. Extract, reproduce, redistribute, or use Transmoggin-specific artwork,
   icons, backgrounds, interface designs, branding, or other original assets
   in another project without permission.

8. Sell, sublicense, rent, monetize access to, or commercially distribute
   Transmoggin or any substantial portion of it.

9. Represent yourself or another party as the creator, owner, or official
   maintainer of Transmoggin.

MODIFICATIONS FOR PERSONAL USE

You may modify your own locally installed copy of Transmoggin for personal use,
provided the modified copy is not redistributed or published.

CONTRIBUTIONS AND TRANSLATIONS

Community suggestions, bug reports, translations, compatibility fixes, and
other contributions are welcome.

Submission of a contribution does not grant permission to redistribute,
fork, rebrand, or publish Transmoggin independently.

WORLD OF WARCRAFT AND THIRD-PARTY DATA

World of Warcraft, Warcraft, Blizzard Entertainment, and related names,
trademarks, game data, and assets are property of Blizzard Entertainment, Inc.
and their respective owners.

This license claims rights only to original Transmoggin code, databases,
compilations, artwork, branding, documentation, and other material to the
extent those materials are owned or copyrightable by the Transmoggin author.

NO WARRANTY

TRANSMOGGIN IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.

IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES, LOSS OF DATA,
GAMEPLAY INTERRUPTION, OR OTHER LIABILITY ARISING FROM THE USE OF OR INABILITY
TO USE TRANSMOGGIN.

PERMISSION

Permissions not expressly granted by this license are reserved by
Rooster_Roost.

For permission to redistribute, integrate, reuse, or otherwise use Transmoggin
outside the terms above, contact Rooster_Roost through an official Transmoggin
project page.

Copyright (c) 2026 Rooster_Roost.
All Rights Reserved.
]]

local ADDON_NAME, ns = ...
local T = _G.Transmoggin or {}
_G.Transmoggin = T
T.name = ADDON_NAME or "Transmoggin"
T.ns = ns
T.version = "0.3.1"
T.copyright = "Copyright (c) 2026 Rooster_Roost. All Rights Reserved."
T.licenseSummary = "Proprietary; personal non-commercial use only. Redistribution, repackaging, forks, and reuse require permission."
T.MEDIA = "Interface\\AddOns\\Transmoggin\\Media\\"
T.ROOSTER = T.MEDIA .. "rooster.tga"

T.COLORS = {
    bg={0.025,0.020,0.018,0.98}, panel={0.060,0.048,0.038,0.98}, panel2={0.095,0.073,0.052,0.98},
    gold={0.95,0.62,0.12,1}, gold2={0.58,0.31,0.07,1}, text={0.93,0.88,0.78,1},
    muted={0.62,0.58,0.52,1}, green={0.25,0.90,0.36,1}, red={0.95,0.28,0.20,1}, purple={0.72,0.34,0.95,1}, blue={0.28,0.60,1,1},
}


T.WEAPON_CATEGORIES = {12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29}

T.SLOTS = {
    {key="HEADSLOT", slotID=1, category=1, label="Head", outfitSlot=0},
    {key="SHOULDERSLOT", slotID=3, category=2, label="Shoulders", outfitSlot=1},
    {key="BACKSLOT", slotID=15, category=3, label="Back", outfitSlot=3},
    {key="CHESTSLOT", slotID=5, category=4, label="Chest", outfitSlot=4},
    {key="SHIRTSLOT", slotID=4, category=5, label="Shirt", outfitSlot=6},
    {key="TABARDSLOT", slotID=19, category=6, label="Tabard", outfitSlot=5},
    {key="WRISTSLOT", slotID=9, category=7, label="Wrist", outfitSlot=7},
    {key="HANDSSLOT", slotID=10, category=8, label="Hands", outfitSlot=8},
    {key="WAISTSLOT", slotID=6, category=9, label="Waist", outfitSlot=9},
    {key="LEGSSLOT", slotID=7, category=10, label="Legs", outfitSlot=10},
    {key="FEETSLOT", slotID=8, category=11, label="Feet", outfitSlot=11},
    {key="MAINHANDSLOT", slotID=16, category=nil, label="Main Hand", outfitSlot=12},
    {key="SECONDARYHANDSLOT", slotID=17, category=nil, label="Off Hand", outfitSlot=13},
}

T.state = {
    vendorOpen=false, selectedSlot="HEADSLOT", selectedCategory=1, page=1, pageSize=32,
    search="", collectionFilter="all", leftMode="looks", working={pieces={}},
    appearanceCache={}, sourceCache={}, setCache={}, hoveredRecord=nil, undo={}, redo={},
    setPage=1, setPageSize=13, setExpansionFilter="all", setRowsCache={},
    sourceListCache={}, visualCollectedCache={}, sourceInfoCache={}, filteredAppearanceCache={},
    refreshPending=false, refreshGeneration=0,
}


function T:EnsureUI()
    if not self.main and self.CreateUI then self:CreateUI() end
    if not self.tracker and self.CreateTracker then self:CreateTracker() end
    return self.main ~= nil
end

-- Coalesce bursts of Blizzard events into one UI refresh. Transmog collection and
-- item-cache events can fire dozens of times in a single frame while opening the
-- salon; rebuilding the model/catalog for every event was the main source of hitching.
function T:RequestRefresh(delay)
    if not (self.main and self.main:IsShown() and self.RefreshAll) then return end
    if self.state.refreshPending then return end
    self.state.refreshPending=true
    local function run()
        self.state.refreshPending=false
        if self.main and self.main:IsShown() then self:RefreshAll() end
    end
    if C_Timer and C_Timer.After then C_Timer.After(delay or 0.05,run) else run() end
end

function T:InvalidateAppearanceCaches(full)
    self.state.appearanceCache={}
    self.state.filteredAppearanceCache={}
    self.state.visualCollectedCache={}
    if full then
        self.state.sourceCache={}
        self.state.sourceListCache={}
        self.state.sourceInfoCache={}
        self.state.validAppearanceCache={}
    end
end

local defaults = {
    profile={minimap={angle=215, hide=false}, window={x=0,y=0}, filters={collection="all"}, help={tooltips=true,tutorialSeen=false}, photo={background="woodland",effect="none",atmosphere="none",effectIntensity=.35,dim=.12,posX=0,posY=0,zoom=.78,presets={}}},
    looks={}, tracked={}, favorites={}, recent={}, sourceIntel={}, version=4,
}

local function copyDefaults(dst, src)
    for k,v in pairs(src) do
        if type(v)=="table" then
            if type(dst[k])~="table" then dst[k]={} end
            copyDefaults(dst[k],v)
        elseif dst[k]==nil then dst[k]=v end
    end
end

function T:Print(msg)
    if DEFAULT_CHAT_FRAME then DEFAULT_CHAT_FRAME:AddMessage("|cffff9d24Transmoggin:|r "..tostring(msg)) end
end

function T:GetSlotDef(key)
    for _,s in ipairs(self.SLOTS) do if s.key==key then return s end end
end

function T:GetPlayerContext()
    local className, classFile, classID = UnitClass("player")
    local specName, specID = "", nil
    if GetSpecialization and GetSpecializationInfo then
        local idx=GetSpecialization()
        if idx then specID, specName = GetSpecializationInfo(idx) end
    end
    return {className=className or "Unknown", classFile=classFile or "UNKNOWN", classID=classID or 0, specName=specName or "", specID=specID}
end

function T:CurrentLookName()
    return self.state.working.name or "New Look"
end

function T:GetWorkingPiece(slotKey) return self.state.working.pieces[slotKey] end

function T:CloneWorking(src)
    src=src or self.state.working or {pieces={}}
    local out={name=src.name,dirty=src.dirty,loadedIndex=src.loadedIndex,pieces={}}
    for k,v in pairs(src.pieces or {}) do
        local c={}; for a,b in pairs(v) do c[a]=b end; out.pieces[k]=c
    end
    return out
end

function T:PushUndo()
    self.state.undo=self.state.undo or {}; self.state.redo=self.state.redo or {}
    self.state.undo[#self.state.undo+1]=self:CloneWorking()
    if #self.state.undo>30 then table.remove(self.state.undo,1) end
    self.state.redo={}
end

function T:UndoWorking()
    local u=self.state.undo or {}; if #u==0 then return end
    self.state.redo=self.state.redo or {}; self.state.redo[#self.state.redo+1]=self:CloneWorking()
    self.state.working=table.remove(u); if self.RefreshAll then self:RefreshAll() end; if self.SyncPendingToWorkingLook then self:SyncPendingToWorkingLook() end
end

function T:RedoWorking()
    local r=self.state.redo or {}; if #r==0 then return end
    self.state.undo=self.state.undo or {}; self.state.undo[#self.state.undo+1]=self:CloneWorking()
    self.state.working=table.remove(r); if self.RefreshAll then self:RefreshAll() end; if self.SyncPendingToWorkingLook then self:SyncPendingToWorkingLook() end
end

function T:SetWorkingPiece(slotKey, rec, noHistory)
    if not noHistory then self:PushUndo() end
    if not rec then self.state.working.pieces[slotKey]=nil else
        self.state.working.pieces[slotKey]={
            visualID=rec.visualID, sourceID=rec.sourceID, itemID=rec.itemID, itemModID=rec.itemModID,
            categoryID=rec.categoryID, name=rec.name, itemName=rec.itemName, appearanceName=rec.appearanceName, itemLink=rec.itemLink, icon=rec.icon,
            collected=rec.collected and true or false, sourceText=rec.sourceText,
        }
    end
    self.state.working.dirty=true
end

function T:ClearWorkingLook(noHistory)
    if not noHistory and self.state.working and next(self.state.working.pieces or {}) then self:PushUndo() end
    self.state.working={pieces={}, name=nil, dirty=false}
    if self.SyncPendingToWorkingLook then self:SyncPendingToWorkingLook(0) end
    if self.RefreshAll then self:RefreshAll() end
end

function T:SnapshotCurrentEquipment()
    local look={pieces={},name="Equipped Look"}
    if not (C_Transmog and C_Transmog.GetSlotVisualInfo) then return look end
    for _,s in ipairs(self.SLOTS) do
        local loc=self:MakeLocation(s)
        if loc then
            local ok,info=pcall(C_Transmog.GetSlotVisualInfo,loc)
            if ok and info then
                local visualID,sourceID
                if type(info)=="table" then
                    visualID = info.pendingVisualID and info.pendingVisualID~=0 and info.pendingVisualID or info.appliedVisualID or info.baseVisualID
                    sourceID = info.pendingSourceID and info.pendingSourceID~=0 and info.pendingSourceID or info.appliedSourceID or info.baseSourceID
                else
                    sourceID=info
                end
                if visualID and visualID~=0 then
                    local rec=self:GetSourceRecord(visualID,self:GetEffectiveCategory(s),sourceID)
                    if rec then look.pieces[s.key]=rec end
                end
            end
        end
    end
    return look
end

function T:SaveLook(name, overwriteIndex)
    name=(name or ""):gsub("^%s+",""):gsub("%s+$","")
    if name=="" then name="Look "..tostring(#self.db.looks+1) end
    local ctx=self:GetPlayerContext()
    local out={name=name, classID=ctx.classID, classFile=ctx.classFile, specID=ctx.specID, created=time and time() or 0, pieces={}}
    for k,v in pairs(self.state.working.pieces or {}) do
        local c={}; for a,b in pairs(v) do c[a]=b end; out.pieces[k]=c
    end
    if overwriteIndex and self.db.looks[overwriteIndex] then self.db.looks[overwriteIndex]=out else table.insert(self.db.looks,out) end
    self.state.working.name=name; self.state.working.dirty=false
    self:Print("Saved look: "..name)
    if self.RefreshAll then self:RefreshAll() end
end

function T:LoadLook(index)
    local look=self.db.looks[index]; if not look then return end
    self.state.working={name=look.name,pieces={},dirty=false,loadedIndex=index}
    for k,v in pairs(look.pieces or {}) do local c={}; for a,b in pairs(v) do c[a]=b end; self.state.working.pieces[k]=c end
    if self.RefreshAll then self:RefreshAll() end
    if self:IsAtTransmogVendor() then
        self:StageWorkingLook()
        if self.ScheduleVendorCostRefresh then self:ScheduleVendorCostRefresh() end
    end
end

function T:DeleteLook(index)
    if not self.db.looks[index] then return end
    local name=self.db.looks[index].name
    table.remove(self.db.looks,index); self.db.tracked[name]=nil
    if self.state.working.loadedIndex==index then self:ClearWorkingLook() end
    if self.RefreshAll then self:RefreshAll() end
end

function T:ToggleTrackLook(index)
    local look=self.db.looks[index]; if not look then return end
    self.db.tracked[look.name]=not self.db.tracked[look.name] or nil
    if self.RefreshAll then self:RefreshAll() end
    if self.RefreshTracker then self:RefreshTracker() end
end

function T:IsVisualFavorite(visualID) return self.db.favorites[tostring(visualID)] and true or false end
function T:ToggleFavorite(rec)
    if not rec or not rec.visualID then return end
    local k=tostring(rec.visualID); self.db.favorites[k]=not self.db.favorites[k] or nil
    self.state.filteredAppearanceCache={}
    if C_TransmogCollection and C_TransmogCollection.SetIsAppearanceFavorite then pcall(C_TransmogCollection.SetIsAppearanceFavorite,rec.visualID,self.db.favorites[k] and true or false) end
    if self.RefreshAppearanceGrid then self:RefreshAppearanceGrid() end
end

function T:CountLookProgress(look)
    local have,total=0,0
    for _,piece in pairs((look and look.pieces) or {}) do total=total+1; if self:IsPieceCollected(piece) then have=have+1 end end
    return have,total
end

function T:IsPieceCollected(piece)
    if not piece then return false end
    if self.IsVisualCollected and piece.visualID then
        local v=self:IsVisualCollected(piece.visualID,piece.categoryID,piece.itemModID or piece.sourceID)
        if v~=nil then piece.collected=v and true or false; return piece.collected end
    end
    if C_TransmogCollection and C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance and piece.itemModID then
        local ok,v=pcall(C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance,piece.itemModID); if ok then piece.collected=v and true or false; return piece.collected end
    end
    return piece.collected and true or false
end

function T:GetSlotDefForCategory(cat)
    for _,s in ipairs(self.SLOTS) do if s.category==cat then return s end end
end

local events=CreateFrame("Frame")
T.events=events
local safeEvents={"ADDON_LOADED","PLAYER_LOGIN","PLAYER_ENTERING_WORLD","PLAYER_EQUIPMENT_CHANGED","PLAYER_SPECIALIZATION_CHANGED","ACTIVE_TALENT_GROUP_CHANGED","GET_ITEM_INFO_RECEIVED","TRANSMOG_COLLECTION_UPDATED","TRANSMOG_SEARCH_UPDATED","TRANSMOGRIFY_UPDATE","TRANSMOGRIFY_SUCCESS","VIEWED_TRANSMOG_OUTFIT_SLOT_REFRESH","VIEWED_TRANSMOG_OUTFIT_CHANGED","PLAYER_INTERACTION_MANAGER_FRAME_SHOW","PLAYER_INTERACTION_MANAGER_FRAME_HIDE","TRANSMOGRIFY_OPEN","TRANSMOGRIFY_CLOSE","MERCHANT_SHOW","MERCHANT_UPDATE","DISPLAY_SIZE_CHANGED","UI_SCALE_CHANGED"}
for _,e in ipairs(safeEvents) do pcall(events.RegisterEvent,events,e) end

events:SetScript("OnEvent",function(_,event,...)
    if event=="ADDON_LOADED" then
        local name=...
        if name~=T.name then return end
        TransmogginDB=TransmogginDB or {}; copyDefaults(TransmogginDB,defaults); T.db=TransmogginDB
        if T.RestoreStaleWorldSelfieCVars then T:RestoreStaleWorldSelfieCVars() end
        T.state.collectionFilter=T.db.profile.filters.collection or "all"
        if T.InitCompat then T:InitCompat() end
        -- Keep login lightweight: the 3D model, hundreds of buttons and large
        -- background textures are created only when the player opens Transmoggin.
        if T.CreateMinimapButton then T:CreateMinimapButton() end
    elseif event=="PLAYER_LOGIN" then
        -- UI is intentionally lazy-created; nothing expensive is needed at login.
    elseif event=="PLAYER_INTERACTION_MANAGER_FRAME_SHOW" then
        local typ=...
        if T:IsTransmogInteractionType(typ) then T:EnterVendorMode("interaction") end
    elseif event=="PLAYER_INTERACTION_MANAGER_FRAME_HIDE" then
        local typ=...
        if T:IsTransmogInteractionType(typ) then T:LeaveVendorMode("interaction") end
    elseif event=="TRANSMOGRIFY_OPEN" then
        T:EnterVendorMode("legacy")
    elseif event=="TRANSMOGRIFY_CLOSE" then
        T:LeaveVendorMode("legacy")
    elseif event=="MERCHANT_SHOW" or event=="MERCHANT_UPDATE" then
        if T.LearnCurrentMerchantSources then T:LearnCurrentMerchantSources() end
    elseif event=="DISPLAY_SIZE_CHANGED" or event=="UI_SCALE_CHANGED" then
        if T.FitSalonToScreen then T:FitSalonToScreen() end
        if T.main and T.main:IsShown() and T.RefreshAll then T:RefreshAll() end
    else
        if event=="TRANSMOG_SEARCH_UPDATED" then
            if T.state.collectionPreparing then return end
            T:InvalidateAppearanceCaches(false)
        elseif event=="TRANSMOG_COLLECTION_UPDATED" then
            T:InvalidateAppearanceCaches(true); T.state.setCache={}
        elseif event=="GET_ITEM_INFO_RECEIVED" then
            -- Item-info responses arrive in large bursts. Keep the expensive catalog
            -- itself intact, but invalidate every formatted name/source string so raw
            -- item IDs are replaced as soon as Retail finishes loading their records.
            T.state.sourceCache={}
            T.state.filteredAppearanceCache={}
            T.state.acquisitionTextCache={}
            if T.state.workingLook then
                for _,piece in pairs(T.state.workingLook) do
                    if type(piece)=="table" then piece.sourceText=nil; piece.sourceTextPending=nil end
                end
            end
            if T.state.selectedRecord and type(T.state.selectedRecord)=="table" then
                T.state.selectedRecord.sourceText=nil; T.state.selectedRecord.sourceTextPending=nil
            end
        end
        if T.state.vendorOpen and T.RefreshVendorStatus then T:RefreshVendorStatus() end
        if event=="PLAYER_SPECIALIZATION_CHANGED" or event=="ACTIVE_TALENT_GROUP_CHANGED" then
            T:InvalidateAppearanceCaches(true); T.state.setCache={}; T.state.setPage=1
        end
        T:RequestRefresh((event=="GET_ITEM_INFO_RECEIVED") and 0.12 or 0.04)
        if T.RefreshTracker and T.tracker and T.tracker:IsShown() then
            if C_Timer and C_Timer.After then C_Timer.After(0.08,function() if T.tracker and T.tracker:IsShown() then T:RefreshTracker() end end) else T:RefreshTracker() end
        end
    end
end)

SLASH_TRANSMOGGIN1="/transmoggin"
SLASH_TRANSMOGGIN2="/tmg"
SlashCmdList.TRANSMOGGIN=function(msg)
    msg=(msg or ""):lower()
    if msg=="tracker" then T:EnsureUI(); T:ToggleTracker()
    elseif msg=="debug" then T:Print(T:GetDebugString())
    elseif msg=="reset" then T:ClearWorkingLook()
    elseif msg=="tutorial" then T:EnsureUI(); if T.ShowTutorial then T:ShowTutorial(true) end
    elseif msg=="help" or msg=="settings" then T:EnsureUI(); if T.ToggleHelpPanel then T:ToggleHelpPanel() end
    elseif msg=="tips" then
        T.db.profile.help.tooltips=not (T.db.profile.help.tooltips~=false)
        T:Print("Button tooltips "..(T.db.profile.help.tooltips and "enabled." or "disabled."))
        if T.RefreshHelpPanel then T:RefreshHelpPanel() end
    else T:EnsureUI(); T:ToggleMain() end
end
