--========================================================
-- Item Inspection UI - VanillaPlus B42.12.0 Legacy
--========================================================

require "ISUI/ISPanel"
require "ISUI/ISButton"
require "ISUI/ISRichTextPanel"
require "InspectSystem/Core/Inspect_Stats"
require "InspectSystem/Core/Inspect_Descriptions"
require "InspectSystem.Render.Inspect_3DPreview"

local function getTimestampMs()
    return getTimeInMillis()
end

InspectUI_VanillaPlus = ISPanel:derive("InspectUI_VanillaPlus")
InspectUI_LastMode = InspectUI_LastMode -- remember last 2D/3D choice across windows

-- Determinar si un item deberia permitir vista 3D (corpses/moveables suelen carecer de modelo valido)
local function InspectUI_canUse3D(item)
    if not item then return false end

    local function hasModel(it)
        if it.getWorldStaticModel then
            local m = it:getWorldStaticModel()
            if m and m ~= "" then return true end
        end
        if it.getStaticModel then
            local m = it:getStaticModel()
            if m and m ~= "" then return true end
        end
        if it.getWeaponSprite then
            local m = it:getWeaponSprite()
            if m and m ~= "" then return true end
        end
        if it.getModel then
            local m = it:getModel()
            if m and m ~= "" then return true end
        end
        return false
    end

    local dispCat = item.getDisplayCategory and string.lower(item:getDisplayCategory() or "") or ""
    local cat     = item.getCategory and string.lower(item:getCategory() or "") or ""

    -- Build 42: cadaveres y moveables suelen no tener modelo renderizable
    if dispCat == "corpse" or cat == "corpse" then
        return false
    end
    if dispCat == "moveable" or dispCat == "movable" or cat == "moveable" or cat == "movable" then
        return false
    end

    return hasModel(item)
end

local function centerWindow(ui)
    local sw = getCore():getScreenWidth()
    local sh = getCore():getScreenHeight()
    ui:setX((sw - ui.width) / 2)
    ui:setY((sh - ui.height) / 2)
end

------------------------------------------------------------
-- Adaptive Font for Stats & Description Based on Window Size
------------------------------------------------------------
function InspectUI_VanillaPlus:getAdaptiveFont(forWidth)
    local w = forWidth or self.width
    local base = self.defaultWidth or 500

    -- Mantener SMALL en el tamaño base o casi base
    if w <= base * 1.15 then
        return UIFont.Small
    end

    -- SMALL -> MEDIUM con un resize moderado
    if w <= base * 1.45 then
        return UIFont.Medium
    end

    -- Solo en tamaños muy grandes pasamos a LARGE
    return UIFont.Large
end

------------------------------------------------------------
-- Adaptive Icon Size for Stats Based on Window Size
------------------------------------------------------------
function InspectUI_VanillaPlus:getAdaptiveIconSize(forWidth)
    local w = forWidth or self.width
    local base = self.defaultWidth or 500

    if w <= base * 1.15 then
        return 16
    end

    if w <= base * 1.45 then
        return 20
    end

    return 24
end

------------------------------------------------------------
-- Adaptive Padding (icon + text + row spacing)
------------------------------------------------------------
function InspectUI_VanillaPlus:getAdaptivePadding(forWidth)
    local w = forWidth or self.width
    local base = self.defaultWidth or 500

    if w <= base * 1.15 then
        return {
            iconText = 4,
            colGap   = 12,
            rowGap   = 2,
            descPad  = 6,
        }
    end

    if w <= base * 1.45 then
        return {
            iconText = 6,
            colGap   = 16,
            rowGap   = 4,
            descPad  = 8,
        }
    end

    return {
        iconText = 8,
        colGap   = 20,
        rowGap   = 6,
        descPad  = 10,
    }
end

--------------------------------------------------
-- Iconos por stat (Damage, Range, Hunger, etc.)
--------------------------------------------------

-- Fallback directo a rutas conocidas (legacy y nuevos packs 32/64)
local TEX32 = "media/textures/ui/InspectStats/32/Inspect_stat_"
local InspectUI_StatIcons = {
    -- BASE / GENERICO
    weight            = TEX32 .. "weight.png",
    category          = TEX32 .. "category.png",
    displayCategory   = TEX32 .. "type.png",
    weaponSubtype     = TEX32 .. "type.png",
    condition         = TEX32 .. "condition.png",
    usesLeft          = TEX32 .. "uses.png",
    lightDistance     = TEX32 .. "lightdist.png",
    lightStrength     = TEX32 .. "lightstr.png",
    fluidAmount       = TEX32 .. "fluidCapacity.png", -- amount (contenido actual)
    fluidCapacity     = TEX32 .. "fluid.png",         -- capacidad total
    rainFactor        = TEX32 .. "rainFactor.png",
    -- Nota: los iconos de veneno estan nombrados como "posion" en los packs 32/64
    fluidPoison       = TEX32 .. "posion.png",
    taintedWater      = TEX32 .. "tainted.png",
    bookBoredom       = TEX32 .. "reducBoredom.png",
    bookStress        = TEX32 .. "reducStress.png",
    crafting          = TEX32 .. "crafting.png",
    foraging          = TEX32 .. "foraging.png",

    -- ARMAS (COMUN)
    damage            = TEX32 .. "damage.png",
    minDamage         = TEX32 .. "minDamage.png",
    maxDamage         = TEX32 .. "maxDamage.png",
    range             = TEX32 .. "range.png",
    minRange          = TEX32 .. "minRange.png",
    maxRange          = TEX32 .. "maxRange.png",
    baseSpeed         = TEX32 .. "baseSpeed.png",
    maxHitCount       = TEX32 .. "maxHitCount.png",
    hands             = TEX32 .. "hands.png",
    knockback         = TEX32 .. "knockback.png",
    crit              = TEX32 .. "criticalChance.png", -- alias
    critChance        = TEX32 .. "criticalChance.png",
    criticalChance    = TEX32 .. "criticalChance.png",
    accuracy          = TEX32 .. "hitChance.png",
    hitChance         = TEX32 .. "hitChance.png",
    noiseRadius       = TEX32 .. "soundRadius.png",
    soundRadius       = TEX32 .. "soundRadius.png",
    recoilDelay       = TEX32 .. "recoilDelay.png",
    aimTime           = TEX32 .. "aimTime.png",
    aimAccuracy       = TEX32 .. "aimAccuracy.png",
    reloadTime        = TEX32 .. "reloadTime.png",
    jamChance         = TEX32 .. "jamChance.png",
    projectileDamage  = TEX32 .. "projectileDamage.png",
    projectileSpeed   = TEX32 .. "projectileSpeed.png",
    caliber           = TEX32 .. "caliber.png",
    doorDamage        = TEX32 .. "doorDamage.png",
    treeDamage        = TEX32 .. "treeDamage.png",
    conditionLowerChance      = TEX32 .. "conditionLowerChance.png",
    doorConditionLowerChance  = TEX32 .. "doorConditionLowerChance.png",
    treeConditionLowerChance  = TEX32 .. "treeConditionLowerChance.png",

    -- COMIDA / BEBIDA
    hunger           = TEX32 .. "hunger.png",
    thirst           = TEX32 .. "thirst.png",
    calories         = TEX32 .. "calories.png",
    carbs            = TEX32 .. "carbs.png",
    proteins         = TEX32 .. "proteins.png",
    fat              = TEX32 .. "fat.png",
    freshState       = TEX32 .. "freshState.png",
    staleState       = TEX32 .. "staleState.png",
    rottenState      = TEX32 .. "rottenState.png",
    burnTime         = TEX32 .. "burnTime.png",
    burnedState      = TEX32 .. "burnState.png",
    burnState        = TEX32 .. "burnState.png",
    unhappiness      = TEX32 .. "unhappiness.png",
    daysFresh        = TEX32 .. "daysFresh.png",
    daysRotten       = TEX32 .. "daysRotten.png",

    -- ROPA
    insulation       = TEX32 .. "insulation.png",
    windRes          = TEX32 .. "windres.png",
    waterRes         = TEX32 .. "waterres.png",
    dirtiness        = TEX32 .. "dirtiness.png",
    bloodiness       = TEX32 .. "bloodiness.png",
    biteDefense      = TEX32 .. "bitedefense.png",
    scratchDefense   = TEX32 .. "scratchdefense.png",
    runSpeed         = TEX32 .. "runSpeed.png",

    -- ELECTRONICA / LUCES
    powerConsumption = TEX32 .. "powerConsumption.png",
    powerOutput      = TEX32 .. "powerOutput.png",
    batteryDrainRate = TEX32 .. "batteryDrainRate.png",
    signalRange      = TEX32 .. "signalRange.png",
    signalClarity    = TEX32 .. "signalClarity.png",
    frequencySupport = TEX32 .. "frequencySupport.png",
    lumensEquivalent = TEX32 .. "lumensEquivalent.png",
    beamDistance     = TEX32 .. "beamDistance.png",
    heatOutput       = TEX32 .. "heatOutput.png",
    riskRating       = TEX32 .. "riskRating.png",
    memoryCapacity   = TEX32 .. "memoryCapacity.png",
    channelCount     = TEX32 .. "channelCount.png",
    audioGain        = TEX32 .. "audioGain.png",
    noiseFilter      = TEX32 .. "noiseFilter.png",

    -- LIBROS
    pagesTotal       = TEX32 .. "pagesTotal.png",
    pagesLeft        = TEX32 .. "pagesLeft.png",
    skillBook        = TEX32 .. "skillBook.png",

    -- VARIOS
    capacity         = TEX32 .. "capacity.png",
    energyLevel      = TEX32 .. "energyLevel.png",
    integrity        = TEX32 .. "integrity.png",
    radiation        = TEX32 .. "radiation.png",
    temperature      = TEX32 .. "temperature.png",
    fluid            = TEX32 .. "fluid.png",
    type             = TEX32 .. "type.png",
    weightFallback   = TEX32 .. "weight.png",
}

