local playerManager = require("playerManager")
local textplus = require("textplus")
local easing = require("ext/easing")

local perPlayerCostumes = require("scripts/perPlayerCostumes")
local textFiles = require("scripts/textFiles")

local battleGeneral = require("scripts/battleGeneral")
local battleMenu = require("scripts/battleMenu")

local onlinePlay = require("scripts/onlinePlay")
local onlinePlayPlayers = require("scripts/onlinePlay_players")

local costumeSelect = {}


costumeSelect.headGap = 40


local costumeChangeCommand = onlinePlay.createCommand("battle_hub_costumeChange",onlinePlay.IMPORTANCE_MAJOR)


local activeFrameDisplays = {}

local characterColors = {
    [CHARACTER_MARIO] = Color.red,
    [CHARACTER_LUIGI] = Color.green,
    [CHARACTER_PEACH] = Color.pink,
    [CHARACTER_TOAD] = Color.lightblue,
    [CHARACTER_LINK] = Color.green,
}


local function getCostumeList(character)
    local characterName = playerManager.getName(character)
    local costumeList = {}

    -- Default costume folder
    local basegameCostumeFolder = getSMBXPath().. "\\costumes\\".. characterName.. "\\"

    for _,costumeName in ipairs(Misc.listDirectories(basegameCostumeFolder)) do
        table.insert(costumeList,{name = costumeName,path = basegameCostumeFolder.. costumeName})
    end

    -- Episode costume folder
    local episodeCostumeFolder = Misc.episodePath().. "\\costumes\\".. characterName.. "\\"

    for _,costumeName in ipairs(Misc.listDirectories(episodeCostumeFolder)) do
        table.insert(costumeList,{name = costumeName,path = episodeCostumeFolder.. costumeName})
    end

    return costumeList
end


local function formatCostumeName(character,costumeName)
    if costumeName == nil then
        return textFiles.costumeSelect.defaultCostumeNames[playerManager.getName(character)]
    end

    -- Remove underscores and hyphens
    return battleGeneral.escapeTextplusTags(costumeName:gsub("[-_]"," "))
end


local function loadHeadImage(character,costumeName,costumePath)
    if costumeName ~= nil then
        -- Use the head image in the costume's folder, if it's present
        local path = costumePath.. "\\battleHead.png"
        
        if io.exists(path) then
            return Graphics.loadImage(path)
        end

        -- Use costume image, if available
        path = Misc.resolveGraphicsFile("resources/characterHeads/".. costumeName.. ".png")

        if path ~= nil then
            return Graphics.loadImage(path)
        end
    else
        -- Use character image
        local path = Misc.resolveGraphicsFile("resources/characterHeads/".. playerManager.getName(character).. ".png")

        if path ~= nil then
            return Graphics.loadImage(path)
        end
    end

    return Graphics.loadImageResolved("stock-0.png")
end

local function loadCharacterFrame(character,powerup,frame,costumePath)
    local characterName = playerManager.getName(character)
    local frameX,frameY = Player.convertFrame(frame)
    local defaultSettings = PlayerSettings.get(character,powerup)

    local frameData = {frameX = frameX,frameY = frameY}

    if costumePath ~= nil then
        local imagePath  = costumePath.. "\\".. characterName.. "-".. powerup.. ".png"
        local configPath = costumePath.. "\\".. characterName.. "-".. powerup.. ".ini"

        -- Load image
        frameData.image = Graphics.loadImage(imagePath) or Graphics.loadImageResolved(playerManager.getName(character).. "-".. powerup.. ".png")

        -- Load the config file
        if io.exists(configPath) then
            local config = perPlayerCostumes.parsePlayerConfigFile(configPath)

            -- Some costumes will have a different hitbox size (e.g., Demo), so that needs to be accounted for
            local widthDifference = (config.hitboxWidth or defaultSettings.hitboxWidth) - defaultSettings.hitboxWidth
            local heightDifference = (config.hitboxHeight or defaultSettings.hitboxHeight) - defaultSettings.hitboxHeight

            frameData.offset = config.offsets[frameX][frameY] + vector(config.hitboxWidth*0.5,config.hitboxHeight*0.5)
        end
    end

    -- If no image was found, use the default player graphics
    if frameData.image == nil then
        frameData.image = Graphics.loadImageResolved(playerManager.getName(character).. "-".. powerup.. ".png")
    end

    -- If no config file was found, use the default offset
    if frameData.offset == nil then
        frameData.offset = vector(
            defaultSettings.hitboxWidth*0.5 - defaultSettings:getSpriteOffsetX(frameX,frameY),
            defaultSettings.hitboxHeight*0.5 - defaultSettings:getSpriteOffsetY(frameX,frameY)
        )
    end

    return frameData
end

local function loadCostumeDescription(costumePath)
    local path = costumePath.. "\\battleDescription.txt"
    local file = io.open(path,"r")

    if file == nil then
        return nil
    end

    local content = file:read("*a")

    local _,startingWhitespacePosition = content:find("^%s*")
    local endingWhitespacePosition,_ = content:find("%s*$")

    return battleGeneral.escapeTextplusTags(content:sub(startingWhitespacePosition + 1,endingWhitespacePosition - 1))
