local textFiles = require("scripts/textFiles")
local textplus = require("textplus")
local easing = require("ext/easing")

local battleGeneral,battleMessages,battlePlayer,battleStone,battlePhanto,battleHUD,onlinePlay,battleMenu

local battleTimer = {}


battleTimer.COUNTDOWN_STATE = {
    INACTIVE = 0,
    FADE_IN = 1,
    SETTLED = 2,
    FADE_OUT = 3,
}

battleTimer.TIME_OPTIONS = {
    DISABLED = 0,
    THREE_MINUTES = 1,
    FOUR_MINUTES = 2,
    FIVE_MINUTES = 3,
    EIGHT_MINUTES = 4,
    TEN_MINUTES = 5,
}

battleTimer.optionTimeValues = {
    [battleTimer.TIME_OPTIONS.THREE_MINUTES] = 3*60,
    [battleTimer.TIME_OPTIONS.FOUR_MINUTES] = 4*60,
    [battleTimer.TIME_OPTIONS.FIVE_MINUTES] = 5*60,
    [battleTimer.TIME_OPTIONS.EIGHT_MINUTES] = 8*60,
    [battleTimer.TIME_OPTIONS.TEN_MINUTES] = 10*60,
}


battleTimer.boxImage = Graphics.loadImageResolved("resources/hud/timerBox.png")
battleTimer.boxWidth = 96
battleTimer.boxHeight = 32
battleTimer.boxOffsetX = 0
battleTimer.boxOffsetY = 32
battleTimer.boxWidthOvertime = 160
battleTimer.boxHeightOvertime = 32

battleTimer.textFont = textplus.loadFont("resources/font/mainFont.ini")
battleTimer.textScale = 2
battleTimer.textOffsetX = 0
battleTimer.textOffsetY = 3
battleTimer.textColorNormal = Color.white
battleTimer.textColorOvertime = Color.fromHexRGB(0xFFB146)
battleTimer.textColorHurry = Color.fromHexRGB(0xEEFC49)
battleTimer.textColorHurryFlash = Color.white

battleTimer.countdownImage = Graphics.loadImageResolved("resources/hud/timerCountdown.png")
battleTimer.countdownStartScale = 20
battleTimer.countdownSettleScale = 8
battleTimer.countdownFadeScale = 0
battleTimer.countdownFadeInTime = 8
battleTimer.countdownFadeOutTime = 8
battleTimer.countdownOpacity = 0.6

battleTimer.secondLength = 64

battleTimer.hurryTime = 60
battleTimer.hurryFlashDuration = 12
battleTimer.countdownTime = 10


battleTimer.doNothingOnTimeUp = false
battleTimer.isHidden = false

battleTimer.isActive = false

battleTimer.secondsLeft = 0
battleTimer.framesLeft = 0

battleTimer.inOvertime = false
battleTimer.overtimeTime = 0

battleTimer.hurryFlashTimer = 0

battleTimer.countdown = {
    state = battleTimer.COUNTDOWN_STATE.INACTIVE,
    stateTimer = 0,

    opacity = 0,
    scale = 0,
}


local hurrySound = Misc.resolveSoundFile("resources/timer_hurry")

local overtimeSound = Misc.resolveSoundFile("resources/overtimeHorn")
local overtimeSoundObj

local timerUpdateCommand
local timerJoinPacket


