-- to-do
-- change to ItemsDatabase.setTier(UIitem) to UIitem:setTier()
ItemsDatabase = {}

-- Rarity cache: server sends rarity data via extended opcode
-- Keys: "c:<containerId>:<slot>" for containers, "i:<slot>" for inventory
ItemsDatabase.rarityCache = {}

local RARITY_OPCODE = 88

ItemsDatabase.rarityColors = {
    ["yellow"] = TextColors.yellow,
    ["purple"] = TextColors.purple,
    ["blue"] = TextColors.blue,
    ["green"] = TextColors.green,
    ["grey"] = TextColors.grey,
}

-- Rarity clip mapping (server-sent rarity string -> frame clip)
local rarityClipMap = {
    rare      = "64 0 32 32",   -- blue
    epic      = "96 0 32 32",   -- purple
    legendary = "128 0 32 32",  -- gold
}

-- Rarity color mapping for loot messages
local rarityLootColors = {
    rare      = "blue",
    epic      = "purple",
    legendary = "yellow",
}

local function getColorForValue(value)
    if value >= 1000000 then
        return "yellow"
    elseif value >= 100000 then
        return "purple"
    elseif value >= 10000 then
        return "blue"
    elseif value >= 1000 then
        return "green"
    elseif value >= 50 then
        return "grey"
    else
        return "white"
    end
end

local function clipfunction(value)
    if value >= 1000000 then
        return "128 0 32 32"
    elseif value >= 100000 then
        return "96 0 32 32"
    elseif value >= 10000 then
        return "64 0 32 32"
    elseif value >= 1000 then
        return "32 0 32 32"
    elseif value >= 50 then
        return "0 0 32 32"
    end
    return ""
end

function ItemsDatabase.getClipAndImagePath(item, rarityOverride)
    if not item and not rarityOverride then
        return nil, nil, nil
    end

    local frameOption = "frames"
    if modules.client_options and modules.client_options.getOption then
        frameOption = string.lower(modules.client_options.getOption('framesRarity') or 'frames')
    end
    if frameOption == "none" then
        frameOption = "frames"
    end

    local imagePath = '/images/ui/item'
    local clip = nil

    -- Use server-sent rarity if available
    if rarityOverride and rarityClipMap[rarityOverride] then
        clip = rarityClipMap[rarityOverride]
        if frameOption == "corners" then
            imagePath = "/images/ui/containerslot-coloredges"
        else
            imagePath = "/images/ui/rarity_frames"
        end
    else
        -- Fallback: price-based rarity (for items without server rarity data)
        if type(item) == "number" then
            item = g_things.getThingType(item, ThingCategoryItem)
        end

        if not item then
            return nil, nil, nil
        end

        local price = type(item) == "number" and item or (item and item:getMeanPrice()) or 0
        local itemRarity = getColorForValue(price)
        if itemRarity then
            clip = clipfunction(price)
            if clip ~= "" then
                if frameOption == "frames" then
                    imagePath = "/images/ui/rarity_frames"
                elseif frameOption == "corners" then
                    imagePath = "/images/ui/containerslot-coloredges"
                end
            else
                clip = nil
            end
        end
    end

    local clipObject = nil
    if clip then
        local x, y, w, h = clip:match("(%d+) (%d+) (%d+) (%d+)")
        clipObject = { x = tonumber(x), y = tonumber(y), width = tonumber(w), height = tonumber(h) }
    end

    return clip, imagePath, clipObject
end

ItemsDatabase.RARITY_COLORS = {
    rare      = "#3390FF",  -- blue
    epic      = "#B048F8",  -- purple
    legendary = "#FFD700",  -- gold
}

ItemsDatabase.RARITY_IMAGES = {
    rare      = "/images/ui/rarity_blue",
    epic      = "/images/ui/rarity_purple",
    legendary = "/images/ui/rarity_yellow",
}

function ItemsDatabase.setRarityItem(widget, item, style, cacheKey)
    if not widget then
        return
    end

    -- Skip if game features not available yet (module init before login)
    if not g_game or not g_game.getFeature(GameColorizedLootValue) then
        return
    end

    -- If slot has no item, just clear the visual frame. DO NOT clear the
    -- cache here: setRarityItem is also called during transient mid-shift
    -- states (e.g. refreshContainerItems fires from onSizeChange BEFORE
    -- onContainerRemoveItem during 0x72 processing) where a slot has no
    -- item yet but its post-shift cache entry must survive. The cache is
    -- canonical via opcode 88; the server tells us when to invalidate it.
    if not item or (type(item.getId) == "function" and item:getId() == 0) then
        ItemsDatabase.applyRarityOverlay(widget, nil)
        return
    end

    -- Check rarity cache for server-sent rarity data
    local rarityOverride = nil
    if cacheKey then
        rarityOverride = ItemsDatabase.rarityCache[cacheKey]
    end

    -- Apply or clear rarity frame
    ItemsDatabase.applyRarityOverlay(widget, rarityOverride)
