-- InspectInput/Inspect_Controller.lua
-- Joypad input router for Inspect UI.

require "InspectInput/Inspect_ControllerMap"
require "InspectInput/Inspect_ControllerUtils"
require "InspectInput/Inspect_FocusGraph"

InspectController = InspectController or {}
InspectController._instances = InspectController._instances or {}
InspectController._byUi = InspectController._byUi or {}
InspectController._active = InspectController._active or {}
InspectController._input = InspectController._input or {}
InspectController._hooked = InspectController._hooked or false

InspectController.B_LONG_PRESS_MS = 650
InspectController.DPAD_REPEAT_DELAY = 300
InspectController.DPAD_REPEAT_INTERVAL = 120

local function resolvePlayerNum(ui)
    if not ui then return 0 end
    if type(ui.player) == "number" then
        return ui.player
    end
    if ui.player and ui.player.getPlayerNum then
        return ui.player:getPlayerNum()
    end
    local player = getPlayer and getPlayer()
    if player and player.getPlayerNum then
        return player:getPlayerNum()
    end
    local p0 = getSpecificPlayer and getSpecificPlayer(0)
    if p0 and p0.getPlayerNum then
        return p0:getPlayerNum()
    end
    return 0
end

local function refreshActive(playerNum)
    local active = InspectController._active[playerNum]
    if active and InspectController._byUi[active] then
        return active
    end
    for i = #InspectController._instances, 1, -1 do
        local entry = InspectController._instances[i]
        if entry.playerNum == playerNum then
            InspectController._active[playerNum] = entry.ui
            return entry.ui
        end
    end
    return nil
end

function InspectController.register(ui)
    if not ui or InspectController._byUi[ui] then
        return
    end

    local playerNum = resolvePlayerNum(ui)
    local graph = InspectFocusGraph:new(ui)

    local entry = {
        ui = ui,
        playerNum = playerNum,
        graph = graph,
    }

    table.insert(InspectController._instances, entry)
    InspectController._byUi[ui] = entry
    InspectController._active[playerNum] = ui

    ui._inspectControllerEntry = entry
    if ui.setWantJoypadEvents then
        ui:setWantJoypadEvents(true)
    end

    if not InspectController._hooked and Events and Events.OnTick then
        Events.OnTick.Add(InspectController._onTick)
        InspectController._hooked = true
    end
end

function InspectController.unregister(ui)
    local entry = InspectController._byUi[ui]
    if not entry then return end

    for i = #InspectController._instances, 1, -1 do
        if InspectController._instances[i].ui == ui then
            table.remove(InspectController._instances, i)
        end
    end

    InspectController._byUi[ui] = nil
    if ui and ui._inspectControllerEntry then
        ui._inspectControllerEntry = nil
    end

    if entry.graph then
        entry.graph:clear()
    end

    if InspectController._active[entry.playerNum] == ui then
        InspectController._active[entry.playerNum] = nil
        refreshActive(entry.playerNum)
    end

    if #InspectController._instances == 0 and InspectController._hooked and Events and Events.OnTick then
        Events.OnTick.Remove(InspectController._onTick)
        InspectController._hooked = false
    end
end

function InspectController.closeActive(playerNum)
    local ui = refreshActive(playerNum)
    if not ui then return end
    if ui.onClose then
        ui:onClose()
    elseif ui.removeFromUIManager then
        ui:removeFromUIManager()
    end
end

function InspectController.closeAll(playerNum)
    local toClose = {}
    for _, entry in ipairs(InspectController._instances) do
        if entry.playerNum == playerNum and entry.ui then
            table.insert(toClose, entry.ui)
        end
    end
    for _, ui in ipairs(toClose) do
        if ui.onClose then
            ui:onClose()
        elseif ui.removeFromUIManager then
            ui:removeFromUIManager()
        end
    end
end

function InspectController._onTick()
    if #InspectController._instances == 0 then return end
    local seen = {}
    for _, entry in ipairs(InspectController._instances) do
        local playerNum = entry.playerNum or 0
        if not seen[playerNum] then
            InspectController.onJoypadTick(playerNum)
            seen[playerNum] = true
        end
    end
end

local function getInputState(playerNum)
    InspectController._input[playerNum] = InspectController._input[playerNum] or {}
    return InspectController._input[playerNum]
end

local function edge(state, key, pressed)
    state.buttons = state.buttons or {}
    local prev = state.buttons[key]
    state.buttons[key] = pressed
    return pressed and not prev, (not pressed) and prev
end

local function shouldRepeat(state, key, pressed, now)
    state.dpad = state.dpad or {}
    local info = state.dpad[key] or { down = false, start = 0, last = 0 }
    state.dpad[key] = info

    if pressed then
        if not info.down then
            info.down = true
            info.start = now
            info.last = now
            return true
        end
        if now - info.start >= InspectController.DPAD_REPEAT_DELAY and now - info.last >= InspectController.DPAD_REPEAT_INTERVAL then
            info.last = now
            return true
        end
    else
        info.down = false
    end
    return false
end