end

local function getAvailableCostumeMap(character)
    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
        return nil
    end

    local costumeCountMap = {}
    local userCount = 0

    for _,user in ipairs(onlinePlay.getUsers()) do
        local userData = onlinePlay.getUserData(user.playerIdx)

        if userData ~= nil and userData.installedCostumes[character] ~= nil then
            for _,costumeName in ipairs(userData.installedCostumes[character]) do
                costumeCountMap[costumeName] = (costumeCountMap[costumeName] or 0) + 1
            end

            userCount = userCount + 1
        end
    end

    return costumeCountMap,userCount
end


costumeSelect.menu = battleMenu.createMenu{
    format = {
        hasBackground = true,elementGapY = 12,
        backgroundPriority = -0.1,
        offsetX = 384,offsetY = 96,rotation = 10,scale = 0.75,
        selectionMoveDuration = 5,
    },
    optionFormat = {
        textScale = 2,useLargestWidth = true,
        textAlign = "left",
        boxMarginX = 20,
        boxMarginY = 16,

        --[[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
        end,]]

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

            local rotation = (option.idx - math.lerp(menu.optionIdx,menu.optionIdxFadeStart,menu.optionIdxFade))*8.5
            rotation = math.clamp(rotation,-90,90)

            local position = vector(option.x + option.totalWidth*0.5 + wheelRadius,0):rotate(rotation)

            return position.x - wheelRadius - 320,position.y,rotation,1
        end,

        updateMainLayoutFunc = function(option)
            local data = option.data

            local textFmt,maxWidth = option:getTextFormat()

            data.nameLayout = textplus.layout(formatCostumeName(data.character,data.costumeName),256,textFmt)

            data.headImage = loadHeadImage(data.character,data.costumeName,data.costumePath)

            data.frameData = loadCharacterFrame(data.character,POWERUP_BIG,-1,data.costumePath)

            option.mainWidth = data.nameLayout.width + costumeSelect.headGap
            option.mainHeight = data.nameLayout.height
        end,
        drawMainLayoutFunc = function(option,mainX,mainY,mainColor)
            local data = option.data

            mainX = option.format.boxMarginX

            Graphics.drawBox{
                texture = data.headImage,target = battleMenu.optionBuffer,priority = option.menu.format.priority,
                x = math.floor(mainX + math.max(0,costumeSelect.headGap*0.5 - data.headImage.width)*0.5),
                y = math.floor(mainY + (option.mainHeight - data.headImage.height)*0.5),
            }

            textplus.render{
                layout = data.nameLayout,target = battleMenu.optionBuffer,priority = option.menu.format.priority,
                x = math.floor(mainX + costumeSelect.headGap),
                y = math.floor(mainY + 2),
                color = mainColor,
            }
        end,
    },
}


local function selectCostume(menu,costumeName)
    local p = Player(menu.args.playerIdx)

    if p.character == menu.args.character and costumeName ~= perPlayerCostumes.getCostume(menu.args.playerIdx,menu.args.character) then
        if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE and p.idx == onlinePlay.playerIdx then
            costumeChangeCommand:send(0)
        end
    
        Effect.spawn(10,p.x + p.width*0.5 - 16,p.y + p.height*0.5 - 16)
	    SFX.play(34)
    end

    perPlayerCostumes.setCostume(menu.args.playerIdx,menu.args.character,costumeName)
    
    battleMenu.closeAll()
end


costumeSelect.menu.openFunc = function(menu)
    local costumeCountMap,userCount = getAvailableCostumeMap(menu.args.character)

    menu:addOption{
        data = {character = menu.args.character},
        runFunction = function()
            selectCostume(menu,nil)
        end,
    }

    for _,costumeData in ipairs(getCostumeList(menu.args.character)) do
        -- Does everyone have this costume installed? If not, it should be unselectable
        local upperCostumeName = costumeData.name:upper()
        
        local unselectable = false
        local descriptionText

        if costumeCountMap ~= nil and (costumeCountMap[upperCostumeName] == nil or costumeCountMap[upperCostumeName] < userCount) then
            descriptionText = textFiles.costumeSelect.unavailableDescription
            unselectable = true
        else
            descriptionText = loadCostumeDescription(costumeData.path)
        end

        -- Create the option
        local costumeOption = menu:addOption{
            data = {character = menu.args.character,costumeName = costumeData.name,costumePath = costumeData.path},
            unselectable = unselectable,descriptionText = descriptionText,
            runFunction = function()
                selectCostume(menu,upperCostumeName)
            end,
        }

        -- Start out on this costume if it's already selected
        if upperCostumeName == perPlayerCostumes.getCostume(menu.args.playerIdx,menu.args.character) then
            menu.optionIdx = costumeOption.optionIdx
        end
    end

    activeFrameDisplays = {
        {
            optionIdx = menu.optionIdx,
            transitionTimer = 1,
            leaving = false,
        },
    }
