-- Author: U_BMP
-- Date: 01.01.2026


BMP_WardrobeClothingAddon = {}
BMP_WardrobeClothingAddon.MOD_DIR  = g_currentModDirectory or ""
BMP_WardrobeClothingAddon.MOD_NAME = g_currentModName or "BMP_WardrobeClothingAddon"
BMP_WardrobeClothingAddon.DEBUG    = true

addModEventListener(BMP_WardrobeClothingAddon)

local function log(fmt, ...)
    if BMP_WardrobeClothingAddon.DEBUG then
        print(string.format("[BMP_WardrobeClothingAddon] " .. fmt, ...))
    end
end

-- =========================================================
-- REAL GLOBAL ENV (shared between mods in FS via modEnvironment __index)
-- =========================================================
local function getRealGlobalEnv()
    local mt = getmetatable(_G)
    if mt ~= nil and type(mt.__index) == "table" then
        return mt.__index
    end
    return _G
end

local G = getRealGlobalEnv()

G.BMP_WCA_PENDING_PACKS = G.BMP_WCA_PENDING_PACKS or {}
G.BMP_WCA_API = G.BMP_WCA_API or {}

-- =========================================================
-- Path helpers (GENERIC: main mod + pack mods)
-- =========================================================
local function normalizeSlashes(p) return (tostring(p):gsub("\\", "/")) end

local function isAbsolutePath(p)
    if p == nil then return false end
    p = normalizeSlashes(p)
    if string.sub(p, 1, 1) == "/" then return true end
    if string.find(p, "^[A-Za-z]:/") ~= nil then return true end
    return false
end

local function fixPathToBaseDir(p, baseDir)
    if p == nil then return nil end
    baseDir = baseDir or ""

    local s = normalizeSlashes(p)
    local lower = string.lower(s)

    if string.sub(lower, 1, 7) == "$datas/" then
        return string.sub(s, 2) -- "dataS/..."
    end
    if string.sub(lower, 1, 6) == "$data/" then
        return string.sub(s, 2) -- "data/..."
    end

    if isAbsolutePath(s) then
        return s
    end

    if string.sub(lower, 1, 6) == "datas/" or string.sub(lower, 1, 5) == "data/" then
        return s
    end

    return Utils.getFilename(s, baseDir)
end

local function patchItemPaths(item, baseDir)
    if item == nil then return end

    if item.filename ~= nil then item.filename = fixPathToBaseDir(item.filename, baseDir) end
    if item.iconFilename ~= nil then item.iconFilename = fixPathToBaseDir(item.iconFilename, baseDir) end
    if item.icon ~= nil then item.icon = fixPathToBaseDir(item.icon, baseDir) end
    if item.normalFilename ~= nil then item.normalFilename = fixPathToBaseDir(item.normalFilename, baseDir) end
    if item.specularFilename ~= nil then item.specularFilename = fixPathToBaseDir(item.specularFilename, baseDir) end

    if item.animation ~= nil then
        if item.animation.filename ~= nil then
            item.animation.filename = fixPathToBaseDir(item.animation.filename, baseDir)
        end
        if item.animation.expressionsFilename ~= nil then
            item.animation.expressionsFilename = fixPathToBaseDir(item.animation.expressionsFilename, baseDir)
        end
    end
end

local function patchPresetPaths(preset, baseDir)
    if preset == nil then return end
    if preset.iconFilename ~= nil then
        preset.iconFilename = fixPathToBaseDir(preset.iconFilename, baseDir)
    end
end

local function patchAllStyleItemPaths(style, baseDir)
    if style == nil or style.configs == nil then return end
    for _, cfg in pairs(style.configs) do
        if cfg ~= nil and cfg.items ~= nil then
            for _, it in ipairs(cfg.items) do
                patchItemPaths(it, baseDir)
            end
        end
    end
end

-- =========================================================
-- Extra player models (playerModelsAddon.xml)
-- =========================================================
BMP_WardrobeClothingAddon._extraModelsLoaded = BMP_WardrobeClothingAddon._extraModelsLoaded or false

