local npcManager = require("npcManager")
local npcutils = require("npcs/npcutils")

local textFiles = require("scripts/textFiles")

local battleMessages,battleGeneral,battlePlayer,battleHUD,battleMap
local onlinePlay,onlinePlayNPC

local battleStars = {}



battleStars.spawnTimeMin = lunatime.toTicks(4)
battleStars.spawnTimeMax = lunatime.toTicks(10)
battleStars.spawnTimeStart = lunatime.toTicks(8)

battleStars.statusMessageColor = Color(1,1,0.25)

battleStars.spawnedNPC = nil
battleStars.lastSpawner = nil
battleStars.spawnTimer = 0

battleStars.getSoundObj = nil


battleStars.spawnerBGOID = 951


battleStars.loseSound = Misc.resolveSoundFile("resources/starLose")
battleStars.getSounds = {}

for i = 1,5 do
    battleStars.getSounds[i] = Misc.resolveSoundFile("resources/starGet_".. i)
end


local starCollectedCommand
local starSpawnCommand
local starLoseCommand
local starTradeCommand



battleStars.collectableIDList = {}
battleStars.collectableIDMap  = {}

battleStars.collectableSpawnID = 0
battleStars.droppedSpawnID = 0


function battleStars.registerCollectable(npcID)
	npcManager.registerEvent(npcID, battleStars, "onTickNPC", "onTickCollectable")
    npcManager.registerEvent(npcID, battleStars, "onDrawNPC", "onDrawCollectable")
    
    table.insert(battleStars.collectableIDList,npcID)
    battleStars.collectableIDMap[npcID] = true

    onlinePlayNPC.onlineHandlingConfig[npcID] = {
        useFairCollection = true,
        getExtraData = function(v)
            local data = v.data
            if not data.initialized then
                return nil
            end

            return {
                timer = data.timer,
            }
        end,
        setExtraData = function(v, receivedData)
            local data = v.data
            if not data.initialized then
                return nil
            end

            data.timer = receivedData.timer
        end,
        shouldStealFunc = function(v)
            return false
        end,
    }
end


local function isDoingFairCollection(v)
    local onlineData = onlinePlayNPC.getData(v)

    return onlineData.isDoingFairCollection
end


function battleStars.onTickCollectable(v)
	if Defines.levelFreeze then return end
	
	local data = v.data
	
	if v.despawnTimer <= 0 then
		data.initialized = false
		return
	end

    local config = NPC.config[v.id]

	if not data.initialized then
		data.initialized = true

        data.rotation = 0
        data.timer = 0

        data.sparkleTimer = 0

        data.invisible = false
	end

    -- Movement
	if v:mem(0x12C,FIELD_WORD) > 0 or v:mem(0x138,FIELD_WORD) > 0 or v:mem(0x136,FIELD_BOOL) then
        data.rotation = 0
        data.timer = 0
        data.invisible = false
    elseif config.isDropped then
        -- Flickering/death
        data.timer = data.timer + 1

        if data.timer >= (config.lifetime - config.flickerTime) or v:mem(0x12E,FIELD_WORD) > 0 then
            data.invisible = (data.timer%6 < 3)
        else
            data.invisible = false
        end

        if data.timer >= config.lifetime and not isDoingFairCollection(v) then
            v:kill(HARM_TYPE_VANISH)
        end

        -- Rotation
        data.rotation = data.rotation + v.speedX*config.rotationSpeed

        -- Bouncing
        if v.y >= (v.sectionObj.boundary.bottom + 16) and not v.sectionObj.wrapV then
            v.speedY = config.pitBounceSpeed
        elseif v.collidesBlockBottom then
            v.speedY = RNG.random(config.bounceMinSpeedY,config.bounceMaxSpeedY)
        end
    else
        v.speedY = math.cos(data.timer/16)*0.5
        v.speedX = 0
        data.timer = data.timer + 1

        npcutils.applyLayerMovement(v)
    end

    -- Don't let it despawn
    v.despawnTimer = math.max(100,v.despawnTimer)

    -- Sparkles
    data.sparkleTimer = data.sparkleTimer - 1

    if data.sparkleTimer <= 0 then
        local e = Effect.spawn(80,v.x + RNG.random(0,v.width),v.y + RNG.random(0,v.height))

        e.x = e.x - e.width *0.5
        e.y = e.y - e.height*0.5

        data.sparkleTimer = config.sparkleTime
    end
