--***********************************************************
--** TM_ResizableColumns.lua                              **
--***********************************************************
-- Composite widget (no vanilla patching) that provides:
--  - Column header bar with resize-by-drag separators
--  - Right-click context menu to toggle column visibility (with checks)
--  - A vanilla ISScrollingListBox underneath for vertical scrolling
--  - Simple API: setColumns(), setItems(), refresh()
--
-- Usage:
--   local grid = TM_ResizableColumns:new(x, y, w, h, parentWindow)
--   grid:setColumns({
--       { name = "Number",   key = "name",     width = 80,  visible = true },
--       { name = "Distance", key = "distance", width = 120, visible = true },
--       { name = "Animal",   key = "animal",   width = 120, visible = true },
--       { name = "Bait",     key = "bait",     width = 200, visible = true },
--   })
--   grid:setItems({ {name=1, distance="(10,5)", animal="raccoon", bait="Carrots(fresh)"}, ... })
--   grid:refresh()

require "ISUI/ISPanel"
require "ISUI/ISScrollingListBox"
require "ISUI/ISContextMenu"
require "ISUI/ISToolTip"

-- Ensure Reset-Defaults API is loaded before we build any menus.
-- This fixes the "all items checked and no-op on click" symptom when load order differs.
do
    if not (TM_ShouldShowColumnOnReset and TM_ToggleResetPref and TM_ResetDefaultColumnsToFactory) then
        pcall(require, "TM_ModOptions")  -- safe attempt; if not found, we degrade gracefully
    end
end

TM_ResizableColumnsHeader = ISPanel:derive("TM_ResizableColumnsHeader")
TM_ResizableColumns       = ISPanel:derive("TM_ResizableColumns")

--=== Column change notification API =========================================
-- Simple callback registration so parent window can persist layout

function TM_ResizableColumns:setOnColumnsChanged(target, fn)
    -- target: object to call with (target, columns)
    -- fn    : function(target, columns)
    self._colsChangedTarget = target
    self._colsChangedFn     = fn
end

function TM_ResizableColumns:_notifyColumnsChanged()
    if self._colsChangedFn then
        -- Pass current columnConfig so the window can save widths/visibility
        self._colsChangedFn(self._colsChangedTarget, self.columnConfig)
    end
end

-- Return current columns array (each {name,key,width,visible})
function TM_ResizableColumns:getColumns()
    return self.columnConfig or {}
end

--============================
-- Internal helper
--============================
local function computeVisibleColumnsAndX(columnConfig)
    -- Returns array of { idx, x, width, ref }
    local vis = {}
    local x = 10 -- left padding for nicer look
    for i,col in ipairs(columnConfig) do
        if col.visible ~= false then
            local w = tonumber(col.width) or 80
            table.insert(vis, { idx=i, x=x, width=w, ref=col })
            x = x + w
        end
    end
    return vis
end

-- Text truncation helper (independent of language/fonts)
-- Ensures any cell/header text never overflows the column width.
local function _truncateTextToWidth(text, maxWidth, font, suffix)
    -- Guard: empty text or non-positive width
    if not text or text == "" or (maxWidth or 0) <= 0 then return "" end
    font   = font   or UIFont.Small
    suffix = suffix or "..."
    local tm = getTextManager()

    -- If it already fits, return as-is
    if tm:MeasureStringX(font, text) <= maxWidth then
        return text
    end

    -- If even the suffix doesn't fit, return empty
    if tm:MeasureStringX(font, suffix) >= maxWidth then
        return ""
    end

    -- Binary search the largest prefix that fits (classic and fast)
    local left, right, best = 1, #text, 0
    local textMax = maxWidth - tm:MeasureStringX(font, suffix)
    while left <= right do
        local mid = math.floor((left + right) / 2)
        local sub = string.sub(text, 1, mid)
        if tm:MeasureStringX(font, sub) <= textMax then
            best = mid; left = mid + 1
        else
            right = mid - 1
        end
    end
    if best == 0 then return suffix end
    return string.sub(text, 1, best) .. suffix
end

-- --- Sorting helpers (normalized comparable values) ------------------------

-- Safe trim+lower for strings
local function _trimLower(s)
    s = tostring(s or "")
    s = s:gsub("^%s+", ""):gsub("%s+$", "")
    return string.lower(s)
end

-- Detect UI "None"/empty-like values (works with localization if getText exists)
local function _isNoneLike(v)
    if v == nil then return true end
    local t = _trimLower(v)
    if t == "" or t == "-" or t == "none" then return true end
    if type(getText) == "function" then
        local u = tostring(getText("UI_None") or ""):lower()
        if u ~= "" and t == u then return true end
    end
    return false
