-- =============================================================================
-- NativeCard.lua
-- Shared model-card rendering + outfit apply for ETB's native transmog tabs
-- (ETB Sets, ETB Saved Outfits, and the Transmog Planner). Everything here
-- operates on a "source map":   { [setSlot] = sourceID, ... }
-- where setSlot uses the transmog / ItemTransmogInfo slot keys (1,3,4,5,6,7,
-- 8,9,10,15,16,17,19).
--
-- This is the proven path for patch 5.5.4+: ModelScene actors built from
-- TransmogCustomSetModelTemplate (native Sets-tab look) and direct staging via
-- C_TransmogOutfitInfo. A DressUpModel fallback is used if the template/scene
-- isn't available.
-- =============================================================================

local ADDON_NAME, NS = ...
local ETB = NS.ETB

local NC = {}
ETB.NativeCard = NC

-- -----------------------------------------------------------------------------
-- Slot + option maps
-- -----------------------------------------------------------------------------
function NC.BuildSlotMap()
    local E = Enum and Enum.TransmogOutfitSlot
    if not E then return nil end
    return {
        [1] = E.Head, [3] = E.ShoulderRight, [4] = E.Body, [5] = E.Chest,
        [6] = E.Waist, [7] = E.Legs, [8] = E.Feet, [9] = E.Wrist, [10] = E.Hand,
        [15] = E.Back, [16] = E.WeaponMainHand, [17] = E.WeaponOffHand, [19] = E.Tabard,
    }
end

local function optionForCategory(outfitSlot, categoryID)
    local O = Enum and Enum.TransmogOutfitSlotOption
    local E = Enum and Enum.TransmogOutfitSlot
    if not (O and E) then return 0 end
    if outfitSlot == E.WeaponMainHand or outfitSlot == E.WeaponOffHand then
        if (categoryID >= 12 and categoryID <= 17) or categoryID == 28 then
            return O.OneHandedWeapon
        elseif categoryID == 18 then
            return O.Shield
        elseif categoryID == 19 then
            return O.OffHand
        elseif categoryID >= 20 and categoryID <= 24 then
            return O.TwoHandedWeapon
        elseif categoryID >= 25 and categoryID <= 27 then
            return O.RangedWeapon
        end
    end
    return 0
end

-- -----------------------------------------------------------------------------
-- Items <-> source map
-- -----------------------------------------------------------------------------
-- Convert an ETB set's item table ({[slot]={itemID,sourceID}} or {[slot]=itemID})
-- into a resolved { [slot] = sourceID } map via the export translator.
function NC.ItemsToSourceMap(items)
    local map = {}
    if type(items) ~= "table" then return map end
    if ETB.CustomSetExport and ETB.CustomSetExport.BuildList then
        local ok, list = pcall(function() return ETB.CustomSetExport:BuildList(items) end)
        if ok and type(list) == "table" then
            for slot, info in pairs(list) do
                local sid = info and info.appearanceID
                if sid and sid > 0 then map[tonumber(slot)] = sid end
            end
        end
    end
    return map
end

function NC.CountSources(sourceMap)
    local n = 0
    if type(sourceMap) == "table" then
        for _, sid in pairs(sourceMap) do
            if sid and sid > 0 then n = n + 1 end
        end
    end
    return n
end

-- -----------------------------------------------------------------------------
-- Apply: stage a source map onto the transmog model (no custom set, no cap).
-- -----------------------------------------------------------------------------
function NC.ApplyOutfit(sourceMap)
    if not (C_TransmogOutfitInfo and C_TransmogOutfitInfo.SetPendingTransmog
        and Enum and Enum.TransmogType) then
        return false
    end
    local SLOT = NC.BuildSlotMap()
    if not (SLOT and type(sourceMap) == "table") then return false end

    pcall(function() C_TransmogOutfitInfo.ClearAllPendingTransmogs() end)
    local appType = Enum.TransmogType.Appearance
    local applied = 0
    for slot, sourceID in pairs(sourceMap) do
        local outfitSlot = SLOT[tonumber(slot)]
        if outfitSlot ~= nil and sourceID and sourceID > 0 then
            local sinfo = C_TransmogCollection and C_TransmogCollection.GetSourceInfo
                and C_TransmogCollection.GetSourceInfo(sourceID)
            local categoryID = (sinfo and sinfo.categoryID) or 0
            local display = (sinfo and sinfo.isHideVisual and 3) or 1
            local option = optionForCategory(outfitSlot, categoryID)
            pcall(function()
                C_TransmogOutfitInfo.SetPendingTransmog(outfitSlot, appType, option, sourceID, display)
            end)
            applied = applied + 1
        end
    end
    return applied > 0