-- Referencia al resolvedor compartido (si existe)
local InspectUI_CoreStatIcon = _G.InspectUI_getStatIcon

-- Cache local para fallback legacy
local InspectUI_StatIconsCache = {}

--------------------------------------------------
-- Obtener icono por statId
--------------------------------------------------
local function InspectUI_getStatIcon(statId, size)
    -- Preferir resolver compartido (usa carpeta 32/64 y fallback)
    if InspectUI_CoreStatIcon then
        local tex = InspectUI_CoreStatIcon(statId, size)
        if tex then return tex end
    end

    -- Fallback legacy a media/ui/Inspect_stat_*.png
    if not statId then return nil end
    local path = InspectUI_StatIcons[statId]
    if not path then return nil end

    local cacheKey = tostring(size or 0) .. ":" .. path
    if InspectUI_StatIconsCache[cacheKey] ~= nil then
        return InspectUI_StatIconsCache[cacheKey]
    end

    local tex = getTexture(path)
    InspectUI_StatIconsCache[cacheKey] = tex or false
    return tex
end

-- Cache para iconos de categoria
local InspectUI_CategoryIconsCache = {}

--------------------------------------------------
-- OJO: este mapa se asume definido en otro archivo compartido:
-- InspectUI_CategoryIcons = { Weapon = "...", Food = "...", ... }
-- Obtener icono por categoria de item
--------------------------------------------------
local function InspectUI_getCategoryIcon(item)
    if not item then return nil end

    local displayCat = item.getDisplayCategory and item:getDisplayCategory() or nil
    local category   = item.getCategory and item:getCategory() or nil

    local key = displayCat
    if not key or key == "" then
        key = category
    end
    if not key or key == "" then return nil end

    local texPath = InspectUI_CategoryIcons and InspectUI_CategoryIcons[key]

    if not texPath and InspectUI_CategoryIcons then
        local k = string.lower(tostring(key))
        if k:find("weapon", 1, true) then
            texPath = InspectUI_CategoryIcons.Weapon
        elseif k:find("food", 1, true) then
            texPath = InspectUI_CategoryIcons.Food
        elseif k:find("ammo", 1, true) or k:find("bullet", 1, true) then
            texPath = InspectUI_CategoryIcons.Ammo
        elseif k:find("book", 1, true) or k:find("literature", 1, true) then
            texPath = InspectUI_CategoryIcons.Literature
        elseif k:find("medical", 1, true) or k:find("bandage", 1, true) then
            texPath = InspectUI_CategoryIcons.Medical
        elseif k:find("electro", 1, true) or k:find("radio", 1, true) then
            texPath = InspectUI_CategoryIcons.Electronics
        elseif k:find("clothing", 1, true) or k:find("clothes", 1, true) then
            texPath = InspectUI_CategoryIcons.Clothing
        end
    end

    if not texPath then return nil end

    local tex = InspectUI_CategoryIconsCache[texPath]
    if tex == false then return nil end

    if not tex then
        tex = getTexture(texPath)
        InspectUI_CategoryIconsCache[texPath] = tex or false
    end

    return tex
end