end

-- Extract number even if value is a string with spaces, etc.
local function _toNumber(v)
    if type(v) == "number" then return v end
    local s = tostring(v or "")
    local n = s:match("[-+]?%d+%.?%d*")
    return n and tonumber(n) or nil
end

-- Parse "12.34%" -> 12.34
local function _percentToNumber(s)
    if type(s) == "number" then return s end
    local str = tostring(s or "")
    local num = str:match("[-+]?%d+%.?%d*")
    return num and tonumber(num) or nil
end

-- Parse "(dx,dy)" or "dx,dy" -> dx^2 + dy^2
local function _pairMagnitude2(s)
    local str = tostring(s or "")
    local x, y = str:match("%(?%s*(-?%d+%.?%d*)%s*,%s*(-?%d+%.?%d*)%s*%)?")
    x, y = tonumber(x or ""), tonumber(y or "")
    if not x or not y then return nil end
    return x*x + y*y
end

-- Column-specific string normalizers
local function _normalizeAnimal(s)
    -- remove trailing " (T)" flag; collapse spaces; lower-case
    local t = _trimLower(s)
    t = t:gsub("%s*%(%s*t%s*%)", "")
    t = t:gsub("%s+", " ")
    return t
end

local function _normalizeBait(s)
    local t = _trimLower(s)
    t = t:gsub("^base%.", "")                 -- drop script prefix
    t = t:gsub("%s*%(%s*fresh%s*%)", "")      -- drop "(fresh)"
    t = t:gsub("%s*%(%s*rotten%s*%)", "")     -- drop "(rotten)"
    t = t:gsub("%s+", " ")
    return t
end

-- Build a comparable key for a given (item, columnKey).
-- Returns (class, value, isNumeric)
--   class: 0 = normal value, 1 = none-like (so they go last in ASC)
--   value: number or string (already normalized)
--   isNumeric: true if numeric comparison should be used for 'value'
local function _makeKeyFor(item, key)
    local v = item and item[key]

    -- Numeric columns
    if key == "name" or key == "skill" or key == "aliveHour" or key == "daysUntilStale" then
        local n = _toNumber(v)
        return (n == nil and 1 or 0), (n or 0), true
    elseif key == "chance" then
        local n = _percentToNumber(v)
        return (n == nil and 1 or 0), (n or 0), true
    elseif key == "distance" or key == "chunks" then
        local n = _pairMagnitude2(v)
        return (n == nil and 1 or 0), (n or 0), true
    elseif key == "showhide" then
        -- Shown < Hidden in ASC
		local s = tostring(v or "")
		local shownTxt  = (getText and getText("UI_TM_Shown"))  or "Shown"
		local hiddenTxt = (getText and getText("UI_TM_Hidden")) or "Hidden"

		-- Prefer the canonical boolean if present, then fall back to string comparison
		local n
		if item and item.__showWanted ~= nil then
			-- shown -> 0, hidden -> 1
			n = (item.__showWanted == true) and 0 or 1
		else
			n = (s == shownTxt and 0) or (s == hiddenTxt and 1) or 0
		end

		return 0, n, true
    end

    -- String columns (apply per-column normalization)
    local s = tostring(v or "")
    local cls = _isNoneLike(s) and 1 or 0
    if key == "animal" then
        return cls, _normalizeAnimal(s), false
    elseif key == "bait" then
        return cls, _normalizeBait(s), false
    else
        return cls, _trimLower(s), false
    end
end

--============================
-- Header Panel
--============================
function TM_ResizableColumnsHeader:new(x, y, w, h, grid)
    local o = ISPanel:new(x, y, w, h)
    setmetatable(o, self); self.__index = self
    o.grid = grid                               -- back-reference to parent grid
    o.backgroundColor = {r=0, g=0, b=0, a=0.7}  -- semi-transparent header
    o.draggingCol = nil
    o.lastMouseX  = nil
    o.resizeGrab  = 4                            -- px hotspot near a separator
    return o
end