end

costumeSelect.menu.changeOptionFunc = function(menu)
    for _,display in ipairs(activeFrameDisplays) do
        display.leaving = true
    end

    table.insert(activeFrameDisplays,{
        optionIdx = menu.optionIdx,
        transitionTimer = 1,
        leaving = false,
    })
end


function costumeChangeCommand.onReceive(sourcePlayerIdx)
    local p = Player(sourcePlayerIdx)

    if onlinePlayPlayers.canMakeSound(p) then
        SFX.play(34)
    end

    Effect.spawn(10,p.x + p.width*0.5 - 16,p.y + p.height*0.5 - 16)
end


local backSilhouetteBuffer = Graphics.CaptureBuffer()

local solidColorShader,greyToAlphaShader


function costumeSelect.onInputUpdate()
    if costumeSelect.menu.isActive then
        local i = 1

        while (activeFrameDisplays[i] ~= nil) do
            local display = activeFrameDisplays[i]

            if display.leaving then
                display.transitionTimer = display.transitionTimer - (#activeFrameDisplays - 1)/12
            else
                display.transitionTimer = math.max(0,display.transitionTimer - 1/12)
            end

            if display.transitionTimer <= -1 then
                table.remove(activeFrameDisplays,i)
            else
                i = i + 1
            end
        end
    end
end

function costumeSelect.onCameraDraw(camIdx)
    local menu = costumeSelect.menu

    if menu.isActive then
        local cam = Camera(camIdx)
        local screenWidth,screenHeight = battleGeneral.getScreenSize()

        local priority = menu.format.priority - 0.05

        local frameWidth = 100
        local frameHeight = 100

        local centreX = screenWidth*0.5 + 208
        local centreY = screenHeight*0.5

        local mainScale = 2
        local backScale = 12


        if solidColorShader == nil then
            -- Compile shaders if we haven't already
            solidColorShader = Shader.fromFile(nil,"resources/solidColor.frag")
            greyToAlphaShader = Shader.fromFile(nil,"resources/greyToAlpha.frag")
        end


        Graphics.drawScreen{
            color = Color.black,target = backSilhouetteBuffer,priority = -100,
        }

        for _,display in ipairs(activeFrameDisplays) do
            local frameData = menu.options[display.optionIdx].data.frameData

            local offsetX = easing.inQuad(math.abs(display.transitionTimer),0,48*math.sign(display.transitionTimer),1)
            local mainOpacity = (1 - math.abs(display.transitionTimer))^2
            local opacity = menu.color.a*mainOpacity

            -- Back silhouette
            Graphics.drawBox{
                texture = frameData.image,target = backSilhouetteBuffer,priority = -100,additive = true,
                color = Color.white*(1 - math.abs(display.transitionTimer)),

                x = centreX - frameData.offset.x*backScale,
                y = centreY - frameData.offset.y*backScale + 64,

                sourceX = frameData.frameX*frameWidth,
                sourceY = frameData.frameY*frameHeight,
                sourceWidth = frameWidth,
                sourceHeight = frameHeight,

                width = frameWidth*backScale,
                height = frameHeight*backScale,

                shader = solidColorShader,uniforms = {
                    color = Color.white,
                    extremity = 1,
                },
            }

            -- Drop shadow
            Graphics.drawBox{
                texture = frameData.image,priority = priority,
                color = menu.color.. opacity*0.5,

                x = centreX - frameData.offset.x*mainScale + 8 + offsetX,
                y = centreY - frameData.offset.y*mainScale + 8,

                sourceX = frameData.frameX*frameWidth,
                sourceY = frameData.frameY*frameHeight,
                sourceWidth = frameWidth,
                sourceHeight = frameHeight,

                width = frameWidth*mainScale,
                height = frameHeight*mainScale,

                shader = solidColorShader,uniforms = {
                    color = Color(0.1,0.1,0.1),
                    extremity = 1,
                },
            }

            -- Main image
            Graphics.drawBox{
                texture = frameData.image,priority = priority,
                color = menu.color.. opacity,

                x = centreX - frameData.offset.x*mainScale + offsetX,
                y = centreY - frameData.offset.y*mainScale,

                sourceX = frameData.frameX*frameWidth,
                sourceY = frameData.frameY*frameHeight,
                sourceWidth = frameWidth,
                sourceHeight = frameHeight,

                width = frameWidth*mainScale,
                height = frameHeight*mainScale,
            }
        end

        -- Back silhouettes
        Graphics.drawScreen{
            texture = backSilhouetteBuffer,priority = priority - 0.01,
            color = characterColors[menu.args.character].. menu.color.a*0.25,

            shader = greyToAlphaShader,
        }
    end
end


function costumeSelect.onInitAPI()
    registerEvent(costumeSelect,"onInputUpdate")
    registerEvent(costumeSelect,"onCameraDraw")
end


return costumeSelect