--========================================================
-- Constructor
--========================================================
function InspectUI_VanillaPlus:new(x, y, w, h, item, player)
    local o = ISPanel:new(x, y, w, h)

    o.background = true
    o.border = false
    o.backgroundColor = { r=0, g=0, b=0, a=0.60 }
    o.borderColor     = { r=0, g=0, b=0, a=0 }

    setmetatable(o, self)
    self.__index = self

    o.player = player
    o:setItem(item)

    o.titleH  = 42
    o.margin  = 12
    o.spriteH = 240
    o.statsH  = 140
    o.statsMinH = 110
    o.statsMaxH = 200
    o.statsScroll = 0
    o.statsScrollMax = 0
    o.bottomPadding = 72

    o.fullHeight  = h
    o.isCollapsed = false
    o.use3D = InspectUI_LastMode
    if o.use3D == nil then o.use3D = true end
    o.autoRotate = false
    o.photoMode = false
    o.gripSize    = 22
    o.initialWidth = o.width
    o.defaultWidth  = w
    o.defaultHeight = h
    o.compactWidth  = math.floor(w * 0.65)
    o.compactHeight = math.floor(h * 0.65)
    o.wideWidth     = math.floor(w * 1.35)
    o.wideHeight    = math.floor(h * 1.15)
    o.lastClickTime = 0
    o.baseMinWidth  = math.max(380, math.floor(w * 0.7))
    o.baseMinHeight = math.max(420, math.floor(h * 0.65))
    o.descMinHeight = 140
    o.defaultX = x
    o.defaultY = y

    -- No keyboard capture: the inspection window should not participate in
    -- vanilla contextual actions such as E/use-object.
    o:setWantKeyEvents(false)

    -- Mover como otras ventanas vanilla
    o.moveWithMouse = true

    -- ?? RESIZABLE
    o.resizable      = true
    o.minimumWidth   = o.baseMinWidth
    o.minimumHeight  = o.baseMinHeight
    o.isResizing     = false

    --------------------------------------------------
    -- Boton cerrar
    --------------------------------------------------
    local closeText = getText("UI_Close") or "Close"
    o.closeBtn = ISButton:new(w/2 - 60, h - 40, 120, 28,
        closeText, o, InspectUI_VanillaPlus.onClose)
    o.closeBtn.backgroundColor          = { r=0,   g=0,   b=0,   a=0.7 }
    o.closeBtn.backgroundColorMouseOver = { r=0.3, g=0.3, b=0.3, a=0.8 }
    o.closeBtn.borderColor              = { r=1,   g=1,   b=1,   a=0.25 }
    o.closeBtn.textColor                = { r=1,   g=1,   b=1,   a=1 }
    o:addChild(o.closeBtn)

    --------------------------------------------------
    -- Pin minimizar/restaurar en el header
    --------------------------------------------------
    local pinSize = 20
    local pinX = w - pinSize - o.margin
    local pinY = (o.titleH - pinSize) / 2

    o.pinBtn = ISButton:new(pinX, pinY, pinSize, pinSize,
        "-", o, InspectUI_VanillaPlus.onToggleCollapse)

    o.pinBtn.backgroundColor          = { r=0,   g=0,   b=0,   a=0 }
    o.pinBtn.backgroundColorMouseOver = { r=1,   g=1,   b=1,   a=0.10 }
    o.pinBtn.borderColor              = { r=1,   g=1,   b=1,   a=0.45 }
    o.pinBtn.textColor                = { r=1,   g=1,   b=1,   a=1 }
    o:addChild(o.pinBtn)

    --------------------------------------------------
    -- Botón 2D / 3D en cabecera
    --------------------------------------------------
    local toggleW = 36
    local toggleH = pinSize
    local toggleX = pinX - toggleW - 6
    local toggleY = pinY

    o.toggleViewBtn = ISButton:new(toggleX, toggleY, toggleW, toggleH, "3D", o, InspectUI_VanillaPlus.onToggleView)
    o.toggleViewBtn.backgroundColor          = { r=0, g=0, b=0, a=0 }
    o.toggleViewBtn.backgroundColorMouseOver = { r=1, g=1, b=1, a=0.10 }
    o.toggleViewBtn.borderColor              = { r=1, g=1, b=1, a=0.45 }
    o.toggleViewBtn.textColor                = { r=1, g=1, b=1, a=1 }
    o:addChild(o.toggleViewBtn)
    if o.supports3D ~= nil then
        o.toggleViewBtn:setEnable(o.supports3D)
        o.toggleViewBtn:setTitle(o.use3D and "3D" or "2D")
    end

    --------------------------------------------------
    -- Botón auto-rotar
    --------------------------------------------------
    local autoW = 36
    local autoH = pinSize
    local autoX = toggleX - autoW - 6
    local autoY = pinY

    o.autoRotateBtn = ISButton:new(autoX, autoY, autoW, autoH, "R", o, InspectUI_VanillaPlus.onToggleAutoRotate)
    o.autoRotateBtn.backgroundColor          = { r=0, g=0, b=0, a=0 }
    o.autoRotateBtn.backgroundColorMouseOver = { r=1, g=1, b=1, a=0.10 }
    o.autoRotateBtn.borderColor              = { r=1, g=1, b=1, a=0.45 }
    o.autoRotateBtn.textColor                = { r=1, g=1, b=1, a=1 }
    o:addChild(o.autoRotateBtn)
    if o.supports3D == false then
        o.autoRotateBtn:setEnable(false)
    end

    --------------------------------------------------
    -- Botón foto / view
    --------------------------------------------------
    local photoW = 44
    local photoH = pinSize
    local photoX = autoX - photoW - 6
    local photoY = pinY
    local photoTxt = getText("IGUI_Inspect_Photo") or getText("UI_Inspect_Photo") or "PHOTO"

    o.photoBtn = ISButton:new(photoX, photoY, photoW, photoH, photoTxt, o, InspectUI_VanillaPlus.onTogglePhotoMode)
    o.photoBtn.backgroundColor          = { r=0, g=0, b=0, a=0 }
    o.photoBtn.backgroundColorMouseOver = { r=1, g=1, b=1, a=0.10 }
    o.photoBtn.borderColor              = { r=1, g=1, b=1, a=0.45 }
    o.photoBtn.textColor                = { r=1, g=1, b=1, a=1 }
    o:addChild(o.photoBtn)

    --------------------------------------------------
    -- Mini close button (visible ONLY when collapsed)
    --------------------------------------------------
    o.collapsedCloseBtn = ISButton:new(0, 0, 40, 20,
        getText("UI_Close") or "X",
        o,
        function()
            o:onClose()
        end)

    o.collapsedCloseBtn.backgroundColor          = { r=0.65, g=0.05, b=0.05, a=0.90 }
    o.collapsedCloseBtn.backgroundColorMouseOver = { r=0.90, g=0.10, b=0.10, a=1.0 }
    o.collapsedCloseBtn.borderColor              = { r=1,   g=1,   b=1,   a=0.45 }
    o.collapsedCloseBtn.textColor                = { r=1,   g=1,   b=1,   a=1 }
    o.collapsedCloseBtn:setVisible(false)
    o:addChild(o.collapsedCloseBtn)

    -- Custom render para pin (+ / -)
    o.pinBtn.prerender = function(btn)
        local self = btn.parent
        local collapsed = self.isCollapsed

        local bgR, bgG, bgB = 0.10, 0.55, 0.10   -- verde un poco mas vivo
        local text = "+"

        if not collapsed then
            bgR, bgG, bgB = 0.65, 0.05, 0.05     -- rojo normal
            text = "-"
        end

        -- Hover intensifica el color
        if btn.mouseOver then
            bgR = math.min(1, bgR + 0.25)
            bgG = math.min(1, bgG + 0.10)
            bgB = math.min(1, bgB + 0.10)
        end

        -- Fondo del botón
        btn:drawRect(0, 0, btn.width, btn.height, 1, bgR, bgG, bgB)

        -- Borde
        btn:drawRectBorder(0, 0, btn.width, btn.height, 1, 1,1,1)

        -- Símbolo centrado
        local fontH = getTextManager():getFontHeight(UIFont.Small)
        btn:drawTextCentre(text, btn.width/2, (btn.height - fontH)/2,
                        1,1,1,1, UIFont.Small)
    end

    --------------------------------------------------
    -- Caja de descripcion
    --------------------------------------------------
    local descY = o.titleH + o.spriteH + o.statsH + (o.margin * 3)
    local descH = h - descY - o.bottomPadding
    if descH < 40 then descH = 40 end

    o.descBox = ISRichTextPanel:new(o.margin, descY, w - o.margin * 2, descH)
    o.descBox.marginLeft   = 10
    o.descBox.marginRight  = 10
    o.descBox.marginTop    = 10
    o.descBox.marginBottom = 10
    o.descBox.background   = true
    -- Fondo para la caja de descripci�n: tono #2E2626 con ligera transparencia
    o.descBox.backgroundColor = { r=46/255, g=38/255, b=38/255, a=0.78 }
    o.descBox.borderColor     = { r=1, g=1, b=1, a=0.20 }
    o.descBox.autosetheight   = false
    o.descBox.clip            = true   -- recortar texto al interior del panel
    o.descBox:initialise()
    o.descBox:addScrollBars()
    o.descBox:setScrollWithParent(true)
    o:addChild(o.descBox)

    return o
end

------------------------------------------------------------
-- Cambiar item y preparar preview
------------------------------------------------------------
function InspectUI_VanillaPlus:setItem(item)
    if self.preview3D and self.preview3D.destroy then
        self.preview3D:destroy()
    end

    self.item = item
    self.preview3D = Inspect3DPreview:new(item)
    self.supports3D = InspectUI_canUse3D(item)

    if not self.supports3D then
        self.use3D = false
    elseif InspectUI_LastMode ~= nil then
        self.use3D = InspectUI_LastMode
    else
        self.use3D = true
    end

    -- Resetear caches ligados al item
    self._inspectMetaComputed = false
    self._inspectCraftable = nil
    self._inspectForageable = nil
    self.statList = nil
    self.cachedIcon = nil
    self.statsScroll = 0
    self.statsScrollMax = 0

    self.titleText = nil
    if item and item.getDisplayName then
        self.titleText = item:getDisplayName()
    end
    if not self.titleText or self.titleText == "" then
        self.titleText = getText("IGUI_Inspect_Title") or "Inspect Item"
    end

    if self.toggleViewBtn then
        self.toggleViewBtn:setEnable(self.supports3D)
        self.toggleViewBtn:setTitle(self.use3D and "3D" or "2D")
    end