function TM_ResizableColumnsHeader:render()
    -- Clear background
    self:drawRect(0, 0, self.width, self.height, 1, 0, 0, 0)

    -- Draw text + separators
    local fh = getTextManager():getFontHeight(UIFont.Small)
    local dy = math.floor((self.height - fh) / 2)
    local columns  = self.grid.columnConfig or {}
    local visibles = computeVisibleColumnsAndX(columns)
	for _,c in ipairs(visibles) do
		-- clip por columna
		self:setStencilRect(c.x, 0, c.width, self.height)
		-- Truncate header text so it never bleeds into the next column
		local headerMaxW = math.max(0, c.width - 8) -- padding inside the cell
		local headerTxt  = _truncateTextToWidth(tostring(c.ref.name or ""), headerMaxW, UIFont.Small, "...")
		self:drawText(headerTxt, c.x + 5, dy, 1,1,1,1, UIFont.Small)

		self:clearStencilRect()
		-- separador
		self:drawRect(c.x + c.width, 0, 1, self.height, 1, 1,1,1)
	end

    -- Update tooltip AFTER drawing so hit-tests use final geometry
    self:updateTooltip()
end

-- Returns the visible column record under local X (excluding the resize hotspot).
function TM_ResizableColumnsHeader:_columnAt(x)
    local visibles = computeVisibleColumnsAndX(self.grid.columnConfig or {})
    for _, c in ipairs(visibles) do
        if x >= c.x and x < c.x + c.width then
            -- Avoid conflict with the resize gesture if we're near the right separator
            local sepX = c.x + c.width
            if math.abs(x - sepX) <= self.resizeGrab then
                return nil
            end
            return c
        end
    end
    return nil
end

-- Right click on header -> open context menu with two submenus:
--   * "Shown columns"  : toggles current visibility (what you see now)
--   * "Default columns": toggles which columns will be shown after pressing Reset
function TM_ResizableColumnsHeader:onRightMouseDown(x, y)
    local context = ISContextMenu.get(0, getMouseX(), getMouseY())

    -- Local helper to attach tooltips to context menu options
    local function _attachTooltip(opt, text)
        if not opt or not text or text == "" then return end
        local tt = ISToolTip:new()
        tt:initialise()
        tt.maxLineWidth = 1000
        tt.description = tostring(text)
        opt.toolTip = tt
    end

    -- --- Submenu: Shown columns (current view) ------------------------------
    local shownLabel = (getText and getText("UI_TM_Menu_ShownCols")) or "Shown columns"
    local parentShown = context:addOption(shownLabel, nil, nil)
    local subShown = ISContextMenu:getNew(context)
    context:addSubMenu(parentShown, subShown)

    -- Tooltip for the option that opens the ShownCols submenu
    local shownTipText = (getText and getText("UI_TM_MenuTip_ShownCols")) or
                         "Toggle which columns are currently visible."
    _attachTooltip(parentShown, shownTipText)

    for i, col in ipairs(self.grid.columnConfig or {}) do
        local label = tostring(col.name or ("Col "..tostring(i)))
        local opt = subShown:addOption(label, self.grid, TM_ResizableColumns.toggleColumnVisibility, i)
        subShown:setOptionChecked(opt, col.visible ~= false)
    end

    -- --- Submenu: Default columns (affects Reset) ---------------------------
    local defLabel = (getText and getText("UI_TM_Menu_DefaultCols")) or "Default columns"
    local parentDef = context:addOption(defLabel, nil, nil)
    local subDef = ISContextMenu:getNew(context)
    context:addSubMenu(parentDef, subDef)

    -- Tooltip for the option that opens the DefaultCols submenu
    local resetBtnLabel = (getText and getText("UI_TM_Menu_Reset")) or nil
    if not resetBtnLabel or resetBtnLabel == "UI_TM_Menu_Reset" then
        resetBtnLabel = (getText and getText("UI_optionscreen_binding_reset")) or "Reset"
        if resetBtnLabel == "UI_optionscreen_binding_reset" then resetBtnLabel = "Reset" end
    end
    local defTipText = (getText and getText("UI_TM_MenuTip_DefaultCols", resetBtnLabel)) or
                       ('Choose which columns will be visible after pressing "' .. resetBtnLabel ..'".')
    _attachTooltip(parentDef, defTipText)

    -- Are the Reset-Defaults functions available?
    local hasResetAPI = (TM_ShouldShowColumnOnReset ~= nil and TM_ToggleResetPref ~= nil)

    -- Keep a map to refresh checks later
    local defOptByKey = {}

    -- Helper: flip pref and (if menu still visible) update the check
    local function toggleAndRefresh(k, optObj)
        if hasResetAPI then
            TM_ToggleResetPref(k)
        end
        local newVal = hasResetAPI and TM_ShouldShowColumnOnReset(k) or false
        if subDef and optObj and subDef.setOptionChecked then
            subDef:setOptionChecked(optObj, newVal and true or false)
        end
    end

    for _, col in ipairs(self.grid.columnConfig or {}) do
        local label = tostring(col.name or col.key or "?")
        do
            local k = col.key
            local checked = hasResetAPI and TM_ShouldShowColumnOnReset(k) or false

            -- Create the option first, then reference it from the handler via the upvalue 'opt'
            local opt
            opt = subDef:addOption(label, nil, function()
                toggleAndRefresh(k, opt)
            end)

            subDef:setOptionChecked(opt, checked)
            defOptByKey[k] = opt
        end
    end

    -- "Apply factory defaults" -> restores TM.RESET_COLUMNS defaults in our prefs file
    local resetLabel = (getText and getText("UI_TM_Menu_ResetToFactory")) or "Apply factory defaults"

    local resetOpt = subDef:addOption(resetLabel, nil, function()
        if TM_ResetDefaultColumnsToFactory then
            TM_ResetDefaultColumnsToFactory()
        end
        -- Refresh checks...
        for k, optObj in pairs(defOptByKey) do
            local val = (hasResetAPI and TM_ShouldShowColumnOnReset(k)) or false
            if subDef and subDef.setOptionChecked then
                subDef:setOptionChecked(optObj, val and true or false)
            end
        end
    end)
    resetOpt.iconTexture = getTexture("media/ui/inventoryPanes/Button_Settings.png")

    -- Tooltip for "Apply factory defaults"
    local resetTipText = (getText and getText("UI_TM_MenuTip_ResetToFactory")) or
                         "Restore the default options to their original values."
    _attachTooltip(resetOpt, resetTipText)
