local configFileReader = require("configFileReader")
local textFiles = require("scripts/textFiles")
local utf8 = require("scripts/utf8")

local lineguide = require("lineguide")
local repl = require("game/repl")

local battlePlayer,battleCamera,battleStars,battlePhanto,battleStart,battleTimer,battleMessages,battleLoadscreen
local onlinePlay,onlineChat,battleMenu

local battleGeneral = {}


battleGeneral.versionNames = {
    [1] = "v1.0.0",
    [2] = "v1.0.1",
    [3] = "v1.1.0",
    [4] = "v1.2.0",
    [5] = "v1.3.0",
}

battleGeneral.gameVersion = 5


battleGeneral.hubLevelFilename = "Hub.lvlx"
battleGeneral.isInHub = (Level.filename() == battleGeneral.hubLevelFilename)


battleGeneral.maxLocalPlayers = 2
battleGeneral.maxOnlinePlayers = 10

battleGeneral.playerColors = {
    Color.fromHexRGB(0xF73910), -- red
    Color.fromHexRGB(0x00E000), -- green
    Color.fromHexRGB(0x2581DD), -- blue
    Color.fromHexRGB(0xF7C410), -- yellow
    Color.fromHexRGB(0xBC21D7), -- purple
    Color.fromHexRGB(0xFF9232), -- orange
    Color.fromHexRGB(0x00D3BE), -- teal
    Color.fromHexRGB(0xFF98FF), -- pink
    Color.fromHexRGB(0xFFFFFF), -- white
    Color.fromHexRGB(0x383858), -- navy
}

battleGeneral.teamColors = {
    [0] = Color.fromHexRGB(0xA0A0A0),
    Color.fromHexRGB(0xF73910), -- red
    Color.fromHexRGB(0x00D3BE), -- teal
}

battleGeneral.usernameMaxWidth = 192

battleGeneral.gameMode = {
    NONE = -2,
    HUB = -1,
    
    ARENA = 0,
    CLASSIC = 0,

    STARS = 1,
    STONE = 2,
    SPECIAL = 3,
    PHANTO = 4,
    BOMBS = 5,
}

battleGeneral.modeList = {
    battleGeneral.gameMode.CLASSIC,
    battleGeneral.gameMode.STARS,
    battleGeneral.gameMode.STONE,
    battleGeneral.gameMode.PHANTO,
    battleGeneral.gameMode.BOMBS,
    battleGeneral.gameMode.SPECIAL,
}

battleGeneral.modeNames = {
    [battleGeneral.gameMode.CLASSIC] = "classic",
    [battleGeneral.gameMode.STARS] = "stars",
    [battleGeneral.gameMode.STONE] = "stone",
    [battleGeneral.gameMode.PHANTO] = "phanto",
    [battleGeneral.gameMode.BOMBS] = "bombs",
    [battleGeneral.gameMode.SPECIAL] = "special",
}

battleGeneral.modeIcons = {
    [battleGeneral.gameMode.ARENA] = Graphics.loadImageResolved("resources/modeIcon_arena.png"),
    [battleGeneral.gameMode.STARS] = Graphics.loadImageResolved("resources/modeIcon_stars.png"),
    [battleGeneral.gameMode.STONE] = Graphics.loadImageResolved("resources/modeIcon_stone.png"),
    [battleGeneral.gameMode.PHANTO] = Graphics.loadImageResolved("resources/modeIcon_phanto.png"),
    [battleGeneral.gameMode.BOMBS] = Graphics.loadImageResolved("resources/modeIcon_bombs.png"),
    [battleGeneral.gameMode.SPECIAL] = Graphics.loadImageResolved("resources/modeIcon_special.png"),
}


battleGeneral.mode = battleGeneral.gameMode.NONE

battleGeneral.musicCanSpeedUp = false
battleGeneral.musicSpedUpPitch = 1
battleGeneral.musicSpedUpTempo = 1
battleGeneral.musicIsSpedUp = false

battleGeneral.disableMap = false
battleGeneral.disableSplitScreen = false

battleGeneral.preventPausing = false

battleGeneral.useReducedHubPauseMenu = false


battleGeneral.levelConfig = nil


SaveData.battle = SaveData.battle or {}
battleGeneral.saveData = SaveData.battle

GameData.battle = GameData.battle or {}
battleGeneral.gameData = GameData.battle

