local battleGeneral = require("scripts/battleGeneral")
local battlePlayer = require("scripts/battlePlayer")
local onlinePlay = require("scripts/onlinePlay")

local battleMenu = require("scripts/battleMenu")
local textFiles = require("scripts/textFiles")
local textplus = require("textplus")

local easing = require("ext/easing")

local levelSelect = {}


local STATE = {
    INACTIVE  = 0,
    COUNTDOWN = 1,
    ENTER     = 2,
    SELECT    = 3,
    WAIT      = 4,

    ROULETTE_SPIN = 5,
    ROULETTE_PICK = 6,

    IDENTICAL_SELECTION_PICK = 7,

    HELL_INITIATE = 8,

    RANDOM_SWAP = 9,
    FADE = 10,
}


local localLevelConfigList
local localLevelConfigMap
local localAvailableModeMap
local onlineAvailableFilenameMap
local onlineAvailableModeMap

local clientLevelFilenameMap


local countdownFont = textplus.loadFont("resources/font/bigFont.ini")
local countdownScale = 3

local readyCountFont = textplus.loadFont("textplus/font/1.ini")
local readyCountScale = 1

local mainFont = textplus.loadFont("resources/font/outlinedFont.ini")
local mainFontScale = 2


local countdownDisplay = onlinePlay.createVariable("battle_ls_countdownTimer","uint16",false,0)
local countdownOpacity = 0

local countdownOldValue,countdownLayout


local decisionLoopSound = Misc.resolveSoundFile("resources/decision_loop")
local decisionEndSound = Misc.resolveSoundFile("resources/decision_end")
local decisionLoopObj

local hellSound = Misc.resolveSoundFile("resources/hell")


local levelRouletteDuration
local levelRouletteLoops

local levelRouletteTimer = 0
local rouletteSelection

local hellTextLayout
local hellTextX
local hellTextTimer

local doHellMap = false


levelSelect.STATE = STATE


levelSelect.state = STATE.INACTIVE
levelSelect.timer = 0

levelSelect.chosenSelection = nil
levelSelect.chosenLevelFilename = nil
levelSelect.chosenLevelMode = nil


levelSelect.enterCountdownTime = lunatime.toTicks(15)
levelSelect.selectCountdownTime = lunatime.toTicks(20)

levelSelect.enterMinTime = 32
levelSelect.enterMaxTimeHost = 96
levelSelect.enterMaxTimeClient = 224

levelSelect.waitTime = 64
levelSelect.fadeTime = 48

levelSelect.decidedLevelMinDuration = 192
levelSelect.decidedLevelMaxDuration = 256
levelSelect.decidedLevelMinLoops = 32
levelSelect.decidedLevelMaxLoops = 48

levelSelect.decisionWaitTime = 192


levelSelect.selectedLevelStartY = 192


local stateCommand = onlinePlay.createCommand("battle_ls_state",onlinePlay.IMPORTANCE_MAJOR)
local returnLevelsCommand = onlinePlay.createCommand("battle_ls_returnLevels",onlinePlay.IMPORTANCE_MAJOR)
local chooseLevelCommand = onlinePlay.createCommand("battle_ls_chooseLevel",onlinePlay.IMPORTANCE_MAJOR)

local readyPlayerCount = 0



local function loadLocalLevels()
    localLevelConfigList,localLevelConfigMap = battleGeneral.loadPlayableLevelConfigs()
    localAvailableModeMap = {}

    for _,config in ipairs(localLevelConfigList) do
        if not config.isHellMap then
            for _,mode in ipairs(config.modeList) do
                localAvailableModeMap[mode] = true
            end
        end
    end
end


local function levelIsAvailableForAllClients(levelFilename)
    for _,user in ipairs(onlinePlay.getUsers()) do
        if not user.isHost and clientLevelFilenameMap[user.playerIdx] ~= nil and not clientLevelFilenameMap[user.playerIdx][levelFilename] then
            return false
        end
    end

    return true
end

local function calculateAvailableOnlineLevelMap()
    onlineAvailableFilenameMap = {}

    for _,config in ipairs(localLevelConfigList) do
        if levelIsAvailableForAllClients(config.filename) then
            onlineAvailableFilenameMap[config.filename] = true
        end
    end
end

local function calculateAvailableOnlineModeMap()
    onlineAvailableModeMap = {}

    for _,config in ipairs(localLevelConfigList) do
        if onlineAvailableFilenameMap[config.filename] then
            for _,mode in ipairs(config.modeList) do
                onlineAvailableModeMap[mode] = true
            end
        end
    end
end


local function chooseRandomLevel(mode,hellMap)
    -- Decide level
    local possibleLevelConfigs = {}

    for _,config in ipairs(localLevelConfigList) do
        if (onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or onlineAvailableFilenameMap[config.filename])
        and (mode == nil or config.modeMap[mode])
        and (hellMap == (not not config.isHellMap))
        then
            table.insert(possibleLevelConfigs,config)
        end
    end

    local config = RNG.irandomEntry(possibleLevelConfigs)

    -- Decide mode if necessary
    if mode == nil then
        mode = RNG.irandomEntry(config.modeList)
    end

    return config.filename,mode
end


local function getLocalLevelMap()
    local map = {}

    for _,config in ipairs(localLevelConfigList) do
        map[config.filename] = true
    end

    return map
end

local function getUnrespondedClients()
    local list = {}

    for _,user in ipairs(onlinePlay.getUsers()) do
        if not user.isHost and clientLevelFilenameMap[user.playerIdx] == nil then
            table.insert(list,user)
        end
    end

    return list
end



local function pickLevelSelection()
    local list = {}

    for _,selection in ipairs(levelSelect.levelSelections) do
        weight = 8

        if battleGeneral.gameData.lastVictoriousPlayers ~= nil and table.icontains(battleGeneral.gameData.lastVictoriousPlayers,selection.playerIdx) then
            weight = weight*0.5
        end
        if selection.playerIdx == battleGeneral.gameData.lastChosenPlayer then
            weight = weight*0.5
        end

        if selection.levelFilename == battleGeneral.gameData.lastChosenLevel then
            weight = weight*0.5
        end

        weight = math.max(1,math.floor(weight + 0.5))

        for i = 1,weight do
            table.insert(list,selection)
        end
    end

    return RNG.irandomEntry(list)
end