end

-- Left click near separator -> start resize
function TM_ResizableColumnsHeader:onMouseDown(x, y)
    if not isMouseButtonDown(0) then return end

    -- (1) Start resizing if near a separator
    local columns  = self.grid.columnConfig or {}
    local visibles = computeVisibleColumnsAndX(columns)
    for _, c in ipairs(visibles) do
        local sepX = c.x + c.width
        if math.abs(x - sepX) <= self.resizeGrab then
            self.draggingCol = c
            self.lastMouseX  = getMouseX()
            return
        end
    end

	-- (2) Normal header click -> sort by that column
	local col = self:_columnAt(x)
	if not col then return end
	if self.grid and self.grid.toggleSortByKey then
		self.grid:toggleSortByKey(col.ref.key)
	end
end

function TM_ResizableColumnsHeader:onMouseUp(x, y)
    local wasDragging = self.draggingCol ~= nil
    self.draggingCol = nil
    self.lastMouseX  = nil
    -- Notify parent grid only once when the drag ends
    if wasDragging and self.grid then
        self.grid:_notifyColumnsChanged()
    end
end


function TM_ResizableColumnsHeader:onMouseMove(dx, dy)
    if self.draggingCol then
        -- Use absolute mouse X to compute delta; dx can be unreliable
        local mx = getMouseX()
        local delta = mx - (self.lastMouseX or mx)
        if delta ~= 0 then
            local newWidth = math.max(60, (self.draggingCol.ref.width or self.draggingCol.width or 80) + delta)
            self.draggingCol.ref.width = newWidth
            self.lastMouseX = mx
            -- Keep list + header in sync
            self.grid:refresh()
        end
    end
end

-- Returns header tooltip text for a given column key, delegating to the parent window if available.
function TM_ResizableColumns:getHeaderTooltipForKey(key)
    local at = self.parentWindow and self.parentWindow.selectedAnimalType or nil
    if self.parentWindow and self.parentWindow.buildHeaderTooltipFor then
        return self.parentWindow:buildHeaderTooltipFor(key)
    end
    return nil
end


