-- InspectInput/Inspect_FocusGraph.lua
-- Focus graph for controller navigation.

InspectFocusGraph = {}
InspectFocusGraph.__index = InspectFocusGraph

function InspectFocusGraph:new(ui)
    local o = {
        ui = ui,
        nodes = {},
        order = {},
        currentId = nil,
        lastId = nil,
    }
    setmetatable(o, self)
    return o
end

function InspectFocusGraph:clear()
    self.nodes = {}
    self.order = {}
    self.currentId = nil
    self.lastId = nil
end

local function rectFromElement(el)
    if not el then return nil end
    local x = el.x or (el.getX and el:getX()) or 0
    local y = el.y or (el.getY and el:getY()) or 0
    local w = el.width or (el.getWidth and el:getWidth()) or 0
    local h = el.height or (el.getHeight and el:getHeight()) or 0
    if w <= 0 or h <= 0 then return nil end
    return { x = x, y = y, w = w, h = h }
end

function InspectFocusGraph:_pushNode(node)
    if not node or not node.id or not node.rect then return end
    node.cx = node.rect.x + node.rect.w / 2
    node.cy = node.rect.y + node.rect.h / 2
    self.nodes[node.id] = node
    table.insert(self.order, node.id)
end

function InspectFocusGraph:_nodeFromTarget(target, zone, id)
    if not target then return nil end

    local node = { zone = zone }
    if type(target) == "table" and target.rect then
        node.id = target.id or id
        node.rect = target.rect
        node.onAccept = target.onAccept
        node.onEnter = target.onEnter
        node.onLeave = target.onLeave
        node.row = target.row
        node.col = target.col
        node.index = target.index
        node.target = target.target
    elseif type(target) == "table" and target.target then
        node.id = target.id or id
        node.target = target.target
        node.onAccept = target.onAccept
        node.onEnter = target.onEnter
        node.onLeave = target.onLeave
        node.row = target.row
        node.col = target.col
        node.index = target.index
        node.rect = rectFromElement(node.target)
    else
        node.id = id
        node.target = target
        node.rect = rectFromElement(target)
    end

    if not node.rect then return nil end
    return node
end

function InspectFocusGraph:setTargets(targets)
    self.nodes = {}
    self.order = {}

    if not targets then
        self.currentId = nil
        return
    end

    if targets.header then
        for i, entry in ipairs(targets.header) do
            local node = self:_nodeFromTarget(entry, "header", "header_" .. tostring(i))
            self:_pushNode(node)
        end
    end

    local preview = self:_nodeFromTarget(targets.preview, "preview", "preview")
    self:_pushNode(preview)

    local statsList = targets.stats
    if statsList then
        if statsList.rect or statsList.target then
            local node = self:_nodeFromTarget(statsList, "stats", "stats")
            self:_pushNode(node)
        elseif type(statsList) == "table" then
            for i, entry in ipairs(statsList) do
                local node = self:_nodeFromTarget(entry, entry.zone or "stats", entry.id or ("stats_" .. tostring(i)))
                self:_pushNode(node)
            end
        end
    end

    local desc = self:_nodeFromTarget(targets.description, "description", "description")
    self:_pushNode(desc)

    local close = self:_nodeFromTarget(targets.close, "close", "close")
    self:_pushNode(close)

    self:_linkNodes()

    if not self.currentId or not self.nodes[self.currentId] then
        self.currentId = self.order[1]
        if self.currentId then
            self:_enter(self.nodes[self.currentId])
        end
    end
end

function InspectFocusGraph:_setTargetFocus(node, focused)
    if not node then return end
    local target = node.target
    if target and target.setJoypadFocused then
        target:setJoypadFocused(focused)
    end
end

function InspectFocusGraph:_enter(node)
    if not node then return end
    self:_setTargetFocus(node, true)
    if node.onEnter then
        node.onEnter(node)
    end
end

function InspectFocusGraph:_leave(node)
    if not node then return end
    self:_setTargetFocus(node, false)
    if node.onLeave then
        node.onLeave(node)
    end