function battleTimer.initVictoryFunctions()
    -- These functions determine who should win after a time out in a specific mode.
    -- Returning 0 will cause a draw, returning nil will cause overtime to kick in.
    battleTimer.getTimeUpWinnerFuncs = {}
    battleTimer.getTeamTimeUpWinnerFuncs = {}

    -- Classic mode
    battleTimer.getTimeUpWinnerFuncs[battleGeneral.gameMode.ARENA] = function()
        local mostLifeCount = 0
        local candidateIdx

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

            if data.lives > mostLifeCount then
                -- Most lives thus far, write that down
                mostLifeCount = data.lives
                candidateIdx = p.idx
            elseif data.lives == mostLifeCount then
                -- Tied with another player, so neither can win
                candidateIdx = nil
            end
        end

        -- If overtime goes on for long enough, draw
        if candidateIdx == nil and battleTimer.overtimeTime >= 60*battleTimer.secondLength then
            return 0
        end

        return candidateIdx
    end

    battleTimer.getTeamTimeUpWinnerFuncs[battleGeneral.gameMode.ARENA] = function()
        -- Count total number of lives
        local totalLivesCounts = {0,0}

        for _,p in ipairs(battlePlayer.getActivePlayers()) do
            local data = battlePlayer.getPlayerData(p)
            local teamIdx = battlePlayer.getTeam(p.idx)

            if data.lives > 0 and teamIdx > 0 then
                totalLivesCounts[teamIdx] = totalLivesCounts[teamIdx] + data.lives
            end
        end

        -- If one team has more lives than the other, declare a winner
        if totalLivesCounts[1] > totalLivesCounts[2] then
            return 1
        elseif totalLivesCounts[2] > totalLivesCounts[1] then
            return 2
        end

        -- If overtime goes on for long enough, draw
        if battleTimer.overtimeTime >= 60*battleTimer.secondLength then
            return 0
        end

        -- Overtime
        return nil
    end

    -- Star catcher
    battleTimer.getTimeUpWinnerFuncs[battleGeneral.gameMode.STARS] = function()
        local mostStarCount = 0
        local candidateIdx

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

            if data.stars > mostStarCount then
                -- Most stars thus far, write that down
                mostStarCount = data.stars
                candidateIdx = p.idx
            elseif data.stars == mostStarCount then
                -- Tied with another player, so neither can win
                candidateIdx = nil
            end
        end

        -- If overtime goes on for long enough, draw
        if candidateIdx == nil and battleTimer.overtimeTime >= 60*battleTimer.secondLength then
            return 0
        end

        return candidateIdx
    end

    battleTimer.getTeamTimeUpWinnerFuncs[battleGeneral.gameMode.STARS] = function()
        -- Count total number of stars
        local totalStarsCounts = {0,0}

        for _,p in ipairs(battlePlayer.getActivePlayers()) do
            local data = battlePlayer.getPlayerData(p)
            local teamIdx = battlePlayer.getTeam(p.idx)

            if teamIdx > 0 then
                totalStarsCounts[teamIdx] = totalStarsCounts[teamIdx] + data.stars
            end
        end

        -- If one team has more stars than the other, declare a winner
        if totalStarsCounts[1] > totalStarsCounts[2] then
            return 1
        elseif totalStarsCounts[2] > totalStarsCounts[1] then
            return 2
        end

        -- If overtime goes on for long enough, draw
        if battleTimer.overtimeTime >= 60*battleTimer.secondLength then
            return 0
        end

        -- Overtime
        return nil
    end

    -- Stone carrier
    battleTimer.getTimeUpWinnerFuncs[battleGeneral.gameMode.STONE] = function()
        -- Who has the most points?
        local mostPointsCount = 0
        local candidateIdx

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

            if data.points > mostPointsCount then
                -- Most points thus far, write that down
                mostPointsCount = data.points
                candidateIdx = p.idx
            elseif data.points == mostPointsCount then
                -- Tied with another player, so neither can win
                candidateIdx = nil
            end
        end

        -- No clear winner, overtime
        if candidateIdx == nil then
            if battleTimer.overtimeTime >= 60*battleTimer.secondLength then
                return 0
            end

            return nil
        end

        -- If somebody other than the leading player last held the stone, put to overtime
        if battleStone.latestHoldingPlayerIdx > 0 and battleStone.latestHoldingPlayerIdx ~= candidateIdx then
            return nil
        end

        return candidateIdx
    end

    battleTimer.getTeamTimeUpWinnerFuncs[battleGeneral.gameMode.STONE] = function()
        -- If one team has more points than the other, declare a winner
        if battlePlayer.teamPoints[1] > battlePlayer.teamPoints[2] then
            return 1
        elseif battlePlayer.teamPoints[1] < battlePlayer.teamPoints[2] then
            return 2
        end

        -- If overtime goes on for long enough, draw
        if battleTimer.overtimeTime >= 60*battleTimer.secondLength then
            return 0
        end

        -- Overtime
        return nil
    end

    -- Phanto
    battleTimer.getTimeUpWinnerFuncs[battleGeneral.gameMode.PHANTO] = function()
        -- Who has the most points?
        local mostPointsCount = -math.huge
        local candidateIdx

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

            if data.points > mostPointsCount then
                -- Most points thus far, write that down
                mostPointsCount = data.points
                candidateIdx = p.idx
            elseif data.points == mostPointsCount then
                -- Tied with another player, so neither can win
                candidateIdx = nil
            end
        end

        -- No clear winner, overtime
        if candidateIdx == nil then
            if battleTimer.overtimeTime >= 60*battleTimer.secondLength then
                return 0
            end

            return nil
        end

        -- First place is wearing a Phanto - give up to 30 seconds of overtime
        if battlePhanto.isWearingMask(Player(candidateIdx)) and battleTimer.overtimeTime < 30*battleTimer.secondLength then
            return nil
        end

        return candidateIdx
    end

    battleTimer.getTeamTimeUpWinnerFuncs[battleGeneral.gameMode.PHANTO] = function()
        -- If one team has more points than the other, declare a winner
        if battlePlayer.teamPoints[1] > battlePlayer.teamPoints[2] then
            return 1
        elseif battlePlayer.teamPoints[1] < battlePlayer.teamPoints[2] then
            return 2
        end

        -- If overtime goes on for long enough, draw
        if battleTimer.overtimeTime >= 60*battleTimer.secondLength then
            return 0
        end

        -- Overtime
        return nil
    end

    -- Bombs
    battleTimer.getTimeUpWinnerFuncs[battleGeneral.gameMode.BOMBS] = function()
        -- Who has the most points?
        local mostPointsCount = 0
        local candidateIdx

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

            if data.points > mostPointsCount then
                -- Most points thus far, write that down
                mostPointsCount = data.points
                candidateIdx = p.idx
            elseif data.points == mostPointsCount then
                -- Tied with another player, so neither can win
                candidateIdx = nil
            end
        end

        -- No clear winner, overtime
        if candidateIdx == nil then
            if battleTimer.overtimeTime >= 60*battleTimer.secondLength then
                return 0
            end

            return nil
        end

        return candidateIdx
    end

    battleTimer.getTeamTimeUpWinnerFuncs[battleGeneral.gameMode.BOMBS] = function()
        -- If one team has more points than the other, declare a winner
        if battlePlayer.teamPoints[1] > battlePlayer.teamPoints[2] then
            return 1
        elseif battlePlayer.teamPoints[1] < battlePlayer.teamPoints[2] then
            return 2
        end

        -- If overtime goes on for long enough, draw
        if battleTimer.overtimeTime >= 60*battleTimer.secondLength then
            return 0
        end

        -- Overtime
        return nil
    end