function InspectController.onJoypadTick(playerNum)
    playerNum = playerNum or 0
    local ui = refreshActive(playerNum)
    if not ui then return end
    if ui.getIsVisible and not ui:getIsVisible() then return end
    if not InspectControllerUtils.isJoypadActive(playerNum) then return end
    if playerNum == 0 and InspectControllerUtils.wasMouseRecentlyActive() then return end

    local entry = InspectController._byUi[ui]
    local graph = entry and entry.graph or nil

    if graph and ui.getFocusTargets then
        graph:setTargets(ui:getFocusTargets())
    end

    local joypadId = InspectControllerUtils.getJoypadId(playerNum)
    if joypadId < 0 then return end
    local map = InspectControllerMap.getMap() or {}

    local state = getInputState(playerNum)
    local now = InspectControllerUtils.getTimeMs()
    local dt = InspectControllerUtils.getRenderDelta()

    local dUp = InspectControllerUtils.isPressed(joypadId, map.dpadUp)
    local dDown = InspectControllerUtils.isPressed(joypadId, map.dpadDown)
    local dLeft = InspectControllerUtils.isPressed(joypadId, map.dpadLeft)
    local dRight = InspectControllerUtils.isPressed(joypadId, map.dpadRight)

    if graph then
        if shouldRepeat(state, "up", dUp, now) then
            if not graph:move("up") and graph:getCurrentZone() == "stats" and ui.scrollStatsBy then
                ui:scrollStatsBy(-24)
            end
        end
        if shouldRepeat(state, "down", dDown, now) then
            if not graph:move("down") and graph:getCurrentZone() == "stats" and ui.scrollStatsBy then
                ui:scrollStatsBy(24)
            end
        end
        if shouldRepeat(state, "left", dLeft, now) then
            graph:move("left")
        end
        if shouldRepeat(state, "right", dRight, now) then
            graph:move("right")
        end
    end

    local acceptPressed = edge(state, "accept", InspectControllerUtils.isPressed(joypadId, map.accept))
    if acceptPressed and graph then
        graph:accept()
    end

    local photoPressed = edge(state, "photo", InspectControllerUtils.isPressed(joypadId, map.photo))
    if photoPressed and ui.onTogglePhotoMode then
        ui:onTogglePhotoMode()
    end

    local pageUpPressed = edge(state, "pageUp", InspectControllerUtils.isPressed(joypadId, map.pageUp))
    local pageDownPressed = edge(state, "pageDown", InspectControllerUtils.isPressed(joypadId, map.pageDown))
    if graph then
        local zone = graph:getCurrentZone()
        if pageUpPressed then
            if zone == "stats" and ui.scrollStatsPage then
                ui:scrollStatsPage(-1)
            elseif zone == "description" and ui.scrollDescriptionPage then
                ui:scrollDescriptionPage(-1)
            end
        end
        if pageDownPressed then
            if zone == "stats" and ui.scrollStatsPage then
                ui:scrollStatsPage(1)
            elseif zone == "description" and ui.scrollDescriptionPage then
                ui:scrollDescriptionPage(1)
            end
        end
    end

    local backPressed, backReleased = edge(state, "back", InspectControllerUtils.isPressed(joypadId, map.back))
    state.back = state.back or { down = false, start = 0, fired = false }
    if backPressed then
        state.back.down = true
        state.back.start = now
        state.back.fired = false
    elseif state.back.down and InspectControllerUtils.isPressed(joypadId, map.back) then
        if not state.back.fired and (now - state.back.start) >= InspectController.B_LONG_PRESS_MS then
            InspectController.closeAll(playerNum)
            state.back.fired = true
        end
    elseif backReleased then
        if not state.back.fired then
            InspectController.closeActive(playerNum)
        end
        state.back.down = false
    end

    local preview = ui.preview3D
    if preview and ui.use3D and ui.supports3D then
        local zone = graph and graph:getCurrentZone() or nil
        if zone == "preview" or ui.photoMode then
            local axisX = InspectControllerUtils.getAxis(joypadId, map.rotateY)
            local axisY = InspectControllerUtils.getAxis(joypadId, map.rotateX)
            if axisX ~= 0 or axisY ~= 0 then
                local scale = 6 * (dt / 33.3)
                preview:onMouseMove(axisX * scale, axisY * scale)
            end

            if InspectControllerUtils.isPressed(joypadId, map.zoomIn) then
                preview:onMouseWheel(-1)
            end
            if InspectControllerUtils.isPressed(joypadId, map.zoomOut) then
                preview:onMouseWheel(1)
            end

            local resetPressed = edge(state, "resetView", InspectControllerUtils.isPressed(joypadId, map.resetView))
            if resetPressed and preview.resetView then
                preview:resetView()
            end
        end
    end

    if graph and graph:getCurrentZone() == "description" and ui.scrollDescriptionBy then
        local axisY = InspectControllerUtils.getAxis(joypadId, map.scrollDesc)
        if axisY ~= 0 then
            ui:scrollDescriptionBy(-axisY * (18 * (dt / 33.3)))
        end
    end
end

return InspectController