end

function InspectFocusGraph:setFocus(id)
    if not id or not self.nodes[id] then return false end
    if self.currentId == id then return true end
    local prev = self.nodes[self.currentId]
    if prev then
        self:_leave(prev)
    end
    self.currentId = id
    self:_enter(self.nodes[id])
    return true
end

function InspectFocusGraph:getCurrentId()
    return self.currentId
end

function InspectFocusGraph:getCurrentNode()
    return self.currentId and self.nodes[self.currentId] or nil
end

function InspectFocusGraph:getCurrentZone()
    local node = self:getCurrentNode()
    return node and node.zone or nil
end

function InspectFocusGraph:move(dir)
    local node = self:getCurrentNode()
    if not node then return false end
    local nextId = node[dir]
    if nextId and self.nodes[nextId] then
        return self:setFocus(nextId)
    end
    return false
end

function InspectFocusGraph:accept()
    local node = self:getCurrentNode()
    if not node then return end
    if node.onAccept then
        node.onAccept(node)
        return
    end
    if node.target and node.target.forceClick then
        node.target:forceClick()
    end
end

local function sortByX(nodes, map)
    table.sort(nodes, function(a, b)
        return map[a].rect.x < map[b].rect.x
    end)
end

function InspectFocusGraph:_linkNodes()
    local headerIds = {}
    local statsIds = {}

    for id, node in pairs(self.nodes) do
        if node.zone == "header" then
            table.insert(headerIds, id)
        elseif node.zone == "stats" then
            table.insert(statsIds, id)
        end
    end

    if #headerIds > 0 then
        sortByX(headerIds, self.nodes)
        for i, id in ipairs(headerIds) do
            local node = self.nodes[id]
            node.left = headerIds[i - 1]
            node.right = headerIds[i + 1]
        end
    end

    if #statsIds > 0 then
        local grid = {}
        for _, id in ipairs(statsIds) do
            local node = self.nodes[id]
            if node.row ~= nil and node.col ~= nil then
                grid[node.row] = grid[node.row] or {}
                grid[node.row][node.col] = id
            end
        end
        for _, id in ipairs(statsIds) do
            local node = self.nodes[id]
            if node.row ~= nil and node.col ~= nil then
                node.left = node.left or (grid[node.row] and grid[node.row][node.col - 1])
                node.right = node.right or (grid[node.row] and grid[node.row][node.col + 1])
                node.up = node.up or (grid[node.row - 1] and grid[node.row - 1][node.col])
                node.down = node.down or (grid[node.row + 1] and grid[node.row + 1][node.col])
            end
        end
    end

    for _, id in ipairs(self.order) do
        local node = self.nodes[id]
        if node then
            node.up = node.up or self:_findNeighbor(node, 0, -1)
            node.down = node.down or self:_findNeighbor(node, 0, 1)
            node.left = node.left or self:_findNeighbor(node, -1, 0)
            node.right = node.right or self:_findNeighbor(node, 1, 0)
        end
    end
end

function InspectFocusGraph:_findNeighbor(node, dx, dy)
    local bestId = nil
    local bestScore = nil
    for id, other in pairs(self.nodes) do
        if id ~= node.id and other.rect then
            local vx = other.cx - node.cx
            local vy = other.cy - node.cy
            if dx ~= 0 then
                if vx * dx > 0 then
                    local dist = math.sqrt(vx * vx + vy * vy)
                    local perp = math.abs(vy)
                    local score = dist + perp * 0.5
                    if not bestScore or score < bestScore then
                        bestScore = score
                        bestId = id
                    end
                end
            else
                if vy * dy > 0 then
                    local dist = math.sqrt(vx * vx + vy * vy)
                    local perp = math.abs(vx)
                    local score = dist + perp * 0.5
                    if not bestScore or score < bestScore then
                        bestScore = score
                        bestId = id
                    end
                end
            end
        end
    end
    return bestId
end

return InspectFocusGraph