end


local function getTimeUpWinner()
    local func

    if battlePlayer.teamsAreEnabled() then
        func = battleTimer.getTeamTimeUpWinnerFuncs[battleGeneral.mode]
    else
        func = battleTimer.getTimeUpWinnerFuncs[battleGeneral.mode]
    end

    if func == nil then
        return 0
    end

    return func()
end


function battleTimer.set(time,isFrames)
    if not isFrames then
        time = time*battleTimer.secondLength
    end

    battleTimer.framesLeft = time
    battleTimer.secondsLeft = math.ceil(battleTimer.framesLeft/battleTimer.secondLength)

    battleTimer.isActive = true

    if onlinePlay.currentMode == onlinePlay.MODE_HOST then
        timerUpdateCommand:send(0, battleTimer.secondsLeft,battleTimer.inOvertime)
    end

    battleTimer.countdown:activate()
end


local textBuffer = Graphics.CaptureBuffer(128,32)

function battleTimer.draw(camIdx)
    if not battleTimer.isActive or battleTimer.isHidden then
        return
    end

    local screenWidth,screenHeight = battleGeneral.getScreenSize()
    local cam = Camera(camIdx)

    local boxWidth = battleTimer.boxWidth
    local boxHeight = battleTimer.boxHeight

    local boxX = math.floor(screenWidth*0.5 + battleTimer.boxOffsetX - cam.renderX + 0.5)
    local boxY = math.floor(battleTimer.boxOffsetY - cam.renderY + 0.5)

    local textX = boxX + battleTimer.textOffsetX
    local textY = boxY + battleTimer.textOffsetY
    local textScale = 1
    local textColor,text

    if battleTimer.inOvertime then
        text = textFiles.hud.timerOvertime
        textColor = battleTimer.textColorOvertime

        boxWidth = battleTimer.boxWidthOvertime
        boxHeight = battleTimer.boxHeightOvertime

        textY = textY - math.abs(math.sin(lunatime.tick()/8))*6 + 2
    else
        local minutes = math.floor(battleTimer.secondsLeft/60)
        local seconds = battleTimer.secondsLeft % 60

        text = string.format("%d:%.2d",minutes,seconds)

        if battleTimer.secondsLeft <= battleTimer.hurryTime then
            local hurryHighlightValue = battleTimer.hurryFlashTimer
            hurryHighlightValue = math.min(hurryHighlightValue,1 - hurryHighlightValue)*2

            textColor = battleTimer.textColorHurry:lerp(battleTimer.textColorHurryFlash,hurryHighlightValue)
            textScale = 1 + hurryHighlightValue*0.1
        else
            textColor = battleTimer.textColorNormal
        end
    end

    textX = math.floor(textX + 0.5)
    textY = math.floor(textY + 0.5)

    -- Draw box
    battleMenu.drawSegmentedBox{
        texture = battleTimer.boxImage,priority = battleHUD.priority,
        color = textColor.. battleHUD.opacity,
        
        width = boxWidth,height = boxHeight,
        x = boxX - boxWidth*0.5,
        y = boxY - boxHeight*0.5,
    }

    -- Draw text
    textplus.print{
        text = text,font = battleTimer.textFont,smooth = false,
        xscale = battleTimer.textScale*textScale,yscale = battleTimer.textScale*textScale,

        color = textColor*battleHUD.opacity,priority = battleHUD.priority,
        x = textX,y = textY,pivot = vector(0.5,0.5),
    }