end

-- Draw item title manually when window is collapsed
local InspectUI_deferredClose = {}
local InspectUI_deferredCloseHooked = false

local function InspectUI_flushDeferredClose()
    local pending = InspectUI_deferredClose
    InspectUI_deferredClose = {}
    InspectUI_deferredCloseHooked = false
    if Events and Events.OnTick then
        Events.OnTick.Remove(InspectUI_flushDeferredClose)
    end

    for _, ui in ipairs(pending) do
        if ui and ui.removeFromUIManager then
            ui:removeFromUIManager()
        end
    end
end

local function InspectUI_queueDeferredClose(ui)
    if not ui then return end
    table.insert(InspectUI_deferredClose, ui)
    if Events and Events.OnTick and not InspectUI_deferredCloseHooked then
        Events.OnTick.Add(InspectUI_flushDeferredClose)
        InspectUI_deferredCloseHooked = true
    end
end

--------------------------------------------------
-- Cerrar
--------------------------------------------------
function InspectUI_VanillaPlus:onClose()
    if self._iiwClosing then return end
    self._iiwClosing = true

    if self.preview3D then
        self.preview3D:destroy()
        self.preview3D = nil
    end

    if InspectController and InspectController.unregister then
        InspectController.unregister(self)
    end
    if self.setWantKeyEvents then
        self:setWantKeyEvents(false)
    end
    if self.setWantJoypadEvents then
        self:setWantJoypadEvents(false)
    end
    self:setVisible(false)
    InspectUI_queueDeferredClose(self)
end

--------------------------------------------------
-- Alternar entre vista 2D y 3D
--------------------------------------------------
function InspectUI_VanillaPlus:onToggleView()
    if not self.supports3D then
        self.use3D = false
        if self.toggleViewBtn then
            self.toggleViewBtn:setTitle("2D")
            self.toggleViewBtn:setEnable(false)
        end
        return
    end

    self.use3D = not self.use3D
    if self.toggleViewBtn then
        self.toggleViewBtn:setTitle(self.use3D and "3D" or "2D")
    end
    InspectUI_LastMode = self.use3D
    self:updatePreviewVisibility()
end

--------------------------------------------------
-- Alternar auto-rotación
--------------------------------------------------
function InspectUI_VanillaPlus:onToggleAutoRotate()
    self.autoRotate = not self.autoRotate
    if self.autoRotateBtn then
        self.autoRotateBtn:setTitle(self.autoRotate and "R*" or "R")
    end
end

--------------------------------------------------
-- Alternar modo foto / view
--------------------------------------------------
function InspectUI_VanillaPlus:onTogglePhotoMode()
    self.photoMode = not self.photoMode
    if self.photoBtn then
        local photoTxt = getText("IGUI_Inspect_Photo") or getText("UI_Inspect_Photo") or "PHOTO"
        local viewTxt  = getText("IGUI_Inspect_Photo_View") or getText("UI_Inspect_Photo_View") or "VIEW"
        self.photoBtn:setTitle(self.photoMode and viewTxt or photoTxt)
    end
    if self.descBox then
        self.descBox:setVisible(not self.photoMode and not self.isCollapsed)
    end
    -- No recreamos preview para evitar escenas duplicadas
    self:updatePreviewVisibility()
end

--------------------------------------------------
-- Ensure the 3D preview matches the current collapsed state
--------------------------------------------------
function InspectUI_VanillaPlus:updatePreviewVisibility()
    if not self.preview3D then return end

    local scene = self.preview3D.scene
    if scene and scene.setVisible then
        -- Ignore errors silently to avoid breaking the UI if the scene is missing
        local allow3D = (not self.isCollapsed) and self.use3D and self.supports3D
        pcall(scene.setVisible, scene, allow3D)
    end
end

--------------------------------------------------
-- Minimizar / restaurar
--------------------------------------------------
function InspectUI_VanillaPlus:onToggleCollapse()
    self.isCollapsed = not self.isCollapsed

    if self.isCollapsed then
        self.preCollapseHeight = self.height
        self._prevMinHeight = self.minimumHeight
        self.minimumHeight = self.titleH
        self:setHeight(self.titleH)
        self:stopResize()

        if self.descBox then
            self.descBox:setVisible(false)
        end
        if self.closeBtn then
            self.closeBtn:setVisible(false)
        end
        if self.collapsedCloseBtn then
            self.collapsedCloseBtn:setVisible(true)
        end
        if self.resizeGrip then
            self.resizeGrip:setVisible(false)
        end
        if self.autoRotateBtn then
            self.autoRotateBtn:setVisible(false)
        end
        if self.toggleViewBtn then
            self.toggleViewBtn:setVisible(false)
        end
        if self.photoBtn then
            self.photoBtn:setVisible(false)
        end
    else
        if self.preCollapseHeight then
            self:setHeight(self.preCollapseHeight)
        end
        if self._prevMinHeight then
            self.minimumHeight = self._prevMinHeight
        end

        if self.descBox then
            self.descBox:setVisible(true)
        end
        if self.closeBtn then
            self.closeBtn:setVisible(true)
        end
        if self.collapsedCloseBtn then
            self.collapsedCloseBtn:setVisible(false)
        end
        if self.resizeGrip then
            self.resizeGrip:setVisible(true)
        end
        if self.autoRotateBtn then
            self.autoRotateBtn:setVisible(true)
        end
        if self.toggleViewBtn then
            self.toggleViewBtn:setVisible(true)
        end
        if self.photoBtn then
            self.photoBtn:setVisible(true)
        end
    end

    self:updatePreviewVisibility()
end

--------------------------------------------------
-- Meta rapida por item (craft / forage)
--------------------------------------------------
function InspectUI_VanillaPlus:computeMeta()
    if self._inspectMetaComputed or not self.item then return end
    self._inspectMetaComputed = true

    local item = self.item

    local craftable = false
    if getAllRecipesFor then
        local ok, recipes = pcall(getAllRecipesFor, item)
        if ok and recipes and recipes.size and recipes:size() > 0 then
            craftable = true
        end
    end
    self._inspectCraftable = craftable

    local forageable = false
    if item.hasTag then
        if item:hasTag("Forage") or item:hasTag("ForageFood") or item:hasTag("ForageWeapon") then
            forageable = true
        end
    end
    self._inspectForageable = forageable
end

--------------------------------------------------
-- AmmoType -> nombre amigable
--------------------------------------------------
local function InspectUI_ConvertAmmoType(ammoType)
    if not ammoType or ammoType == "" then return nil end

    local scriptItem = ScriptManager.instance:getItem(ammoType)
    if scriptItem then
        return scriptItem:getDisplayName()
    end

    local short = ammoType:match("%.(.+)") or ammoType
    return short
end