-- GameData/SaveData
do
    battleGeneral.saveData.playSessions = (battleGeneral.saveData.playSessions or 0)

    if not battleGeneral.gameData.startedSession then
        battleGeneral.saveData.playSessions = battleGeneral.saveData.playSessions + 1
        battleGeneral.gameData.startedSession = true
    end

    if battleGeneral.saveData.firstPlayedDate == nil then
        battleGeneral.saveData.firstPlayedDate = os.date("*t")
    end

    battleGeneral.saveData.localGames = battleGeneral.saveData.localGames or 0
    battleGeneral.saveData.onlineGames = battleGeneral.saveData.onlineGames or 0
    battleGeneral.saveData.onlineDeaths = battleGeneral.saveData.onlineDeaths or 0

    battleGeneral.saveData.onlineGamesByMode = battleGeneral.saveData.onlineGamesByMode or {}
    battleGeneral.saveData.onlineVictoriesByMode = battleGeneral.saveData.onlineVictoriesByMode or {}

    battleGeneral.saveData.levelChoiceCounts = battleGeneral.saveData.levelChoiceCounts or {}
    battleGeneral.saveData.modeChoiceCounts = battleGeneral.saveData.modeChoiceCounts or {}

    battleGeneral.saveData.playTime = battleGeneral.saveData.playTime or 0
    battleGeneral.gameData.playTime = battleGeneral.gameData.playTime or 0
end


local recentlyKilledNPCs = {}


function lineguide.isPaused()
    return false
end


function battleGeneral.getScreenSize()
    return Graphics.getMainFramebufferSize()
    --return 800,600
end


function battleGeneral.fancyEffectsDisabled()
    return battleGeneral.saveData.options.disableFancy
end

function battleGeneral.musicMuted()
    return battleGeneral.saveData.options.muteMusic
end


function battleGeneral.escapeTextplusTags(text)
    return utf8.replace(text,{
        ["<"] = "<lt>",
        [">"] = "<gt>",
    })
end