end

-- -----------------------------------------------------------------------------
-- Camera: upper-body set-card framing, per race/sex (base + race offset).
-- -----------------------------------------------------------------------------
local CAM_BASE = { -0.108, -2.78, 1.55 }
local CAM_TRANSLATE = {
    BloodElf2 = { 0.058, 0.68, 0.05 }, BloodElf3 = { 0.058, 0.98, -0.05 },
    DarkIronDwarf2 = { -0.012, 0.28, -0.45 }, DarkIronDwarf3 = { 0.038, 0.78, -0.48 },
    Draenei2 = { -0.092, -0.72, 0.25 }, Draenei3 = { -0.042, 1.23, 0.35 },
    Dracthyr2 = { -0.862, -0.52, 0.65 }, Dracthyr3 = { -0.862, -0.52, 0.65 },
    Dwarf2 = { 0.038, 0.28, -0.45 }, Dwarf3 = { 0.008, 0.78, -0.48 },
    EarthenDwarf2 = { 0.038, 0.28, -0.45 }, EarthenDwarf3 = { 0.008, 0.78, -0.48 },
    Gnome2 = { 0.108, 1.28, -0.85 }, Gnome3 = { -0.112, 1.08, -0.90 },
    Goblin2 = { 0.108, 0.98, -0.75 }, Goblin3 = { -0.112, 0.78, -0.68 },
    Harronir2 = { -0.042, 0.88, 0.40 }, Harronir3 = { 0.058, 1.08, 0.25 },
    HighmountainTauren2 = { -0.012, -1.32, 0.35 }, HighmountainTauren3 = { -0.242, -0.52, 0.35 },
    Human2 = { 0.058, 0.48, -0.05 }, Human3 = { 0.058, 0.98, -0.05 },
    KulTiran2 = { 0.108, -0.02, 0.45 }, KulTiran3 = { 0.038, 0.68, 0.45 },
    LightforgedDraenei2 = { -0.092, -0.72, 0.25 }, LightforgedDraenei3 = { -0.042, 0.98, 0.35 },
    MagharOrc2 = { 0.158, -0.82, -0.05 }, MagharOrc3 = { -0.092, 0.58, 0.00 },
    Mechagnome2 = { 0.038, 1.18, -0.85 }, Mechagnome3 = { -0.062, 1.18, -0.90 },
    Nightborne2 = { 0.108, -0.02, 0.35 }, Nightborne3 = { 0.108, 0.73, 0.30 },
    NightElf2 = { -0.042, -0.02, 0.35 }, NightElf3 = { 0.058, 0.73, 0.30 },
    Orc2 = { 0.058, -0.82, -0.05 }, Orc3 = { -0.092, 0.58, 0.00 }, Orc4 = { -0.092, -0.82, -0.05 },
    Pandaren2 = { -0.242, -1.02, 0.05 }, Pandaren3 = { -0.192, -0.42, 0.05 },
    Scourge2 = { -0.042, 0.58, -0.15 }, Scourge3 = { 0.178, 0.98, -0.10 },
    Tauren2 = { -0.012, -1.32, 0.35 }, Tauren3 = { -0.242, -0.52, 0.35 },
    Troll2 = { 0.278, 0.08, 0.15 }, Troll3 = { -0.012, 0.68, 0.35 },
    VoidElf2 = { 0.058, 0.68, 0.05 }, VoidElf3 = { 0.058, 0.98, -0.05 },
    Vulpera2 = { -0.112, 0.68, -0.75 }, Vulpera3 = { -0.112, 0.68, -0.70 },
    Worgen2 = { 0.158, -0.72, 0.15 }, Worgen3 = { 0.008, 0.08, 0.25 },
    ZandalariTroll2 = { 0.058, -0.17, 0.60 }, ZandalariTroll3 = { -0.142, 0.38, 0.75 },
}

local function frameActorCamera(scene, actor)
    local off = NC.GetRaceCamOffset(actor)
    pcall(function() scene:SetCameraOrientationByAxisVectors(0, 1, 0, -1, 0, 0, 0, 0, 1) end)
    pcall(function() scene:SetCameraPosition(CAM_BASE[1] + off[1], CAM_BASE[2] + off[2], CAM_BASE[3] + off[3]) end)
end