--------------------------------------------------
-- RESIZE (Estable, herencia ISPanel)
--------------------------------------------------
function InspectUI_VanillaPlus:measureStatsLayout(targetWidth)
    local width = targetWidth or self.width
    local stats = self.statList or {}

    local tm       = getTextManager()
    local font     = self:getAdaptiveFont(width)
    local fontH    = tm:getFontHeight(font)
    local iconSize = self:getAdaptiveIconSize(width)
    local pad      = self:getAdaptivePadding(width)
    local padDesc  = pad.descPad or self.margin

    local rowHeight = math.max(fontH, iconSize)
    local rowStep   = rowHeight + pad.rowGap
    local contentX  = self.margin
    local contentW  = math.max(60, width - (self.margin * 2))

    local longest = 0
    for i = 1, #stats do
        local entry = stats[i]
        local w = tm:MeasureStringX(font, entry.text or "")
        if entry.icon then
            w = w + iconSize + pad.iconText
        end
        if w > longest then
            longest = w
        end
    end

    local colsByWidth = 1
    if longest > 0 then
        colsByWidth = math.floor(contentW / (longest + pad.colGap))
    end
    if colsByWidth < 1 then colsByWidth = 1 end
    if colsByWidth > 4 then colsByWidth = 4 end

    local total = #stats
    local desired = 1
    if total >= 24 then
        desired = 4
    elseif total >= 14 then
        desired = 3
    elseif total >= 6 then
        desired = 2
    end

    local cols = math.max(1, math.min(colsByWidth, desired))
    local colW = (contentW - pad.colGap * (cols - 1)) / cols
    local rowsTotal = (cols > 0) and math.ceil(total / cols) or 0

    local paddingTop, paddingBottom = 8, 8
    local statsBoxH = paddingTop + (rowsTotal * rowStep) + paddingBottom
    statsBoxH = math.max(self.statsMinH or 60, statsBoxH)

    local descMin = self.descMinHeight or 140
    -- Altura mínima del panel NO debe depender de todo el alto de stats (permitimos scroll)
    local statsMinVisible = math.max(self.statsMinH or 60, math.min(statsBoxH, self.statsMaxH or statsBoxH))
    local minH = self.titleH + self.margin + self.spriteH + self.margin + statsMinVisible + padDesc + descMin + self.bottomPadding
    local minHeight = math.max(self.baseMinHeight or 0, math.ceil(minH))

    return {
        tm = tm,
        font = font,
        fontH = fontH,
        iconSize = iconSize,
        pad = pad,
        padDesc = padDesc,
        rowHeight = rowHeight,
        rowStep = rowStep,
        contentX = contentX,
        contentW = contentW,
        longest = longest,
        cols = cols,
        colW = colW,
        rowsTotal = rowsTotal,
        paddingTop = paddingTop,
        paddingBottom = paddingBottom,
        statsBoxH = statsBoxH,
        minHeight = minHeight,
        total = total,
    }
end

--------------------------------------------------
-- Aplicar resize basado en el movimiento del ratón
--------------------------------------------------
function InspectUI_VanillaPlus:applyResize()
    if self.isCollapsed or not self.resizable then return end
    if not self.resizeStartW or not self.resizeStartH then return end
    if not self.resizeStartMouseX or not self.resizeStartMouseY then return end

    local desiredW = self.resizeStartW + (getMouseX() - self.resizeStartMouseX)
    local desiredH = self.resizeStartH + (getMouseY() - self.resizeStartMouseY)

    local targetW = math.max(self.minimumWidth or 0, desiredW)
    self:setWidth(targetW)

    local layout = self:measureStatsLayout(targetW)
    self.minimumHeight = layout.minHeight
    if self.statsScroll then
        self.statsScroll = 0
    end

    local targetH = math.max(self.minimumHeight, desiredH)
    self:setHeight(targetH)
    self.preCollapseHeight = targetH
end

--------------------------------------------------
-- Manejar doble clic en el grip de resize
--------------------------------------------------
function InspectUI_VanillaPlus:handleResizeClick(mx, my)
    if self.isCollapsed or not self.resizable then return false end

    local hit = 20
    local inResize = (mx >= self.width - hit) and (my >= self.height - hit)
    if not inResize then return false end

    local now = getTimestampMs()

    -- DOBLE CLIC
    if (now - (self.lastResizeClick or 0)) < 250 then

        if isShiftKeyDown() then
            -- Shift ahora usa el tamaño ancho (antes Alt)
            self:setWidth(self.wideWidth)
            self:setHeight(self.wideHeight)

        elseif isAltKeyDown() then
            -- Alt ahora expande casi a pantalla completa
            local core = getCore()
            local sw   = core and core:getScreenWidth() or (self.defaultWidth * 2)
            local sh   = core and core:getScreenHeight() or (self.defaultHeight * 2)
            local margin = 10
            local newW = math.max(self.minimumWidth, sw - margin * 2)
            local newH = math.max(self.minimumHeight, sh - margin * 2)
            self:setWidth(newW)
            self:setHeight(newH)
            self:setX(margin)
            self:setY(margin)

        else
            self:setWidth(self.defaultWidth)
            self:setHeight(self.defaultHeight)
            if self.defaultX and self.defaultY then
                self:setX(self.defaultX)
                self:setY(self.defaultY)
            else
                centerWindow(self)
            end
        end

        local layout = self:measureStatsLayout(self.width)
        self.minimumHeight = layout.minHeight
        if self.height < self.minimumHeight then
            self:setHeight(self.minimumHeight)
        end
        self.preCollapseHeight = self.height

        self.isResizing = false         -- IMPORTANTÍSIMO
        self.resizing = false           -- Para compatibilidad
        return true
    end

    self.lastResizeClick = now

    return false
end

--------------------------------------------------
-- Hit test para el area del sprite/preview
--------------------------------------------------
function InspectUI_VanillaPlus:isMouseOverSpriteBox()
    if self.isCollapsed then return false end
    local box = self.spriteBox
    if not box then return false end

    local mx = self:getMouseX()
    local my = self:getMouseY()

    return mx >= box.x and mx <= box.x + box.w
        and my >= box.y and my <= box.y + box.h
end

--------------------------------------------------
-- Hit test para el grip de resize (esquina inferior derecha)
--------------------------------------------------
function InspectUI_VanillaPlus:isOverResizeGrip(x, y)
    if not self.resizeGrip or not self.resizeGrip:isVisible() then return false end
    local gx = self.resizeGrip:getX()
    local gy = self.resizeGrip:getY()
    local gw = self.resizeGrip:getWidth()
    local gh = self.resizeGrip:getHeight()
    return x >= gx and x <= gx + gw and y >= gy and y <= gy + gh
end

--------------------------------------------------
-- Eventos de ratón
--------------------------------------------------
function InspectUI_VanillaPlus:onMouseDown(x, y)
    -- Prioridad: grip de resize (cualquier botón)
    if self:isOverResizeGrip(x, y) then
        if not self.resizable or self.isCollapsed then return true end

        local handled = self:handleResizeClick(self:getMouseX(), self:getMouseY())
        if handled then
            self:stopResize()
            return true
        end

        self.isResizing = true
        self.resizing = true
        self.resizeStartMouseX = getMouseX()
        self.resizeStartMouseY = getMouseY()
        self.resizeStartW = self.width
        self.resizeStartH = self.height
        self:bringToTop()
        return true
    end

    -- Si clic en el area del sprite/preview pero NO en el grip, solo focus
    if self:isMouseOverSpriteBox() and not self:isOverResizeGrip(x, y) then
        self:bringToTop()
        return
    end

    ISPanel.onMouseDown(self, x, y)
end

--------------------------------------------------
-- Eventos de ratón (continuación)
--------------------------------------------------
function InspectUI_VanillaPlus:onMouseUp(x, y)
    ISPanel.onMouseUp(self, x, y)
    self:stopResize()
end

--------------------------------------------------
-- Eventos de ratón (continuación)
--------------------------------------------------
function InspectUI_VanillaPlus:onMouseUpOutside(x, y)
    ISPanel.onMouseUpOutside(self, x, y)
    self:stopResize()
end

--------------------------------------------------
-- Eventos de ratón (continuación)
--------------------------------------------------
function InspectUI_VanillaPlus:onMouseMove(dx, dy)
    ISPanel.onMouseMove(self, dx, dy)

    if self.isResizing then
        self:applyResize()
        return true
    end

    if self.use3D and self.supports3D and self.preview3D and self:isMouseOverSpriteBox() then
        self.preview3D:onMouseMove(dx, dy)
        return true
    end
end