end

function battleStars.onDrawCollectable(v)
    if v.despawnTimer <= 0 then
        return
    end

    local texture = Graphics.sprites.npc[v.id].img
    local config = NPC.config[v.id]

    local data = v.data

    if data.invisible then
        npcutils.hideNPC(v)
        return
    end

    local priority = -45
    if config.foreground then
        priority = -15
    end

    local opacity = (isDoingFairCollection(v) and 0.5) or 1

    local width = config.gfxwidth
    local height = config.gfxheight

    local x = v.x + v.width*0.5 + config.gfxoffsetx
    local y = v.y + v.height - height*0.5 + config.gfxoffsety

    Graphics.drawBox{
        texture = texture,priority = priority,
        color = Color.white.. opacity,
        centred = true,sceneCoords = true,

        sourceWidth = width,sourceHeight = height,
        sourceY = v.animationFrame*height,

        rotation = data.rotation or 0,
        x = x,y = y,
    }

    npcutils.hideNPC(v)
end


local function playGetSound(index)
    if battleStars.getSoundObj ~= nil and battleStars.getSoundObj:isPlaying() then
        battleStars.getSoundObj:stop()
    end

    battleStars.getSoundObj = SFX.play(battleStars.getSounds[index])
end


local function giveStars(p,amount)
    local data = battlePlayer.getPlayerData(p)
    local max = battleStars.getStarsToWin()

    if max > 0 then
        data.stars = math.min(max,data.stars + amount)

        if data.stars == (max - 1) then
            local name = battlePlayer.getName(p.idx)

            battleMessages.spawnStatusMessage(textFiles.funcs.replace(textFiles.battleMessages.stars.oneAway,{NAME = name}),p.idx)
        end

        if data.stars < max then
            local t = 0
            if max > 1 then
                t = data.stars/max
            end

            local sounds = battleStars.getSounds
            local count = #sounds

            local index = math.clamp(math.floor(t*count + 0.5),1,count)

            playGetSound(index)

            battleGeneral.spawnNumberEffects(951,p.x + p.width*0.5,p.y - 4,data.stars)
        elseif onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
            if battlePlayer.teamsAreEnabled() then
                battleMessages.startVictory(battlePlayer.getTeam(p.idx))
            else
                battleMessages.startVictory(p.idx)
            end
        end
    end

    SFX.play(59)
end


function battleStars.onPostNPCCollect(v,p)
    if not battleStars.collectableIDMap[v.id] then
        return
    end

    local data = battlePlayer.getPlayerData(p)

    local isSpawnedStar = (v == battleStars.spawnedNPC)

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        if onlinePlayNPC.getOwner(v) == onlinePlay.playerIdx then
            starCollectedCommand:send(0, p.idx,data.stars,isSpawnedStar)
        else
            return
        end
    end

    if isSpawnedStar and onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        local name = battlePlayer.getName(p.idx)
        
        battleMessages.spawnStatusMessage(textFiles.funcs.replace(textFiles.battleMessages.stars.get,{NAME = name}),p.idx)
    end

    giveStars(p,1)

    v.spawnId = 0
end

function battleStars.onNPCCollect(eventObj,v,p)
    if not battleStars.collectableIDMap[v.id] then
        return
    end

    if v:mem(0x12E,FIELD_WORD) > 0 then
        eventObj.cancelled = true
    end
end



local function canSpawnStars()
    return (battleGeneral.mode == battleGeneral.gameMode.STARS and not battleMessages.victoryActive and onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT)
end

local function findSpawnPosition()
    local spawners = {}

    for _,v in BGO.iterate(battleStars.spawnerBGOID) do
        if not v.isHidden and battleStars.lastSpawner ~= v then
            table.insert(spawners,v)
        end
    end

    if #spawners == 0 then
        for _,v in BGO.iterate(battleStars.spawnerBGOID) do
            if not v.isHidden then
                return v
            end
        end
    end

    return RNG.irandomEntry(spawners)