function BMP_WardrobeClothingAddon:loadExtraPlayerModels()
    if self._extraModelsLoaded then
        return 0
    end

    if PlayerSystem == nil or PlayerSystem.charactersXMLSchema == nil then
        return 0
    end

    local xmlPath = Utils.getFilename("playerModelsAddon.xml", self.MOD_DIR)
    local xmlFile = XMLFile.loadIfExists("PlayerModels", xmlPath, PlayerSystem.charactersXMLSchema)
    if xmlFile == nil then
        log("No playerModelsAddon.xml found: %s", tostring(xmlPath))
        self._extraModelsLoaded = true
        return 0
    end

    local added = 0
    for _, key in xmlFile:iterator("playerModels.playerModel") do
        local filename = xmlFile:getValue(key .. "#filename", nil)
        local name     = xmlFile:getValue(key .. "#name", nil)
        local gender   = xmlFile:getValue(key .. "#gender", "male")

        if string.isNilOrWhitespace(filename) then
            Logging.xmlError(xmlFile, "playerModel at %s has invalid filename!", key)
        else
            filename = fixPathToBaseDir(filename, self.MOD_DIR)

            local style = PlayerStyle.new()
            style:loadConfigurationXML(filename)

            patchAllStyleItemPaths(style, self.MOD_DIR)

            if PlayerSystem.PLAYER_STYLES_BY_FILENAME[style.xmlFilename] ~= nil then
                log("Extra playerModel '%s' already registered (%s) -> skip", tostring(name), tostring(style.xmlFilename))
            else
                local entry = {
                    filename = style.xmlFilename,
                    name     = name,
                    gender   = gender,
                    style    = style
                }
                PlayerSystem.PLAYER_STYLES_BY_FILENAME[style.xmlFilename] = entry
                PlayerSystem.PLAYER_STYLES[#PlayerSystem.PLAYER_STYLES + 1] = entry

                added = added + 1
                log("Added extra playerModel '%s' gender=%s (%s)", tostring(name), tostring(gender), tostring(style.xmlFilename))
            end
        end
    end

    xmlFile:delete()
    self._extraModelsLoaded = true
    log("Extra playerModels loaded: %d", added)
    return added
end

-- =========================================================
-- Patch PlayerSystem early (to hook loadStyleConfigurationsXML)
-- =========================================================
BMP_WardrobeClothingAddon._psPatched = BMP_WardrobeClothingAddon._psPatched or false

function BMP_WardrobeClothingAddon.tryPatchPlayerSystemEarly()
    if BMP_WardrobeClothingAddon._psPatched then
        return
    end
    if PlayerSystem == nil or PlayerSystem.loadStyleConfigurationsXML == nil then
        return
    end

    local old = PlayerSystem.loadStyleConfigurationsXML
    PlayerSystem.loadStyleConfigurationsXML = function(xmlFilename)
        old(xmlFilename)

        if BMP_WardrobeClothingAddon ~= nil then
            BMP_WardrobeClothingAddon:loadExtraPlayerModels()
            BMP_WardrobeClothingAddon:applyAllPacks()
        end
    end

    BMP_WardrobeClothingAddon._psPatched = true
    log("PlayerSystem.loadStyleConfigurationsXML patched (EARLY)")
end

BMP_WardrobeClothingAddon.tryPatchPlayerSystemEarly()

-- =========================================================
-- XML schema for WardrobeClothesAddon.xml (main + packs)
-- =========================================================
local ADDON_SCHEMA = nil

local function registerPresetXMLPaths(schema, baseKey)
    local p = baseKey .. ".preset(?)"

    schema:register(XMLValueType.STRING, p .. "#name", "Preset name", nil, true)
    schema:register(XMLValueType.STRING, p .. "#text", "Preset text", nil, true)
    schema:register(XMLValueType.STRING, p .. "#brand", "Preset brand", nil, false)
    schema:register(XMLValueType.STRING, p .. "#iconFilename", "Preset icon", nil, true)
    schema:register(XMLValueType.BOOL,   p .. "#isSelectable", "Preset selectable", true, false)
    schema:register(XMLValueType.STRING, p .. "#extraContentId", "Preset extra content id", nil, false)

    for configName, _ in pairs(PlayerStyleConfig.CONFIG_BASE_KEY_NAMES_BY_NAME) do
        schema:register(XMLValueType.STRING, p .. "." .. configName .. "#name", "Preset item name", nil, false)
        schema:register(XMLValueType.INT,    p .. "." .. configName .. "#color", "Preset item color", nil, false)
    end
end

local function createAddonSchema()
    local schema = XMLSchema.new("bmpClothingAddon")

    local function regItem(baseKey, itemName)
        PlayerStyleItem.registerXMLPaths(schema, baseKey, itemName)
    end

    -- male
    regItem("clothingAddon.male.tops",       "top")
    regItem("clothingAddon.male.bottoms",    "bottom")
    regItem("clothingAddon.male.footwear",   "footwear")
    regItem("clothingAddon.male.headgear",   "headgear")
    regItem("clothingAddon.male.gloves",     "gloves")
    regItem("clothingAddon.male.glasses",    "glasses")
    regItem("clothingAddon.male.hairStyles", "hairStyle")
    regItem("clothingAddon.male.beards",     "beard")
    regItem("clothingAddon.male.faces",      "face")
    regItem("clothingAddon.male.onepieces",  "onepiece")

    -- female
    regItem("clothingAddon.female.tops",       "top")
    regItem("clothingAddon.female.bottoms",    "bottom")
    regItem("clothingAddon.female.footwear",   "footwear")
    regItem("clothingAddon.female.headgear",   "headgear")
    regItem("clothingAddon.female.gloves",     "gloves")
    regItem("clothingAddon.female.glasses",    "glasses")
    regItem("clothingAddon.female.hairStyles", "hairStyle")
    regItem("clothingAddon.female.beards",     "beard")
    regItem("clothingAddon.female.faces",      "face")
    regItem("clothingAddon.female.onepieces",  "onepiece")

    registerPresetXMLPaths(schema, "clothingAddon.male.presets")
    registerPresetXMLPaths(schema, "clothingAddon.female.presets")

    return schema
end

-- =========================================================
-- Apply items/presets from XML into base male/female styles
-- =========================================================
local function ensureStylePresetTables(style)
    if style == nil then return end
    style.presets = style.presets or {}
    style.presetsByName = style.presetsByName or {}
end

local function addItemsFromSection(xmlFile, sectionKey, style, configName, isHair, baseDir)
    local cfg = style ~= nil and style.configs ~= nil and style.configs[configName] or nil
    if cfg == nil then
        return 0
    end

    local added = 0
    for _, itemKey in xmlFile:iterator(sectionKey) do
        local item = PlayerStyleItem.new()
        item:loadFromConfigurationXMLFile(
            xmlFile,
            itemKey,
            isHair and style.hairColors or style.defaultClothingColors,
            style.attachPoints,
            isHair == true
        )

        patchItemPaths(item, baseDir)

        if item ~= nil and item.name ~= nil and item.filename ~= nil then
            if cfg.itemsByName[item.name] == nil then
                cfg:addItem(item)
                added = added + 1
                log("Added %s '%s' (%s)", tostring(configName), tostring(item.name), tostring(item.filename))
            end
        end
    end

    return added
end

local function addPresetsFromSection(xmlFile, sectionKey, style, baseDir)
    if style == nil then
        return 0
    end

    ensureStylePresetTables(style)

    local added = 0
    for _, presetKey in xmlFile:iterator(sectionKey) do
        local preset = PlayerStylePreset.new(style.xmlFilename)
        preset:loadFromXMLNode(xmlFile, presetKey)

        patchPresetPaths(preset, baseDir)

        if preset ~= nil and preset.name ~= nil and preset.isSelectable then
            if style.presetsByName[preset.name] == nil then
                table.insert(style.presets, preset)
                style.presetsByName[preset.name] = preset
                added = added + 1
                log("Added preset '%s' (icon=%s)", tostring(preset.name), tostring(preset.iconFilename))
            else
                log("Preset '%s' already exists -> skip", tostring(preset.name))
            end
        end
    end

    return added
end

local function applyAddonToStyle(xmlFile, genderKey, style, baseDir)
    local total = 0

    total = total + addItemsFromSection(xmlFile, "clothingAddon."..genderKey..".headgear.headgear",      style, "headgear", false, baseDir)
    total = total + addItemsFromSection(xmlFile, "clothingAddon."..genderKey..".glasses.glasses",        style, "glasses", false, baseDir)
    total = total + addItemsFromSection(xmlFile, "clothingAddon."..genderKey..".tops.top",               style, "top", false, baseDir)
    total = total + addItemsFromSection(xmlFile, "clothingAddon."..genderKey..".bottoms.bottom",         style, "bottom", false, baseDir)
    total = total + addItemsFromSection(xmlFile, "clothingAddon."..genderKey..".footwear.footwear",      style, "footwear", false, baseDir)
    total = total + addItemsFromSection(xmlFile, "clothingAddon."..genderKey..".faces.face",             style, "face", false, baseDir)
    total = total + addItemsFromSection(xmlFile, "clothingAddon."..genderKey..".onepieces.onepiece",     style, "onepiece", false, baseDir)
    total = total + addItemsFromSection(xmlFile, "clothingAddon."..genderKey..".hairStyles.hairStyle",   style, "hairStyle", true,  baseDir)
    total = total + addItemsFromSection(xmlFile, "clothingAddon."..genderKey..".beards.beard",           style, "beard", true,      baseDir)

    total = total + addPresetsFromSection(xmlFile, "clothingAddon."..genderKey..".presets.preset", style, baseDir)

    if style.facesByName ~= nil and style.configs ~= nil and style.configs.face ~= nil then
        table.clear(style.facesByName)
        for _, faceItem in pairs(style.configs.face.items) do
            style.facesByName[faceItem.name] = faceItem
        end
    end

    return total
end

local function findStyleEntryByEndsWith(endsWithLower)
    if PlayerSystem == nil or PlayerSystem.PLAYER_STYLES_BY_FILENAME == nil then
        return nil, nil
    end

    for k, entry in pairs(PlayerSystem.PLAYER_STYLES_BY_FILENAME) do
        local lk = string.lower(tostring(k))
        if string.sub(lk, -string.len(endsWithLower)) == endsWithLower then
            return entry, k
        end
    end
    return nil, nil
end

-- =========================================================
-- PUBLIC API for PACK MODS (order-independent)
-- =========================================================
BMP_WardrobeClothingAddon.API_VERSION   = 1
BMP_WardrobeClothingAddon._packs        = BMP_WardrobeClothingAddon._packs or {}
BMP_WardrobeClothingAddon._packsOrder   = BMP_WardrobeClothingAddon._packsOrder or {}
BMP_WardrobeClothingAddon._packsApplied = BMP_WardrobeClothingAddon._packsApplied or {}

function BMP_WardrobeClothingAddon.registerPack(packId, modDir, xmlRelPath, opts)
    if string.isNilOrWhitespace(packId) then
        return false
    end

    modDir = modDir or ""
    xmlRelPath = xmlRelPath or "WardrobeClothesAddon.xml"
    opts = opts or {}

    BMP_WardrobeClothingAddon._packs = BMP_WardrobeClothingAddon._packs or {}
    BMP_WardrobeClothingAddon._packsOrder = BMP_WardrobeClothingAddon._packsOrder or {}
    BMP_WardrobeClothingAddon._packsApplied = BMP_WardrobeClothingAddon._packsApplied or {}

    if BMP_WardrobeClothingAddon._packs[packId] ~= nil then
        return true
    end

    local pack = {
        id     = packId,
        modDir = modDir,
        xmlRel = xmlRelPath,
        debug  = opts.debug == true
    }

    BMP_WardrobeClothingAddon._packs[packId] = pack
    table.insert(BMP_WardrobeClothingAddon._packsOrder, packId)

    log("PACK registered: %s (dir=%s xml=%s)", tostring(packId), tostring(modDir), tostring(xmlRelPath))

    if type(BMP_WardrobeClothingAddon.applyAllPacks) == "function" then
        BMP_WardrobeClothingAddon:applyAllPacks()
    end

    return true
end

G.BMP_WCA_API.apiVersion = BMP_WardrobeClothingAddon.API_VERSION
G.BMP_WCA_API.registerPack = function(packId, modDir, xmlRelPath, opts)
    return BMP_WardrobeClothingAddon.registerPack(packId, modDir, xmlRelPath, opts)
end

function BMP_WardrobeClothingAddon:drainPendingPacks()
    local pend = G.BMP_WCA_PENDING_PACKS
    if pend == nil then
        return 0
    end

    local n = 0
    for packId, p in pairs(pend) do
        n = n + 1
        if p ~= nil then
            BMP_WardrobeClothingAddon.registerPack(p.id, p.modDir, p.xmlRel, p.opts)
        end
        pend[packId] = nil
    end
    return n
end

function BMP_WardrobeClothingAddon:applyAllPacks()
    if PlayerSystem == nil or PlayerSystem.PLAYER_STYLES_BY_FILENAME == nil then
        return 0
    end

    local maleEntry   = select(1, findStyleEntryByEndsWith("datas/character/playerm/playerm.xml"))
    local femaleEntry = select(1, findStyleEntryByEndsWith("datas/character/playerf/playerf.xml"))

    if maleEntry == nil and femaleEntry == nil then
        return 0
    end

    if ADDON_SCHEMA == nil then
        ADDON_SCHEMA = createAddonSchema()
    end

    local drained = self:drainPendingPacks()
    if drained > 0 then
        log("Pending packs drained from REAL GLOBAL: %d", drained)
    end

    local totalAdded = 0

    for _, packId in ipairs(self._packsOrder) do
        if not self._packsApplied[packId] then
            local pack = self._packs[packId]
            if pack ~= nil then
                local xmlPath = Utils.getFilename(pack.xmlRel, pack.modDir)
                local xmlFile = XMLFile.loadIfExists("bmpClothingAddonPack", xmlPath, ADDON_SCHEMA)

                if xmlFile == nil then
                    log("PACK xml not found -> skip (pack=%s path=%s)", tostring(packId), tostring(xmlPath))
                    self._packsApplied[packId] = true
                else
                    local added = 0
                    if maleEntry ~= nil and maleEntry.style ~= nil then
                        added = added + applyAddonToStyle(xmlFile, "male", maleEntry.style, pack.modDir)
                    end
                    if femaleEntry ~= nil and femaleEntry.style ~= nil then
                        added = added + applyAddonToStyle(xmlFile, "female", femaleEntry.style, pack.modDir)
                    end

                    xmlFile:delete()
                    self._packsApplied[packId] = true
                    totalAdded = totalAdded + added

                    log("PACK applied: %s added=%d (xml=%s)", tostring(packId), added, tostring(xmlPath))
                end
            end
        end
    end

    return totalAdded
end

-- =========================================================
-- Wardrobe UI refresh patches
-- =========================================================
BMP_WardrobeClothingAddon._wcfPatched = BMP_WardrobeClothingAddon._wcfPatched or false

function BMP_WardrobeClothingAddon.tryPatchWardrobeCharactersFrame()
    if BMP_WardrobeClothingAddon._wcfPatched then
        return
    end
    if WardrobeCharactersFrame == nil or WardrobeCharactersFrame.onFrameOpen == nil then
        return
    end

    local oldOnFrameOpen = WardrobeCharactersFrame.onFrameOpen
    WardrobeCharactersFrame.onFrameOpen = function(self, ...)
        oldOnFrameOpen(self, ...)

        if self.loadPlayers ~= nil then
            self:loadPlayers()
        end
        if self.resetList ~= nil then
            self:resetList()
        end

        if BMP_WardrobeClothingAddon ~= nil and BMP_WardrobeClothingAddon.DEBUG then
            print(string.format("[BMP_WardrobeClothingAddon] WardrobeCharactersFrame refreshed. players=%s mapping=%s",
                tostring(self.players ~= nil and #self.players or "nil"),
                tostring(self.mapping ~= nil and #self.mapping or "nil")
            ))
        end
    end

    BMP_WardrobeClothingAddon._wcfPatched = true
    log("WardrobeCharactersFrame.onFrameOpen patched (refresh players)")
end

BMP_WardrobeClothingAddon._wclothesPatched = BMP_WardrobeClothingAddon._wclothesPatched or false

local function tryCall(self, name)
    local f = self ~= nil and self[name] or nil
    if type(f) == "function" then
        local ok, err = pcall(f, self)
        if not ok then
            log("Call failed %s: %s", tostring(name), tostring(err))
        end
        return ok
    end
    return false
end

function BMP_WardrobeClothingAddon.tryPatchWardrobeClothesFrames()
    if BMP_WardrobeClothingAddon._wclothesPatched then
        return
    end

    local function patchFrameClass(frameClass, className)
        if frameClass == nil or frameClass.onFrameOpen == nil then
            return false
        end

        local old = frameClass.onFrameOpen
        frameClass.onFrameOpen = function(self, ...)
            old(self, ...)

            tryCall(self, "reloadItems")
            tryCall(self, "loadItems")
            tryCall(self, "updateItems")
            tryCall(self, "rebuildItems")
            tryCall(self, "updateList")
            tryCall(self, "populateList")
            tryCall(self, "resetList")

            tryCall(self, "loadPresets")
            tryCall(self, "updatePresets")

            if BMP_WardrobeClothingAddon.DEBUG then
                print(string.format("[BMP_WardrobeClothingAddon] %s refreshed", tostring(className)))
            end
        end

        log("%s.onFrameOpen patched (refresh items/presets)", tostring(className))
        return true
    end

    local patchedAny = false
    patchedAny = patchFrameClass(WardrobeClothesFrame, "WardrobeClothesFrame") or patchedAny
    patchedAny = patchFrameClass(WardrobeClothesPresetsFrame, "WardrobeClothesPresetsFrame") or patchedAny
    patchedAny = patchFrameClass(WardrobePresetsFrame, "WardrobePresetsFrame") or patchedAny

    if patchedAny then
        BMP_WardrobeClothingAddon._wclothesPatched = true
    end
end

-- =========================================================
-- Mod lifecycle
-- =========================================================
function BMP_WardrobeClothingAddon:update(dt)
    if not self._psPatched then
        BMP_WardrobeClothingAddon.tryPatchPlayerSystemEarly()
    end

    if not self._wcfPatched then
        BMP_WardrobeClothingAddon.tryPatchWardrobeCharactersFrame()
    end
    if not self._wclothesPatched then
        BMP_WardrobeClothingAddon.tryPatchWardrobeClothesFrames()
    end

    self:applyAllPacks()
end

function BMP_WardrobeClothingAddon:loadMap()
    if PlayerSystem == nil or PlayerSystem.PLAYER_STYLES_BY_FILENAME == nil then
        return
    end

    if ADDON_SCHEMA == nil then
        ADDON_SCHEMA = createAddonSchema()
    end

    -- XML
    local xmlPath = Utils.getFilename("WardrobeClothesAddon.xml", self.MOD_DIR)
    local xmlFile = XMLFile.loadIfExists("bmpClothingAddon", xmlPath, ADDON_SCHEMA)

    if xmlFile == nil then
        log("No WardrobeClothesAddon.xml found: %s", tostring(xmlPath))
    else
        local maleEntry   = select(1, findStyleEntryByEndsWith("datas/character/playerm/playerm.xml"))
        local femaleEntry = select(1, findStyleEntryByEndsWith("datas/character/playerf/playerf.xml"))

        local addedAll = 0
        if maleEntry ~= nil and maleEntry.style ~= nil then
            addedAll = addedAll + applyAddonToStyle(xmlFile, "male", maleEntry.style, self.MOD_DIR)
            log("Male style presets now: %s", tostring(maleEntry.style.presets ~= nil and #maleEntry.style.presets or "nil"))
        else
            log("Male base style not found (playerM.xml) -> no male items added")
        end

        if femaleEntry ~= nil and femaleEntry.style ~= nil then
            addedAll = addedAll + applyAddonToStyle(xmlFile, "female", femaleEntry.style, self.MOD_DIR)
            log("Female style presets now: %s", tostring(femaleEntry.style.presets ~= nil and #femaleEntry.style.presets or "nil"))
        else
            log("Female base style not found (playerF.xml) -> no female items added")
        end

        xmlFile:delete()
        log("Initial apply done. Added total (MAIN): %d", addedAll)
    end

    local addedPacks = self:applyAllPacks()
    log("Packs apply done. Added from packs: %d", addedPacks)
end