--------------------------------------------------
-- Eventos de ratón (continuación)
--------------------------------------------------
function InspectUI_VanillaPlus:onMouseMoveOutside(dx, dy)
    ISPanel.onMouseMoveOutside(self, dx, dy)

    -- Mantener el drag incluso fuera del panel
    if self.isResizing then
        self:applyResize()
        return
    end
end

--------------------------------------------------
-- Eventos de ratón (continuación)
--------------------------------------------------
function InspectUI_VanillaPlus:onMouseWheel(del)
    -- Wheel solo sobre el area 3D
    if self.use3D and self.supports3D and self.preview3D and self.preview3D:isMouseOverBox() then
        self.preview3D:onMouseWheel(del)
        return true
    end

    if self.isCollapsed then return false end

    local mx = self:getMouseX()
    local my = self:getMouseY()

    local spriteH = self.spriteHCurrent or self.spriteH
    local statsTop = self.titleH + self.margin + spriteH + self.margin
    local statsBottom = statsTop + (self.statsH or 0)

    local overStats = mx >= self.margin and mx <= (self.width - self.margin) and my >= statsTop and my <= statsBottom

    if overStats and self.statsScrollMax and self.statsScrollMax > 0 then
        local step = 24
        self.statsScroll = self.statsScroll + (del * step)
        if self.statsScroll < 0 then self.statsScroll = 0 end
        if self.statsScroll > self.statsScrollMax then self.statsScroll = self.statsScrollMax end
        return true
    end

    return ISPanel.onMouseWheel(self, del)
end

--------------------------------------------------
-- Iniciar / detener resize
--------------------------------------------------
function InspectUI_VanillaPlus:startResize(x,y)
    if self.isCollapsed or not self.resizable then return end

    self.isResizing = true
    self.resizing = true

    self.resizeStartMouseX = getMouseX()
    self.resizeStartMouseY = getMouseY()

    self.resizeStartW = self.width
    self.resizeStartH = self.height

    self:bringToTop()
end

function InspectUI_VanillaPlus:stopResize()
    self.isResizing = false
    self.resizing = false
end