end


function battleTimer.onStart()
    local modeRuleset = battleOptions.getModeRuleset()

    if modeRuleset.timeLimit ~= nil and modeRuleset.timeLimit ~= battleTimer.TIME_OPTIONS.DISABLED then
        battleTimer.set(battleTimer.optionTimeValues[modeRuleset.timeLimit])
    end
end

function battleTimer.onTick()
    if not battleTimer.isActive then
        return
    end


    battleTimer.hurryFlashTimer = math.max(0,battleTimer.hurryFlashTimer - 1/battleTimer.hurryFlashDuration)


    if onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
        -- Decrement timer
        if not battleMessages.victoryActive then
            battleTimer.framesLeft = math.max(0,battleTimer.framesLeft - 1)
        end

        if battleTimer.framesLeft <= 0 and not battleMessages.victoryActive and not battleTimer.doNothingOnTimeUp then
            -- Who should win?
            local winnerIdx = getTimeUpWinner()

            if winnerIdx ~= nil then
                battleMessages.startVictory(winnerIdx)
            else
                battleTimer.inOvertime = true
            end
        end

        -- Update number of seconds
        local newSeconds = math.ceil(battleTimer.framesLeft/battleTimer.secondLength)
        
        if battleTimer.secondsLeft ~= newSeconds then
            if onlinePlay.currentMode == onlinePlay.MODE_HOST then
                timerUpdateCommand:send(0, newSeconds,battleTimer.inOvertime)
            end

            if newSeconds <= battleTimer.hurryTime then
                if battleTimer.secondsLeft > battleTimer.hurryTime and not battleTimer.isHidden then
                    SFX.play(hurrySound)
                end

                battleTimer.hurryFlashTimer = 1
            end

            battleTimer.secondsLeft = newSeconds

            battleTimer.countdown:activate()
        end
    end

    -- Overtime sound
    if battleTimer.inOvertime and not battleMessages.victoryActive and not battleTimer.isHidden then
        if overtimeSoundObj == nil or not overtimeSoundObj:isPlaying() then
            overtimeSoundObj = SFX.play(overtimeSound)
        end
    else
        if overtimeSoundObj ~= nil and overtimeSoundObj:isPlaying() then
            overtimeSoundObj:stop()
        end
    end

    -- Overime time
    if battleTimer.inOvertime then
        battleTimer.overtimeTime = battleTimer.overtimeTime + 1
    else
        battleTimer.overtimeTime = 0
    end

    battleTimer.countdown:update()
end


function battleTimer.onCameraDraw(camIdx)
    battleTimer.countdown:draw(camIdx)
end


function battleTimer.countdown:activate()
    if battleTimer.secondsLeft > battleTimer.countdownTime then
        return
    end

    self.state = battleTimer.COUNTDOWN_STATE.FADE_IN
    self.stateTimer = 0

    self.opacity = 0
    self.scale = battleTimer.countdownStartScale
end

