--[[
    Commander 3.0 - Layout System

    Constraint-based layout engine inspired by Ratatui/Yazi.
    Allows declarative UI layout without hardcoded pixel coordinates.
]]

local ADDON_NAME, C3 = ...

-------------------------------------------------------------------------------
-- Rect: A rectangular area
-------------------------------------------------------------------------------

local Rect = {}
Rect.__index = Rect
C3.Rect = Rect

-- Rect object pool for reducing GC pressure in TUI apps
local RectPool = {}
local RECT_POOL_MAX = 100

---Acquire a Rect from pool (reuses existing or creates new)
---@param x number
---@param y number
---@param width number
---@param height number
---@return table
function Rect.Acquire(x, y, width, height)
    local rect = table.remove(RectPool)
    if rect then
        rect.x = x or 0
        rect.y = y or 0
        rect.width = width or 0
        rect.height = height or 0
        return rect
    end
    return Rect.new(x, y, width, height)
end

---Release a Rect back to the pool
---@param rect table
function Rect.Release(rect)
    if #RectPool < RECT_POOL_MAX then
        table.insert(RectPool, rect)
    end
end

---Release all Rects in an array back to the pool
---@param rects table
function Rect.ReleaseAll(rects)
    if type(rects) ~= "table" then return end
    for _, rect in ipairs(rects) do
        Rect.Release(rect)
    end
end

---Create a new Rect
---@param x number Left position
---@param y number Top position (negative from top in WoW)
---@param width number Width
---@param height number Height
---@return table
function Rect.new(x, y, width, height)
    return setmetatable({
        x = x or 0,
        y = y or 0,
        width = width or 0,
        height = height or 0,
    }, Rect)
end

---Create Rect from WoW frame
---@param frame table WoW frame
---@return table
function Rect.fromFrame(frame)
    local width = frame:GetWidth() or 0
    local height = frame:GetHeight() or 0
    return Rect.new(0, 0, math.floor(width), math.floor(height))
end

---Get right edge
function Rect:right()
    return self.x + self.width
end

---Get bottom edge
function Rect:bottom()
    return self.y + self.height
end

---Get area (width * height)
function Rect:area()
    return self.width * self.height
end

---Check if empty
function Rect:isEmpty()
    return self.width <= 0 or self.height <= 0
end

---Create inner rect with margin (uses pooling for reduced GC)
---@param margin number|table Single value or {top, right, bottom, left}
---@return table
function Rect:inner(margin)
    if type(margin) == "number" then
        margin = { margin, margin, margin, margin }
    elseif type(margin) == "table" then
        if #margin == 2 then
            -- {vertical, horizontal}
            margin = { margin[1], margin[2], margin[1], margin[2] }
        elseif #margin == 1 then
            margin = { margin[1], margin[1], margin[1], margin[1] }
        end
    end

    local top = margin[1] or 0
    local right = margin[2] or 0
    local bottom = margin[3] or 0
    local left = margin[4] or 0

    return Rect.Acquire(
        self.x + left,
        self.y + top,
        math.max(0, self.width - left - right),
        math.max(0, self.height - top - bottom)
    )
end

---Clone the rect (uses pooling for reduced GC)
function Rect:clone()
    return Rect.Acquire(self.x, self.y, self.width, self.height)
end

function Rect:__tostring()
    return string.format("Rect(%d, %d, %d, %d)", self.x, self.y, self.width, self.height)
end

-------------------------------------------------------------------------------
-- Constraint: Layout constraints
-------------------------------------------------------------------------------

local Constraint = {}
C3.Constraint = Constraint

---Fixed length
---@param n number
---@return table
function Constraint.Length(n)
    return { type = "length", value = n }
end

---Minimum size
---@param n number
---@return table
function Constraint.Min(n)
    return { type = "min", value = n }
end

---Maximum size
---@param n number
---@return table
function Constraint.Max(n)
    return { type = "max", value = n }
end

---Percentage of available space
---@param n number (0-100)
---@return table
function Constraint.Percentage(n)
    return { type = "percentage", value = n }
end

---Ratio of available space
---@param num number Numerator
---@param den number Denominator
---@return table
function Constraint.Ratio(num, den)
    return { type = "ratio", value = { num, den } }
end

---Fill remaining space (with optional weight)
---@param weight? number Weight for proportional filling (default 1)
---@return table
function Constraint.Fill(weight)
    return { type = "fill", value = weight or 1 }