-- Exposed for per-slot framing (ModelGrid): the upper-body base camera and the
-- per-race/sex translate, so callers can add their own slot delta on top.
NC.CAM_BASE = CAM_BASE
function NC.GetRaceCamOffset(actor)
    local _, race = UnitRace("player")
    local sex = UnitSex("player")
    local key = (race or "Human") .. (sex or 2)
    local file = actor and actor.GetModelFileID and actor:GetModelFileID()
    if file == 1968587 then key = "Orc4"
    elseif file == 4395382 then key = "BloodElf" .. (sex or 2)
    elseif file == 1000764 or file == 1011653 or file == 4220448 then key = "Human" .. (sex or 2) end
    return CAM_TRANSLATE[key] or { 0, 0, 0 }
end

-- -----------------------------------------------------------------------------
-- Model attach + dress
-- -----------------------------------------------------------------------------
-- Create the card's model: a ModelScene actor (native look) or a DressUpModel
-- fallback. Anchored to leave a 20px label strip at the bottom.
function NC.AttachModel(card)
    local scene
    local ok = pcall(function()
        scene = CreateFrame("ModelScene", nil, card)
        card.actor = scene:CreateActor(nil, "TransmogCustomSetModelTemplate")
    end)
    if ok and scene and card.actor then
        card.scene = scene
        scene:SetPoint("TOPLEFT", 4, -4)
        scene:SetPoint("BOTTOMRIGHT", -4, 20)
        scene:EnableMouse(false)
        card.actor:Show()
    else
        card.scene, card.actor = nil, nil
        if scene then scene:Hide() end
        card.model = CreateFrame("DressUpModel", nil, card)
        card.model:SetPoint("TOPLEFT", 4, -4)
        card.model:SetPoint("BOTTOMRIGHT", -4, 20)
        card.model:EnableMouse(false)
    end
end

local function sourceToItemLink(sourceID)
    if C_TransmogCollection and C_TransmogCollection.GetSourceInfo then
        local info = C_TransmogCollection.GetSourceInfo(sourceID)
        local itemID = info and info.itemID
        if itemID then
            local _, link = GetItemInfo(itemID)
            return link or ("item:" .. itemID)
        end
    end
    return nil
end

-- Dress a card from a source map. opts = { idx = stagger index, isValid = fn }.
function NC.DressCard(card, sourceMap, opts)
    opts = opts or {}
    local idx = opts.idx or 1
    local isValid = opts.isValid or function() return true end

    if card.actor then
        local scene, actor = card.scene, card.actor
        local function run()
            if not isValid() then return end
            local alteredForm = false
            if C_PlayerInfo and C_PlayerInfo.GetAlternateFormInfo then
                local _, af = C_PlayerInfo.GetAlternateFormInfo()
                alteredForm = af
            end
            pcall(function() actor:SetModelByUnit("player", false, false, false, not alteredForm) end)
            if actor.SetAutoDress then pcall(function() actor:SetAutoDress(false) end) end
            pcall(function() actor:SetYaw(-1.5) end)
            pcall(function() scene:SetPaused(true) end)
            frameActorCamera(scene, actor)
            local function applyPieces()
                if not isValid() then return end
                pcall(function() actor:Undress() end)
                if type(sourceMap) == "table" then
                    for _, sourceID in pairs(sourceMap) do
                        if sourceID and sourceID > 0 then
                            -- TryOn shows the appearance whether or not it's
                            -- collected, so the card previews the FULL set.
                            pcall(function() actor:TryOn(sourceID) end)
                        end
                    end
                end
            end
            applyPieces()
            if C_Timer and C_Timer.After then C_Timer.After(0.25, applyPieces) end
        end
        if C_Timer and C_Timer.After then C_Timer.After(0.03 * idx, run) else run() end

    elseif card.model then
        local model = card.model
        local function run()
            if not isValid() then return end
            pcall(function() model:SetUnit("player") end)
            pcall(function() model:Undress() end)
            if model.SetPortraitZoom then pcall(model.SetPortraitZoom, model, 0) end
            pcall(model.SetFacing, model, 0.35)
            local function applyPieces()
                if not isValid() then return end
                if type(sourceMap) == "table" then
                    for _, sourceID in pairs(sourceMap) do
                        local link = sourceToItemLink(sourceID)
                        if link then pcall(model.TryOn, model, link) end
                    end
                end
                pcall(function() if model.SetSheathed then model:SetSheathed(true) end end)
            end
            if C_Timer and C_Timer.After then C_Timer.After(0.1, applyPieces) else applyPieces() end
        end
        if C_Timer and C_Timer.After then C_Timer.After(0.03 * idx, run) else run() end
    end
end