--------------------------------------------------
-- Prerender
--------------------------------------------------
function InspectUI_VanillaPlus:prerender()
    ISPanel.prerender(self)

    self:updatePreviewVisibility()

    local w = self.width
    local h = self.height

    -- Clamp panel to screen bounds (evita que quede fuera tras un resize previo)
    local core = getCore()
    if core then
        local sw = core:getScreenWidth()
        local sh = core:getScreenHeight()
        local margin = 10
        if w > sw - margin * 2 then
            w = sw - margin * 2
            self:setWidth(w)
        end
        if h > sh - margin * 2 then
            h = sh - margin * 2
            self:setHeight(h)
        end
        if self.x < margin then self:setX(margin) end
        if self.y < margin then self:setY(margin) end
        if self.x + w > sw - margin then self:setX(sw - margin - w) end
        if self.y + h > sh - margin then self:setY(sh - margin - h) end
    end

    --------------------------------------------------
    -- CABECERA
    --------------------------------------------------
    local title = self.titleText or "Inspect Item"

    self:drawRect(0, 0, w, self.titleH, 0.85, 0, 0, 0)
    self:drawRectBorder(0, 0, w, self.titleH, 0.35, 1, 1, 1)
    self:drawTextCentre(title, w/2, 10, 1,1,1,1, UIFont.Large)

    -- Icono en cabecera (incluido colapsado)
    if self.item then
        local icon = self:getItemIcon()
        if icon then
            local iconSize = 18
            local paddingX = 8
            local paddingY = math.floor((self.titleH - iconSize) / 2)
            self:drawTextureScaled(icon, paddingX, paddingY, iconSize, iconSize, 1)
        end
    end

    -- Header buttons (right to left)
    if self.pinBtn then
        local pinY = (self.titleH - self.pinBtn.height) / 2
        local nextX = w - self.margin - self.pinBtn.width
        self.pinBtn:setX(nextX)
        self.pinBtn:setY(pinY)

        nextX = nextX - 6
        if self.toggleViewBtn then
            self.toggleViewBtn:setX(nextX - self.toggleViewBtn.width)
            self.toggleViewBtn:setY(pinY)
            nextX = self.toggleViewBtn.x - 6
        end
        if self.autoRotateBtn then
            self.autoRotateBtn:setX(nextX - self.autoRotateBtn.width)
            self.autoRotateBtn:setY(pinY)
            nextX = self.autoRotateBtn.x - 6
        end
        if self.photoBtn then
            self.photoBtn:setX(nextX - self.photoBtn.width)
            self.photoBtn:setY(pinY)
        end
    end

    if self.descBox then
        self.descBox:setVisible(not self.isCollapsed and not self.photoMode)
    end
    if self.closeBtn then
        self.closeBtn:setVisible(not self.photoMode and not self.isCollapsed)
    end

    -- Botón cerrar mini (solo visible si está colapsado)
    if self.collapsedCloseBtn then
        if self.isCollapsed then
            local refX = self.pinBtn and self.pinBtn.x or (w - self.margin - self.collapsedCloseBtn.width)
            local btnW = 55
            local btnH = self.pinBtn and self.pinBtn.height or self.collapsedCloseBtn.height
            local btnY = (self.titleH - btnH) / 2
            self.collapsedCloseBtn:setWidth(btnW)
            self.collapsedCloseBtn:setHeight(btnH)
            self.collapsedCloseBtn:setX(refX - btnW - 6)
            self.collapsedCloseBtn:setY(btnY)
            self.collapsedCloseBtn:setVisible(true)
        else
            self.collapsedCloseBtn:setVisible(false)
        end
    end

    if self.isCollapsed then
        return
    end

    -- Altura dinámica del área de preview 3D/2D (crece con la ventana)
    local availableBody = h - self.titleH - self.bottomPadding - (self.margin * 3)
    local minDesc = self.descMinHeight or 140
    local statsBlock = self.statsH or 0
    local baseSpriteH = self.spriteH or 240
    local desiredSpriteH = baseSpriteH
    local extraSpace = availableBody - (baseSpriteH + statsBlock + minDesc)
    if extraSpace > 0 then
        -- Tomar un 60% del espacio sobrante para el preview
        desiredSpriteH = baseSpriteH + math.floor(extraSpace * 0.60)
    end
    if desiredSpriteH < 120 then desiredSpriteH = 120 end
    self.spriteHCurrent = desiredSpriteH

    if self.photoMode then
        self:renderPhotoMode()
        return
    end

    --------------------------------------------------
    -- CUERPO
    --------------------------------------------------
    self:computeMeta()

    local y = self.titleH + self.margin
    local innerX = self.margin
    local innerW = w - (self.margin * 2)
    local spriteH = self.spriteHCurrent or self.spriteH

    -----------------------------------------
    -- SPRITE BOX
    -----------------------------------------
    local spriteBoxY = y
    local spriteBoxH = spriteH

    self.spriteBox = { x = innerX, y = spriteBoxY, w = innerW, h = spriteBoxH }

    self:drawRect(innerX, spriteBoxY, innerW, spriteBoxH, 0.8, 0, 0, 0)
    self:drawRectBorder(innerX, spriteBoxY, innerW, spriteBoxH, 0.4, 1, 1, 1)

    if self.item then
        local itemTex = self.item:getTex()

        ---------------------------------------------------------
        -- 3D PREVIEW OR 2D SPRITE
        ---------------------------------------------------------
        local used3D = false

        if self.use3D and self.supports3D and self.preview3D then
            if self.autoRotate then
                self.preview3D.angleY = (self.preview3D.angleY or 0) + 0.8
                if self.preview3D.angleY >= 360 then
                    self.preview3D.angleY = self.preview3D.angleY - 360
                end
            end
            used3D = self.preview3D:render(innerX, spriteBoxY, innerW, spriteBoxH, self)
            if used3D and (not self.preview3D.valid3D or not self.preview3D.loadedModel) then
                used3D = false
            end
        end

        -- Fallback icon (centered/scaled)
        if (not used3D) and itemTex then
            local tw = itemTex:getWidth()
            local th = itemTex:getHeight()

            local maxW = innerW * 0.6
            local maxH = spriteBoxH * 0.6

            local scale = math.min(maxW / tw, maxH / th, 3)
            local spriteW = tw * scale
            local spriteH = th * scale

            local cx = innerX + innerW / 2
            local cy = spriteBoxY + spriteBoxH / 2
            local spriteX = cx - spriteW / 2
            local spriteY = cy - spriteH / 2

            self:drawTextureScaled(itemTex, spriteX, spriteY, spriteW, spriteH, 1, 1, 1, 1)
        end
    end

    y = spriteBoxY + spriteBoxH + self.margin

    -----------------------------------------
    -- STATS
    -----------------------------------------
    local stats = {}

    if self.item then
        local item = self.item
        local isRanged = InspectUI_isRangedWeapon and InspectUI_isRangedWeapon(item) or false
        local rangedOnly = {
            projectileDamage = true,
            projectileSpeed  = true,
            soundRadius      = true,
            minRange         = true,
            maxRange         = true,
            aimAccuracy      = true,
            hitChance        = true,
            critChance       = true,
            recoilDelay      = true,
            aimTime          = true,
            reloadTime       = true,
            jamChance        = true,
        }

        local seenStatIds = {}
        for _, def in ipairs(STAT_DEFS) do
            local skipRanged = (not isRanged) and rangedOnly[def.id]
            if (not skipRanged) and (not seenStatIds[def.id]) then
                local ok, value = pcall(def.getter, item)
                if ok and value ~= nil and value ~= "" then
                    local label = InspectUI_tr(def.labelKey, def.fallback)
                    -- Normalizar valores numericos a 2 decimales
                    if type(value) == "number" then
                        value = InspectUI_fmtNumber(value, 2)
                    elseif type(value) == "string" then
                        local num = tonumber(value)
                        if num then
                            value = InspectUI_fmtNumber(num, 2)
                        end
                    end
                    table.insert(stats, {
                        id    = def.id,
                        label = label,
                        value = tostring(value),
                        raw   = value,
                    })
                    seenStatIds[def.id] = true
                end
            end
        end

        local yesText = getText("IGUI_Inspect_Yes") or getText("UI_Yes") or "Yes"
        local noText  = getText("IGUI_Inspect_No")  or getText("UI_No")  or "No"

        if self._inspectCraftable ~= nil then
            table.insert(stats, {
                id    = "crafting",
                label = getText("IGUI_Inspect_Stat_Craftable") or "Craft",
                value = self._inspectCraftable and yesText or noText,
            })
        end

        if self._inspectForageable ~= nil then
            table.insert(stats, {
                id    = "foraging",
                label = getText("IGUI_Inspect_Stat_Forageable") or "Forage",
                value = self._inspectForageable and yesText or noText,
            })
        end
    end
    -- cache formatted stat strings for rendering
    local statEntries = {}
    local adaptiveIconSize = self:getAdaptiveIconSize()
    for _, stat in ipairs(stats) do
        local text = string.format("%s: %s", stat.label or "", stat.value or "")
        local icon = InspectUI_getStatIcon and InspectUI_getStatIcon(stat.id, adaptiveIconSize) or nil
        table.insert(statEntries, { text = text, icon = icon })
    end
    self.statList = statEntries

    ----------------------------------------------------------
    -- DRAW STATS BOX
    ----------------------------------------------------------
    local layout     = self:measureStatsLayout(w)
    local tm         = layout.tm
    local font       = layout.font
    local fontH      = layout.fontH
    local iconSize   = layout.iconSize
    local pad        = layout.pad
    local rowHeight  = layout.rowHeight
    local rowStep    = layout.rowStep
    local contentX   = layout.contentX
    local contentW   = layout.contentW
    local cols       = layout.cols
    local colW       = layout.colW
    local rowsTotal  = layout.rowsTotal
    local paddingTop = layout.paddingTop
    local paddingBottom = layout.paddingBottom
    local descPad    = layout.padDesc or self.margin

    -- Calcular espacio visible disponible para stats (el resto queda scrolleable)
    local descMin = self.descMinHeight or 140
    local availableStatsH = h - (y + (layout.padDesc or self.margin) + descMin + self.bottomPadding)
    if availableStatsH < self.statsMinH then
        availableStatsH = self.statsMinH
    end
    if availableStatsH < 40 then
        availableStatsH = 40
    end

    local totalStatsH = layout.statsBoxH
    local visibleStatsH = math.min(totalStatsH, availableStatsH)
    self.statsH = visibleStatsH
    self.minimumHeight = layout.minHeight

    if h < self.minimumHeight then
        self:setHeight(self.minimumHeight)
        h = self.height
        availableStatsH = h - (y + (layout.padDesc or self.margin) + descMin + self.bottomPadding)
        if availableStatsH < self.statsMinH then availableStatsH = self.statsMinH end
        visibleStatsH = math.min(totalStatsH, availableStatsH)
        self.statsH = visibleStatsH
    end

    --------------------------------------------------
    -- Scroll interno de stats
    --------------------------------------------------
    local maxScroll = math.max(0, totalStatsH - visibleStatsH)
    self.statsScrollMax = maxScroll
    if not self.statsScroll then self.statsScroll = 0 end
    if self.statsScroll > maxScroll then self.statsScroll = maxScroll end
    if self.statsScroll < 0 then self.statsScroll = 0 end

    -- Fondo de la caja de stats: tono #202123 con transparencia ligera
    self:drawRect(innerX, y, innerW, self.statsH, 0.65, 32/255, 33/255, 35/255)
    -- Softer border (Clean UI style)
    self:drawRectBorder(innerX, y, innerW, self.statsH, 0.45, 0.75, 0.75, 0.75)

    if #stats > 0 then
        ---------------------------------------------------------
        -- TARKOV-STYLE ADAPTIVE GRID FOR STATS (Robust)
        ---------------------------------------------------------

        local stats = self.statList or {}
        local statY = y + paddingTop - (self.statsScroll or 0)
        local statsBottom = statY + (rowsTotal * rowStep) + paddingBottom

        -- Clip al área visible
        self:setStencilRect(innerX, y, innerW, self.statsH)

        for i = 1, #stats do
            local col = (i - 1) % cols
            local row = math.floor((i - 1) / cols)

            local x = contentX + (col * (colW + pad.colGap))
            local y = statY + (row * rowStep)

            -- clamp horizontally so text never escapes-box
            if x + layout.longest > contentX + contentW then
                x = contentX
            end

            local entry = stats[i]
            local drawX = x

            if entry.icon then
                local iy = y + (rowHeight - iconSize) / 2
                self:drawTextureScaled(entry.icon, drawX, iy, iconSize, iconSize, 1, 1, 1, 1)
                drawX = drawX + iconSize + pad.iconText
            end

            local ty = y + (rowHeight - fontH) / 2
            self:drawText(entry.text, drawX, ty, 1, 1, 1, 1, font)
        end

        -- Draw vertical lineas entre columnas
        for c = 1, cols - 1 do
            local lx = contentX + (colW + pad.colGap) * c - (pad.colGap / 2)
            self:drawRect(lx, statY, 1, statsBottom - statY, 0.25, 1, 1, 1)
        end

        -- Draw horizontal lineas entre filas
        for r = 1, rowsTotal - 1 do
            local ly = statY + (rowStep * r)
            self:drawRect(contentX, ly, contentW, 1, 0.25, 1, 1, 1)
        end

        -- Scrollbar visual
        if self.statsScrollMax and self.statsScrollMax > 0 then
            local barW = 6
            local barX = innerX + innerW - barW - 2
            local barH = math.max(18, (self.statsH / totalStatsH) * self.statsH)
            local barY = y + ((self.statsScroll or 0) / self.statsScrollMax) * (self.statsH - barH)
            self:drawRect(barX, barY, barW, barH, 0.50, 1, 1, 1)
            self:drawRectBorder(barX, barY, barW, barH, 0.75, 0, 0, 0)
        end

        self:clearStencilRect()
    end

    -----------------------------------------
    -- DESCRIPCION
    -----------------------------------------
    local desc = nil

    if self.item and self.item.getDescription then
        desc = self.item:getDescription()
    end

    if (not desc or desc == "") and InspectDescription and InspectDescription.build then
        desc = InspectDescription.build(self.item, stats)
    end

    local padDesc = descPad
    local descFont = self.getAdaptiveFont and self:getAdaptiveFont(self.width) or UIFont.Small

    local descY = y + self.statsH + padDesc
    local descMin = self.descMinHeight or 140
    local descH = h - descY - self.bottomPadding
    if descH < descMin then
        descH = descMin
    end
    if descH < 40 then descH = 40 end

    self.descBox:setX(self.margin)
    self.descBox:setY(descY)
    self.descBox:setWidth(w - self.margin * 2)
    self.descBox:setHeight(descH)

    --------------------------------------------------
    -- Forzar fuente adaptativa en la descripción
    --------------------------------------------------
    if self.descBox then
        self.descBox.font = descFont
        self.descBox.defaultFont = descFont
        if self.descBox.setFont then
            self.descBox:setFont(descFont)
        end
    end

    --------------------------------------------------
    -- Ajustar margenes y texto para que no se solape con el borde
    --------------------------------------------------
    self.descBox.marginLeft   = 10
    self.descBox.marginRight  = 10
    self.descBox.marginTop    = 10
    self.descBox.marginBottom = 10
    self.descBox.clip          = true
    self.descBox:setText(desc)
    self.descBox:paginate()
    self.descBox:setVisible(true)

    --------------------------------------------------
    -- Grip de resize como hijo para que el click no lo capture el scroll
    --------------------------------------------------
    if not self.resizeGrip then
        self.resizeGrip = ISPanel:new(w - self.gripSize, h - self.gripSize, self.gripSize, self.gripSize)
        self.resizeGrip.background = false
        self.resizeGrip.borderColor = { r=1, g=1, b=1, a=0 }

        self.resizeGrip.onMouseDown = function(_, x, y)
            if not self.resizable or self.isCollapsed then return end

            -- primero intentamos manejar doble clic
            local handled = self:handleResizeClick(self:getMouseX(), self:getMouseY())
            if handled then
                -- si fue doble clic, no empezamos drag
                self:stopResize()
                return true
            end

            -- si no fue doble clic → iniciar drag normal
            self.isResizing = true
            self.resizing = true
            self.resizeStartMouseX = getMouseX()
            self.resizeStartMouseY = getMouseY()
            self.resizeStartW = self.width
            self.resizeStartH = self.height
            self:bringToTop()
            return true
        end

        self.resizeGrip.onMouseUp = function()
            self:stopResize()
        end
        self.resizeGrip.onMouseUpOutside = function()
            self:stopResize()
        end
        self:addChild(self.resizeGrip)
    end

    self.resizeGrip:setX(w - self.gripSize)
    self.resizeGrip:setY(h - self.gripSize)
    self.resizeGrip:setWidth(self.gripSize)
    self.resizeGrip:setHeight(self.gripSize)
    self.resizeGrip:setVisible(not self.isCollapsed and self.resizable)

    --------------------------------------------------
    -- Asegurar que el grip queda por encima de la escena 3D en modo foto
    --------------------------------------------------
    if self.resizeGrip:getParent() == self then
        self:removeChild(self.resizeGrip)
        self:addChild(self.resizeGrip)
        if self.resizeGrip.bringToTop then
            self.resizeGrip:bringToTop()
        end
    end

    -----------------------------------------
    -- BOTON CERRAR
    -----------------------------------------
    self.closeBtn:setX(w / 2 - self.closeBtn.width / 2)
    self.closeBtn:setY(h - 40)
    self.closeBtn:setVisible(true)