end

-------------------------------------------------------------------------------
-- Layout: Constraint-based layout splitter
-------------------------------------------------------------------------------

local Layout = {}
Layout.__index = Layout
C3.Layout = Layout

-- Direction constants
Layout.HORIZONTAL = "horizontal"
Layout.VERTICAL = "vertical"

---Create a new Layout
---@return table
function Layout.new()
    return setmetatable({
        _direction = Layout.VERTICAL,
        _constraints = {},
        _margin = nil,
        _spacing = 0,
    }, Layout)
end

---Set direction
---@param dir string "horizontal" or "vertical"
---@return table self
function Layout:direction(dir)
    self._direction = dir
    return self
end

---Set constraints
---@param constraints table Array of constraints
---@return table self
function Layout:constraints(constraints)
    self._constraints = constraints
    return self
end

---Set margin
---@param margin number|table
---@return table self
function Layout:margin(margin)
    self._margin = margin
    return self
end

---Set spacing between chunks
---@param spacing number
---@return table self
function Layout:spacing(spacing)
    self._spacing = spacing
    return self
end

---Split an area according to constraints
---@param area table Rect to split
---@return table Array of Rects
function Layout:split(area)
    -- Apply margin
    if self._margin then
        area = area:inner(self._margin)
    end

    if #self._constraints == 0 then
        return { area:clone() }
    end

    local isVertical = self._direction == Layout.VERTICAL
    local totalSize = isVertical and area.height or area.width
    local spacing = self._spacing
    local totalSpacing = spacing * math.max(0, #self._constraints - 1)
    local availableSize = totalSize - totalSpacing

    -- First pass: calculate fixed sizes and collect fill weights
    local sizes = {}
    local fillIndices = {}
    local totalFillWeight = 0
    local usedSize = 0

    for i, constraint in ipairs(self._constraints) do
        local size = 0

        if constraint.type == "length" then
            size = constraint.value
        elseif constraint.type == "min" then
            size = constraint.value
        elseif constraint.type == "max" then
            size = math.min(constraint.value, availableSize - usedSize)
        elseif constraint.type == "percentage" then
            size = math.floor(availableSize * constraint.value / 100)
        elseif constraint.type == "ratio" then
            local num, den = constraint.value[1], constraint.value[2]
            size = math.floor(availableSize * num / den)
        elseif constraint.type == "fill" then
            fillIndices[i] = constraint.value
            totalFillWeight = totalFillWeight + constraint.value
            size = 0  -- Will be calculated in second pass
        end

        sizes[i] = size
        if constraint.type ~= "fill" then
            usedSize = usedSize + size
        end
    end

    -- Second pass: distribute remaining space to fill constraints
    local remainingSize = math.max(0, availableSize - usedSize)

    for i, weight in pairs(fillIndices) do
        sizes[i] = math.floor(remainingSize * weight / totalFillWeight)
    end

    -- Handle rounding errors - add remainder to last fill
    local totalAllocated = 0
    for _, size in ipairs(sizes) do
        totalAllocated = totalAllocated + size
    end
    local remainder = availableSize - totalAllocated

    -- Find last fill index or last index
    local lastFillIndex = #sizes
    for i = #sizes, 1, -1 do
        if fillIndices[i] then
            lastFillIndex = i
            break
        end
    end
    sizes[lastFillIndex] = sizes[lastFillIndex] + remainder

    -- Third pass: create rectangles (using pool to reduce GC)
    local chunks = {}
    local pos = isVertical and area.y or area.x

    for i, size in ipairs(sizes) do
        local chunk
        if isVertical then
            chunk = Rect.Acquire(area.x, pos, area.width, size)
        else
            chunk = Rect.Acquire(pos, area.y, size, area.height)
        end

        chunks[i] = chunk
        pos = pos + size + spacing
    end

    return chunks
end

-------------------------------------------------------------------------------
-- Helpers
-------------------------------------------------------------------------------

---Convert row/col to character position (for TUI apps)
---@param row number 1-indexed row
---@param col number 1-indexed column
---@param area table Rect area
---@return number x, number y
function Layout.toPosition(row, col, area)
    return area.x + col - 1, area.y + row - 1
end

---Check if a position is within an area
---@param x number
---@param y number
---@param area table Rect
---@return boolean
function Layout.contains(x, y, area)
    return x >= area.x and x < area:right() and
           y >= area.y and y < area:bottom()
end

return Layout