function battleTimer.countdown:update()
    if self.state == battleTimer.COUNTDOWN_STATE.FADE_IN then
        self.stateTimer = math.min(1,self.stateTimer + 1/battleTimer.countdownFadeInTime)
        self.opacity = self.stateTimer
        self.scale = easing.outQuad(self.stateTimer,battleTimer.countdownStartScale,battleTimer.countdownSettleScale - battleTimer.countdownStartScale,1)

        if self.stateTimer >= 1 then
            self.state = battleTimer.COUNTDOWN_STATE.SETTLED
            self.stateTimer = 0
        end
    elseif self.state == battleTimer.COUNTDOWN_STATE.SETTLED then
        self.stateTimer = self.stateTimer + 1

        if self.stateTimer >= (battleTimer.secondLength - battleTimer.countdownFadeInTime - battleTimer.countdownFadeOutTime) then
            self.state = battleTimer.COUNTDOWN_STATE.FADE_OUT
            self.stateTimer = 0
        end
    elseif self.state == battleTimer.COUNTDOWN_STATE.FADE_OUT then
        self.stateTimer = math.min(1,self.stateTimer + 1/battleTimer.countdownFadeOutTime)
        self.opacity = 1 - self.stateTimer
        self.scale = easing.inQuad(self.stateTimer,battleTimer.countdownSettleScale,battleTimer.countdownFadeScale - battleTimer.countdownSettleScale,1)

        if self.stateTimer >= 1 then
            self.state = battleTimer.COUNTDOWN_STATE.INACTIVE
            self.stateTimer = 0
        end
    end 
end

function battleTimer.countdown:draw(camIdx)
    if self.opacity <= 0 or battleTimer.isHidden then
        return
    end

    local cam = Camera(camIdx)

    local screenWidth,screenHeight = battleGeneral.getScreenSize()

    local image = battleTimer.countdownImage
    local width = image.width
    local height = image.height/battleTimer.countdownTime

    Graphics.drawBox{
        texture = image,priority = battleHUD.priority + 0.1,
        color = Color.white.. battleTimer.countdownOpacity*self.opacity,
        centred = true,

        x = screenWidth*0.5 - cam.renderX,
        y = screenHeight*0.5 - cam.renderY,
        width = math.floor(width*self.scale*0.5)*2,
        height = math.floor(height*self.scale*0.5)*2,
        sourceWidth = width,
        sourceHeight = height,
        sourceY = (battleTimer.secondsLeft - 1)*height,
    }
end


function battleTimer.onInitAPI()
    battleGeneral = require("scripts/battleGeneral")
    battleMessages = require("scripts/battleMessages")
    battlePlayer = require("scripts/battlePlayer")
    battleStone = require("scripts/battleStone")
    battlePhanto = require("scripts/battlePhanto")
    battleHUD = require("scripts/battleHUD")
    onlinePlay = require("scripts/onlinePlay")
    battleMenu = require("scripts/battleMenu")
    battleOptions = require("scripts/battleOptions")

    battleTimer.initVictoryFunctions()


    registerEvent(battleTimer,"onStart")
    registerEvent(battleTimer,"onTick")
    registerEvent(battleTimer,"onCameraDraw")


    timerUpdateCommand = onlinePlay.createCommand("battle_timer_update",onlinePlay.IMPORTANCE_MINOR)

    function timerUpdateCommand.onReceive(sourcePlayerIdx, newSeconds,inOvertime)
        if onlinePlay.currentMode == onlinePlay.MODE_HOST or sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        if newSeconds <= battleTimer.hurryTime then
            if battleTimer.secondsLeft > battleTimer.hurryTime and not battleTimer.isHidden then
                SFX.play(hurrySound)
            end

            battleTimer.hurryFlashTimer = 1
        end

        battleTimer.secondsLeft = newSeconds
        battleTimer.framesLeft = newSeconds*battleTimer.secondLength

        battleTimer.inOvertime = inOvertime

        battleTimer.isActive = true

        battleTimer.countdown:activate()
    end


    timerJoinPacket = onlinePlay.createJoinPacket("battle_timer",2)

    function timerJoinPacket.encodeData(targetPlayerIdx)
        if battleTimer.isActive then
            return battleTimer.secondsLeft,battleTimer.inOvertime
        else
            return nil
        end
    end

    function timerJoinPacket.onReceive(secondsLeft,inOvertime)
        if secondsLeft ~= nil then
            battleTimer.secondsLeft = secondsLeft
            battleTimer.framesLeft = secondsLeft*battleTimer.secondLength

            battleTimer.inOvertime = inOvertime

            battleTimer.isActive = true
        else
            battleTimer.isActive = false
        end

        battleTimer.hurryFlashTimer = 0

        battleTimer.countdown:activate()
    end
end


return battleTimer