end

function ItemsDatabase.applyRarityOverlay(widget, rarity)
    -- Save original image source on first call so we can restore it later
    if not widget._originalImageSource then
        widget._originalImageSource = widget:getImageSource() or ''
    end

    if not rarity or not ItemsDatabase.RARITY_IMAGES[rarity] then
        -- Restore original slot background
        widget:setImageSource(widget._originalImageSource)
        return
    end

    -- Swap the widget background image to the rarity frame
    widget:setImageSource(ItemsDatabase.RARITY_IMAGES[rarity])
end

function ItemsDatabase.clearRarityBorder(widget)
    if not widget then return end
    local original = widget._originalImageSource or ''
    widget:setImageSource(original)
end

function ItemsDatabase.getColorForRarity(rarity)
    return ItemsDatabase.rarityColors[rarity] or TextColors.white
end

function ItemsDatabase.setColorLootMessage(text)
    local function coloringLootName(match)
        local id, itemName = match:match("(%d+)|(.+)")
        if not id or not itemName then
            return "{" .. match .. "}"
        end

        local itemId = tonumber(id)
        if not itemId then
            return itemName or match
        end

        local thingType = g_things.getThingType(itemId, ThingCategoryItem)
        if not thingType then
            return itemName
        end

        local itemInfo = thingType:getMeanPrice()
        if itemInfo then
            local color = ItemsDatabase.getColorForRarity(getColorForValue(itemInfo))
            return "{" .. itemName .. ", " .. color .. "}"
        else
            return itemName
        end
    end
    return text:gsub("{(.-)}", coloringLootName)
end