function TM_ResizableColumnsHeader:updateTooltip()
	-- do not show tooltip if there is an open combo box
	if self.grid and self.grid.parentWindow and self.grid.parentWindow._isAnyComboOpen
	   and self.grid.parentWindow:_isAnyComboOpen() then
		if self.tooltipUI and self.tooltipUI:getIsVisible() then
			self.tooltipUI:setVisible(false); self.tooltipUI:removeFromUIManager()
		end
		return
	end	
	
    local mx, my = self:getMouseX(), self:getMouseY()
    if mx < 0 or my < 0 or mx >= self.width or my >= self.height then
        if self.tooltipUI and self.tooltipUI:getIsVisible() then
            self.tooltipUI:setVisible(false); self.tooltipUI:removeFromUIManager()
        end
        return
    end
    -- Column under mouse
    local visibles = computeVisibleColumnsAndX(self.grid.columnConfig or {})
    local colKey = nil
    for _,c in ipairs(visibles) do
        if mx >= c.x and mx < c.x + c.width then
            colKey = c.ref.key
            break
        end
    end
	if not colKey then
        if self.tooltipUI and self.tooltipUI:getIsVisible() then
            self.tooltipUI:setVisible(false); self.tooltipUI:removeFromUIManager()
        end
        return
    end

    -- Ask grid/window for header tooltip for this key
    local desc = self.grid:getHeaderTooltipForKey(colKey)
    if not desc or desc == "" then
        if self.tooltipUI and self.tooltipUI:getIsVisible() then
            self.tooltipUI:setVisible(false); self.tooltipUI:removeFromUIManager()
        end
        return
    end

    if not self.tooltipUI then
        self.tooltipUI = ISToolTip:new()
        self.tooltipUI:setOwner(self)
        self.tooltipUI:setAlwaysOnTop(true)
        self.tooltipUI.maxLineWidth = 1000
    end
    if not self.tooltipUI:getIsVisible() then
        self.tooltipUI:addToUIManager()
        self.tooltipUI:setVisible(true)
    end
    self.tooltipUI.description = desc
    self.tooltipUI:setX(getMouseX() + 23)
    self.tooltipUI:setY(getMouseY() + 23)
end

--============================
-- Grid Panel (header + list)
--============================
function TM_ResizableColumns:new(x, y, w, h, parentWindow)
    local o = ISPanel:new(x, y, w, h)
    setmetatable(o, self); self.__index = self
    o.parentWindow = parentWindow
    o.columnConfig = {}
    o.items = {}
    o.headerHeight = 22 -- a bit taller to comfortably center text
    o.backgroundColor = {r=0, g=0, b=0, a=0} -- list draws its own bg
    return o
end