function battleGeneral.generateRandomID()
    local saveIDCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
    local saveID = ""

    for i = 1,32 do
        saveID = saveID.. saveIDCharacters[RNG.randomInt(1,#saveIDCharacters)]
    end

    return saveID
end



local configEnums = {
    hub     = battleGeneral.gameMode.HUB,
    arena   = battleGeneral.gameMode.ARENA,
    classic = battleGeneral.gameMode.ARENA,
    stars   = battleGeneral.gameMode.STARS,
    stone   = battleGeneral.gameMode.STONE,
    phanto  = battleGeneral.gameMode.PHANTO,
    bombs   = battleGeneral.gameMode.BOMBS,
    special = battleGeneral.gameMode.SPECIAL,
}

function battleGeneral.loadLevelConfig(filename,folderPath)
    local configPath = folderPath.. "battleConfig.ini"
    if not io.exists(configPath) then
        return nil
    end

    local parsed = configFileReader.parseWithHeaders(configPath,{},configEnums,false,false)
    if parsed == nil then
        return nil
    end

    local rawConfig = parsed[1]
    if rawConfig == nil then
        return nil
    end

    local config = {}

    -- Load modes
    config.modeList = {}
    config.modeMap  = {}

    local i = 1

    while (true) do
        local modeValue = rawConfig["mode".. i]
        if modeValue == nil then
            break
        end

        config.modeList[i] = modeValue
        config.modeMap[modeValue] = true
        i = i + 1
    end


    config.filename = filename

    config.folderPath = folderPath
    config.iconPath = folderPath.. "battleIcon.png"

    config.name = rawConfig.name or "[Placeholder]"
    config.author = rawConfig.author or ""

    config.useSeamlessWrap = rawConfig.useSeamlessWrap or false

    config.musicCanSpeedUp = rawConfig.musicCanSpeedUp
    if config.musicCanSpeedUp == nil then
        config.musicCanSpeedUp = true
    end

    config.disableMap = rawConfig.disableMap or false
    config.disableSplitScreen = rawConfig.disableSplitScreen or false

    config.musicSpedUpSpeed = rawConfig.musicSpedUpSpeed or 1.1
    config.musicSpedUpTempo = rawConfig.musicSpedUpTempo or 1.3

    config.minPlayers = rawConfig.minPlayers or 0
    config.maxPlayers = rawConfig.maxPlayers or 0
    config.unplayableLocally = rawConfig.unplayableLocally or false
    config.unplayableOnline = rawConfig.unplayableOnline or false

    config.isHellMap = rawConfig.isHellMap or false

    return config
end

function battleGeneral.loadPlayableLevelConfigs()
    local episodePath = Misc.episodePath()
    local filenames = Misc.listFiles(episodePath)

    local list = {}
    local map = {}

    for _,filename in ipairs(filenames) do
        local folderName = filename:match("^(.+)%.lvlx?$")

        if folderName ~= nil then
            local folderPath = episodePath.. folderName.. "/"
            local config = battleGeneral.loadLevelConfig(filename,folderPath)

            if config ~= nil and #config.modeList > 0 and not config.modeMap[battleGeneral.gameMode.HUB] then
                map[config.filename] = config
                table.insert(list,config)
            end
        end
    end

    return list,map
end

function battleGeneral.loadPlayableLevelFilenames()
    local list = {}
    
    for _,config in ipairs(battleGeneral.loadPlayableLevelConfigs()) do
        table.insert(list,config.filename)
    end

    return list
end


function battleGeneral.loadConfigFile()
    -- Load config file
    battleGeneral.levelConfig = battleGeneral.loadLevelConfig(Level.filename(),Misc.levelPath())
    if battleGeneral.levelConfig == nil then
        return
    end

    local config = battleGeneral.levelConfig

    battleGeneral.musicCanSpeedUp = config.musicCanSpeedUp
    battleGeneral.musicSpedUpSpeed = config.musicSpedUpSpeed
    battleGeneral.musicSpedUpTempo = config.musicSpedUpTempo

    battleGeneral.disableMap = config.disableMap
    battleGeneral.disableSplitScreen = config.disableSplitScreen
end


function battleGeneral.spawnNumberEffects(id,x,y,value)
    local text = tostring(value)
    local textLength = #text

    -- Figure out size
    local config = Effect.config[id][1]

    local effectWidth = config.width
    local effectHeight = config.height
    local totalWidth = effectWidth*textLength
    local totalHeight = effectHeight

    -- Spawn each
    for i = 1,textLength do
        local effectX = x - totalWidth*0.5 + (i - 1)*effectWidth
        local effectY = y - totalHeight

        local variant = string.byte(text[i]) - 47

        local e = Effect.spawn(id,effectX,effectY,variant)

        e.startTimes[1] = (i - 1)*4 -- add a lil delay
    end
end


local circleBackImage = Graphics.loadImageResolved("resources/progressCircle_back.png")
local circleFillImage = Graphics.loadImageResolved("resources/progressCircle_fill.png")

local defaultOutlineColor = Color.fromHexRGBA(0x000000D0)
local defaultBackColor = Color.fromHexRGBA(0x392449D0)

local defaultFillColorFull = Color(0.25,1,0.25)
local defaultFillColorEmpty = Color(1,0.25,0.25)

local circleShader


function battleGeneral.drawProgressCircle(args)
    local backImage = args.backImage or circleBackImage
    local fillImage = args.mainImage or circleFillImage

    local fillColor = args.fillColor or defaultFillColorEmpty:lerpHSV(defaultFillColorFull,args.progress)

    if circleShader == nil then
        -- Compile the shader if we haven't already
        circleShader = Shader.fromFile(nil,"resources/progressCircle.frag")
    end

    Graphics.drawBox{
        target = args.target,priority = args.priority,sceneCoords = args.sceneCoords,
        texture = backImage,centred = true,

        x = args.x,y = args.y,

        shader = circleShader,uniforms = {
            fillImage = fillImage,

            outlineColor = args.outlineColor or defaultOutlineColor,
            backColor = args.backColor or defaultBackColor,
            fillColor = fillColor,

            progress = args.progress,
        },
    }
end


local layerNameModes = {
    classic = battleGeneral.gameMode.CLASSIC,
    stars = battleGeneral.gameMode.STARS,
    stone = battleGeneral.gameMode.STONE,
    special = battleGeneral.gameMode.SPECIAL,
    phanto = battleGeneral.gameMode.PHANTO,
    bombs = battleGeneral.gameMode.BOMBS,
}

function battleGeneral.getLayerModes(layerName)
    -- Find whatever's in brackets
    local inBrackets = layerName:match("%[([^]]+)%]")
    if inBrackets == nil then
        return nil
    end

    -- Find names in a comma-separated list
    local map = {}

    for _,modeName in ipairs(inBrackets:split(",")) do
        local withoutSpaces = modeName:match("^ *([^ ]+) *$")

        if withoutSpaces ~= nil then
            withoutSpaces = withoutSpaces:lower()

            if layerNameModes[withoutSpaces] ~= nil then
                map[layerNameModes[withoutSpaces]] = true
            end
        end
    end

    return map
end

function battleGeneral.setupModeLayers()
    for _,layer in ipairs(Layer.get()) do
        local modeMap = battleGeneral.getLayerModes(layer.name)

        if modeMap ~= nil then
            if modeMap[battleGeneral.mode] then
                layer:show(true)
            else
                layer:hide(true)
            end
        end
    end
end


function battleGeneral.initMusicSpeedFunctions()
    -- These mode-specific functions will return true if music should be sped up.
    battleGeneral.musicShouldSpeedUpFuncs = {}

    battleGeneral.musicShouldSpeedUpFuncs[battleGeneral.gameMode.ARENA] = function()
        local players = battlePlayer.getActivePlayers()

        if #players <= 1 then
            return false
        end

        for _,p in ipairs(players) do
            local data = battlePlayer.getPlayerData(p)

            if data.lives > 1 then
                return false
            end
        end

        return true
    end

    battleGeneral.musicShouldSpeedUpFuncs[battleGeneral.gameMode.STARS] = function()
        for _,p in ipairs(battlePlayer.getActivePlayers()) do
            local data = battlePlayer.getPlayerData(p)

            if data.stars == (battleStars.getStarsToWin() - 1) then
                return true
            end
        end

        return false
    end
end

-- Returns true if the music should be sped up
-- Feel free to overwrite
function battleGeneral.musicShouldBeSpedUp()
    if not battleGeneral.musicCanSpeedUp then
        return false
    end

    if battleGeneral.musicShouldSpeedUpFuncs[battleGeneral.mode] ~= nil and battleGeneral.musicShouldSpeedUpFuncs[battleGeneral.mode]() then
        return true
    end

    if battleTimer.isActive and battleTimer.secondsLeft <= battleTimer.hurryTime then
        return true
    end
    
    return false
end


function battleGeneral.onStart()
    battleGeneral.setupModeLayers()
    Audio.MusicVolume(51)

    if battleGeneral.musicMuted() then
        Audio.SeizeStream(-1)
        Audio.MusicStop()
    end
end

function battleGeneral.onTick()
    -- Disable cheated flag
    Defines.player_hasCheated = false

    -- Should music be sped up?
    local shouldBeSpedUp = battleGeneral.musicShouldBeSpedUp()

    if Audio.MusicSetSpeed ~= nil and Audio.MusicSetTempo ~= nil then
        if shouldBeSpedUp and not battleGeneral.musicIsSpedUp then
            Audio.MusicSetSpeed(battleGeneral.musicSpedUpSpeed)
            Audio.MusicSetTempo(battleGeneral.musicSpedUpTempo)
        elseif not shouldBeSpedUp and battleGeneral.musicIsSpedUp then
            Audio.MusicSetSpeed(1)
            Audio.MusicSetTempo(1)
        end
    end

    battleGeneral.musicIsSpedUp = shouldBeSpedUp
end

function battleGeneral.onTickEnd()
    -- Fix blinking when starting the level/changing sections
    mem(0x00B250D4,FIELD_BOOL,false)

    -- So, in battle mode, NPCs will respawn after being killed.
    -- This is done by having them simply despawn when they die instead of them getting outright removed from the array.
    -- However, onNPCTransform seemingly doesn't account for this scenario (at least, not entirely? Frozen NPCs being killed generally doesn't call onNPCTransform)
    -- This code is here to account for that edge case (important for extra settings to be initialised correctly)
    while (recentlyKilledNPCs[1] ~= nil) do
        local entry = recentlyKilledNPCs[#recentlyKilledNPCs]

        local npc = entry[1]
        local oldID = entry[2]

        if npc.isValid and npc.id ~= oldID then
            EventManager.callEvent("onNPCTransform", npc,oldID,NPC_TFCAUSE_DESPAWN)
        else
            recentlyKilledNPCs[#recentlyKilledNPCs] = nil
        end
    end
end


function battleGeneral.onInputUpdate()
    battleGeneral.saveData.playTime = battleGeneral.saveData.playTime + 1
    battleGeneral.gameData.playTime = battleGeneral.gameData.playTime + 1
end


function battleGeneral.onPostNPCKill(v,reason)
    if v.spawnId > 0 then
        table.insert(recentlyKilledNPCs,{v,v.id})
    end
end

function battleGeneral.onNPCTransform(v,oldID,reason)
    local i = 1

    while (recentlyKilledNPCs[i] ~= nil) do
        if recentlyKilledNPCs[i][1] == v then
            table.remove(recentlyKilledNPCs,i)
        else
            i = i + 1
        end
    end
end


function battleGeneral.onExitLevel(winType)
    Misc.saveGame()
end


local function canPause(p)
    if battleMenu.currentMenu ~= nil or battleMessages.victoryActive or battleGeneral.preventPausing then
        return false
    end

    local data = battlePlayer.getPlayerData(p)

    if data.forfeited then
        return false
    end

    return true
end

function battleGeneral.onPause(eventObj,p)
    local actualPlayer
    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        actualPlayer = Player(onlinePlay.playerIdx)
    else
        actualPlayer = p
    end


    if canPause(actualPlayer) then
        battleGeneral.pauseMenu:open({},1,actualPlayer,(onlinePlay.currentMode == onlinePlay.MODE_OFFLINE))
    end

    eventObj.cancelled = true
end


local joinPacketData

function battleGeneral.onKeyboardPressDirect(vk,repeated,char)
    if not repeated and Misc.GetKeyState(VK_SHIFT) and Misc.inEditor() and not onlineChat.active and not repl.active and battleMenu.currentMenu == nil then
        if vk == VK_P and not Misc.isPaused() then
            if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
                if canPause(Player(onlinePlay.playerIdx)) then
                    battleGeneral.pauseMenu:open({},1,Player(onlinePlay.playerIdx),false)
                end
            elseif canPause(Player(1)) then
                battleGeneral.pauseMenu:open({},1,Player(1),true)
            end
        elseif vk == VK_R then
            Level.load(Level.filename())
            Misc.unpause()
        elseif vk == VK_O then
            onlinePlay.debugMode = not onlinePlay.debugMode
        elseif vk == VK_H then
            local err = onlinePlay.host("TestHost",1)

            if err ~= nil then
                Misc.dialog("Error: ".. err)
            end
        elseif vk == VK_C then
            local err = onlinePlay.joinHost("127.0.0.1","TestClient",2)

            if err ~= nil then
                Misc.dialog("Error: ".. err)
            end
        elseif vk == VK_D then
            onlinePlay.disconnect()
        elseif vk == VK_B then
            --Level.load("battleshrooms.lvlx")
            --Level.load("Vanilla Dome.lvlx")
            Level.load("woody-warzone.lvlx")
        elseif vk == VK_M then
            battlePhanto.alignmentDebug = not battlePhanto.alignmentDebug
        elseif vk == VK_U then
            onlinePlay.debugMode = not onlinePlay.debugMode
        elseif vk == 0xDB then -- left square bracket
            joinPacketData = onlinePlay.encodeAllJoinPackets(0)
            print(#joinPacketData)
        elseif vk == 0xDD then -- right square bracket
            if joinPacketData ~= nil then
                onlinePlay.decodeJoinPackets(joinPacketData)
            end
        end
    end
end


function battleGeneral.onDrawUnfocusOverlay(eventObj)
    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE and onlinePlay.getUserCount() > 1 then
        battleMessages.spawnStatusMessage(textFiles.battleMessages.unfocusWarning,Color.lightred)
    end
end


function battleGeneral.onInitAPI()
    registerEvent(battleGeneral,"onStart")

    registerEvent(battleGeneral,"onTick")
    registerEvent(battleGeneral,"onTickEnd")

    registerEvent(battleGeneral,"onInputUpdate")

    registerEvent(battleGeneral,"onPostNPCKill")
    registerEvent(battleGeneral,"onNPCTransform")

    registerEvent(battleGeneral,"onPause")
    registerEvent(battleGeneral,"onKeyboardPressDirect")

    registerEvent(battleGeneral,"onDrawUnfocusOverlay")

    registerEvent(battleGeneral,"onExitLevel","onExitLevel",false)

    registerCustomEvent(battleGeneral,"onPlayerSelectFromPause")
    registerCustomEvent(battleGeneral,"onMusicSelectFromPause")
    registerCustomEvent(battleGeneral,"onMusicMuteSet")

    battlePlayer = require("scripts/battlePlayer")
    battleCamera = require("scripts/battleCamera")
    battleStart = require("scripts/battleStart")
    battleStars = require("scripts/battleStars")
    battlePhanto = require("scripts/battlePhanto")
    battleTimer = require("scripts/battleTimer")
    battleOptions = require("scripts/battleOptions")
    battleMessages = require("scripts/battleMessages")
    battleLoadscreen = require("scripts/battleLoadscreen")

    onlinePlay = require("scripts/onlinePlay")
    onlineChat = require("scripts/onlineChat")
    battleMenu = require("scripts/battleMenu")

    battleGeneral.initMusicSpeedFunctions()
    battleGeneral.loadConfigFile()

    if battleGeneral.levelConfig ~= nil then
        if battleGeneral.levelConfig.modeMap[battleGeneral.gameData.mode] then
            battleGeneral.mode = battleGeneral.gameData.mode
        else
            battleGeneral.mode = battleGeneral.levelConfig.modeList[1] or battleGeneral.gameMode.NONE
            battleGeneral.gameData.mode = nil
        end

        battleCamera.useSeamlessWrap = battleGeneral.levelConfig.useSeamlessWrap
    else
        battleGeneral.mode = battleGeneral.gameMode.NONE
    end

    if not battleGeneral.isInHub then
        onlinePlay.dontAllowNewClients = true
    end


    battleGeneral.saveData.mainRuleset = battleGeneral.saveData.mainRuleset or {}
    battleOptions.defaultEmptyRulesetOptions(battleGeneral.saveData.mainRuleset)
    
    
    textFiles.funcs.load("resources/text_english.json")


    -- Kick the player out of the level when disconnected
    function onlinePlay.onDisconnect(playerIdx)
        if playerIdx == onlinePlay.playerIdx and not battleGeneral.isInHub then
            battleGeneral.gameData.hasSelectedPlayerMode = false
            Level.load(battleGeneral.hubLevelFilename)
            Misc.unpause()
        end
    end

    function onlinePlay.getVersionNumber()
        return battleGeneral.gameVersion
    end

    -- battleMenu functions to overwrite
    function battleMenu.getKeys()
        return battlePlayer.getPlayerData(battleMenu.controllingPlayer).rawKeys
    end

    battleGeneral.initialiseMenus()
end


-- Pause menu
function battleGeneral.initialiseMenus()
    battleGeneral.saveData.options = battleGeneral.saveData.options or {}


    local menuFormat = {hasBackground = true,maxElementsPerLine = 1,selectionMoveDuration = 5}
    local optionFormat = {useColumnWidth = true}

    local optionFormatWheel = table.join({getGraphicsPosFunc = function(option)
        local menu = option.menu
        local offset = 800

        local rotation = (option.idx - math.lerp(menu.optionIdx,menu.optionIdxFadeStart,menu.optionIdxFade))*10
        local position = vector(option.x + offset,0):rotate(rotation)

        return position.x - offset,position.y - 16,rotation,1
        --return position.x - offset,position.y,rotation,1
        --return 0,0
    end},optionFormat)


    local menuFormatSettings = table.join({offsetX = 384,offsetY = 96,rotation = 10,scale = 0.75,elementGapX = 12,elementGapY = 16},menuFormat)
    local optionFormatSettings = table.join({textScale = 2,boxMarginX = 24,boxMarginY = 12,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*0.5
    end},optionFormat)
    


    --- Pause menu ---
    battleGeneral.pauseMenu = battleMenu.createMenu{format = table.join({cameraOffsetX = 160},menuFormat),optionFormat = optionFormatWheel}
    battleGeneral.pauseMenu.openFunc = function(menu)
        local text = textFiles.menu.main

        menu:addOption{text = text.resume,runFunction = function() menu:close() end}

        if battleGeneral.useReducedHubPauseMenu then
            menu:addOption{text = textFiles.menu.settings.optionTypes.personal,openMenu = battleGeneral.optionsMenu}
        else
            -- Settings
            if battleGeneral.isInHub then
                menu:addOption{text = text.settings,openMenu = battleGeneral.settingsMenu}
            end

            -- Forfeit
            if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE and not battleGeneral.isInHub then
                menu:addOption{text = text.forfeit,runFunction = function()
                    local p = Player(onlinePlay.playerIdx)
                    local data = battlePlayer.getPlayerData(p)

                    data.forfeited = true
                    battleMenu.closeAll()
                end}
            end

            -- Change players
            if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE and battleGeneral.isInHub then
                menu:addOption{text = text.players,runFunction = function()
                    battleMenu.closeAll()
                    battleGeneral.onPlayerSelectFromPause()
                end}
            end

            -- Change music
            if battleGeneral.isInHub then
                menu:addOption{text = text.hubMusic,runFunction = function()
                    battleGeneral.onMusicSelectFromPause()
                end}
            end

            -- Stats
            if battleGeneral.isInHub then
                menu:addOption{text = text.stats,openMenu = battleGeneral.statsMenu}
            end

            -- Go back to the hub
            if onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT and not battleGeneral.isInHub then
                menu:addOption{text = text.toHub,runFunction = function()
                    Level.load(battleGeneral.hubLevelFilename)
                    Misc.unpause()
                end}
            end

            -- Disconnect
            if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
                menu:addOption{text = text.disconnect,runFunction = function()
                    onlinePlay.disconnect()
                end}
            end

            -- Quit
            if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE and battleGeneral.isInHub then
                menu:addOption{text = text.quit,runFunction = function()
                    Misc.exitEngine()
                end}
            end
        end
    end

    --- Settings menu ---
    battleGeneral.settingsMenu = battleMenu.createMenu{format = table.join({offsetX = 384,cameraOffsetX = 120,cameraOffsetY = 16,rotation = 10,scale = 0.75},menuFormat),optionFormat = optionFormatWheel}
    battleGeneral.settingsMenu.openFunc = function(menu)
        local text = textFiles.menu.settings

        menu:addOption{text = text.optionTypes.personal,openMenu = battleGeneral.optionsMenu}
        
        for _,rulesetType in ipairs(battleOptions.rulesetTypeList) do
            local modeName = battleGeneral.modeNames[rulesetType] or rulesetType

            menu:addOption{text = text.optionTypes[modeName],openMenu = battleGeneral.rulesetMenu,openMenuArgs = {rulesetType = rulesetType}}
        end

        -- Change teams
        if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
            menu:addOption{
                text = text.optionTypes.teams,openMenu = battleGeneral.teamsMenu,
                unselectable = (onlinePlay.currentMode == onlinePlay.MODE_CLIENT),
            }
        end
    end


    --- Options menu ---
    local allowClientRuleChangeSelection = battleMenu.createSelection("allowClientRuleChange",battleGeneral.saveData.options, battleMenu.SELECTION_CHECKBOX,false)
    local disableFancySelection = battleMenu.createSelection("disableFancy",battleGeneral.saveData.options, battleMenu.SELECTION_CHECKBOX,false)
    local muteMusicSelection = battleMenu.createSelection("muteMusic",battleGeneral.saveData.options, battleMenu.SELECTION_CHECKBOX,false)

    battleGeneral.optionsMenu = battleMenu.createMenu{format = menuFormatSettings,optionFormat = optionFormatSettings}
    battleGeneral.optionsMenu.openFunc = function(menu)
        local text = textFiles.menu.settings.personalOptions

        menu:addOption{text = text.allowClientRuleChange.option,descriptionText = text.allowClientRuleChange.description,selection = allowClientRuleChangeSelection}
        menu:addOption{text = text.disableFancy.option,descriptionText = text.disableFancy.description,selection = disableFancySelection}
        menu:addOption{text = text.muteMusic.option,descriptionText = text.muteMusic.description,selection = muteMusicSelection}
    end

    battleGeneral.optionsMenu.closeFunc = function(menu)
        if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
            battleOptions.sendRulesUpdate()
        end
    end

    muteMusicSelection.onSet = function(value)
        if value then
            Audio.SeizeStream(-1)
            Audio.MusicStop()
        else
            Audio.ReleaseStream(-1)
            playMusic(0)
        end

        battleGeneral.onMusicMuteSet()
    end


    --- Ruleset menu ---
    battleGeneral.rulesetMenu = battleMenu.createMenu{format = menuFormatSettings,optionFormat = optionFormatSettings}
    battleGeneral.rulesetMenu.openFunc = function(menu)
        local rulesetType = menu.args.rulesetType
        local text = textFiles.menu.settings

        local canChangeRules = battleOptions.canChangeRules()
        local fullRuleset = battleOptions.getFullRuleset()

        for _,option in ipairs(battleOptions.rulesetOptions[rulesetType]) do
            local optionText = text.ruleset[option.name]
            local unselectable = not canChangeRules
            local selection,openMenu,openMenuArgs

            if option.selection ~= nil then
                selection = battleMenu.createSelection(option.name,fullRuleset[rulesetType], table.unpack(option.selection))
            end

            if option.name == "bannedCharacters" then
                openMenu = battleGeneral.bannedCharactersMenu
                openMenuArgs = {mode = rulesetType}
                unselectable = false
            end

            menu:addOption{
                text = optionText.option,descriptionText = optionText.description,
                selection = selection,selectionNames = optionText.selectionNames,
                openMenu = openMenu,openMenuArgs = openMenuArgs,
                unselectable = unselectable,
            }
        end

        menu:addOption{text = text.reset,unselectable = not canChangeRules,format = {useColumnWidth = false},runFunction = function(option)
            battleOptions.resetRulesetTypeToDefault(rulesetType)
            SFX.play(14)
        end}
    end

    battleGeneral.rulesetMenu.closeFunc = function(menu)
        if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
            battleOptions.sendRulesUpdate()
        end
    end

    --- Banned characters ---
    local characterNames = {"mario","luigi","peach","toad","link"}

    local function canToggleCharacterBan(option)
        local character = option.data.character
        local mode = option.menu.args.mode

        -- Can't change rules in general
        if not battleOptions.canChangeRules() then
            return false
        end

        -- Already banned, can always unban a character
        if battleOptions.characterIsBanned(character,mode) then
            return true
        end

        -- Are there any other unbanned characters left?
        for otherCharacter = 1,5 do
            if otherCharacter ~= character and not battleOptions.characterIsBanned(otherCharacter,mode) then
                return true
            end
        end

        return false
    end

    local function toggleCharacterBan(option)
        if not canToggleCharacterBan(option) then
            SFX.play(option.format.unselectableSound)
            return
        end

        local bannedCharacterMap = option.menu.data.bannedCharacterMap

        bannedCharacterMap[option.data.character] = not bannedCharacterMap[option.data.character]
        SFX.play(option.format.confirmSound)
    end

    battleGeneral.bannedCharactersMenu = battleMenu.createMenu{
        format = table.join({
            offsetX = 480,offsetY = 0,rotation = 5,scale = 0.75,
            elementGapX = 12,maxElementsPerLine = 5,
            hasPinchers = true,pinchersMoveDuration = 5,
            hasBox = true,
        },menuFormat),
        optionFormat = {
            hasBox = false,

            updateMainLayoutFunc = function(option)
                local data = option.data
                local characterName = characterNames[data.character]

                data.images = {
                    [false] = Graphics.loadImageResolved("resources/menu/bannedCharacters/".. characterName.. "_enabled.png"),
                    [true] = Graphics.loadImageResolved("resources/menu/bannedCharacters/".. characterName.. "_disabled.png"),
                }

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

                local image = data.images[option.menu.data.bannedCharacterMap[data.character]]

                Graphics.drawBox{
                    texture = image,priority = option.menu.format.priority,
                    target = battleMenu.optionBuffer,
                    x = 0,y = 0,
                }
            end,
        },
    }
    battleGeneral.bannedCharactersMenu.openFunc = function(menu)
        menu.data.bannedCharacterMap = battleOptions.getModeRuleset(menu.args.mode).bannedCharacters

        for character = 1,5 do
            if not battleOptions.alwaysBannedCharacters[menu.args.mode][character] then
                menu:addOption{data = {character = character},runFunction = toggleCharacterBan}
            end
        end
    end


    --- Statistics menu ---
    battleGeneral.statsMenu = battleMenu.createMenu{
        format = table.join({hasBox = true,elementGapY = 4},menuFormatSettings),
        textFormat = {hasBox = false,alignX = 0,textScale = 2},
    }


    local function findFavouriteLevelName()
        local configs = battleGeneral.loadPlayableLevelConfigs()

        local highestChoiceCount = 0
        local favouriteLevelName

        for _,config in ipairs(configs) do
            local count = battleGeneral.saveData.levelChoiceCounts[config.filename]

            if count ~= nil and count > highestChoiceCount then
                favouriteLevelName = config.name
                highestChoiceCount = count
            end
        end

        return favouriteLevelName
    end

    local function findFavouriteMode()
        local highestChoiceCount = 0
        local favouriteMode

        for _,mode in ipairs(battleGeneral.modeList) do
            local count = battleGeneral.saveData.modeChoiceCounts[mode]

            if count ~= nil and count > highestChoiceCount then
                highestChoiceCount = count
                favouriteMode = mode
            end
        end

        return favouriteMode
    end

    battleGeneral.statsMenu.openFunc = function(menu)
        local text = textFiles.menu.stats

        local headerFormat = {textColor = Color(0.6,0.6,0.6)}

        --- General ---
        menu:addText{text = text.generalHeader,format = headerFormat}

        -- First played
        local date = battleGeneral.saveData.firstPlayedDate
        local year = tostring(date.year)
        local month = string.format("%.2d",date.month)
        local day = string.format("%.2d",date.day)

        menu:addText{text = textFiles.funcs.replace(text.firstPlayed,{YEAR = year,MONTH = month,DAY = day})}

        -- Play time
        local seconds = lunatime.toSeconds(battleGeneral.saveData.playTime)
        local minutes = string.format("%.2d",math.floor(seconds/60) % 60)
        local hours = tostring(math.floor(seconds/3600))

        menu:addText{text = textFiles.funcs.replace(text.playTime,{HOURS = hours,MINUTES = minutes})}

        -- Play sessions
        menu:addText{text = textFiles.funcs.replace(text.playSessions,{COUNT = battleGeneral.saveData.playSessions})}

        -- Favourite level
        local favouriteLevelName = findFavouriteLevelName()

        if favouriteLevelName ~= nil then
            menu:addText{text = textFiles.funcs.replace(text.favoriteLevel,{NAME = favouriteLevelName})}
        end

        -- Favourite mode
        local favouriteMode = findFavouriteMode()

        if favouriteMode ~= nil then
            menu:addText{text = textFiles.funcs.replace(text.favoriteMode,{NAME = text.favoriteModeNames[favouriteMode]})}
        end

        
        --- Local ---
        menu:addText{text = text.localHeader,format = headerFormat}
        menu:addText{text = textFiles.funcs.replace(text.localGames,{COUNT = battleGeneral.saveData.localGames})}

        --- Online ---
        menu:addText{text = text.onlineHeader,format = headerFormat}
        menu:addText{text = textFiles.funcs.replace(text.onlineGames,{COUNT = battleGeneral.saveData.onlineGames})}
        menu:addText{text = textFiles.funcs.replace(text.onlineDeaths,{COUNT = battleGeneral.saveData.onlineDeaths})}

        for _,mode in ipairs(battleGeneral.modeList) do
            local gamesPlayed = battleGeneral.saveData.onlineGamesByMode[mode] or 0

            if gamesPlayed > 0 then
                local victories = battleGeneral.saveData.onlineVictoriesByMode[mode] or 0
                local percentage = math.floor(victories/gamesPlayed*100 + 0.5)

                menu:addText{text = textFiles.funcs.replace(text.onlineVictories[mode],{
                    VICTORIES = victories,GAMES = gamesPlayed,PERCENTAGE = percentage,
                })}
            end
        end
    end


    --- Teams menu ---
    local teamsEnabledSelection = battleMenu.createSelection("teamsEnabled",battleGeneral.gameData, battleMenu.SELECTION_CHECKBOX,false)

    battleGeneral.teamsMenu = battleMenu.createMenu{format = menuFormatSettings,optionFormat = optionFormatSettings}
    battleGeneral.teamsMenu.openFunc = function(menu)
        local text = textFiles.menu.teams

        -- Teams enabled setting
        menu:addOption{text = text.enabledSetting,selection = teamsEnabledSelection}

        -- Teams for each player
        for _,user in ipairs(onlinePlay.getUsers()) do
            local selection = battleMenu.createSelection(user.playerIdx,battleGeneral.gameData.playerTeams, battleMenu.SELECTION_NUMBERS,0,2,0)

            menu:addOption{
                text = battleGeneral.escapeTextplusTags(battlePlayer.getName(user.playerIdx)),
                selection = selection,selectionNames = text.teamNames,
                format = {
                    dontTintSelection = true,
                },
            }
        end
    end

    battleGeneral.teamsMenu.closeFunc = function(menu)
        battlePlayer.sendTeamsUpdate()
    end
end


return battleGeneral