end

local function spawnStar()
    local spawner = findSpawnPosition()
    if spawner == nil then
        return
    end

    if battleStars.collectableSpawnID <= 0 then
        return
    end

    local v = NPC.spawn(battleStars.collectableSpawnID,spawner.x + spawner.width*0.5,spawner.y + spawner.height*0.5,nil,false,true)

    v.layerName = spawner.layerName

    v.direction = DIR_LEFT
    v.spawnDirection = v.direction

    battleStars.lastSpawner = spawner
    battleStars.spawnedNPC = v

    -- Spawn an effect
    local e = Effect.spawn(10,v.x + v.width*0.5,v.y + v.height*0.5)

    e.x = e.x - e.width *0.5
    e.y = e.y - e.height*0.5

    -- Send a message 
    if onlinePlay.currentMode == onlinePlay.MODE_HOST then
        local data = onlinePlayNPC.getData(v)

        onlinePlayNPC.tryClaimNPC(v)

        starSpawnCommand:send(0, data.onlineUID)
    end

    battleMessages.spawnStatusMessage(textFiles.battleMessages.stars.spawn,battleStars.statusMessageColor)

    SFX.play(20)
end


function battleStars.onStart()
    battleStars.spawnTimer = battleStars.spawnTimeStart
end

function battleStars.onTick()
    if not canSpawnStars() then
        return
    end

    if battleStars.spawnedNPC ~= nil then
        if not battleStars.spawnedNPC.isValid then
            battleStars.spawnTimer = RNG.randomInt(battleStars.spawnTimeMin,battleStars.spawnTimeMax)
            battleStars.spawnedNPC = nil
        end
    else
        battleStars.spawnTimer = math.max(0,battleStars.spawnTimer - 1)

        if battleStars.spawnTimer <= 0 then
            spawnStar()
        end

        --Text.print(battleStars.spawnTimer,32,32)
    end
end


function battleStars.lose(p,amount)
    if battleStars.droppedSpawnID <= 0 or battleMessages.victoryActive then
        return
    end

    local data = battlePlayer.getPlayerData(p)

    amount = amount or math.max(1,math.floor(data.stars*0.5 + 0.5))
    amount = math.min(data.stars,amount)

    if amount <= 0 then
        return
    end

    local newCount = (data.stars - amount)

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        -- Only allow it if it's us and send a message
        if p.idx == onlinePlay.playerIdx then
            starLoseCommand:send(0, newCount,amount)
        else
            return
        end
    end

    -- Spawn stars
    local offset = RNG.randomInt(0,1)

    for i = 1,amount do
        local v = NPC.spawn(battleStars.droppedSpawnID,p.x + p.width*0.5,p.y + p.height*0.5,p.section,false,true)
        local config = NPC.config[v.id]

        if (i + offset)%2 == 0 then
            v.direction = DIR_LEFT
        else
            v.direction = DIR_RIGHT
        end

        v.speedX = RNG.random(config.droppedMinSpeedX,config.droppedMaxSpeedX)*v.direction
        v.speedY = RNG.random(config.droppedMinSpeedY,config.droppedMaxSpeedY)

        v.spawnDirection = v.direction

        v:mem(0x12E,FIELD_WORD,50)
        v:mem(0x130,FIELD_WORD,p.idx)

        if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
            onlinePlayNPC.tryClaimNPC(v)
        end
    end

    data.lostStarsAmount = data.lostStarsAmount + (data.stars - newCount)
    data.stars = newCount

    SFX.play(battleStars.loseSound)
end

function battleStars.trade(sourcePlayer,inheritorPlayer,amount)
    local sourcePlayerData = battlePlayer.getPlayerData(sourcePlayer)
    local inheritorPlayerData = battlePlayer.getPlayerData(inheritorPlayer)

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE and sourcePlayer.idx ~= onlinePlay.playerIdx then
        return
    end

    amount = math.min(amount or 1,sourcePlayerData.stars)

    if amount <= 0 then
        return
    end

    sourcePlayerData.stars = sourcePlayerData.stars - amount

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        starTradeCommand:send(0, inheritorPlayer.idx,sourcePlayerData.stars,amount)
    end

    giveStars(inheritorPlayer,amount)