end

--------------------------------------------------
-- Render-only flow for photo mode (full preview)
--------------------------------------------------
function InspectUI_VanillaPlus:renderPhotoMode()
    local w = self.width
    local margin = self.margin
    local reserveForGrip = (self.gripSize or 22) + 2

    -- Área disponible (dejando margen y hueco de grip)
    local availW = w - (margin * 2)
    local availH = self.height - (self.titleH + margin) - margin - reserveForGrip

    -- Usar 90% del área disponible
    local innerW = availW * 0.98
    local innerH = availH * 0.98

    -- Centrar dentro del área disponible
    local innerX = margin + (availW - innerW) / 2
    local y = self.titleH + margin + (availH - innerH) / 2

    self.spriteBox = { x = innerX, y = y, w = innerW, h = innerH }

    self:drawRect(innerX, y, innerW, innerH, 0.9, 0, 0, 0)
    self:drawRectBorder(innerX, y, innerW, innerH, 0.4, 1, 1, 1)

    if not self.item then return end

    local itemTex = self.item.getTex and self.item:getTex() or nil
    local used3D = false

    if self.use3D and self.supports3D and self.preview3D then
        if self.autoRotate then
            self.preview3D.angleY = (self.preview3D.angleY or 0) + 0.8
            if self.preview3D.angleY >= 360 then
                self.preview3D.angleY = self.preview3D.angleY - 360
            end
        end
        used3D = self.preview3D:render(innerX, y, innerW, innerH, self)
        if used3D and (not self.preview3D.valid3D or not self.preview3D.loadedModel) then
            used3D = false
        end
    end

    if (not used3D) and itemTex then
        local tw = itemTex:getWidth()
        local th = itemTex:getHeight()
        local maxW = innerW * 0.9
        local maxH = innerH * 0.9
        local scale = math.min(maxW / tw, maxH / th, 3)
        local spriteW = tw * scale
        local spriteH = th * scale
        local cx = innerX + innerW / 2
        local cy = y + innerH / 2
        local spriteX = cx - spriteW / 2
        local spriteY = cy - spriteH / 2
        self:drawTextureScaled(itemTex, spriteX, spriteY, spriteW, spriteH, 1, 1, 1, 1)
    end
end

--------------------------------------------------
-- Crear inspector centrado (API)
--------------------------------------------------
function InspectUI_VanillaPlus.open(item)
    local w = 500
    local h = 650

    local core = getCore()
    local screenW = core:getScreenWidth()
    local screenH = core:getScreenHeight()
    local x = (screenW - w) / 2
    local y = (screenH - h) / 2

    local ui = InspectUI_VanillaPlus:new(x, y, w, h, item, nil)
    ui:initialise()
    ui:addToUIManager()
end

--------------------------------------------------
-- Cache icon texture for faster collapsed rendering
--------------------------------------------------
function InspectUI_VanillaPlus:getItemIcon()
    if not self.item then return nil end
    if self.cachedIcon then return self.cachedIcon end

    local texObj = self.item:getTex()
    local texName = texObj and texObj.getName and texObj:getName()
    if not texName then return nil end

    local tex = getTexture(texName)
    self.cachedIcon = tex
    return tex
end

--------------------------------------------------
-- Render hook (keeps resize grip drawing)
--------------------------------------------------
function InspectUI_VanillaPlus:render()
    -- Draw base panel elements
    ISPanel.render(self)

    -- Preserve resize grip drawing when expanded
    if self.resizable and not self.isCollapsed then
        local size = self.gripSize or 14
        local x = self.width - size
        local y = self.height - size

        self:drawRect(x, y, size, size, 0.4, 1, 1, 1)
        self:drawRectBorder(x, y, size, size, 0.8, 1, 1, 1)
    end

end

return InspectUI_VanillaPlus