function TM_ResizableColumns:initialise()
    ISPanel.initialise(self)

    -- Header (top)
    self.header = TM_ResizableColumnsHeader:new(0, 0, self.width, self.headerHeight, self)
    self.header:initialise()
	self.header.background = true
	self.header.backgroundColor = { r = 0, g = 0, b = 0, a = 0.50 }
	self.header:setAnchorRight(true)  
    self:addChild(self.header)

    -- List (below header)
    self.list = ISScrollingListBox:new(0, self.headerHeight, self.width, self.height - self.headerHeight)
    self.list:initialise(); self.list:instantiate()
    self.list:setFont(UIFont.Small, 2)
    self.list.drawBorder = true
	self.list:setAnchorLeft(true)
	self.list:setAnchorRight(true)
	self.list:setAnchorBottom(true)
	
	-- Force background repaint to avoid “ghost” text when resizing
	self.list.background = true
	self.list.backgroundColor = { r = 0, g = 0, b = 0, a = 0.50 }

	-- Per-cell tooltips: override list:updateTooltip() to pick tooltip by column key
	self.list.updateTooltip = function(list)
		local pw = self.parentWindow
		if not list:isMouseOver() or not list:isReallyVisible() then
			if list.tooltipUI and list.tooltipUI:getIsVisible() then
				list.tooltipUI:setVisible(false); list.tooltipUI:removeFromUIManager()
			end
			return
		end
		if pw and pw._isAnyComboOpen and pw:_isAnyComboOpen() then
			if list.tooltipUI and list.tooltipUI:getIsVisible() then
				list.tooltipUI:setVisible(false); list.tooltipUI:removeFromUIManager()
			end
			return
		end

		-- Row under mouse
		local row = list:rowAt(list:getMouseX(), list:getMouseY())
		if row < 1 or row > #list.items then
			if list.tooltipUI and list.tooltipUI:getIsVisible() then
				list.tooltipUI:setVisible(false); list.tooltipUI:removeFromUIManager()
			end
			return
		end

		-- Item for this row
		local item = list.items[row]
		local itemTbl = item and item.item or nil

		-- notify the parent window about the last hovered row,
		--      so it can publish the vector target to ModData when Vector is ON.
		if itemTbl and self.parentWindow and self.parentWindow._noteHoverRow then
			self.parentWindow:_noteHoverRow(itemTbl)
		end

		-- Determine column under mouse X (kept for per-cell tooltip resolution)
		local mx = list:getMouseX()
		local visibles = computeVisibleColumnsAndX(self.columnConfig or {})
		local colKey = nil
		for _,c in ipairs(visibles) do
			if mx >= c.x and mx < c.x + c.width then
				colKey = c.ref.key
				break
			end
		end

		local perCell = itemTbl and itemTbl.__tooltips
		local desc = (colKey and perCell and perCell[colKey]) or nil
		if not desc or desc == "" then
			-- No tooltip for this cell -> hide if showing and return
			if list.tooltipUI and list.tooltipUI:getIsVisible() then
				list.tooltipUI:setVisible(false); list.tooltipUI:removeFromUIManager()
			end
			return
		end

		-- (unchanged tooltip UI creation and positioning below) ...
		if not list.tooltipUI then
			list.tooltipUI = ISToolTip:new()
			list.tooltipUI:setOwner(list)
			list.tooltipUI:setAlwaysOnTop(true)
			list.tooltipUI.maxLineWidth = 1000
		end
		if not list.tooltipUI:getIsVisible() then
			list.tooltipUI:addToUIManager()
			list.tooltipUI:setVisible(true)
		end
		list.tooltipUI.description = desc
		list.tooltipUI:setX(getMouseX() + 23)
		list.tooltipUI:setY(getMouseY() + 23)
	end

	-- Click handling on cells: toggle per-row Show/Hide when clicking the "showhide" column
    self.list.onMouseDown = function(list, x, y)
        if not list:isMouseOver() or not list:isReallyVisible() then return end
        local row = list:rowAt(x, y)
        if row < 1 or row > #list.items then return end

        -- Detect column under mouse
        local mx = x -- already relative to list
        local visibles = computeVisibleColumnsAndX(self.columnConfig or {})
        local colKey = nil
        for _, c in ipairs(visibles) do
            if mx >= c.x and mx < c.x + c.width then
                colKey = c.ref.key
                break
            end
        end
        if colKey == "showhide" and self.parentWindow and self.parentWindow._toggleRowShowWanted then
            local item = list.items[row] and list.items[row].item
            if item and item.__rowKey then
                self.parentWindow:_toggleRowShowWanted(item.__rowKey)
                self.parentWindow:updateTraps() -- reapply filter + refresh table
            end
            return
        end

        -- Fallback to default behavior (row selection etc.)
        if list.onMouseDownOrig then return list:onMouseDownOrig(x, y) end
    end
    -- keep original, if any (vanilla sometimes sets this); store once
    if not self.list.onMouseDownOrig then
        self.list.onMouseDownOrig = ISScrollingListBox.onMouseDown
    end

    -- Custom row renderer: draw cells aligned to current columns
	-- Pill-like button for the "showhide" column
	self.list.doDrawItem = function(list, y, wrapped, alt)
		local columns  = self.columnConfig or {}
		local visibles = computeVisibleColumnsAndX(columns)
		local item     = wrapped.item
		local colors   = item.__colors or nil
		local tm       = getTextManager()
		local font     = UIFont.Small

		for _,c in ipairs(visibles) do
			local key   = c.ref.key
			local r,g,b,a = 1,1,1,1
			if colors and colors[key] then
				local cc = colors[key]; r,g,b,a = cc[1] or 1, cc[2] or 1, cc[3] or 1, cc[4] or 1
			end

			-- Clip to the cell rect
			local clipX = c.x + 1
			local clipW = math.max(0, c.width - 2)
			list:setStencilRect(clipX, y, clipW, list.itemheight)

			if key == "showhide" then
				-- Draw as a colored button (green for Shown, brick red for Hidden)
				local isShown = (item.__showWanted == true)

				-- Localized label for the pill
				local labelShown  = (getText and getText("UI_TM_Shown"))  or "Shown"
				local labelHidden = (getText and getText("UI_TM_Hidden")) or "Hidden"
				local label = isShown and labelShown or labelHidden

				-- Colors (grass green / brick red)
				local br, bg, bb = (isShown and 0.09 or 0.27), (isShown and 0.31 or 0.09), (isShown and 0.09 or 0.06)
				local alpha = 0.80

				-- Button geometry (centered)
				local textW = tm:MeasureStringX(font, label)
				local padX, padY = 7, 3
				local btnW = math.min(c.width - 8, textW + padX * 2)
				local btnH = list.itemheight - 6
				local bx   = c.x + math.max(4, math.floor((c.width - btnW) / 2))
				local by   = y + 3

				list:drawRect(bx, by, btnW, btnH, alpha, br, bg, bb)     -- fill
				list:drawRectBorder(bx, by, btnW, btnH, 1, 0, 0, 0)       -- border
				list:drawTextCentre(label, bx + btnW / 2, by + math.floor((btnH - tm:getFontHeight(font)) / 2), 1,1,1,1, font)
			else
				-- Default text cell (truncate to fit column width)
				local value = tostring(item[key] or "")
				-- available text width inside the cell (subtract a small padding)
				local maxW  = math.max(0, c.width - 8)
				local shown = _truncateTextToWidth(value, maxW, font, "...")
				list:drawText(shown, c.x + 5, y + 2, r, g, b, a, font)
			end

			list:clearStencilRect()
		end
		return y + list.itemheight
	end

	--=== Right-click context menu on rows (Debug only): "Teleport here" =========
	if getDebug() then
		do
			local list = self.list
			if list and not list._tm_ctx_teleport then
				list._tm_ctx_teleport = true

				-- Keep any existing right-click handlers if present (defensive).
				local prevOnRMD = list.onRightMouseDown
				local prevOnRMU = list.onRightMouseUp

				-- Track press state to mirror vanilla pattern.
				function list:onRightMouseDown(x, y)
					self._tm_rightDown = true
					if prevOnRMD then prevOnRMD(self, x, y) end
				end

				function list:onRightMouseUp(x, y)
					if prevOnRMU then prevOnRMU(self, x, y) end

					-- Only proceed on a proper right-click sequence and in Debug mode.
					if not self._tm_rightDown then return end
					self._tm_rightDown = false
					if not getDebug() then return end

					-- Identify the row under the mouse; bail if none.
					local row = self:rowAt(x, y)
					if row < 1 or row > #self.items then return end

					-- Payload is stored in items[row].item (your row table from TrapManagerWindow).
					local payload = self.items[row] and self.items[row].item
					if not payload then return end

					-- World coordinates are already attached by Trap Manager as __wx/__wy/__wz.
					local wx, wy = tonumber(payload.__wx), tonumber(payload.__wy)
					local wz     = tonumber(payload.__wz) or 0
					if not (wx and wy) then return end

					-- Open the context menu at the mouse position.
					local context = ISContextMenu.get(0, getMouseX(), getMouseY())

					-- Same validity guard as the minimap (avoid offering invalid teleports).
					local ok = true
					local world = getWorld()
					if world and world.getMetaGrid then
						local mg = world:getMetaGrid()
						if mg and mg.isValidChunk then
							ok = mg:isValidChunk(wx / 8, wy / 8)
						end
					end

					if ok then
						-- Reuse minimap's translation key; fallback to English if missing.
						local label = getText("IGUI_ZombiePopulation_TeleportHere")
						if not label or label == "IGUI_ZombiePopulation_TeleportHere" then
							label = "Teleport here"
						end

						-- The window is grid.parentWindow; call its onTeleport handler.
						local win = self.parent and self.parent.parentWindow or nil
						if win and win.onTeleport then
							context:addOption(label, win, win.onTeleport, wx, wy, wz)
						end
					end

					-- Hide empty context defensively.
					if context and context.numOptions == 0 then
						context:setVisible(false)
					end
				end
			end
		end
	end

	-- --- Keep the vertical scrollbar flush to the right edge (no fixed gap) ---
	-- Insert inside TM_ResizableColumns:initialise(), after self.list is created.
	do
		-- Save original prerender so we can extend it safely.
		local _origListPr = self.list.prerender
		self.list.prerender = function(list)
			-- Reposition the vscroll on every frame so it hugs the list's right edge.
			-- This avoids a constant empty gap when the list width changes (window resize).
			if list.vscroll and list.vscroll.getWidth and list.vscroll.setX then
				local sbw = list.vscroll:getWidth()
				local wantX = list.width - sbw
				if list.vscroll.x ~= wantX then
					-- Move the vertical scrollbar so its RIGHT edge coincides with the list's RIGHT edge.
					-- i.e., x := list.width - scrollbar.width
					list.vscroll:setX(wantX)
				end
			end

			-- Call the original prerender afterwards (keeps vanilla behavior intact).
			if _origListPr then _origListPr(list) end
		end
	end

    self:addChild(self.list)
