local tplusUtils = require("textplus/tplusutils")
local textplus = require("textplus")
local easing = require("ext/easing")
local utf8 = require("scripts/utf8")

local onlinePlay

local battleMenu = {}



local DESCRIPTION_STATE = {
    INACTIVE = 0,
    IN = 1,
    ACTIVE = 2,
    OUT = 3,
}

battleMenu.DESCRIPTION_STATE = DESCRIPTION_STATE


battleMenu.activeMenus = {}
battleMenu.menuHistory = {}
battleMenu.currentMenu = nil

battleMenu.activeBackgroundFunc = nil
battleMenu.backgroundOpacity = 0
battleMenu.backgroundTimer = 0
battleMenu.backgroundFadeSpeed = 0
battleMenu.backgroundPriority = 0

battleMenu.descriptionState = DESCRIPTION_STATE.INACTIVE
battleMenu.descriptionMoveTimer = 0
battleMenu.descriptionWidth = 0
battleMenu.descriptionHeight = 0
battleMenu.descriptionPriority = 0
battleMenu.descriptionNewPriority = 0
battleMenu.descriptionText = ""
battleMenu.descriptionLayout = nil

battleMenu.cameraTransform = Transform()
battleMenu.cameraMoveTimer = 0
battleMenu.cameraMoveDuration = 0
battleMenu.cameraMoveEaseFunc = nil
battleMenu.cameraMoveStart = Transform()
battleMenu.cameraMoveGoal = Transform()

battleMenu.menuPauses = false
battleMenu.menuRunWhilePaused = false
battleMenu.controllingPlayer = nil

battleMenu.disableControlsPlayer = nil


-- General utility functions
do
    function battleMenu.getKeys()
        return battleMenu.controllingPlayer.rawKeys
    end


    -- Default background
    function battleMenu.defaultDrawBackground(opacity,camIdx)
        local settings = battleMenu.backgroundSettings
        local cam = Camera(camIdx)

        Graphics.drawBox{
            texture = settings.image,priority = battleMenu.backgroundPriority - 0.5,
            color = Color.white.. opacity,

            sourceWidth = cam.width,sourceHeight = cam.height,
            sourceX = settings.speed.x*battleMenu.backgroundTimer + cam.renderX,
            sourceY = settings.speed.y*battleMenu.backgroundTimer + cam.renderY,
            x = 0,y = 0,

            shader = settings.shader,
        }
    end

    -- Default description
    function battleMenu.defaultUpdateDescriptionLayout(text)
        local settings = battleMenu.descriptionSettings
        local maxWidth = settings.textMaxWidth

        if maxWidth <= 0 then
            maxWidth = nil
        end

        battleMenu.descriptionLayout = textplus.layout(text,maxWidth,{font = settings.textFont,xscale = settings.textScale,yscale = settings.textScale,align = settings.textAlign})
        battleMenu.descriptionWidth = battleMenu.descriptionLayout.width
        battleMenu.descriptionHeight = battleMenu.descriptionLayout.height

        if settings.hasBox then
            battleMenu.descriptionWidth = battleMenu.descriptionWidth + settings.boxMarginX*2
            battleMenu.descriptionHeight = battleMenu.descriptionHeight + settings.boxMarginY*2
        end
    end

    function battleMenu.defaultDrawDescription(camIdx)
        local screenWidth,screenHeight = Graphics.getMainFramebufferSize()
        local settings = battleMenu.descriptionSettings

        local cam = Camera(camIdx)

        local x = (screenWidth - battleMenu.descriptionWidth)*0.5 - cam.renderX
        local y = screenHeight - battleMenu.descriptionHeight + settings.offsetY

        local easedMoveTime = easing.inQuad(battleMenu.descriptionMoveTimer,0,1,1)

        y = math.lerp(y,screenHeight,easedMoveTime)
        y = y - cam.renderY

        if settings.hasBox then
            battleMenu.drawSegmentedBox{
                texture = settings.boxImage,priority = battleMenu.descriptionPriority,
                x = x,y = y,width = battleMenu.descriptionWidth,height = battleMenu.descriptionHeight,
            }
        end

        textplus.render{
            layout = battleMenu.descriptionLayout,priority = battleMenu.descriptionPriority,
            x = x + settings.boxMarginX,
            y = y + settings.boxMarginY,
        }
    end


    -- Draws a box split into 9 parts.
    function battleMenu.drawSegmentedBox(args)
        local texture = args.texture or args.image
        local target = args.target or nil
    
        local priority = args.priority or 0
        local sceneCoords = args.sceneCoords or false
        local color = args.color or Color.white
    
        local shader = args.shader
        local uniforms = args.uniforms
    
        local x = args.x
        local y = args.y
        local width = args.width
        local height = args.height
    
        local segmentWidth = texture.width / 3
        local segmentHeight = texture.height / 3
    
        local segmentCountX = math.ceil(width / segmentWidth)
        local segmentCountY = math.ceil(height / segmentHeight)
    
    
        local vertexCoords = {}
        local textureCoords = {}
        local vertexCount = 0
    
        for segmentX = 1,segmentCountX do
            for segmentY = 1,segmentCountY do
                local thisX = x
                local thisY = y
                local thisWidth = math.min(width*0.5,segmentWidth)
                local thisHeight = math.min(height*0.5,segmentHeight)
                local thisSourceX = 0
                local thisSourceY = 0
    
                if segmentX == segmentCountX then
                    thisX = thisX + width - thisWidth
                    thisSourceX = texture.width - thisWidth
                elseif segmentX > 1 then
                    thisX = thisX + thisWidth + (segmentX-2)*segmentWidth
                    thisWidth = math.min(width - segmentWidth - (thisX - x),segmentWidth)
                    thisSourceX = segmentWidth
                end
    
                if segmentY == segmentCountY then
                    thisY = thisY + height - thisHeight
                    thisSourceY = texture.height - thisHeight
                elseif segmentY > 1 then
                    thisY = thisY + thisHeight + (segmentY-2)*segmentHeight
                    thisHeight = math.min(height - segmentHeight - (thisY - y),segmentHeight)
                    thisSourceY = segmentHeight
                end
    
    
                if thisWidth > 0 and thisHeight > 0 then
                    -- Add to vertexCoords
                    local x1 = thisX
                    local y1 = thisY
                    local x2 = thisX + thisWidth
                    local y2 = thisY + thisHeight
    
                    vertexCoords[vertexCount+1 ] = x1 -- top left
                    vertexCoords[vertexCount+2 ] = y1
                    vertexCoords[vertexCount+3 ] = x1 -- bottom left
                    vertexCoords[vertexCount+4 ] = y2
                    vertexCoords[vertexCount+5 ] = x2 -- top right
                    vertexCoords[vertexCount+6 ] = y1
                    vertexCoords[vertexCount+7 ] = x1 -- bottom left
                    vertexCoords[vertexCount+8 ] = y2
                    vertexCoords[vertexCount+9 ] = x2 -- top right
                    vertexCoords[vertexCount+10] = y1
                    vertexCoords[vertexCount+11] = x2 -- bottom right
                    vertexCoords[vertexCount+12] = y2
    
                    -- Add to textureCoords
                    local x1 = thisSourceX / texture.width
                    local y1 = thisSourceY / texture.height
                    local x2 = (thisSourceX + thisWidth) / texture.width
                    local y2 = (thisSourceY + thisHeight) / texture.height
    
                    textureCoords[vertexCount+1 ] = x1 -- top left
                    textureCoords[vertexCount+2 ] = y1
                    textureCoords[vertexCount+3 ] = x1 -- bottom left
                    textureCoords[vertexCount+4 ] = y2
                    textureCoords[vertexCount+5 ] = x2 -- top right
                    textureCoords[vertexCount+6 ] = y1
                    textureCoords[vertexCount+7 ] = x1 -- bottom left
                    textureCoords[vertexCount+8 ] = y2
                    textureCoords[vertexCount+9 ] = x2 -- top right
                    textureCoords[vertexCount+10] = y1
                    textureCoords[vertexCount+11] = x2 -- bottom right
                    textureCoords[vertexCount+12] = y2
    
                    vertexCount = vertexCount + 12
                end
            end
        end
    
        Graphics.glDraw{
            texture = texture,target = target,
            priority = priority,sceneCoords = sceneCoords,color = color,
            vertexCoords = vertexCoords,
            textureCoords = textureCoords,
            shader = shader,uniforms = uniforms,
        }
    end


    -- Draws the selection pinchers
    function battleMenu.drawPinchers(args)
        local texture = args.texture

        local unitWidth = texture.width*0.5
        local unitHeight = texture.height*0.5

        local rotation = args.rotation or 0
        local scale = args.scale or 1

        local rotationSin = math.sin(math.rad(rotation))
        local rotationCos = math.cos(math.rad(rotation))

        for unitX = 0,1 do
            for unitY = 0,1 do
                local offsetX = args.width*scale*(unitX - 0.5)
                local offsetY = args.height*scale*(unitY - 0.5)

                Graphics.drawBox{
                    texture = args.texture,priority = args.priority,sceneCoords = args.sceneCoords,
                    target = args.target,shader = args.shader,uniforms = args.uniforms,
                    color = args.color,
                    centred = true,

                    x = args.x + offsetX*rotationCos - offsetY*rotationSin,
                    y = args.y + offsetY*rotationCos + offsetX*rotationSin,
                    rotation = rotation,

                    sourceWidth = unitWidth,
                    sourceHeight = unitHeight,
                    width = unitWidth*scale,
                    height = unitHeight*scale,

                    sourceX = unitWidth*unitX,
                    sourceY = unitHeight*unitY,
                }
            end
        end
    end


    function battleMenu.defaultGetElementGraphicsPos(element)
        return element.x,element.y,0,1
    end

    function battleMenu.defaultGetElementColor(element)
        return Color.white
    end

    function battleMenu.defaultElementUpdate(element)

    end

    function battleMenu.defaultGetMenuColor(menu)
        return Color.white.. menu.enterProgress
    end

    function battleMenu.defaultUpdateMainLayout(element)
        local textFmt,maxWidth = element:getTextFormat()

        element.textLayout = textplus.layout(element.text,maxWidth,textFmt)
        element.mainWidth = element.textLayout.width
        element.mainHeight = element.textLayout.height
    end

    function battleMenu.defaultDrawMainLayout(element,mainX,mainY,mainColor)
        textplus.render{
            layout = element.textLayout,target = battleMenu.optionBuffer,priority = element.menu.format.priority,
            x = math.floor(mainX),y = math.floor(mainY),
            color = mainColor,
        }
    end

    -- Selection things
    function battleMenu.defaultCanEnterFunc(text)
        return true
    end