end


function battleStars.getStarsToWin()
    local modeRuleset = battleOptions.getModeRuleset()

    return modeRuleset.starsToWin or 0
end



function battleStars.onInitAPI()
    registerEvent(battleStars,"onPostNPCCollect")
    registerEvent(battleStars,"onNPCCollect")

    registerEvent(battleStars,"onStart")
    registerEvent(battleStars,"onTick")

    battleMessages = require("scripts/battleMessages")
    battleGeneral = require("scripts/battleGeneral")
    battlePlayer = require("scripts/battlePlayer")
    battleOptions = require("scripts/battleOptions")
    battleHUD = require("scripts/battleHUD")
    battleMap = require("scripts/battleMap")
    onlinePlay = require("scripts/onlinePlay")
    onlinePlayNPC = require("scripts/onlinePlay_npc")


    starCollectedCommand = onlinePlay.createCommand("battle_stars_collected",onlinePlay.IMPORTANCE_MAJOR)
    starSpawnCommand = onlinePlay.createCommand("battle_stars_spawn",onlinePlay.IMPORTANCE_MAJOR)
    starLoseCommand = onlinePlay.createCommand("battle_stars_lose",onlinePlay.IMPORTANCE_MAJOR)
    starTradeCommand = onlinePlay.createCommand("battle_stars_trade",onlinePlay.IMPORTANCE_MAJOR)
    
    function starCollectedCommand.onReceive(sourcePlayerIdx, playerIdx,starCount,isSpawnedStar)
        local p = Player(playerIdx)
        local data = battlePlayer.getPlayerData(p)

        if isSpawnedStar then
            local name = battlePlayer.getName(p.idx)
            
            battleMessages.spawnStatusMessage(textFiles.funcs.replace(textFiles.battleMessages.stars.get,{NAME = name}),p.idx)
        end

        data.stars = starCount
        giveStars(p,1)
    end

    function starSpawnCommand.onReceive(sourcePlayerIdx, onlineUID)
        if onlinePlay.currentMode == onlinePlay.MODE_HOST or sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        local npc = onlinePlayNPC.getNPCFromUID(onlineUID)

        if npc ~= nil and npc.isValid then
            -- Spawn an effect
            local e = Effect.spawn(10,npc.x + npc.width*0.5,npc.y + npc.height*0.5)
    
            e.x = e.x - e.width *0.5
            e.y = e.y - e.height*0.5

            battleStars.spawnedNPC = npc
        end

        battleMessages.spawnStatusMessage(textFiles.battleMessages.stars.spawn,battleStars.statusMessageColor)

        SFX.play(20)
    end

    function starLoseCommand.onReceive(sourcePlayerIdx, newCount,lostAmount)
        local data = battlePlayer.getPlayerData(Player(sourcePlayerIdx))

        data.stars = newCount

        SFX.play(battleStars.loseSound)
    end

    function starTradeCommand.onReceive(sourcePlayerIdx, inheritorPlayerIdx,starCount,amount)
        local sourcePlayer = Player(sourcePlayerIdx)
        local inheritorPlayer = Player(inheritorPlayerIdx)

        local sourcePlayerData = battlePlayer.getPlayerData(sourcePlayer)
        local inheritorPlayerData = battlePlayer.getPlayerData(inheritorPlayer)

        sourcePlayerData.stars = starCount
        giveStars(inheritorPlayer,amount)
    end


    function battleMap.onMapDraw(sectionIdx,priority,opacity,mapX,mapY,mapWidth,mapHeight)
        local star = battleStars.spawnedNPC

        if star ~= nil and star.isValid and star.section == sectionIdx then
            local relativeX,relativeY = battleMap.getRelativePosition(star.x + star.width*0.5,star.y + star.height*0.5,Section(sectionIdx).boundary)

            Graphics.drawBox{
                texture = battleHUD.starImage,priority = priority + 0.1,centred = true,
                color = battleHUD.starNeutralColor.. battleMap.iconOpacity*opacity,

                x = mapX + mapWidth*relativeX,
                y = mapY + mapHeight*relativeY,
            }
        end
    end
end


return battleStars