end

-- Apply current sort using a strict, symmetric, stable comparator.
-- Strategy:
--   * Build (class, value) for each side.
--   * For DESC, swap (a,b) and reuse ASC rules (perfect symmetry).
--   * class sorts before value (class=1 are "none-like": end in ASC).
--   * Tie-breaker is __baseIndex to keep sort stable.
function TM_ResizableColumns:_applySort()
    if not self._sort or not self._sort.key then return end
    local key, asc = self._sort.key, self._sort.asc
    if not key or key == "" then return end

    table.sort(self.items, function(a, b)
        local aC, aV = _makeKeyFor(a, key)
        local bC, bV = _makeKeyFor(b, key)

        -- DESC = compare(b, a) with ASC rules (ensures symmetry)
        if not asc then
            aC, bC, aV, bV, a, b = bC, aC, bV, aV, b, a
        end

        -- 1) class (none-like last in ASC)
        if aC ~= bC then
            return aC < bC
        end

        -- 2) primary value (both numbers OR both strings for a given column)
        if aV ~= bV then
            return aV < bV
        end

        -- 3) stable tie-breaker
        local ia = tonumber(a.__baseIndex or math.huge)
        local ib = tonumber(b.__baseIndex or math.huge)
        return ia < ib
    end)
end

-- Toggle sorting for a given column key.
-- First click on a key -> ascending; next click -> descending.
-- When changing to a different key, reset to the original base order first.
function TM_ResizableColumns:toggleSortByKey(key)
    self._sort = self._sort or { key = nil, asc = true }
    if self._sort.key ~= key then
        self._sort.key = key
        self._sort.asc = true
        -- Reset to the base order so we don't carry over any previous sort side-effects.
        if self._itemsBase then
            self.items = {}
            for i, it in ipairs(self._itemsBase) do self.items[i] = it end
        end
    else
        self._sort.asc = not self._sort.asc
    end
    self:refresh()  -- rebuild list with the new order