end


local function transformsAreSame(a,b)
    return (a.position == b.position and a.rotation == b.rotation and a.scale == b.scale)
end



-- Menu/option classes
do
    local elementMT = {}
    elementMT.__index = elementMT


    function elementMT:getTextFormat()
        local textFmt = {font = self.format.textFont,xscale = self.format.textScale,yscale = self.format.textScale,align = self.format.textAlign}
        local maxWidth = self.format.textMaxWidth
        if maxWidth <= 0 then
            maxWidth = nil
        end

        return textFmt,maxWidth
    end



    function elementMT:getTextSelectionText()
        if self.selectionIsHiddenFunction ~= nil and self:selectionIsHiddenFunction() then
            local length = utf8.len(self.selection:get())
            local text = ""

            for i = 1,length do
                text = text.. "*"
            end

            return text
        end

        return self.selection:get()
    end

    function elementMT:createTextSelectionLayout(text)
        local textFmt = {font = self.format.textFont,xscale = self.format.textScale,yscale = self.format.textScale}

        if text == "" then
            return textplus.layout(self.selectionPlaceholder,self.format.selectionMaxWidth,textFmt),true
        end

        textFmt.plaintext = true

        return textplus.layout(text,self.format.selectionMaxWidth,textFmt),false
    end


    function elementMT:updateLayout()
        -- Main layout
        self.format.updateMainLayoutFunc(self)

        -- Selection layout
        local selection = self.selection

        if selection ~= nil then
            self.mainWidth = self.mainWidth + self.format.selectionGap

            if selection.type == battleMenu.SELECTION_CHECKBOX then
                self.mainWidth = self.mainWidth + self.format.checkboxImage.width
                self.mainHeight = math.max(self.mainHeight,self.format.checkboxImage.height*0.5)
            elseif selection.type == battleMenu.SELECTION_NUMBERS then
                local layoutCount = math.floor((selection.maxValue - selection.minValue)/selection.step + 0.5)
                local textFmt,maxWidth = self:getTextFormat()

                self.selectionTextLayouts = {}

                for i = 0,layoutCount do
                    local value = selection.minValue + selection.step*i
                    local text = self.selectionNames[value] or tostring(value)

                    local layout = textplus.layout(text,maxWidth,textFmt)

                    self.selectionWidth = math.max(self.selectionWidth,layout.width)
                    self.selectionHeight = math.max(self.selectionHeight,layout.height)
                    
                    self.selectionTextLayouts[value] = layout
                end

                self.selectionWidth = self.selectionWidth + self.format.arrowsImage.width
                self.selectionHeight = math.max(self.selectionHeight,self.format.arrowsImage.height/self.format.arrowFrames)

                self.mainWidth = self.mainWidth + self.selectionWidth
                self.mainHeight = math.max(self.mainHeight,self.selectionHeight)
            elseif selection.type == battleMenu.SELECTION_TEXT then
                local textFmt,maxWidth = self:getTextFormat()

                local font = textFmt.font
                local lineHeight = (font.ascent + font.descent)*textFmt.yscale

                self.selectionText = self:getTextSelectionText()
                self.selectionTextLayout,self.selectionTextIsPlaceholder = self:createTextSelectionLayout(self.selectionText)

                self.selectionWidth = self.format.selectionMaxWidth
                self.selectionHeight = lineHeight*self.format.selectionMaxLines

                self.mainWidth = self.mainWidth + self.selectionWidth
                self.mainHeight = math.max(self.mainHeight,self.selectionHeight)
            end
        end


        if self.format.hasBox then
            self.totalWidth  = self.mainWidth  + self.format.boxMarginX*2
            self.totalHeight = self.mainHeight + self.format.boxMarginY*2
        else
            self.totalWidth  = self.mainWidth
            self.totalHeight = self.mainHeight
        end

        -- Row/column value
        local maxElementsPerLine = self.menu.format.maxElementsPerLine

        self.row = math.floor((self.idx - 1)/maxElementsPerLine) + 1
        self.column = ((self.idx - 1) % maxElementsPerLine) + 1
    end

    function elementMT:resetLayout()
        self.textLayout = nil

        self.mainWidth = 0
        self.mainHeight = 0
        self.totalWidth = 0
        self.totalHeight = 0

        self.selectionTextLayouts = {}
        self.selectionWidth = 0
        self.selectionHeight = 0

        self.selectionTextIsPlaceholder = false
        self.selectionTextLayout = nil
        self.selectionText = nil


        self.x = 0
        self.y = 0

        self.graphicsX = 0
        self.graphicsY = 0
        self.rotation = 0
        self.scale = 1

        self.color = Color.alphawhite
    end


    function elementMT:updateGraphics()
        local x,y,rotation,scale = self.format.getGraphicsPosFunc(self)

        assert(x ~= nil and y ~= nil,"X/Y not returned by getGraphicsPosFunc")

        self.graphicsX = x
        self.graphicsY = y
        self.rotation = rotation or 0
        self.scale = scale or 1

        local color = self.format.getColorFunc(self)

        assert(color ~= nil,"Color not returned by getColorFunc")

        self.color = color
    end

    function elementMT:update()
        local menu = self.menu

        if self.format.updateFunc ~= nil then
            self.format.updateFunc(self)
        end

        self:updateGraphics()
    end

    function elementMT:setAsDescription()
        if self.descriptionText ~= "" then
            battleMenu.changeDescription(self.descriptionText,self.menu.format.priority)
        else
            battleMenu.clearDescription()
        end
    end


    function elementMT:click()
        if self.unselectable then
            SFX.play(self.format.unselectableSound)
            return
        end

        if self.selection ~= nil then
            if self.selection.type == battleMenu.SELECTION_CHECKBOX then
                self.selection:set(not self.selection:get())
                SFX.play(self.format.confirmSound)
            else
                if self.selection.type == battleMenu.SELECTION_TEXT then
                    self.menu.selectionCursorPos = utf8.len(self.selection:get())
                    self.menu.selectionCursorTimer = 0
                end

                self.menu.changingSelection = true
                SFX.play(self.format.confirmSound)
            end
        else
            if self.runFunction ~= nil then
                self:runFunction()
            end

            if self.openMenu ~= nil then
                self.openMenu:open(self.openMenuArgs,self.openOption or 1)
            end
        end
    end


    local menuMT = {}
    menuMT.__index = menuMT


    local function addElement(menu,args)
        local element = setmetatable({},elementMT)

        element.menu = menu
        element.text = args.text or ""

        element.data = args.data or {}

        element.format = {}

        return element
    end


    function menuMT:addOption(args)
        local element = addElement(self,args)

        element.descriptionText = args.descriptionText or ""

        element.openMenu = args.openMenu
        element.openOption = args.openOption
        element.openMenuArgs = args.openMenuArgs
        element.runFunction = args.runFunction

        element.selection = args.selection
        element.selectionNames = args.selectionNames or {}
        element.selectionPlaceholder = args.selectionPlaceholder or ""
        element.selectionIsHiddenFunction = args.selectionIsHiddenFunction

        element.unselectable = args.unselectable or false


        for k,defaultValue in pairs(battleMenu.optionFormatDefaults) do
            if args.format ~= nil and args.format[k] ~= nil then
                element.format[k] = args.format[k]
            elseif self.optionFormat[k] ~= nil then
                element.format[k] = self.optionFormat[k]
            else
                element.format[k] = defaultValue
            end
        end


        table.insert(self.options,element)
        table.insert(self.elements,element)
        element.optionIdx = #self.options
        element.idx = #self.elements

        element:resetLayout()

        return element
    end

    function menuMT:addText(args)
        local element = addElement(self,args)


        for k,defaultValue in pairs(battleMenu.textElementFormatDefaults) do
            if args.format ~= nil and args.format[k] ~= nil then
                element.format[k] = args.format[k]
            elseif self.textFormat[k] ~= nil then
                element.format[k] = self.textFormat[k]
            else
                element.format[k] = defaultValue
            end
        end


        table.insert(self.elements,element)
        element.idx = #self.elements

        element:resetLayout()

        return element
    end


    function menuMT:updateElementsLayout()
        self.elementsWidth = 0
        self.elementsHeight = 0

        local largestOptionWidth = 0
        local largestOptionHeight = 0
        local columnWidths = {}
        local rowHeights = {}

        for _,element in ipairs(self.elements) do
            element:updateLayout()

            largestOptionWidth  = math.max(largestOptionWidth ,element.totalWidth )
            largestOptionHeight = math.max(largestOptionHeight,element.totalHeight)

            columnWidths[element.column] = math.max(columnWidths[element.column] or 0,element.totalWidth )
            rowHeights[element.row]      = math.max(rowHeights[element.row]      or 0,element.totalHeight)
        end

        -- Calculate option positions
        local lineCount = 0
        local x = 0
        local y = 0

        for _,element in ipairs(self.elements) do
            local columnWidth = columnWidths[element.column]
            local rowHeight = rowHeights[element.row]

            if element.format.useLargestWidth then
                element.totalWidth = largestOptionWidth
            elseif element.format.useColumnWidth then
                element.totalWidth = columnWidth
            end
            if element.format.useLargestHeight then
                element.totalHeight = largestOptionHeight
            elseif element.format.useRowHeight then
                element.totalHeight = rowHeight
            end

            element.x = x + element.totalWidth *0.5 + (columnWidth - element.totalWidth)*element.format.alignX
            element.y = y + element.totalHeight*0.5

            lineCount = lineCount + 1

            if self.format.maxElementsPerLine > 1 then
                element.y = element.y + (rowHeight - element.totalHeight)*element.format.alignY

                self.elementsWidth = math.max(self.elementsWidth,x + columnWidth)
                x = x + columnWidth + self.format.elementGapX
            else
                self.elementsWidth = math.max(self.elementsWidth,x + element.totalWidth)
                x = x + element.totalWidth + self.format.elementGapX
            end

            self.elementsHeight = y + rowHeight

            if lineCount >= self.format.maxElementsPerLine then
                y = y + rowHeight + self.format.elementGapY

                lineCount = 0
                x = 0
            end
        end

        -- Realign things
        for _,element in ipairs(self.elements) do
            element.x = element.x - self.elementsWidth*0.5
            element.y = element.y - self.elementsHeight*0.5

            element:updateGraphics()
        end
    end

    function menuMT:resetElementsLayouts()
        for _,element in ipairs(self.elements) do
            element:resetLayout()
        end
    end

    function menuMT:clearElements()
        for i = 1,#self.elements do
            self.elements[i] = nil
        end
        for i = 1,#self.options do
            self.options[i] = nil
        end
    end

    function menuMT:getCameraPos()
        local rotation = -(self.transform.wrotation + self.format.cameraRotation)
        local scale = 1/(self.transform.wscale*self.format.cameraScale)
        local position = -self.transform.wposition

        position = position - vector(self.format.cameraOffsetX,self.format.cameraOffsetY):rotate(-rotation)/scale
        position = position:rotate(rotation)*scale

        return position,rotation,scale
    end

    local function startCameraMove(self,goal)
        battleMenu.cameraMoveDuration = self.format.cameraMoveDuration
        battleMenu.cameraMoveEaseFunc = self.format.cameraMoveEaseFunc
        battleMenu.cameraMoveTimer = battleMenu.cameraMoveDuration

        for i = 1,2 do
            battleMenu.cameraMoveStart.position[i] = battleMenu.cameraTransform.position[i]
            battleMenu.cameraMoveStart.scale[i] = battleMenu.cameraTransform.scale[i]
        end
        battleMenu.cameraMoveStart.rotation = battleMenu.cameraTransform.rotation

        battleMenu.cameraMoveGoal.position = goal.position
        battleMenu.cameraMoveGoal.rotation = goal.rotation
        battleMenu.cameraMoveGoal.scale = goal.scale
    end


    function menuMT:open(args,optionIdx,controllingPlayer,pauses,runWhilePaused)
        -- Default values
        controllingPlayer = controllingPlayer or player
        optionIdx = optionIdx or 1

        if pauses == nil then
            pauses = true
        end

        if runWhilePaused == nil then
            runWhilePaused = pauses
        end

        -- If online, don't pause, and don't open if it's not for us.
        --[[if battleMenu.currentMenu == nil and onlinePlay ~= nil and onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
            if controllingPlayer.idx ~= onlinePlay.playerIdx then
                return
            end

            pauses = false
        end]]

        -- Initialise the menu
        self.transform.position = vector(self.format.offsetX,self.format.offsetY)
        self.transform.rotation = self.format.rotation
        self.transform.scale = vector(self.format.scale)

        self.visualTransform.position = self.transform.position
        self.visualTransform.rotation = self.transform.rotation
        self.visualTransform.scale = self.transform.scale

        self.args = args or {}


        if battleMenu.currentMenu == nil then
            -- These only work for the first menu. Any more will just ignore it.
            battleMenu.controllingPlayer = controllingPlayer
            battleMenu.menuPauses = pauses
            battleMenu.menuRunWhilePaused = runWhilePaused

            if battleMenu.menuPauses then
                Misc.pause()
            end
            
            -- Reset some stuff
            for i = 1,#battleMenu.activeMenus do
                local menu = battleMenu.activeMenus[i]

                menu.enterProgress = 0
                menu.isActive = false
                menu.isOpen = false
                menu:resetElementsLayouts()

                battleMenu.activeMenus[i] = nil
            end

            battleMenu.cameraTransform.position,battleMenu.cameraTransform.rotation,battleMenu.cameraTransform.scale = self:getCameraPos()
            battleMenu.cameraMoveTimer = 0

            self.transform:setParent(nil,false)
            self.visualTransform:setParent(battleMenu.cameraTransform,false)
        else
            self.transform:setParent(battleMenu.currentMenu.transform,false)
            self.visualTransform:setParent(battleMenu.currentMenu.visualTransform,false)

            -- Setup camera move
            startCameraMove(self,Transform(self:getCameraPos()))
        end


        self.optionIdx = optionIdx
        self.optionIdxFade = 0
        self.optionIdxFadeStart = self.optionIdx

        self.mouseWheelAccumulator = 0
        self.mouseWheelTimer = 0

        self.pinchersAnimationTimer = 0
        self.pinchersMoveTimer = 0

        self.changingSelection = false

        self:clearElements()

        if self.openFunc ~= nil then
            self:openFunc()
        end

        self:updateGraphics()
        self:updateElementsLayout()

        -- Make sure it isn't already in the history
        if self.isOpen then
            error("Cannot have the same menu open multiple times",2)
        end

        self.isOpen = true

        -- Make active
        if not self.isActive then
            table.insert(battleMenu.activeMenus,self)
            self.isActive = true
        end
        
        table.insert(battleMenu.menuHistory,self)
        battleMenu.currentMenu = self

        self:setCurrentOptionAsDescription()
    end


    local function closeInternal(self,triggerCameraMove)
        if self.closeFunc ~= nil then
            self:closeFunc()
        end

        table.remove(battleMenu.menuHistory)

        self.isOpen = false

        battleMenu.currentMenu = battleMenu.menuHistory[#battleMenu.menuHistory]

        if battleMenu.currentMenu == nil then
            -- Last menu to be closed
            if battleMenu.menuPauses then
                battleMenu.menuPauses = false
                Misc.unpause()
            end

            battleMenu.disableControlsPlayer = battleMenu.controllingPlayer
        elseif triggerCameraMove then
            startCameraMove(self,Transform(battleMenu.currentMenu:getCameraPos()))
        end
    end


    function menuMT:close()
        if battleMenu.currentMenu ~= self then
            error("Cannot close a menu that is not the active one",2)
        end

        closeInternal(self,true)

        if battleMenu.currentMenu ~= nil then
            battleMenu.currentMenu:setCurrentOptionAsDescription()
        else
            battleMenu.clearDescription()
        end
    end


    function battleMenu.closeAll()
        while (battleMenu.currentMenu ~= nil) do
            closeInternal(battleMenu.currentMenu,false)
        end

        battleMenu.clearDescription()
    end


    function menuMT:changeOptionIdx(newValue)
        if self.format.maxElementsPerLine == 1 then
            newValue = ((newValue - 1) % #self.options) + 1
        end

        if self.options[newValue] ~= nil and newValue ~= self.optionIdx then
            if self.format.selectionMoveDuration > 0 then
                self.optionIdxFadeStart = self.optionIdx
                self.optionIdxFade = 1
            end

            if self.format.hasPinchers and self.format.pinchersMoveDuration > 0 then
                self.pinchersPreviousOptionIdx = self.optionIdx
                self.pinchersMoveTimer = 1
            end

            self.optionIdx = newValue
            SFX.play(self.format.moveSound)
        end

        self:setCurrentOptionAsDescription()

        if self.changeOptionFunc ~= nil then
            self:changeOptionFunc()
        end
    end

    function menuMT:setCurrentOptionAsDescription()
        if self.options[self.optionIdx] ~= nil then
            self.options[self.optionIdx]:setAsDescription()
        end
    end


    function menuMT:updateGraphics()
        local color = self.format.getColorFunc(self)

        assert(color ~= nil,"Color not returned by getColorFunc")

        self.color = color
    end

    function menuMT:update()
        if self.changingSelection then
            self.selectionCursorTimer = self.selectionCursorTimer + 1
        else
            self.selectionCursorTimer = 0
        end

        if self.isOpen then
            if self.format.enterDuration > 0 then
                self.enterProgress = math.min(1,self.enterProgress + 1/self.format.enterDuration)
            else
                self.enterProgress = 1
            end
        else
            if self.format.exitDuration > 0 then
                self.enterProgress = math.max(0,self.enterProgress - 1/self.format.exitDuration)
            else
                self.enterProgress = 0
            end

            if self.enterProgress <= 0 then
                self.isActive = false
            end
        end

        if self.format.selectionMoveDuration > 0 then
            self.optionIdxFade = math.max(0,self.optionIdxFade - 1/self.format.selectionMoveDuration)
        end

        if self.format.hasPinchers then
            if self.format.pinchersMoveDuration > 0 then
                self.pinchersMoveTimer = math.max(0,self.pinchersMoveTimer - 1/self.format.pinchersMoveDuration)
            end

            self.pinchersAnimationTimer = self.pinchersAnimationTimer + 1
        end

        if self.mouseWheelTimer > 0 then
            self.mouseWheelTimer = math.max(0,self.mouseWheelTimer - 1)

            if self.mouseWheelTimer == 0 then
                self.mouseWheelAccumulator = 0
            end
        end

        self:updateGraphics()

        for _,element in ipairs(self.elements) do
            element:update()
        end
    end
    

    battleMenu.optionBuffer = Graphics.CaptureBuffer(800,400)
    battleMenu.menuBoxBuffer = Graphics.CaptureBuffer(800,1600)

    function menuMT:transformPoint(baseX,baseY)
        local transform = self.visualTransform

        local position = transform:apply(vector(baseX,baseY))
        local scale = transform.wscale.x
        local rotation = transform.wrotation

        local screenWidth,screenHeight = Graphics.getMainFramebufferSize()

        position.x = position.x + screenWidth *0.5
        position.y = position.y + screenHeight*0.5

        return position.x,position.y,scale,rotation
    end


    function elementMT:inverseTransformPoint(screenX,screenY)
        local transform = self.menu.visualTransform
        local position = transform:invTransformPoint(vector(screenX,screenY))

        local screenWidth,screenHeight = Graphics.getMainFramebufferSize()

        position.x = (position.x - screenWidth*0.5 - self.graphicsX)/self.scale
        position.y = (position.y - screenHeight*0.5 - self.graphicsY)/self.scale
        position = position:rotate(-self.rotation)

        return position.x,position.y
    end

    function elementMT:getTextColor(unselectable,invalid)
        if self.optionIdx == nil then
            return self.format.textColor
        end

        local colors = self.format.textColor
        if unselectable then
            colors = colors[2]
        else
            colors = colors[1]
        end
        

        local color = colors[1]
        if invalid then
            color = colors[3]
        end

        if self.menu.optionIdx == self.optionIdx then
            color = math.lerp(colors[2],color,self.menu.optionIdxFade)
        elseif self.menu.optionIdxFadeStart == self.optionIdx then
            color = math.lerp(color,colors[2],self.menu.optionIdxFade)
        end

        return color
    end


    function elementMT:draw(menu,priority,camIdx)
        local cam = Camera(camIdx)

        local x,y,scale,rotation = menu:transformPoint(self.graphicsX,self.graphicsY)

        x = x - cam.renderX
        y = y - cam.renderY

        local totalRotation = self.rotation + rotation
        local totalScale = self.scale*scale

        local width = self.totalWidth*totalScale
        local height = self.totalHeight*totalScale

        local cullingWidth = width*1.5
        local cullingHeight = height*1.5

        if x < -cullingWidth or x > cam.width+cullingWidth or y < -cullingHeight or y > cam.height+cullingHeight then
            return
        end

        local selection = self.selection

        battleMenu.optionBuffer:clear(priority)

        local mainX = (self.totalWidth  - self.mainWidth )*0.5
        local mainY = (self.totalHeight - self.mainHeight)*0.5

        if self.format.hasBox then
            battleMenu.drawSegmentedBox{
                texture = self.format.boxImage,target = battleMenu.optionBuffer,priority = priority,
                x = 0,y = 0,width = self.totalWidth,height = self.totalHeight,
            }
        end


        local textColor = self:getTextColor(self.unselectable,false)


        if selection ~= nil then
            local selectionX = self.totalWidth - self.selectionWidth*0.5
            local selectionY = self.totalHeight*0.5
            local color = textColor

            if self.format.hasBox then
                selectionX = selectionX - self.format.boxMarginX
                mainX = self.format.boxMarginX
            else
                mainX = 0
            end

            if self.format.dontTintSelection then
                color = Color.white
            end

            local value = selection:get()

            if selection.type == battleMenu.SELECTION_CHECKBOX then
                local checkboxWidth = self.format.checkboxImage.width
                local checkboxHeight = self.format.checkboxImage.height*0.5
                local sourceY = 0

                if value then
                    sourceY = checkboxHeight
                end

                Graphics.drawBox{
                    texture = self.format.checkboxImage,color = color,target = battleMenu.optionBuffer,priority = priority,centred = true,
                    sourceWidth = checkboxWidth,sourceHeight = checkboxHeight,
                    sourceX = 0,sourceY = sourceY,
                    x = selectionX,
                    y = selectionY,
                }
            elseif selection.type == battleMenu.SELECTION_NUMBERS then
                local layout = self.selectionTextLayouts[value]

                if layout ~= nil then
                    local arrowWidth = self.format.arrowsImage.width*0.5
                    local arrowHeight = self.format.arrowsImage.height/self.format.arrowFrames

                    local selectionTextX = selectionX + self.selectionWidth*0.5 - layout.width - arrowWidth
                    local selectionTextY = selectionY - layout.height*0.5

                    textplus.render{
                        layout = layout,color = color,target = battleMenu.optionBuffer,priority = priority,
                        x = math.floor(selectionTextX),
                        y = math.floor(selectionTextY),
                    }

                    if menu.optionIdx == self.optionIdx and menu.changingSelection then
                        local frame = math.floor(menu.selectionCursorTimer/self.format.arrowFrameDelay) % self.format.arrowFrames
    
                        for i = 0,1 do
                            Graphics.drawBox{
                                texture = self.format.arrowsImage,color = textColor,target = battleMenu.optionBuffer,priority = priority,
                                sourceWidth = arrowWidth,sourceHeight = arrowHeight,
                                sourceX = i*arrowWidth,
                                sourceY = frame*arrowHeight,
                                x = selectionX + self.selectionWidth*0.5 + math.lerp(-layout.width - arrowWidth*2,-arrowWidth,i),
                                y = selectionY - arrowHeight*0.5,
                            }
                        end
                    end
                end
            elseif selection.type == battleMenu.SELECTION_TEXT then
                local newText = self:getTextSelectionText()

                if self.selectionText ~= newText then
                    self.selectionTextLayout,self.selectionTextIsPlaceholder = self:createTextSelectionLayout(newText)
                    self.selectionText = newText
                end

                if not self.format.dontTintSelection then
                    color = self:getTextColor(self.selectionTextIsPlaceholder,not selection:isValid())
                end

                -- Draw text
                local selectionTextX = selectionX - self.selectionWidth*0.5
                local selectionTextY = selectionY - self.selectionHeight*0.5

                textplus.render{
                    layout = self.selectionTextLayout,color = color,target = battleMenu.optionBuffer,priority = priority,
                    x = math.floor(selectionTextX),
                    y = math.floor(selectionTextY),
                }

                -- Draw cursor
                local blinkTime = self.format.selectionCursorBlinkTime

                if menu.optionIdx == self.optionIdx and menu.changingSelection and menu.selectionCursorTimer%(blinkTime*2) < blinkTime then
                    local cursorX,cursorY = battleMenu.getCursorPosition(self.selectionTextLayout,menu.selectionCursorPos)

                    Graphics.drawBox{
                        texture = self.format.selectionCursorImage,color = textColor,target = battleMenu.optionBuffer,priority = priority,
                        x = selectionTextX + cursorX,y = selectionTextY + cursorY,
                    }
                end
            end
        end

        
        self.format.drawMainLayoutFunc(self,mainX,mainY,textColor,optionBuffer)


        Graphics.drawBox{
            texture = battleMenu.optionBuffer,priority = priority,centred = true,linearFiltered = false,
            color = menu.color*self.color,
            x = math.floor(x),y = math.floor(y),rotation = totalRotation,
            width = math.floor(width*0.5 + 0.5)*2,height = math.floor(height*0.5 + 0.5)*2,
            sourceWidth = math.floor(self.totalWidth*0.5 + 0.5)*2,sourceHeight = math.floor(self.totalHeight*0.5 + 0.5)*2,
        }
    end

    function menuMT:draw(camIdx)
        if self.color.a <= 0 then
            return
        end

        local cam = Camera(camIdx)

        local screenWidth,screenHeight = Graphics.getMainFramebufferSize()
        local priority = self.format.priority

        -- Box
        if self.format.hasBox then
            battleMenu.menuBoxBuffer:clear(priority)

            local width  = self.elementsWidth  + self.format.boxMarginX*2
            local height = self.elementsHeight + self.format.boxMarginY*2

            battleMenu.drawSegmentedBox{
                texture = self.format.boxImage,target = battleMenu.menuBoxBuffer,priority = priority,
                x = 0,y = 0,width = width,height = height,
            }

            local x,y,scale,rotation = self:transformPoint(0,0)

            Graphics.drawBox{
                texture = battleMenu.menuBoxBuffer,priority = priority,centred = true,
                color = self.color,
                x = x - cam.renderX,y = y - cam.renderY,rotation = rotation,
                width = width*scale,height = height*scale,
                sourceWidth = width,sourceHeight = height,
            }
        end

        -- Elements
        for _,option in ipairs(self.elements) do
            option:draw(self,priority,camIdx)
        end

        -- Pinchers
        if self.format.hasPinchers then
            local option = self.options[self.optionIdx]
            local pinchersX = option.graphicsX
            local pinchersY = option.graphicsY
            local pinchersWidth = option.totalWidth
            local pinchersHeight = option.totalHeight
            local pinchersRotation = option.rotation

            if self.pinchersMoveTimer > 0 then
                local previousOption = self.options[self.pinchersPreviousOptionIdx]

                pinchersX = math.lerp(pinchersX,previousOption.graphicsX,self.pinchersMoveTimer)
                pinchersY = math.lerp(pinchersY,previousOption.graphicsY,self.pinchersMoveTimer)
                pinchersWidth = math.lerp(pinchersWidth,previousOption.totalWidth,self.pinchersMoveTimer)
                pinchersHeight = math.lerp(pinchersHeight,previousOption.totalHeight,self.pinchersMoveTimer)
                pinchersRotation = math.lerp(pinchersRotation,previousOption.rotation,self.pinchersMoveTimer)
            end

            local animationTime = math.abs(math.sin(self.pinchersAnimationTimer*math.pi/48)) - 0.5

            pinchersWidth = pinchersWidth + animationTime*16
            pinchersHeight = pinchersHeight + animationTime*16

            local x,y,scale,rotation = self:transformPoint(pinchersX,pinchersY)

            battleMenu.drawPinchers{
                texture = self.format.pinchersImage,priority = priority,
                x = x,y = y,width = pinchersWidth,height = pinchersHeight,
                rotation = pinchersRotation + rotation,scale = scale,
                color = self.color,
            }
        end
    end


    function battleMenu.createMenu(args)
        local menu = setmetatable({},menuMT)

        menu.elements = {}
        menu.options = {}
        menu.optionIdx = 0

        menu.elementsWidth = 0
        menu.elementsHeight = 0

        menu.enterProgress = 0
        menu.isActive = false
        menu.isOpen = false

        menu.color = Color.alphawhite

        menu.changingSelection = false
        menu.selectionCursorPos = 0
        menu.selectionCursorTimer = 0

        menu.mouseWheelAccumulator = 0
        menu.mouseWheelTimer = 0

        menu.data = args.data or {}


        menu.cantGoBack = args.cantGoBack or false


        menu.transform = Transform()
        menu.visualTransform = Transform()

        menu.optionFormat = args.optionFormat or {}
        menu.textFormat = args.textFormat or {}
        menu.format = {}

        for k,defaultValue in pairs(battleMenu.menuFormatDefaults) do
            if args.format ~= nil and args.format[k] ~= nil then
                menu.format[k] = args.format[k]
            else
                menu.format[k] = defaultValue
            end
        end

        return menu
    end
end


-- Selections
do
    battleMenu.SELECTION_CHECKBOX = 0
    battleMenu.SELECTION_NUMBERS  = 1
    battleMenu.SELECTION_TEXT     = 2

    local selectionMT = {}

    selectionMT.__index = selectionMT


    local function defaultSet(self,value)
        local min = self.minValue
        local max = self.maxValue

        if min ~= nil and max ~= nil then
            value = ((value - min) % (max - min + 1)) + min
        end

        self.saveTable[self.saveName] = value

        if self.onSet ~= nil then
            self.onSet(value)
        end
    end

    local function defaultGet(self)
        local value = self.saveTable[self.saveName]

        if value ~= nil then
            return value
        else
            return self.defaultValue
        end
    end

    local function defaultIsValid(self)
        local value = self:get()

        if value == "" then
            return true
        end

        if self.isValidFunc ~= nil then
            return self.isValidFunc(value)
        else
            return true
        end
    end
    

    function battleMenu.createSelection(saveName,saveTable, selectionType,a,b,c,d)
        local selection = {}

        selection.saveName = saveName
        selection.saveTable = saveTable

        selection.set = defaultSet
        selection.get = defaultGet

        selection.type = selectionType

        if selectionType == battleMenu.SELECTION_CHECKBOX then
            selection.defaultValue = a or false
        elseif selectionType == battleMenu.SELECTION_NUMBERS then
            selection.minValue = a or 1
            selection.maxValue = b or selection.minValue
            selection.defaultValue = c or selection.minValue
            selection.step = d or 1
        elseif selectionType == battleMenu.SELECTION_TEXT then
            selection.defaultValue = a or ""
            selection.canEnterFunc = b or battleMenu.defaultCanEnterFunc
            selection.isValidFunc  = c or selection.canEnterFunc
        end

        selection.isValid = defaultIsValid

        return selection
    end
end


-- SELECTION_TEXT stuff
do
    function battleMenu.getCursorPosition(layout,pos)
        -- Count up until we reach our position
        local charIdx = 0
        local textX = 0
        local textY = 0
    
        for _,line in ipairs(layout) do
            local elemCount = #line
            local i = 1
    
            textX = line.startX
    
            if charIdx >= pos then
                return textX,textY
            end
    
            while (i <= elemCount) do
                local segment = line[i]
    
                if segment.img ~= nil then
                    textX = textX + segment.width
                    charIdx = charIdx + 1
    
                    if charIdx >= pos then
                        return textX,textY
                    end
                else
                    local fmt = segment.fmt
                    local font = fmt.font
                    local spacing = fmt.spacing or font.spacing
                    
                    local startIdx = line[i+1]
                    local endIdx = line[i+2]
                    
                    for idx = startIdx,endIdx do
                        local glyph = fmt.font.glyphs[segment[idx]]
                        if glyph ~= nil then
                            textX = textX + (glyph.width + spacing)*fmt.xscale
                        else
                            textX = textX + (font.cellWidth + spacing)*fmt.xscale
                        end
    
                        charIdx = charIdx + 1
    
                        if charIdx >= pos then
                            return textX,textY
                        end
                    end
                end
    
                i = i + 4
            end
    
            charIdx = charIdx + 1 -- new lines count as a character
    
            textY = textY + line.ascent + line.descent
        end
    
        return textX,textY
    end


    local function getTypingOption()
        local menu = battleMenu.currentMenu
        if menu == nil or not menu.changingSelection then
            return
        end

        if not battleMenu.menuRunWhilePaused and Misc.isPaused() then
            return
        end

        local option = menu.options[menu.optionIdx]
        if option == nil then
            return
        end

        local selection = option.selection
        if selection == nil or selection.type ~= battleMenu.SELECTION_TEXT then
            return
        end

        return menu,option,selection
    end


    local function setTypingText(menu,option,selection, text)
        local layout,isPlaceholder = option:createTextSelectionLayout(text)

        if not isPlaceholder then
            -- Don't allow any invalid characters
            local codes = tplusUtils.strToCodes(text)
            local font = option.format.textFont
            
            for _,char in ipairs(codes) do
                if font.glyphs[char] == nil and char ~= string.byte("\n") then
                    return false
                end
            end

            -- Don't allow too many lines
            if #layout > option.format.selectionMaxLines then
                return false
            end

            -- Check against the selection's can enter function
            if selection.canEnterFunc ~= nil and not selection.canEnterFunc(text) then
                return false
            end
        end


        menu.selectionCursorTimer = 0
        selection:set(text)

        return true
    end

    local function addText(menu,option,selection, addedText,pos)
        local text = selection:get()

        local newText = utf8.sub(text,1,pos).. addedText.. utf8.sub(text,pos + 1)
        local successful = setTypingText(menu,option,selection, newText)

        if successful then
            if menu.selectionCursorPos >= pos then
                menu.selectionCursorPos = menu.selectionCursorPos + utf8.len(addedText)
            end
        else
            -- This text can't fit, so try removing characters until it does
            local textLength = utf8.len(addedText)
    
            if textLength > 1 then
                local newText = utf8.sub(addedText,1,textLength - 1)
    
                addText(menu,option,selection, newText,pos)
            end
        end
    end


    function battleMenu.onKeyboardPressDirect(vk,repeated,char)
        local menu,option,selection = getTypingOption()
        if menu == nil then
            return
        end


        if vk == VK_RETURN then
            if Misc.GetKeyState(VK_SHIFT) then
                addText(menu,option,selection, "\n",menu.selectionCursorPos)
            else
                menu.changingSelection = false
            end

            return
        elseif vk == VK_ESCAPE then
            menu.changingSelection = false
            return
        end
        
        if vk == VK_BACK then
            if menu.selectionCursorPos > 0 then
                local text = selection:get()
                local newText = utf8.sub(text,1,menu.selectionCursorPos - 1).. utf8.sub(text,menu.selectionCursorPos + 1)
                
                local successful = setTypingText(menu,option,selection, newText)
    
                if successful then
                    menu.selectionCursorPos = menu.selectionCursorPos - 1
                    menu.selectionCursorTimer = 0
                end
            end
        elseif vk == VK_LEFT then
            menu.selectionCursorPos = math.max(0,menu.selectionCursorPos - 1)
            menu.selectionCursorTimer = 0
        elseif vk == VK_RIGHT then
            menu.selectionCursorPos = math.min(utf8.len(selection:get()),menu.selectionCursorPos + 1)
            menu.selectionCursorTimer = 0
        elseif vk == VK_HOME then
            menu.selectionCursorPos = 0
            menu.selectionCursorTimer = 0
        elseif vk == VK_END then
            menu.selectionCursorPos = utf8.len(selection:get())
            menu.selectionCursorTimer = 0
        elseif char ~= nil then
            addText(menu,option,selection, char,menu.selectionCursorPos)
        end
    end

    function battleMenu.onPasteText(text)
        local menu,option,selection = getTypingOption()
        if menu == nil then
            return
        end
        
        addText(menu,option,selection, text,menu.selectionCursorPos)
    end
end



function battleMenu.clearDescription()
    if battleMenu.descriptionState == DESCRIPTION_STATE.INACTIVE then
        return
    end
    
    battleMenu.descriptionState = DESCRIPTION_STATE.OUT
    battleMenu.descriptionText = ""
end

function battleMenu.changeDescription(text,priority)
    if battleMenu.descriptionText == text then
        return
    end
    
    if battleMenu.descriptionState == DESCRIPTION_STATE.INACTIVE then
        battleMenu.descriptionState = DESCRIPTION_STATE.IN
        battleMenu.descriptionMoveTimer = 1
        battleMenu.descriptionPriority = priority

        battleMenu.descriptionSettings.updateLayoutFunc(text)
    else
        battleMenu.descriptionState = DESCRIPTION_STATE.OUT
        battleMenu.descriptionNewPriority = priority
    end

    battleMenu.descriptionText = text    
end


local function handleMenuControl(menu)
    local keys = battleMenu.getKeys()

    if #menu.options > 0 then
        if menu.changingSelection then
            local option = menu.options[menu.optionIdx]
            local selection = option.selection

            if selection.type == battleMenu.SELECTION_NUMBERS then
                if keys.left == KEYS_PRESSED then
                    selection:set(selection:get() - selection.step)
                    SFX.play(menu.format.moveSound)
                elseif keys.right == KEYS_PRESSED then
                    selection:set(selection:get() + selection.step)
                    SFX.play(menu.format.moveSound)
                end

                if keys.jump == KEYS_PRESSED or keys.run == KEYS_PRESSED then
                    menu.changingSelection = false
                end
            end

            return
        end

        if keys.up == KEYS_PRESSED then
            menu:changeOptionIdx(menu.optionIdx - menu.format.maxElementsPerLine)
        elseif keys.down == KEYS_PRESSED then
            menu:changeOptionIdx(menu.optionIdx + menu.format.maxElementsPerLine)
        elseif menu.format.maxElementsPerLine > 1 then
            if keys.left == KEYS_PRESSED then
                menu:changeOptionIdx(menu.optionIdx - 1)
            elseif keys.right == KEYS_PRESSED then
                menu:changeOptionIdx(menu.optionIdx + 1)
            end
        end

        if keys.jump == KEYS_PRESSED then
            local option = menu.options[menu.optionIdx]

            option:click()

            return
        end
    end

    if not menu.cantGoBack and keys.run == KEYS_PRESSED then
        menu:close()
        return
    end
end

local function updateMenus()
    local menu = battleMenu.currentMenu

    -- Handle background
    if menu ~= nil and menu.format.hasBackground then
        battleMenu.activeBackgroundFunc = menu.format.drawBackgroundFunc
        battleMenu.backgroundFadeSpeed = menu.format.backgroundFadeSpeed
        battleMenu.backgroundPriority = menu.format.priority + menu.format.backgroundPriorityOffset

        battleMenu.backgroundOpacity = math.min(1,battleMenu.backgroundOpacity + battleMenu.backgroundFadeSpeed)
    else
        battleMenu.backgroundOpacity = math.max(0,battleMenu.backgroundOpacity - battleMenu.backgroundFadeSpeed)
    end
    
    if battleMenu.backgroundOpacity > 0 then
        battleMenu.backgroundTimer = battleMenu.backgroundTimer + 1
    else
        battleMenu.backgroundTimer = 0
    end

    -- Handle descriptions
    if battleMenu.descriptionState == DESCRIPTION_STATE.IN then
        battleMenu.descriptionMoveTimer = math.max(0,battleMenu.descriptionMoveTimer - 1/battleMenu.descriptionSettings.moveInDuration)

        if battleMenu.descriptionMoveTimer <= 0 then
            battleMenu.descriptionState = DESCRIPTION_STATE.ACTIVE
        end
    elseif battleMenu.descriptionState == DESCRIPTION_STATE.OUT then
        battleMenu.descriptionMoveTimer = math.min(1,battleMenu.descriptionMoveTimer + 1/battleMenu.descriptionSettings.moveOutDuration)

        if battleMenu.descriptionMoveTimer >= 1 then
            if battleMenu.descriptionText ~= "" then
                battleMenu.descriptionState = DESCRIPTION_STATE.IN
                battleMenu.descriptionPriority = battleMenu.descriptionNewPriority

                battleMenu.descriptionSettings.updateLayoutFunc(battleMenu.descriptionText)
            else
                battleMenu.descriptionState = DESCRIPTION_STATE.INACTIVE
            end
        end
    end

    -- Option handling
    if menu ~= nil then
        handleMenuControl(menu)
    end

    -- Update all active menus
    for i = #battleMenu.activeMenus, 1, -1 do
        local menu = battleMenu.activeMenus[i]
        
        menu:update()

        if not menu.isActive then
            menu:resetElementsLayouts()
            table.remove(battleMenu.activeMenus,i)
        end            
    end

    -- Camera handling
    if battleMenu.cameraMoveTimer > 0 then
        battleMenu.cameraMoveTimer = battleMenu.cameraMoveTimer - 1

        local t = 1 - battleMenu.cameraMoveTimer/battleMenu.cameraMoveDuration
        local current = battleMenu.cameraTransform
        local start = battleMenu.cameraMoveStart
        local goal = battleMenu.cameraMoveGoal

        for i = 1,2 do
            current.position[i] = battleMenu.cameraMoveEaseFunc(start.position[i],goal.position[i],t)
            current.scale[i] = battleMenu.cameraMoveEaseFunc(start.scale[i],goal.scale[i],t)
        end

        current.rotation = battleMenu.cameraMoveEaseFunc(start.rotation,goal.rotation,t)
    end

    --Text.printWP(battleMenu.cameraTransform.scale,32,32,10)

    --battleMenu.cameraTransform.position = vector(math.cos(lunatime.drawtick()/32)*32,0)
    --battleMenu.cameraTransform.rotation = math.cos(lunatime.drawtick()/32)*25
    --battleMenu.cameraTransform.scale = math.lerp(2,1,vector(math.cos(lunatime.drawtick()/32)*0.5 + 0.5))

    --battleMenu.cameraTransform.position = vector(0,0)
    --battleMenu.cameraTransform.rotation = 0
    --battleMenu.cameraTransform.scale = vector(0.2)
end


function battleMenu.onTick()
    local p = battleMenu.disableControlsPlayer

    if p ~= nil then
        if p.isValid then
            p:mem(0x11E,FIELD_BOOL,false) -- stop from jumping
            p:mem(0x120,FIELD_BOOL,false) -- stop from spin jumping
        end

        battleMenu.disableControlsPlayer = nil
    end

    if not battleMenu.menuRunWhilePaused then
        updateMenus()
    end
end

function battleMenu.onInputUpdate()
    if battleMenu.menuRunWhilePaused then
        updateMenus()
    end

    if battleMenu.currentMenu ~= nil then
        local keys = battleMenu.controllingPlayer.keys

        for k,v in pairs(keys) do
            keys[k] = false
        end
    end
end

function battleMenu.onCameraDraw(camIdx)
    if battleMenu.backgroundOpacity > 0 then
        battleMenu.activeBackgroundFunc(battleMenu.backgroundOpacity,camIdx)
    end

    for _,menu in ipairs(battleMenu.activeMenus) do
        menu:draw(camIdx)
    end

    if battleMenu.descriptionState ~= battleMenu.DESCRIPTION_STATE.INACTIVE then
        battleMenu.descriptionSettings.drawFunc(camIdx)
    end
end


local function elementClicked(element,relativeX,relativeY)
    if element.menu.optionIdx ~= element.optionIdx then
        element.menu.changingSelection = false
        element.menu:changeOptionIdx(element.optionIdx)

        return
    end

    if element.menu.changingSelection then
        local selection = element.selection

        if selection.type == battleMenu.SELECTION_NUMBERS then
            local selectionX = element.totalWidth*0.5 - element.selectionWidth*0.5
            if element.format.hasBox then
                selectionX = selectionX - element.format.boxMarginX
            end

            if relativeX < selectionX then
                selection:set(selection:get() - selection.step)
                SFX.play(element.menu.format.moveSound)
            else
                selection:set(selection:get() + selection.step)
                SFX.play(element.menu.format.moveSound)
            end
        elseif selection.type == battleMenu.SELECTION_TEXT then
            element.menu.changingSelection = false
        end

        return
    end

    element:click()
end

function battleMenu.onMouseButtonEvent(button,state,mouseX,mouseY)
    local menu = battleMenu.currentMenu

    if menu == nil then
        return
    end

    if state ~= 0 then
        return
    end

    if button == 1 then
        if menu.changingSelection then
            menu.changingSelection = false
        elseif not menu.cantGoBack then
            menu:close()
        end

        return
    elseif button ~= 0 then
        return
    end


    for _,element in ipairs(menu.options) do
        local relativeX,relativeY = element:inverseTransformPoint(mouseX,mouseY)
        local width = element.totalWidth*element.scale*0.5
        local height = element.totalHeight*element.scale*0.5

        if relativeX >= -width and relativeX <= width and relativeY >= -height and relativeY <= height then
            elementClicked(element,relativeX,relativeY)
            return
        end
    end

    -- Nothing was clicked
    if menu.changingSelection then
        menu.changingSelection = false
    end
end

function battleMenu.onMouseWheelEvent(axis,delta,mouseX,mouseY)
    local menu = battleMenu.currentMenu

    if menu == nil or #menu.options <= 1 then
        return
    end

    menu.mouseWheelAccumulator = menu.mouseWheelAccumulator + math.abs(delta)/180
    menu.mouseWheelTimer = 12

    while (menu.mouseWheelAccumulator > 1) do
        if axis == 0 then
            menu:changeOptionIdx(menu.optionIdx - math.sign(delta)*menu.format.maxElementsPerLine)
        elseif menu.format.maxElementsPerLine > 1 then
            menu:changeOptionIdx(menu.optionIdx - math.sign(delta))
        end

        menu.mouseWheelAccumulator = menu.mouseWheelAccumulator - 1
    end
end


function battleMenu.onInitAPI()
    registerEvent(battleMenu,"onTick")
    registerEvent(battleMenu,"onInputUpdate")
    registerEvent(battleMenu,"onCameraDraw")

    registerEvent(battleMenu,"onKeyboardPressDirect")
    registerEvent(battleMenu,"onPasteText")

    registerEvent(battleMenu,"onMouseButtonEvent")
    registerEvent(battleMenu,"onMouseWheelEvent")

    onlinePlay = require("scripts/onlinePlay")
end


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

battleMenu.menuFormatDefaults = {
    priority = 7,

    offsetX = 0,
    offsetY = 0,
    rotation = 0,
    scale = 1,

    cameraOffsetX = 0,
    cameraOffsetY = 0,
    cameraRotation = 0,
    cameraScale = 1,

    cameraMoveDuration = 24,
    cameraMoveEaseFunc = function(a,b,t)
        return easing.inOutQuad(t,a,b - a,1)
    end,


    enterDuration = 8,
    exitDuration = 8,

    selectionMoveDuration = 0,


    maxElementsPerLine = 1,

    elementGapX = 24,
    elementGapY = 32,


    hasBackground = false,
    drawBackgroundFunc = battleMenu.defaultDrawBackground,
    backgroundFadeSpeed = 0.1,
    backgroundPriorityOffset = 0,

    hasBox = false,
    boxImage = Graphics.loadImageResolved("resources/menu/box.png"),
    boxMarginX = 32,
    boxMarginY = 20,

    hasPinchers = false,
    pinchersImage = Graphics.loadImageResolved("resources/menu/pinchers.png"),
    pinchersMoveDuration = 8,

    moveSound = 26,


    getColorFunc = battleMenu.defaultGetMenuColor,
}

battleMenu.optionFormatDefaults = {
    textFont = mainFont,
    textScale = 3,

    textAlign = "center",
    textMaxWidth = 0,

    textColor = {
        -- selectable
        {Color.white,Color(1,1,0.25),Color(1,0.5,0.5)},
        -- unselectable
        {Color(0.4,0.4,0.4),Color(0.5,0.5,0),Color(0.5,0,0)},
    },

    dontTintSelection = false,

    useLargestWidth = false,
    useLargestHeight = false,
    useColumnWidth = false,
    useRowHeight = false,
    alignX = 0.5,
    alignY = 0.5,


    -- Checkboxes
    checkboxImage = Graphics.loadImageResolved("resources/menu/checkbox.png"),
    arrowsImage = Graphics.loadImageResolved("resources/menu/selectionArrows.png"),

    -- Numbers
    arrowFrames = 2,
    arrowFrameDelay = 24,

    -- Text
    selectionCursorImage = Graphics.loadImageResolved("resources/font/textCursor.png"),
    selectionCursorBlinkTime = 32,
    selectionMaxWidth = 192,
    selectionMaxLines = 1,

    selectionGap = 32,


    hasBox = true,
    boxImage = Graphics.loadImageResolved("resources/menu/box.png"),
    boxMarginX = 32,
    boxMarginY = 20,

    unselectableSound = Misc.resolveSoundFile("resources/menu/unselectable"),
    confirmSound = Misc.resolveSoundFile("resources/menu/confirm"),


    updateFunc = battleMenu.defaultElementUpdate,
    getGraphicsPosFunc = battleMenu.defaultGetElementGraphicsPos,
    getColorFunc = battleMenu.defaultGetElementColor,
    updateMainLayoutFunc = battleMenu.defaultUpdateMainLayout,
    drawMainLayoutFunc = battleMenu.defaultDrawMainLayout,
}

battleMenu.textElementFormatDefaults = {
    textFont = mainFont,
    textScale = 3,
    textColor = Color.white,
    textAlign = "center",
    textMaxWidth = 0,

    useLargestWidth = false,
    useLargestHeight = false,
    useColumnWidth = false,
    useRowHeight = false,
    alignX = 0.5,
    alignY = 0.5,


    hasBox = true,
    boxImage = Graphics.loadImageResolved("resources/menu/box.png"),
    boxMarginX = 32,
    boxMarginY = 20,


    updateFunc = battleMenu.defaultElementUpdate,
    getGraphicsPosFunc = battleMenu.defaultGetElementGraphicsPos,
    getColorFunc = battleMenu.defaultGetElementColor,
    updateMainLayoutFunc = battleMenu.defaultUpdateMainLayout,
    drawMainLayoutFunc = battleMenu.defaultDrawMainLayout,
}


battleMenu.backgroundSettings = {
    image = Graphics.loadImageResolved("resources/menu/background.png"),
    shader = Shader.fromFile(nil,"resources/menu/background.frag"),
    speed = vector(0.25,0),
}

battleMenu.descriptionSettings = {
    textFont = mainFont,
    textScale = 2,
    textColor = Color.white,
    textAlign = "left",
    textMaxWidth = 512,

    moveInDuration = 10,
    moveOutDuration = 10,

    offsetY = -8,


    hasBox = true,
    boxImage = Graphics.loadImageResolved("resources/menu/box.png"),
    boxMarginX = 16,
    boxMarginY = 10,

    updateLayoutFunc = battleMenu.defaultUpdateDescriptionLayout,
    drawFunc = battleMenu.defaultDrawDescription,
}


return battleMenu