-- Extended opcode handler for rarity data from server
function ItemsDatabase.onExtendedOpcode(protocol, opcode, buffer)
    if opcode ~= RARITY_OPCODE then return end

    local success, data = pcall(json.decode, buffer)
    if not success or not data then return end

    if data.a == "c" then
        local prefix = "c:" .. tostring(data.id) .. ":"
        for k, _ in pairs(ItemsDatabase.rarityCache) do
            if k:sub(1, #prefix) == prefix then
                ItemsDatabase.rarityCache[k] = nil
            end
        end
        if data.items then
            for slot, rarity in pairs(data.items) do
                ItemsDatabase.rarityCache["c:" .. tostring(data.id) .. ":" .. slot] = rarity
            end
        end
    elseif data.a == "t" then
        -- Trade rarity update: store with own/counter prefix
        local prefix = data.ack and "t:own:" or "t:counter:"
        -- Clear old trade entries for this side
        for k, _ in pairs(ItemsDatabase.rarityCache) do
            if k:sub(1, #prefix) == prefix then
                ItemsDatabase.rarityCache[k] = nil
            end
        end
        if data.items then
            for slot, rarity in pairs(data.items) do
                ItemsDatabase.rarityCache[prefix .. slot] = rarity
            end
        end
    elseif data.a == "i" then
        -- Inventory rarity update.
        -- The C++ side (ProtocolGame::sendInventoryItem) sends a single-slot packet
        -- with `data.slot` set on every slot change (rune consumption, ammo decrement,
        -- etc.). That packet has NO bonus fields — so we MUST NOT use it to refresh
        -- skills-window bonuses, or the panel wipes whenever a rune is used.
        -- The Lua side (Rarity.sendInventoryRarity) sends a full packet with no
        -- `data.slot` and includes the bonus aggregate; that's the only one that
        -- should drive applyRarityBonuses.
        if data.slot then
            ItemsDatabase.rarityCache["i:" .. data.slot] = nil
        end
        if data.slots then
            for slot, rarity in pairs(data.slots) do
                ItemsDatabase.rarityCache["i:" .. slot] = rarity
            end
        end
    end

    -- Refresh UI after cache update
    ItemsDatabase.refreshRarityFrames(data.a, data.a == "c" and data.id or nil)

    -- Store stat bonuses for display in skills window.
    -- Only the full Lua-side inventory packet carries bonuses; the C++ single-slot
    -- packet (data.slot present) must be ignored here.
    if data.a == "i" and data.slot == nil then
        ItemsDatabase.rarityBonuses = {
            hp = data.hpBonus or 0,
            mana = data.manaBonus or 0,
            speed = data.speedBonus or 0,
            attack = data.attackBonus or 0,
            defense = data.defenseBonus or 0,
            extraDefense = data.extraDefenseBonus or 0,
            armor = data.armorBonus or 0,
            skillShield = data.skillShieldBonus or 0,
            skillDist = data.skillDistBonus or 0,
            magicLevel = data.magicLevelBonus or 0,
            lifeLeech = data.lifeLeechBonus or 0,
            manaLeech = data.manaLeechBonus or 0,
            absorb = data.absorbBonus or {},
            -- Totals across ALL equipped items (common + rarity combined)
            armorTotal = data.armorTotal or 0,
            attackTotal = data.attackTotal or 0,
            defenseTotal = data.defenseTotal or 0,
            extraDefenseTotal = data.extraDefenseTotal or 0,
        }
        -- Backward-compat fields consumed by other modules
        ItemsDatabase.hpBonus = ItemsDatabase.rarityBonuses.hp
        ItemsDatabase.manaBonus = ItemsDatabase.rarityBonuses.mana
        if modules.game_skills then
            if modules.game_skills.setStatTooltip then
                modules.game_skills.setStatTooltip('health', ItemsDatabase.rarityBonuses.hp)
                modules.game_skills.setStatTooltip('mana', ItemsDatabase.rarityBonuses.mana)
            end
            if modules.game_skills.applyRarityBonuses then
                modules.game_skills.applyRarityBonuses(ItemsDatabase.rarityBonuses)
            end
        end
    end
end


function ItemsDatabase.refreshRarityFrames(action, containerId)
    if action == "i" then
        -- Refresh inventory slots: apply or clear borders based on new cache state
        if modules.game_inventory and modules.game_inventory.refreshRaritySlot then
            local player = g_game.getLocalPlayer()
            if not player then return end
            for slot = InventorySlotHead, InventorySlotAmmo do
                local item = player:getInventoryItem(slot)
                modules.game_inventory.refreshRaritySlot(slot, item)
            end
        end
    elseif action == "c" and containerId ~= nil then
        -- Refresh container items
        local container = g_game.getContainers()[containerId]
        if container and container.itemsPanel then
            for slot = 0, container:getCapacity() - 1 do
                local itemWidget = container.itemsPanel:getChildById('item' .. slot)
                if itemWidget then
                    local cacheKey = "c:" .. containerId .. ":" .. slot
                    ItemsDatabase.applyRarityOverlay(itemWidget, ItemsDatabase.rarityCache[cacheKey])
                end
            end
        end
    end
end

function ItemsDatabase.getTierClip(tier)
    local xOffset = (math.min(math.max(tier, 1), 10) - 1) * 9
    return {
        x = xOffset,
        y = 0,
        width = 10,
        height = 9
    }
end

function ItemsDatabase.setTier(widget, item, isSmall)
    if not g_game.getFeature(GameThingUpgradeClassification) or not widget or not widget.tier then
        return
    end
    if isSmall == nil then
        isSmall = true
    end
    local tier = type(item) == "number" and item or (item and item:getTier()) or 0
    if tier <= 0 then
        widget.tier:setVisible(false)
        return
    end
    local config
    if isSmall then
        local normalizedTier = math.min(math.max(tier, 1), 10)
        config = {
            xOffset = (normalizedTier - 1) * 9,
            width = 10,
            height = 9,
            size = "10 9",
            source = '/images/inventory/tiers-strip'
        }
    else
        local normalizedTier = math.min(math.max(tier, 1), 18)
        local xOffset = (normalizedTier - 1) * 18 + 1
        config = {
            xOffset = xOffset,
            width = 18,
            height = 16,
            size = "18 16",
            source = '/images/inventory/tiers-strip-big'
        }
    end

    widget.tier:setImageClip({
        x = config.xOffset,
        y = 0,
        width = config.width,
        height = config.height
    })
    widget.tier:setSize(config.size)
    widget.tier:setImageSource(config.source)
    widget.tier:setImageSize(config.size)
    widget.tier:setVisible(true)
end

function ItemsDatabase.setCharges(widget, item, style)
    if not g_game.getFeature(GameThingCounter) or not widget then
        return
    end

    if item and item:getCharges() > 0 then
        widget.charges:setText(item:getCharges())
    else
        widget.charges:setText("")
    end

    if style then
        widget:setStyle(style)
    end
end

function ItemsDatabase.setDuration(widget, item, style)
    if not g_game.getFeature(GameThingClock) or not widget then
        return
    end

    if item and item:getDurationTime() > 0 then
        local durationTimeLeft = item:getDurationTime()
        widget.duration:setText(string.format("%dm%02d", durationTimeLeft / 60, durationTimeLeft % 60))
    else
        widget.duration:setText("")
    end

    if style then
        widget:setStyle(style)
    end
end