local function listsAreIdentical(listA,listB)
    local lengthA = #listA
    local lengthB = #listB

    if lengthA ~= lengthB then
        return false
    end

    for i = 1,lengthA do
        if listA[i] ~= listB[i] then
            return false
        end
    end

    return true
end

local function allSelectionsAreIdentical()
    -- Compare each selection to the first one. If any one of them is different from the first, they must not all be the same
    local selectionA = levelSelect.levelSelections[1]

    for selectionIndex = 2,#levelSelect.levelSelections do
        local selectionB = levelSelect.levelSelections[selectionIndex]

        if selectionA.levelFilename ~= selectionB.levelFilename or not listsAreIdentical(selectionA.modes,selectionB.modes) then
            return false
        end
    end

    return true
end


local function shouldDoHellMap()
    if #levelSelect.levelSelections <= 2 then
        return false
    end

    for _,selection in ipairs(levelSelect.levelSelections) do
        if not (selection.levelFilename == "lethal-lava-level.lvlx" and #selection.modes == 1 and selection.modes[1] == battleGeneral.gameMode.CLASSIC) then
            return false
        end
    end

    return true
end



local function levelMenuIsActive()
    if levelSelect.state == STATE.SELECT then
        return (onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or levelSelect.selectionPlayerMap[onlinePlay.playerIdx] == nil)
    end

    return false
end


local stateHostFuncs = {}
local stateGlobalFuncs = {}
local stateStartFuncs = {}

local function updateState()
    if stateStartFuncs[levelSelect.state] ~= nil then
        stateStartFuncs[levelSelect.state]()
    end

    if onlinePlay.currentMode ~= onlinePlay.MODE_HOST then
        return
    end

    local tbl = {state = levelSelect.state,timer = levelSelect.timer}

    if tbl.state == STATE.SELECT then
        tbl.availableFilenameMap = onlineAvailableFilenameMap
    end

    if levelSelect.chosenSelection ~= nil then
        tbl.chosenPlayerIdx = levelSelect.chosenSelection.playerIdx
        tbl.chosenLevelFilename = levelSelect.chosenLevelFilename
        tbl.chosenLevelMode = levelSelect.chosenLevelMode

        tbl.levelRouletteDuration = levelRouletteDuration
        tbl.levelRouletteLoops = levelRouletteLoops
    end

    stateCommand:send(0, tbl)
end


stateHostFuncs[STATE.INACTIVE] = function()
    if readyPlayerCount > 0 then
        levelSelect.state = STATE.COUNTDOWN
        levelSelect.timer = levelSelect.enterCountdownTime

        updateState()
    end
end

stateHostFuncs[STATE.COUNTDOWN] = function()
    if readyPlayerCount <= 0 then
        levelSelect.state = STATE.INACTIVE
        levelSelect.timer = 0

        countdownDisplay.active = false

        updateState()

        return
    end

    levelSelect.timer = math.max(0,levelSelect.timer - 1)
    countdownDisplay.value = math.ceil(lunatime.toSeconds(levelSelect.timer))
    countdownDisplay.active = true

    if levelSelect.timer <= 0 or readyPlayerCount >= battlePlayer.getActivePlayerCount() then
        levelSelect.state = STATE.ENTER
        levelSelect.timer = 0

        onlinePlay.dontAllowNewClients = true
        countdownDisplay.active = false

        clientLevelFilenameMap = {}

        updateState()
    end
end


stateStartFuncs[STATE.ENTER] = function()
    battleGeneral.preventPausing = true
    battleMenu.closeAll()
end

stateHostFuncs[STATE.ENTER] = function()
    if levelSelect.timer >= levelSelect.enterMinTime and (onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or #getUnrespondedClients() == 0) then
        levelSelect.state = STATE.SELECT
        levelSelect.timer = levelSelect.selectCountdownTime

        loadLocalLevels()
        calculateAvailableOnlineLevelMap()
        calculateAvailableOnlineModeMap()

        updateState()

        return
    elseif levelSelect.timer >= levelSelect.enterMaxTimeHost then
        if not levelSelect.warningMenu.isOpen then
            levelSelect.warningMenu:open({},1,Player(onlinePlay.playerIdx),false)
        end
    end

    levelSelect.timer = levelSelect.timer + 1
end

stateGlobalFuncs[STATE.ENTER] = function()
    if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
        if levelSelect.timer >= levelSelect.enterMaxTimeClient then
            if not levelSelect.warningMenu.isOpen then
                levelSelect.warningMenu:open({},1,Player(onlinePlay.playerIdx),false)
            end
        end

        levelSelect.timer = levelSelect.timer + 1
    end
end


stateStartFuncs[STATE.SELECT] = function()
    if levelSelect.warningMenu.isOpen then
        levelSelect.warningMenu:close()
    end
end

stateHostFuncs[STATE.SELECT] = function()
    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
        return
    end

    -- Continue to next state
    if #levelSelect.levelSelections >= onlinePlay.getUserCount() or (levelSelect.timer <= 0 and #levelSelect.levelSelections > 0) then
        levelSelect.state = STATE.WAIT
        levelSelect.timer = 0

        countdownDisplay.active = false

        updateState()

        return
    end

    -- Countdown timer
    if #levelSelect.levelSelections > 0 then
        levelSelect.timer = math.max(0,levelSelect.timer - 1)
        countdownDisplay.value = math.ceil(lunatime.toSeconds(levelSelect.timer))
        countdownDisplay.active = true
    end
end

stateHostFuncs[STATE.WAIT] = function()
    levelSelect.timer = levelSelect.timer + 1

    if levelSelect.timer >= levelSelect.waitTime then
        -- Choose a level to play, going into the roulette state if there is more than one possible level
        -- Though even if it goes into the roulette, it's already been picked. The lottery's a sham!
        if #levelSelect.levelSelections > 1 then
            -- First, check if everyone has voted random. If so, it's a hell map!
            doHellMap = shouldDoHellMap()

            levelSelect.chosenSelection = pickLevelSelection()

            if doHellMap then
                levelSelect.state = STATE.HELL_INITIATE
            elseif allSelectionsAreIdentical() then
                levelSelect.state = STATE.IDENTICAL_SELECTION_PICK
            else
                levelRouletteDuration = RNG.randomInt(levelSelect.decidedLevelMinDuration,levelSelect.decidedLevelMaxDuration)
                levelRouletteLoops = RNG.randomInt(levelSelect.decidedLevelMinLoops,levelSelect.decidedLevelMaxLoops)
                levelRouletteTimer = 0

                levelSelect.state = STATE.ROULETTE_SPIN
            end
        else
            levelSelect.chosenSelection = levelSelect.levelSelections[1]

            if levelSelect.chosenSelection.levelFilename == "" then
                levelSelect.state = STATE.RANDOM_SWAP
            else
                levelSelect.state = STATE.FADE
            end

            doHellMap = false
        end

        -- Handle random levels/modes
        if doHellMap then
            levelSelect.chosenLevelFilename,levelSelect.chosenLevelMode = chooseRandomLevel(nil,true)
        elseif levelSelect.chosenSelection.levelFilename == "" then
            levelSelect.chosenLevelFilename,levelSelect.chosenLevelMode = chooseRandomLevel(levelSelect.chosenSelection.modes[1],false)
        else
            levelSelect.chosenLevelFilename = levelSelect.chosenSelection.levelFilename
            levelSelect.chosenLevelMode = RNG.irandomEntry(levelSelect.chosenSelection.modes)
        end

        -- Used for biasing the results next time
        battleGeneral.gameData.lastChosenLevel = levelSelect.chosenLevelFilename
        battleGeneral.gameData.lastChosenPlayer = levelSelect.chosenSelection.playerIdx

        levelSelect.timer = 0

        updateState()
    end
end


stateStartFuncs[STATE.ROULETTE_SPIN] = function()
    for _,selection in ipairs(levelSelect.levelSelections) do
        selection:transition({scale = selection.baseScale*0.8},6,easing.outQuad)
    end

    decisionLoopObj = SFX.play{sound = decisionLoopSound,volume = 0,loops = 0}
end

stateHostFuncs[STATE.ROULETTE_SPIN] = function()
    if levelRouletteTimer >= levelRouletteDuration then
        levelSelect.state = STATE.ROULETTE_PICK
        levelSelect.timer = 0

        updateState()
    end
end

stateGlobalFuncs[STATE.ROULETTE_SPIN] = function() -- note that this runs for ALL players, not just the host!
    local selectionCount = #levelSelect.levelSelections
    local chosenSelectionIndex = table.ifind(levelSelect.levelSelections,levelSelect.chosenSelection)

    -- Decide what selection should be highlighted as part of the roulette
    local loops = math.floor(20/math.min(5,selectionCount))*4
    local rouletteSelectionIndex,transitionDuration

    if levelRouletteTimer < levelRouletteDuration*0.5 then
        rouletteSelectionIndex = math.ceil((selectionCount*loops*0.75 + chosenSelectionIndex - 1)*levelRouletteTimer/levelRouletteDuration*2)%selectionCount + 1
        transitionDuration = 1
    else
        rouletteSelectionIndex = math.ceil(easing.outSine(levelRouletteTimer - levelRouletteDuration*0.5,0,selectionCount*loops*0.25,levelRouletteDuration*0.5) + chosenSelectionIndex - 1)%selectionCount + 1
        transitionDuration = math.lerp(1,2.5,levelRouletteTimer/levelRouletteDuration*2 - 1)
    end

    -- Highlight that selection
    local newRouletteSelection = levelSelect.levelSelections[rouletteSelectionIndex]

    if newRouletteSelection ~= rouletteSelection then
        if rouletteSelection ~= nil then
            rouletteSelection:transition({scale = rouletteSelection.baseScale*0.8},transitionDuration*2,easing.linear)
            rouletteSelection.selected = false
        end

        newRouletteSelection:transition({scale = newRouletteSelection.baseScale},transitionDuration,easing.linear)
        newRouletteSelection.selected = true
        rouletteSelection = newRouletteSelection
        SFX.play{sound = 26,delay = 1}
    end


    levelRouletteTimer = math.min(levelRouletteDuration,levelRouletteTimer + 1)

    -- Sound effect
    if decisionLoopObj ~= nil and decisionLoopObj:isPlaying() then
        decisionLoopObj.volume = math.min(0.5,levelRouletteTimer/32)
    end
end


stateStartFuncs[STATE.ROULETTE_PICK] = function()
    for _,selection in ipairs(levelSelect.levelSelections) do
        if selection == levelSelect.chosenSelection then
            selection:transition({x = camera.width*0.5,y = levelSelect.selectedLevelStartY,scale = 1},32,easing.inOutQuad)
        else
            selection:transition({y = selection.baseY + 32,opacity = 0},24,easing.inQuad)
        end

        selection.selected = false
    end

    if decisionLoopObj ~= nil and decisionLoopObj:isPlaying() then
        decisionLoopObj:stop()
    end

    SFX.play(decisionEndSound,decisionLoopObj.volume)
end

stateHostFuncs[STATE.ROULETTE_PICK] = function()
    levelSelect.timer = levelSelect.timer + 1

    if levelSelect.timer >= levelSelect.decisionWaitTime then
        if levelSelect.chosenSelection.levelFilename ~= levelSelect.chosenLevelFilename then
            levelSelect.state = STATE.RANDOM_SWAP
        else
            levelSelect.state = STATE.FADE
        end

        levelSelect.timer = 0

        updateState()
    end
end


stateStartFuncs[STATE.RANDOM_SWAP] = function()
    levelSelect.chosenSelection.levelFilename = levelSelect.chosenLevelFilename
    levelSelect.chosenSelection.modes = {levelSelect.chosenLevelMode}
    levelSelect.chosenSelection:updateLayout()
    levelSelect.chosenSelection:shake()

    SFX.play(9)
end

stateHostFuncs[STATE.RANDOM_SWAP] = function()
    levelSelect.timer = levelSelect.timer + 1

    if levelSelect.timer >= 64 then
        levelSelect.state = STATE.FADE
        levelSelect.timer = 0

        updateState()
    end
end


stateStartFuncs[STATE.IDENTICAL_SELECTION_PICK] = function()
    for _,selection in ipairs(levelSelect.levelSelections) do
        if selection == levelSelect.chosenSelection then
            selection:transition({x = camera.width*0.5,y = levelSelect.selectedLevelStartY,scale = 1},32,easing.inOutQuad)
        else
            selection:transition({y = selection.baseY + 32,opacity = 0},24,easing.inQuad)
        end

        selection.selected = false
    end
end

stateHostFuncs[STATE.IDENTICAL_SELECTION_PICK] = function()
    levelSelect.timer = levelSelect.timer + 1

    if levelSelect.timer >= 48 then
        if levelSelect.chosenSelection.levelFilename ~= levelSelect.chosenLevelFilename then
            levelSelect.state = STATE.RANDOM_SWAP
        else
            levelSelect.state = STATE.FADE
        end

        levelSelect.timer = 0

        updateState()
    end
end


stateStartFuncs[STATE.HELL_INITIATE] = function()
    for _,selection in ipairs(levelSelect.levelSelections) do
        if selection == levelSelect.chosenSelection then
            selection:transition({x = camera.width*0.5,y = levelSelect.selectedLevelStartY,scale = 1},32,easing.inOutQuad)
        else
            selection:transition({y = selection.baseY + 32,opacity = 0},24,easing.inQuad)
        end

        selection.selected = false
    end

    hellTextLayout = textplus.layout("Hell Map !!",nil,{
        font = mainFont,xscale = mainFontScale,yscale = mainFontScale,
        color = Color(1,0.25,0.25),
        tremble = 1,
        posFilter = function(x,y, fmt,glyph, width,height)
            local descendTimer = math.clamp(hellTextTimer/24 - (x - hellTextX)/hellTextLayout.width)

            return x,math.lerp(-24,y,easing.outQuad(descendTimer,0,1,1))
        end,
    })
    hellTextX = (camera.width - hellTextLayout.width)*0.5
    hellTextTimer = 0

    SFX.play(hellSound)
end

stateHostFuncs[STATE.HELL_INITIATE] = function()
    levelSelect.timer = levelSelect.timer + 1

    if levelSelect.timer >= 128 then
        levelSelect.state = STATE.RANDOM_SWAP
        levelSelect.timer = 0

        updateState()
    end
end


stateHostFuncs[STATE.FADE] = function()
    if levelSelect.timer >= (levelSelect.fadeTime + 4) then
        -- Statistics
        if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
            battleGeneral.saveData.localGames = battleGeneral.saveData.localGames + 1
        else
            battleGeneral.saveData.onlineGames = battleGeneral.saveData.onlineGames + 1
        end

        -- Load the level
        battleGeneral.gameData.mode = levelSelect.chosenLevelMode
        Level.load(levelSelect.chosenLevelFilename)
    end
end

stateGlobalFuncs[STATE.FADE] = function()
    levelSelect.timer = levelSelect.timer + 1
end


function levelSelect.onInputUpdate()
    if levelSelect.state >= STATE.SELECT then
        for _,p in ipairs(Player.get()) do
            for key,_ in pairs(p.keys) do
                p.keys[key] = false
            end
        end
    end
end


local function warpIsInMainSection(warp)
    return (warp.entranceSection == 0 and warp.exitSection == 0)
end


function levelSelect.onTick()
    -- Handle players in pipes
    readyPlayerCount = 0

    for _,p in ipairs(Player.get()) do
        if p.forcedState == FORCEDSTATE_PIPE and p.forcedTimer >= 100 and p.forcedTimer <= 101 and warpIsInMainSection(Warp(p:mem(0x15E,FIELD_WORD) - 1)) then
            if p.keys.run == KEYS_PRESSED and levelSelect.state == STATE.COUNTDOWN then
                p.forcedTimer = 110
            else
                readyPlayerCount = readyPlayerCount + 1
                p.forcedTimer = 100
            end
        end
    end

    
    if onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
        -- Run state function
        local func = stateHostFuncs[levelSelect.state]

        if func ~= nil then
            func()
        end
    end

    local func = stateGlobalFuncs[levelSelect.state]

    if func ~= nil then
        func()
    end


    -- Handle level select menu
    if levelMenuIsActive() then
        if not levelSelect.modeMenu.isOpen then
            local p = player
            if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
                p = Player(onlinePlay.playerIdx)
            end

            battleMenu.closeAll()
            levelSelect.modeMenu:open({},1,p,false)
        end

        for _,p in ipairs(Player.get()) do
            for k,v in pairs(p.keys) do
                p.keys[k] = false
            end
        end
    elseif levelSelect.modeMenu.isOpen then
        battleMenu.closeAll()
    end

    -- Update countdown timer
    if countdownDisplay.active then
        if countdownDisplay.value ~= countdownOldValue then
            local text = textFiles.funcs.replace(textFiles.levelSelect.countdownText,{TIMER = tostring(countdownDisplay.value)})

            if countdownOldValue ~= nil and levelSelect.state == STATE.COUNTDOWN then
                SFX.play(26)
            end

            countdownLayout = textplus.layout(text,nil,{font = countdownFont,xscale = countdownScale,yscale = countdownScale})
            countdownOldValue = countdownDisplay.value
        end

        countdownOpacity = math.min(1,countdownOpacity + 0.1)
    else
        countdownOpacity = math.max(0,countdownOpacity - 0.1)
        countdownOldValue = nil
    end

    -- Fade out music
    if levelSelect.state >= STATE.WAIT then
        Audio.MusicVolume(math.max(0,Audio.MusicVolume() - 1))
    end

    -- Update selected levels
    for _,selection in ipairs(levelSelect.levelSelections) do
        selection:update()
    end

    -- Update hell text
    if hellTextLayout ~= nil then
        hellTextTimer = hellTextTimer + 1
    end
end



function levelSelect.onDraw()
    local screenWidth,screenHeight = battleGeneral.getScreenSize()

    -- Draw countdown
    if countdownOpacity > 0 then
        local color = Color.white*countdownOpacity
        local priority = 7.1
        
        local y = 64

        if countdownDisplay.value > 5 or lunatime.tick()%16 < 12 then
            textplus.render{
                layout = countdownLayout,color = color,priority = priority,
                x = math.floor((screenWidth - countdownLayout.width)*0.5),
                y = math.floor(y),
            }
        end

        y = y + countdownLayout.height + 8

        if levelSelect.state == STATE.COUNTDOWN then
            local playerText = textFiles.funcs.replace(textFiles.levelSelect.playerText,{
                CURRENT = tostring(readyPlayerCount),
                MAX = tostring(battlePlayer.getActivePlayerCount()),
            })

            textplus.print{
                text = playerText,color = color,priority = priority,pivot = vector(0.5,0),
                font = readyCountFont,xscale = readyCountScale,yscale = readyCountScale,
                x = math.floor(screenWidth*0.5),y = math.floor(y),
            }
        end
    end

    -- Draw level icons
    for _,selection in ipairs(levelSelect.levelSelections) do
        selection:draw()
    end

    -- Draw hell text
    if hellTextLayout ~= nil then
        textplus.render{
            layout = hellTextLayout,priority = 8,
            x = hellTextX,
            y = 64 - hellTextLayout.height*0.5,
        }
    end

    -- Fade out
    if levelSelect.state == STATE.FADE then
        local opacity = math.min(1,levelSelect.timer/levelSelect.fadeTime)

        Graphics.drawBox{
            color = Color.black.. opacity,priority = 9,
            x = 0,y = 0,width = screenWidth,height = screenHeight,
        }
    end
end


function stateCommand.onReceive(sourcePlayerIdx, tbl)
    if sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
        return
    end

    local oldState = levelSelect.state

    levelSelect.state = tbl.state or levelSelect.state
    levelSelect.timer = tbl.timer or levelSelect.timer

    if tbl.chosenPlayerIdx ~= nil and levelSelect.chosenSelection == nil then
        levelSelect.chosenSelection = levelSelect.selectionPlayerMap[tbl.chosenPlayerIdx]
        levelSelect.chosenLevelFilename = tbl.chosenLevelFilename
        levelSelect.chosenLevelMode = tbl.chosenLevelMode

        levelRouletteDuration = tbl.levelRouletteDuration
        levelRouletteLoops = tbl.levelRouletteLoops
    end

    if tbl.state == STATE.ENTER then
        -- Send over our levels to the host
        loadLocalLevels()
        returnLevelsCommand:send(onlinePlay.hostPlayerIdx, getLocalLevelMap())
    elseif tbl.state == STATE.SELECT then
        -- Available levels have been sent over too
        onlineAvailableFilenameMap = tbl.availableFilenameMap
        calculateAvailableOnlineModeMap()
    elseif tbl.state == STATE.INACTIVE then
        if oldState == STATE.ENTER then
            -- Close warning menu
            if levelSelect.warningMenu.isOpen then
                levelSelect.warningMenu:close()
            end

            -- Get out of the pipe
            local ownPlayer = Player(onlinePlay.playerIdx)

            if ownPlayer.forcedState == FORCEDSTATE_PIPE then
                ownPlayer.forcedTimer = 110
            end

            onlinePlay.dontAllowNewClients = false
            battleGeneral.preventPausing = false
        end
    end

    if stateStartFuncs[levelSelect.state] ~= nil then
        stateStartFuncs[levelSelect.state]()
    end
end

function returnLevelsCommand.onReceive(sourcePlayerIdx, availableFilenameMap)
    if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
        return
    end

    clientLevelFilenameMap[sourcePlayerIdx] = availableFilenameMap
end

function chooseLevelCommand.onReceive(sourcePlayerIdx, levelFilename,modes)
    levelSelect.addLevelSelection(sourcePlayerIdx, levelFilename,modes)
end


-- Icons
do
    local modeToModeGap = 0
    local modeLineGap = 2
    local modeToTitleGap = 8

    local levelIconGap = 8
    local levelNameGap = 2

    local iconBoxImage = Graphics.loadImageResolved("resources/menu/box.png")
    local iconBoxMarginX = 16
    local iconBoxMarginY = 16

    local levelNameFormat = {font = textplus.loadFont("resources/font/mainFont.ini"),xscale = 1,yscale = 1,plaintext = true}
    local levelAuthorFormat = table.join({color = Color.lightgrey},levelNameFormat)

    local greyscaleShader


    function levelSelect.drawIcon(args)
        local layout = args.layout
        local mainColor = args.mainColor or Color.white

        local iconColor = Color.white
        local iconShader

        if args.unselectable then
            if greyscaleShader == nil then
                -- Compile the shader if we haven't already
                greyscaleShader = Shader.fromFile(nil,"resources/greyscale.frag")
            end
            
            iconShader = greyscaleShader
            iconColor = Color.lightgrey
        end

        -- Box
        battleMenu.drawSegmentedBox{
            texture = iconBoxImage,target = args.target,priority = args.priority,
            x = args.x,y = args.y,width = layout.totalWidth,height = layout.totalHeight,
            color = Color.white.. mainColor.a,
        }

        local currentY = args.y + iconBoxMarginY

        -- Icon
        Graphics.drawBox{
            texture = layout.iconImage,target = args.target,priority = args.priority,
            x = (layout.totalWidth - layout.iconImage.width)*0.5 + args.x,y = currentY,
            color = iconColor,shader = iconShader,
        }

        currentY = currentY + layout.iconImage.height + levelIconGap

        -- Modes
        local modesX = args.x + layout.totalWidth - iconBoxMarginX
        local modesY = currentY

        for _,modeLine in ipairs(layout.modeLines) do
            local modeLineX = modesX - modeLine.width

            for _,mode in ipairs(modeLine.modes) do
                local image = battleGeneral.modeIcons[mode]

                Graphics.drawBox{
                    texture = image,target = args.target,priority = args.priority,
                    x = modeLineX,y = modesY + (modeLine.height - image.height)*0.5,
                    color = iconColor,shader = iconShader,
                }

                modeLineX = modeLineX + image.width + modeToModeGap
            end

            modesY = modesY + modeLine.height + modeLineGap
        end

        -- Text
        local textColor = Color(mainColor.r*mainColor.a,mainColor.g*mainColor.a,mainColor.b*mainColor.a,mainColor.a)
        local textX = math.floor(args.x + iconBoxMarginX)

        textplus.render{
            layout = layout.nameLayout,target = args.target,priority = args.priority,
            x = textX,y = math.floor(currentY),color = textColor,smooth = false,
        }

        currentY = currentY + layout.nameLayout.height + levelNameGap

        -- Author
        if layout.authorLayout ~= nil then
            textplus.render{
                layout = layout.authorLayout,target = args.target,priority = args.priority,
                x = textX,y = math.floor(currentY),color = textColor,smooth = false,
            }

            currentY = currentY + layout.authorLayout.height
        end
    end


    local function setUpLayoutModes(layout)
        -- Split modes into lines
        layout.modeLines = {}

        for _,mode in ipairs(layout.modes) do
            local lastLine = layout.modeLines[#layout.modeLines]

            if lastLine == nil or #lastLine.modes >= layout.modesPerLine then
                table.insert(layout.modeLines,{
                    modes = {mode},
                    width = 0,
                    height = 0,
                })
            else
                table.insert(lastLine.modes,mode)
            end
        end

        -- Figure out the sizes of each line
        layout.modesWidth = 0
        layout.modesHeight = math.max(0,#layout.modeLines - 1)*modeLineGap

        for _,modeLine in ipairs(layout.modeLines) do
            modeLine.width = (#modeLine.modes - 1)*modeToModeGap
            modeLine.height = 0

            for i,mode in ipairs(modeLine.modes) do
                local image = battleGeneral.modeIcons[mode]

                modeLine.width = modeLine.width + image.width
                modeLine.height = math.max(modeLine.height,image.height)
            end

            layout.modesWidth = math.max(layout.modesWidth,modeLine.width)
            layout.modesHeight = layout.modesHeight + modeLine.height
        end
    end

    local function setUpLayoutText(layout)
        local maxWidth = layout.mainWidth - layout.modesWidth - modeToTitleGap

        layout.nameLayout = textplus.layout(layout.name,maxWidth,levelNameFormat)

        layout.textWidth = layout.nameLayout.width
        layout.textHeight = layout.nameLayout.height

        if layout.author ~= "" then
            local authorText = textFiles.funcs.replace(textFiles.levelSelect.authorFormat,{NAME = layout.author})

            layout.authorLayout = textplus.layout(authorText,maxWidth,levelAuthorFormat)

            layout.textWidth = math.max(layout.textWidth,layout.authorLayout.width)
            layout.textHeight = layout.textHeight + levelNameGap + layout.authorLayout.height
        end
    end

    function levelSelect.getIconLayoutData(config,modes)
        local layout = {}

        if config ~= nil then
            layout.config = config
            layout.modes = modes

            layout.name = config.name
            layout.author = config.author

            layout.iconImage = Graphics.loadImage(config.iconPath)
        else
            layout.modes = modes

            layout.name = textFiles.levelSelect.randomLevel
            layout.author = ""
        end

        if layout.iconImage == nil then
            layout.iconImage = Graphics.loadImageResolved("resources/randomLevel.png")
        end

        layout.mainWidth = layout.iconImage.width

        -- Initialise the level text and the modes
        layout.modesPerLine = #layout.modes

        setUpLayoutModes(layout)
        setUpLayoutText(layout)

        -- If the level text is just way too tall compared to the modes, split the modes into multiple lines
        while (layout.modesPerLine > 1 and layout.textHeight > layout.modesHeight*2) do
            layout.modesPerLine = math.ceil(layout.modesPerLine*0.5)

            setUpLayoutModes(layout)
            setUpLayoutText(layout)
        end

        -- Total size
        layout.totalWidth = layout.mainWidth + iconBoxMarginX*2
        layout.totalHeight = layout.iconImage.height + levelIconGap + math.max(layout.modesHeight,layout.textHeight) + iconBoxMarginY*2
        
        return layout
    end
end


-- Menus
do
    -- Mode select
    local modeList = table.append({-1},battleGeneral.modeList)

    levelSelect.modeMenu = battleMenu.createMenu{
        format = {
            hasBackground = true,maxElementsPerLine = 10,cameraOffsetY = 32,
            selectionMoveDuration = 6,
        },
        optionFormat = {
            getColorFunc = function(option)
                local menu = option.menu

                local progress = 0
                if levelSelect.levelMenu.isActive then
                    progress = levelSelect.levelMenu.enterProgress
                end
    
                local activeness = 0
                if menu.optionIdx == option.optionIdx then
                    activeness = 1 - menu.optionIdxFade
                elseif menu.optionIdxFadeStart == option.optionIdx then
                    activeness = menu.optionIdxFade
                end
    
                local inactiveOpacity = (1 - progress)
                local activeOpacity = math.lerp(1,0.75,progress)
                local opacity = math.lerp(inactiveOpacity,activeOpacity,activeness)
    
                return Color.white.. opacity
            end,
            getGraphicsPosFunc = function(option)
                local menu = option.menu
                local offset = 1400
        
                local rotation = (math.lerp(menu.optionIdx,menu.optionIdxFadeStart,menu.optionIdxFade) - option.idx)*15
                local position = vector(0,option.y + offset):rotate(rotation)
        
                return position.x,position.y - offset,rotation,1
            end,
        },
    }
    levelSelect.modeMenu.openFunc = function(menu)
        local text = textFiles.levelSelect

        menu.cantGoBack = (onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE)

        for _,mode in ipairs(modeList) do
            if mode == -1 or localAvailableModeMap[mode] then
                menu:addOption{text = text.modeNames[mode],descriptionText = text.modeDescriptions[mode],openMenu = levelSelect.levelMenu,openMenuArgs = {mode = mode}}
            end
        end
    end
    levelSelect.modeMenu.closeFunc = function(menu)
        if levelSelect.state == STATE.SELECT and onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
            levelSelect.state = STATE.INACTIVE
            levelSelect.timer = 0

            countdownDisplay.active = false

            battleGeneral.preventPausing = false
            updateState()

            -- Force players to exit the pipe
            for _,p in ipairs(Player.get()) do
                if p.forcedState == FORCEDSTATE_PIPE and p.forcedTimer >= 100 and p.forcedTimer <= 101 then
                    p.forcedTimer = 110
                end
            end
        end
    end
    
    -- Level select
    local function chooseLevel(filename,modes)
        if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
            levelSelect.addLevelSelection(onlinePlay.playerIdx, filename,modes)
            chooseLevelCommand:send(0, filename,modes)
        else
            --[[for i = 1,10 do
                levelSelect.addLevelSelection(i, filename,modes)
            end]]

            levelSelect.addLevelSelection(1, filename,modes)

            levelSelect.state = STATE.WAIT
            levelSelect.timer = 0
        end

        battleMenu.closeAll()

        SFX.play(14)

        -- Statistics stuff
        battleGeneral.saveData.levelChoiceCounts[filename] = (battleGeneral.saveData.levelChoiceCounts[filename] or 0) + 1

        if #modes == 1 then
            local mode = modes[1]

            battleGeneral.saveData.modeChoiceCounts[mode] = (battleGeneral.saveData.modeChoiceCounts[mode] or 0) + 1
        end
    end

    local function getPlayerCount()
        if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
            return battleGeneral.gameData.playerCount
        else
            return onlinePlay.getUserCount()
        end
    end

    levelSelect.levelMenu = battleMenu.createMenu{
        format = {
            hasBackground = true,hasBox = false,maxElementsPerLine = 3,elementGapX = 16,elementGapY = 8,
            selectionMoveDuration = 5,cameraMoveDuration = 12,priority = 6.9,
            hasPinchers = true,pinchersMoveDuration = 5,
            offsetY = 176,cameraOffsetY = 0,
            --offsetY = 600,cameraOffsetY = -32,
        },
        optionFormat = {
            hasBox = false,textScale = 2,

            getGraphicsPosFunc = function(option)
                local menu = option.menu

                local selectedOptionNow = menu.options[menu.optionIdx]
                local selectedOptionOld = menu.options[menu.optionIdxFadeStart]
                local y = math.lerp(selectedOptionNow.y,selectedOptionOld.y,menu.optionIdxFade)

                return option.x,option.y - y + 32
            end,

            updateMainLayoutFunc = function(option)
                local data = option.data
                
                data.layout = levelSelect.getIconLayoutData(data.config,data.modes)

                option.mainWidth = data.layout.totalWidth
                option.mainHeight = data.layout.totalHeight
            end,
            drawMainLayoutFunc = function(option,mainX,mainY,mainColor)
                local data = option.data

                levelSelect.drawIcon{
                    layout = data.layout,priority = option.menu.format.priority,target = battleMenu.optionBuffer,
                    mainColor = mainColor,unselectable = option.unselectable,
                    x = 0,y = 0,
                }
            end,
        },
    }
    levelSelect.levelMenu.openFunc = function(menu)
        local mode = menu.args.mode
        
        local modeUnavailable = (mode ~= -1 and onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE and not onlineAvailableModeMap[mode])
        local playerCount = getPlayerCount()

        -- Random mode option
        local randomModes = {}
        if mode >= 0 then
            randomModes = {mode}
        end

        menu:addOption{data = {modes = randomModes},unselectable = modeUnavailable,runFunction = function()
            chooseLevel("",randomModes)
        end}

        -- Actual levels
        for _,config in ipairs(localLevelConfigList) do
            if (mode == -1 or config.modeMap[mode]) and not config.isHellMap then
                local relevantModes
                if mode == -1 then
                    relevantModes = config.modeList
                else
                    relevantModes = {mode}
                end

                -- Cases where a level is not playable
                -- This is ugly, but... whatever
                local unselectable = false
                local descriptionText

                if modeUnavailable or (onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE and not onlineAvailableFilenameMap[config.filename]) then
                    descriptionText = textFiles.levelSelect.unavailableDescription
                    unselectable = true
                elseif config.unplayableLocally and onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
                    descriptionText = textFiles.levelSelect.unplayableLocally
                    unselectable = true
                elseif config.unplayableOnline and onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
                    descriptionText = textFiles.levelSelect.unplayableOnline
                    unselectable = true
                elseif config.minPlayers > 0 and playerCount < config.minPlayers then
                    descriptionText = textFiles.funcs.replace(textFiles.levelSelect.tooFewPlayers,{AMOUNT = config.minPlayers})
                    unselectable = true
                elseif config.maxPlayers > 0 and playerCount > config.maxPlayers then
                    descriptionText = textFiles.funcs.replace(textFiles.levelSelect.tooManyPlayers,{AMOUNT = config.maxPlayers})
                    unselectable = true
                end


                menu:addOption{data = {config = config,modes = relevantModes},unselectable = unselectable,descriptionText = descriptionText,runFunction = function()
                    chooseLevel(config.filename,relevantModes)
                end}
            end
        end
    end


    -- Softlock prevention warning
    levelSelect.warningMenu = battleMenu.createMenu{
        format = {hasBackground = true,hasBox = true,elementGapY = 16},
        optionFormat = {hasBox = false,textScale = 2},
        textFormat = {hasBox = false,textScale = 2,textMaxWidth = 512},
    }
    levelSelect.warningMenu.cantGoBack = true
    levelSelect.warningMenu.openFunc = function(menu)
        local text = textFiles.levelSelect.unresponsiveWarning

        if onlinePlay.currentMode == onlinePlay.MODE_HOST then
            menu:addText{text = text.host.header}

            menu:addOption{text = text.host.disconnectOthers,runFunction = function(option)
                for _,user in ipairs(getUnrespondedClients()) do
                    user:disconnect()
                end

                menu:close()

                levelSelect.timer = 0
            end}

            menu:addOption{text = text.host.cancel,runFunction = function(option)
                -- Reset the state
                levelSelect.state = STATE.INACTIVE
                levelSelect.timer = 0

                onlinePlay.dontAllowNewClients = false
                battleGeneral.preventPausing = false
                clientLevelFilenameMap = {}

                updateState()

                -- Get out of the pipe
                local ownPlayer = Player(onlinePlay.playerIdx)

                if ownPlayer.forcedState == FORCEDSTATE_PIPE then
                    ownPlayer.forcedTimer = 110
                end

                menu:close()
            end}
        else
            menu:addText{text = text.client.header}

            menu:addOption{text = text.client.disconnectSelf,runFunction = function(option)
                onlinePlay.disconnect()

                -- Reset the state
                levelSelect.state = STATE.INACTIVE
                levelSelect.timer = 0

                onlinePlay.dontAllowNewClients = false
                battleGeneral.preventPausing = false

                -- Get out of the pipe
                local ownPlayer = Player(onlinePlay.playerIdx)

                if ownPlayer.forcedState == FORCEDSTATE_PIPE then
                    ownPlayer.forcedTimer = 110
                end

                if battleMenu.currentMenu == menu then
                    menu:close()
                end
            end}
        end
    end
end


do
    local selectionFunctions = {}
    local selectionMT = {__index = selectionFunctions}

    local playerNameFormat = {font = textplus.loadFont("resources/font/mainFont.ini"),xscale = 2,yscale = 2,plaintext = true}

    levelSelect.levelSelections = {}
    levelSelect.selectionPlayerMap = {}
    
    function levelSelect.addLevelSelection(playerIdx, levelFilename,modes)
        local selection = setmetatable({},selectionMT)

        selection.playerIdx = playerIdx
        selection.levelFilename = levelFilename
        selection.modes = modes

        selection.playerNameLayout = textplus.layout(battlePlayer.getName(selection.playerIdx),nil,playerNameFormat)
        --selection.playerNameLayout = textplus.layout("whattheblast",nil,playerNameFormat)


        selection.opacity = 0

        selection.moveStart = {}
        selection.moveGoal = {}

        selection.moveDuration = 0
        selection.moveTimer = 0
        selection.moveEaseFunc = nil

        selection.shakeTimer = 0
        selection.rotation = 0

        selection.selected = false

        selection:updateLayout()

        table.insert(levelSelect.levelSelections,selection)
        levelSelect.selectionPlayerMap[selection.playerIdx] = selection

        for selectionIndex,otherSelection in ipairs(levelSelect.levelSelections) do
            if otherSelection == selection then
                otherSelection.baseX,otherSelection.baseY,otherSelection.baseScale = otherSelection:getBasePosition(selectionIndex)
                otherSelection.scale = otherSelection.baseScale
                otherSelection.x = otherSelection.baseX
                otherSelection.y = otherSelection.baseY + 32

                otherSelection:transition(
                    {y = selection.baseY,opacity = 1},
                    24,easing.outQuad
                )
            else
                local baseX,baseY,baseScale = otherSelection:getBasePosition(selectionIndex)

                if otherSelection.baseX ~= baseX or otherSelection.baseY ~= baseY or otherSelection.baseScale ~= baseScale then
                    otherSelection.baseScale = baseScale
                    otherSelection.baseX = baseX
                    otherSelection.baseY = baseY

                    otherSelection:transition(
                        {x = otherSelection.baseX,y = otherSelection.baseY,scale = otherSelection.baseScale,opacity = 1},
                        24,easing.inOutQuad
                    )
                end
            end
        end

        return selection
    end


    function selectionFunctions:updateLayout()
        self.layout = levelSelect.getIconLayoutData(localLevelConfigMap[self.levelFilename],self.modes)
    end


    local transitionFields = {"x","y","scale","opacity"}

    function selectionFunctions:transition(goalFields,duration,easeFunc)
        for _,key in ipairs(transitionFields) do
            self.moveGoal[key] = goalFields[key] or self.moveGoal[key] or self[key]
            self.moveStart[key] = self[key]
        end

        self.moveDuration = duration
        self.moveTimer = duration
        self.moveEaseFunc = easeFunc or easing.inOutQuad
    end

    function selectionFunctions:shake()
        self.shakeTimer = 1
    end


    function selectionFunctions:update()
        if self.moveTimer > 0 then
            self.moveTimer = math.max(0,self.moveTimer - 1)

            local easedTime = self.moveEaseFunc(1 - self.moveTimer/self.moveDuration,0,1,1)

            for _,key in ipairs(transitionFields) do
                self[key] = math.lerp(self.moveStart[key],self.moveGoal[key],easedTime)
            end

            if self.moveTimer <= 0 then
                for _,key in ipairs(transitionFields) do
                    self.moveGoal[key] = nil
                    self.moveStart[key] = nil
                end
            end
        end

        if self.shakeTimer > 0 then
            self.shakeTimer = math.max(0,self.shakeTimer - 0.08)
            self.rotation = math.sin(self.shakeTimer*math.pi*2)*self.shakeTimer*10
        end
    end

    local selectionBuffer = Graphics.CaptureBuffer(256,256)

    function selectionFunctions:draw()
        if self.opacity <= 0 then
            return
        end

        local priority = 1 + self.scale*0.1

        selectionBuffer:clear(priority)

        -- Draw the level icon into a buffer
        levelSelect.drawIcon{
            layout = self.layout,priority = priority,target = selectionBuffer,
            --mainColor = (self.selected and Color(1,1,0.25)) or Color.white,
            x = math.floor((selectionBuffer.width - self.layout.totalWidth)*0.5),
            y = math.floor((selectionBuffer.height - self.layout.totalHeight)*0.5),
        }

        -- Draw player's head and name
        if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE and onlinePlay.isConnected(self.playerIdx) then
            local playerInfoX = (selectionBuffer.width - self.layout.totalWidth)*0.5 + 8
            local playerInfoY = (selectionBuffer.height - self.layout.totalHeight)*0.5

            battlePlayer.renderOutlinedPlayerHead{
                playerIdx = self.playerIdx,priority = priority,target = selectionBuffer,
                x = playerInfoX,y = playerInfoY,
            }

            textplus.render{
                layout = self.playerNameLayout,priority = priority,target = selectionBuffer,
                color = battlePlayer.getColor(self.playerIdx),
                x = math.floor(playerInfoX + 24),
                y = math.floor(playerInfoY - self.playerNameLayout.height*0.5 + 8),
            }
        end

        -- Draw the buffer to the screen
        Graphics.drawBox{
            texture = selectionBuffer,priority = priority,centred = true,
            color = Color.white.. self.opacity,
            width = selectionBuffer.width*self.scale,
            height = selectionBuffer.height*self.scale,
            x = self.x,y = self.y,
            rotation = self.rotation,
        }
    end


    function selectionFunctions:getBasePosition(selectionIndex)
        local selectionCount = math.max(#levelSelect.levelSelections,onlinePlay.getUserCount())

        local centreX = camera.width*0.5
        local centreY = levelSelect.selectedLevelStartY

        -- Three or fewer players, so they can all fit in one line at 100% scale
        if selectionCount <= 3 then
            return centreX + ((selectionIndex - 1) - (selectionCount - 1)*0.5)*204,centreY,1
        end

        -- Four players, put in a line as well, but at a smaller scale
        if selectionCount <= 4 then
            return centreX + ((selectionIndex - 1) - (selectionCount - 1)*0.5)*160,centreY,0.75
        end

        -- Any more, split by row and column at a smaller scale
        local columnCount = math.min(5,math.ceil(selectionCount*0.5))
        local rowCount = math.floor((selectionCount - 1)/columnCount) + 1

        local row = math.floor((selectionIndex - 1)/columnCount)
        local column = (selectionIndex - 1) % columnCount

        local countWithinRow = math.min(columnCount,selectionCount - row*columnCount)

        return centreX + (column - (countWithinRow - 1)*0.5)*160,centreY + (row - (rowCount - 1)*0.5)*128,0.75
    end
end


function levelSelect.onInitAPI()
    registerEvent(levelSelect,"onInputUpdate","onInputUpdate",false)
    registerEvent(levelSelect,"onTick")
    registerEvent(levelSelect,"onDraw")
end


return levelSelect