
-- 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

-- ---------------------------------------------------------
-- Path helpers
-- ---------------------------------------------------------
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 fixPathToMod(p)
    if p == nil then return nil end

    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, BMP_WardrobeClothingAddon.MOD_DIR)
end

-- ---------------------------------------------------------
local function patchItemPaths(item)
    if item == nil then return end
    if item.filename ~= nil then item.filename = fixPathToMod(item.filename) end
    if item.iconFilename ~= nil then item.iconFilename = fixPathToMod(item.iconFilename) end
    if item.icon ~= nil then item.icon = fixPathToMod(item.icon) end
    if item.normalFilename ~= nil then item.normalFilename = fixPathToMod(item.normalFilename) end
    if item.specularFilename ~= nil then item.specularFilename = fixPathToMod(item.specularFilename) end

    if item.animation ~= nil then
        if item.animation.filename ~= nil then item.animation.filename = fixPathToMod(item.animation.filename) end
        if item.animation.expressionsFilename ~= nil then item.animation.expressionsFilename = fixPathToMod(item.animation.expressionsFilename) end
    end
end

local function patchAllStyleItemPaths(style)
    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)
            end
        end
    end
end

-- ---------------------------------------------------------
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 = self.MOD_DIR .. "playerModelsAddon.xml"
    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 = fixPathToMod(filename)

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

            patchAllStyleItemPaths(style)

            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

-- ---------------------------------------------------------
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()
        end
    end

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

BMP_WardrobeClothingAddon.tryPatchPlayerSystemEarly()

-- ---------------------------------------------------------
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")

    -- presets are NOT PlayerStyleItem -> custom schema paths
    registerPresetXMLPaths(schema, "clothingAddon.male.presets")
    registerPresetXMLPaths(schema, "clothingAddon.female.presets")

    return schema
end

-- ---------------------------------------------------------
local function addItemsFromSection(xmlFile, sectionKey, style, configName, isHair)
    local cfg = style.configs[configName]
    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)

        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 patchPresetPaths(preset)
    if preset == nil then return end
    if preset.iconFilename ~= nil then
        preset.iconFilename = fixPathToMod(preset.iconFilename)
    end
end

local function ensureStylePresetTables(style)
    if style == nil then return end
    style.presets = style.presets or {}
    style.presetsByName = style.presetsByName or {}
end

local function addPresetsFromSection(xmlFile, sectionKey, style)
    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)

        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)
    local total = 0

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


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

    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

-- ---------------------------------------------------------
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

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

    if not self._wcfPatched then
        self.tryPatchWardrobeCharactersFrame()
    end
    if not self._wclothesPatched then
        self.tryPatchWardrobeClothesFrames()
    end
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

    local xmlPath = self.MOD_DIR .. "WardrobeClothesAddon.xml"
    local xmlFile = XMLFile.loadIfExists("bmpClothingAddon", xmlPath, ADDON_SCHEMA)
    if xmlFile == nil then
        log("No WardrobeClothesAddon.xml found: %s", tostring(xmlPath))
        return
    end

    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)
        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)
        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: %d", addedAll)
end