end

-- Public API: set column definitions
function TM_ResizableColumns:setColumns(cols)
    -- fields: name, key, width, visible (optional, default true)
    self.columnConfig = {}
    for _,col in ipairs(cols or {}) do
        table.insert(self.columnConfig, {
            name = col.name or "",
            key = col.key   or "",
            width = tonumber(col.width) or 80,
            visible = (col.visible ~= false)
        })
    end
    if self.header then self.header:setWidth(self.width) end
end

-- Set the data rows and snapshot a stable "base order" used for tie-breaking.
function TM_ResizableColumns:setItems(items)
    self.items = items or {}
    self._itemsBase = {}
    for i, it in ipairs(self.items) do
		it.__baseIndex = i  -- always reset base index (stable, no stale carryover)
        self._itemsBase[i] = it
    end
end

-- Public API: toggle a column by index (1-based in columnConfig)
function TM_ResizableColumns.toggleColumnVisibility(grid, idx)
    local col = grid.columnConfig[idx]
    if not col then return end
    col.visible = not (col.visible ~= false)
    grid:refresh()
    grid:_notifyColumnsChanged() -- tell parent to persist
end

-- Public API: refresh UI after changing columns/items/sizes
function TM_ResizableColumns:refresh()
    -- Apply current sort order (if any) before rebuilding the list entries
    self:_applySort()
	-- Rebuild list items
    self.list:clear(); self.list.items = {}
    for i,it in ipairs(self.items) do
        -- No per-row tooltip: we show per-cell tooltips via self.list.updateTooltip
        self.list:addItem(tostring(i), it, nil)
    end
    -- Resize list to current panel
    self.list:setWidth(self.width)
    self.list:setHeight(self.height - self.headerHeight)
    -- Update header width too
    if self.header then self.header:setWidth(self.width) end
end

-- Keep children aligned when parent resizes
function TM_ResizableColumns:prerender()
    ISPanel.prerender(self)
    -- Header width
    if self.header and (self.header.width ~= self.width) then
        self.header:setWidth(self.width)
    end
    -- List size
    local desiredListH = self.height - self.headerHeight
    if self.list and (self.list.width ~= self.width or self.list.height ~= desiredListH) then
        self.list:setWidth(self.width)
        self.list:setHeight(desiredListH)
    end
end


-- Single onMouseDown wrapper for the grid container (centralized here).
-- Click on the grid: Notifies the parent and keeps tooltips front and center
if TM_ResizableColumns and not TM_ResizableColumns._TM_wrap then
    TM_ResizableColumns._TM_wrap = true
    local _orig_onMouseDown = TM_ResizableColumns.onMouseDown
    function TM_ResizableColumns:onMouseDown(x, y)
        -- Notify window if it exposes a callback
        local pw = self.parentWindow
        if pw and pw.onGridMouseDown then
            pcall(pw.onGridMouseDown, pw, self, x, y)  -- safe even if the handler does not exist
        end

        -- Original behavior (if it existed)
        local r = _orig_onMouseDown and _orig_onMouseDown(self, x, y)

        -- Keep tooltips up after clicking
        if self.tooltipUI and self.tooltipUI.bringToTop then self.tooltipUI:bringToTop() end
        if self.header and self.header.tooltipUI and self.header.tooltipUI.bringToTop then self.header.tooltipUI:bringToTop() end
        if self.list and self.list.tooltipUI and self.list.tooltipUI.bringToTop then self.list.tooltipUI:bringToTop() end
        return r
    end
end
