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

local textFiles = require("scripts/textFiles")

local easing = require("ext/easing")

local battleGeneral,battlePlayer,battleMessages
local onlinePlay,onlinePlayPlayers,onlinePlayNPC

local battleBombs = {}


battleBombs.idList = {}
battleBombs.idMap = {}


battleBombs.maxBombs = 9

battleBombs.bagBombCount = 5
battleBombs.bagRandomPowerupMaxChance = 1
battleBombs.bagRandomPowerupMinChance = 0

battleBombs.bagReplaceableIDMap = table.map{
    9,14,34,169,170,264, -- SMB3 powerups
    184,182, -- SMB1 powerups
    249, -- SMB2 mushroom
    185,183,277, -- SMW powerups
    35,191,193, -- Boots
}


battleBombs.damagePoints = 2
battleBombs.killPoints = 2

battleBombs.deathLosePoints = 1


battleBombs.explosionHitInvincibilityTime = 50


battleBombs.unstashSounds = {
    Misc.resolveSoundFile("resources/bombs_unstash_1"),
    Misc.resolveSoundFile("resources/bombs_unstash_2"),
    Misc.resolveSoundFile("resources/bombs_unstash_3"),
    Misc.resolveSoundFile("resources/bombs_unstash_4"),
    Misc.resolveSoundFile("resources/bombs_unstash_5"),
}

battleBombs.stashSound = Misc.resolveSoundFile("resources/bombs_stash")

battleBombs.pointSound = Misc.resolveSoundFile("resources/point")


battleBombs.explosionGradient = Graphics.loadImageResolved("resources/bombs_explosionGradient.png")


battleBombs.lightColorHueOffset = 0
battleBombs.lightColorSaturationFactor = 0.8
battleBombs.lightColorValueFactor = 1

battleBombs.darkColorHueOffset = -0.05
battleBombs.darkColorSaturationFactor = 1
battleBombs.darkColorValueFactor = 0.6


battleBombs.unstashThrowCooldown = 12


battleBombs.linkThrowSound = 75
battleBombs.linkDropSound = 23
battleBombs.linkStashSound = 79

battleBombs.linkThrowSpeedX = 5
battleBombs.linkThrowSpeedY = -6
battleBombs.linkThrowCooldown = 10


battleBombs.stashableNPCIDMap = table.map{134}

battleBombs.bunchNPCID = 0
battleBombs.mainNPCID = 0
battleBombs.bagNPCID = 0


battleBombs.displayedTip = false


battleBombs.explosionEffects = {}


local bombColorationShader = Shader.fromFile(nil,"resources/bombs_coloration.frag")
local solidColorShader = Shader.fromFile(nil,"resources/solidColor.frag")
local explosionShader = Shader.fromFile(nil,"resources/bombs_explosion.frag")


local unstashCommand,bunchThrowCommand,linkThrowCommand
local stashCommand
local bombExplodeCommand,bunchExplodeCommand
local grantPointsCommand

local bombOnlineHandlingConfig


function battleBombs.register(npcID)
	npcManager.registerEvent(npcID, battleBombs, "onTickNPC")
	npcManager.registerEvent(npcID, battleBombs, "onDrawNPC")

    table.insert(battleBombs.idList,npcID)
    battleBombs.idMap[npcID] = true

    onlinePlayNPC.onlineHandlingConfig[npcID] = bombOnlineHandlingConfig
end


function battleBombs.displayTip(p)
    if not battleBombs.displayedTip then
        battleMessages.spawnStatusMessage(textFiles.battleMessages.bombs.tip,Color.lightgrey,160)
        battleBombs.displayedTip = true
    end
end


function battleBombs.shouldDisplayBombs(p)
    if battleGeneral.mode == battleGeneral.gameMode.BOMBS then
        return true
    end

    local data = battlePlayer.getPlayerData(p)

    return (data.bombs > 0)
end

function battleBombs.stashingIsEnabled(p)
    return (battleGeneral.mode == battleGeneral.gameMode.BOMBS)
end

function battleBombs.getChanceFromRandomPowerup(p)
    if battleGeneral.mode ~= battleGeneral.gameMode.BOMBS then
        return 0
    end

    if p == nil then
        return (battleBombs.bagRandomPowerupMaxChance + battleBombs.bagRandomPowerupMinChance)*0.5
    end

    local data = battlePlayer.getPlayerData(p)

    return math.lerp(battleBombs.bagRandomPowerupMaxChance,battleBombs.bagRandomPowerupMinChance,math.min(1,data.bombs/battleBombs.maxBombs))
end


function battleBombs.getBunchOffset(v,bombIndex)
    if bombIndex == 1 then
        return vector.zero2
    end

    return vector(0,-(bombIndex - 1)*16)

    --return vector(((bombIndex % 2)*2 - 1)*4,-(bombIndex - 1)*16)

    --return vector(0,-(bombIndex - 1)*20)
end


local function explodeInternal(v,playerIdx)
    local data = v.data

    if data.hasExploded then
        return
    end

    local mimicConfig = NPC.config[data.mimicID]
    local config = NPC.config[v.id]

    data.playerIdx = playerIdx or data.playerIdx

    data.hasExploded = true -- used to prevent recursion from the bomb exploding itself

    if config.isBunch then
        for bombIndex = 1,data.quantity do
            local offset = battleBombs.getBunchOffset(v,bombIndex)

            Explosion.spawn(v.x + v.width*0.5 + offset.x,v.y + v.height*0.5 + offset.y,mimicConfig.explosionID,Player(data.playerIdx))
        end
    else
        Explosion.spawn(v.x + v.width*0.5,v.y + v.height*0.5,mimicConfig.explosionID,Player(data.playerIdx))
    end

    activePlayerExplosions = {}

    table.insert(battleBombs.explosionEffects,{
		playerIdx = data.playerIdx,
		x = v.x + v.width*0.5,
		y = v.y + v.height*0.5,
		radius = mimicConfig.explosionEffectRadius,
		randomValue = RNG.random(),
		timer = 0,
	})

    onlinePlayNPC.forceKillNPC(v,HARM_TYPE_VANISH)
end

function battleBombs.explode(v,playerIdx)
    local data = v.data

    if data.hasExploded then
        return
    end

    local config = NPC.config[v.id]

    playerIdx = playerIdx or data.playerIdx

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        if config.isBunch then
            bunchExplodeCommand:send(0, playerIdx)
        else
            bombExplodeCommand:send(v,0, playerIdx)
        end
    end

    explodeInternal(v,playerIdx)
end


local function getBlockBottom(block, x,width)
    if Block.SLOPE_LR_CEIL_MAP[block.id] then
        local progress = math.clamp(1 - (x - block.x)/block.width,0,1)

        return block.y + block.height*progress
    end

    if Block.SLOPE_RL_CEIL_MAP[block.id] then
        local progress = math.clamp((x + width - block.x)/block.width,0,1)

        return block.y + block.height*progress
    end

    return block.y + block.height
end

local function blockIsSolid(v,block)
    if block.isHidden or block:mem(0x5A,FIELD_BOOL) then
        return false
    end

    local config = Block.config[block.id]

    if config == nil or config.passthrough then
        return false
    end

    if config.npcfilter < 0 or config.npcfilter == v.id then
        return false
    end

    if config.semisolid or config.sizeable then
        return false
    end

    return true
end

local function boxCastUp(x,y,width,offset,blocks)
    local lowestY = y - offset

    for _,block in ipairs(blocks) do
        local bottom = getBlockBottom(block, x,width)

        if (x + width) >= block.x and x <= (block.x + block.height) and y >= bottom then
            lowestY = math.max(lowestY,bottom)
        end
    end

    return lowestY
end


local function isSamePlayerOrOnSameTeam(indexA,indexB)
    return (indexA == indexB or battlePlayer.playersAreOnSameTeam(indexA,indexB))
end


local function resetCollisionFlags(v)
    v:mem(0x0A,FIELD_WORD,0)
    v:mem(0x0C,FIELD_WORD,0)
    v:mem(0x0E,FIELD_WORD,0)
    v:mem(0x10,FIELD_WORD,0)
    v:mem(0x22,FIELD_WORD,0)
end


local function initialise(v)
	local config = NPC.config[v.id]
	local data = v.data

    if config.isBunch then
        data.mimicID = data.mimicID or battleBombs.mainNPCID
    else
        data.mimicID = v.id
    end

    local mimicConfig = NPC.config[data.mimicID]

    data.fuseTimer = mimicConfig.fuseDuration

    data.playerIdx = data.playerIdx or v.heldIndex
    data.ownerIdx = data.ownerIdx or 0

    data.animationTimer = 0

    data.hasBeenProjectile = v.isProjectile

    data.hasExploded = false

	data.initialised = true
end


local function throwBunch(v)
    local data = v.data

    local mimicConfig = NPC.config[data.mimicID]
    local config = NPC.config[v.id]

    local blocks = Colliders.getColliding{
        a = Colliders.Box(v.x + (v.width - mimicConfig.width)*0.5,v.y + v.height*0.5 - mimicConfig.height*data.quantity,mimicConfig.width,mimicConfig.height*data.quantity),
        btype = Colliders.BLOCK,
        filter = function(block)
            return blockIsSolid(v,block)
        end,
    }

    for bombIndex = 1,data.quantity do
        local offset = battleBombs.getBunchOffset(v,bombIndex)

        if not v.isProjectile then
            offset = vector(0,-(bombIndex - 1)*mimicConfig.height)
        end

        local spawnX = v.x + v.width*0.5 + offset.x
        local spawnY = boxCastUp(spawnX - mimicConfig.width*0.5,v.y + (v.height - mimicConfig.height)*0.5,mimicConfig.width,-offset.y,blocks) + mimicConfig.height*0.5

        local bomb = NPC.spawn(data.mimicID,spawnX,spawnY,v.section,false,true)
        local bombData = bomb.data

        bomb:mem(0x12E,FIELD_WORD,v:mem(0x12E,FIELD_WORD))
        bomb:mem(0x130,FIELD_WORD,v:mem(0x130,FIELD_WORD))
        bomb:mem(0x132,FIELD_WORD,v:mem(0x132,FIELD_WORD))

        bomb.direction = v.direction

        bomb.speedX = v.speedX
        bomb.speedY = v.speedY

        bomb.isProjectile = v.isProjectile

        if data.quantity > 1 and v.isProjectile then
            local angleVariance = config.forwardThrowAngleVariance
            local baseAngle = config.forwardThrowBaseAngle
            local speed = config.forwardThrowSpeed

            if v.speedY <= -8.5 then -- thrown upward
                angleVariance = config.upwardThrowAngleVariance
                baseAngle = config.upwardThrowBaseAngle
                speed = config.upwardThrowSpeed
            end

            local angle = baseAngle + angleVariance*((bombIndex - 1)/(data.quantity - 1))

            local speed = vector(speed,0):rotate(angle)

            bomb.speedX = speed.x*v.direction
            bomb.speedY = speed.y
        end

        initialise(bomb)

        bombData.fuseTimer = data.fuseTimer
        bombData.playerIdx = data.playerIdx
        bombData.ownerIdx = data.ownerIdx

        bombData.animationTimer = data.animationTimer

        if bomb.isProjectile then
            bomb.collisionGroup = "battleBombs_thrownBomb"
        end

        if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
            onlinePlayNPC.claimNPCSpawn(bomb)
        end
    end

    onlinePlayNPC.forceKillNPC(v,HARM_TYPE_VANISH)

    -- Send the command
    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        bunchThrowCommand:send(0)
    end
end


function battleBombs.onTickNPC(v)
	if Defines.levelFreeze then return end

	local data = v.data

	if v.despawnTimer <= 0 then
		data.initialised = false
		return
	end

	local config = NPC.config[v.id]

	if not data.initialised then
		initialise(v)
	end

    local mimicConfig = NPC.config[data.mimicID]

    if v.heldIndex ~= 0 then
        if v.heldIndex > 0 then
            data.playerIdx = v.heldIndex
        else
            data.playerIdx = 0
        end

        data.hasBeenProjectile = v.isProjectile

        -- Reset collision flags (really weird that this isn't done usually?)
        resetCollisionFlags(v)
    elseif not config.isBunch then
        data.hasBeenProjectile = data.hasBeenProjectile or v.isProjectile

        -- Explode on contact with solids
        if onlinePlayNPC.ownsNPC(v) and data.hasBeenProjectile and (v.collidesBlockBottom or v.collidesBlockUp or v.collidesBlockLeft or v.collidesBlockRight or v:mem(0x22,FIELD_WORD) ~= 0) then
            battleBombs.explode(v)
            return
        end

        -- Explode on contact with players
        if v.isProjectile then
            for _,p in ipairs(Player.getIntersecting(v.x,v.y,v.x + v.width,v.y + v.height)) do
                if p.forcedState == FORCEDSTATE_NONE
                and not isSamePlayerOrOnSameTeam(p.idx,data.playerIdx)
                and not isSamePlayerOrOnSameTeam(p.idx,v:mem(0x130,FIELD_WORD))
                and not isSamePlayerOrOnSameTeam(p.idx,v:mem(0x132,FIELD_WORD))
                and Misc.canCollideWith(v,p)
                then
                    if onlinePlayNPC.ownsNPC(v) or onlinePlayPlayers.ownsPlayer(p) then
                        battleBombs.explode(v)
                    else
                        explodeInternal(v)
                    end

                    return
                end
            end
        end
    end

    -- Fuse time
	data.fuseTimer = math.max(0,data.fuseTimer - 1)

    if data.fuseTimer == 0 and (onlinePlayNPC.ownsNPC(v) or (config.isBunch and data.playerIdx == onlinePlay.playerIdx)) then
        battleBombs.explode(v)
    end

    -- Animation
    data.animationTimer = data.animationTimer + 1

    -- Throwing a bunch
    if config.isBunch then
        if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or data.playerIdx == onlinePlay.playerIdx then
            -- Handle being thrown
            if v.heldIndex == 0 then
                throwBunch(v)
            end
        else
            -- Keep the holder hanging on tight! No butter fingers!
            if v.heldIndex > 0 and v.heldIndex ~= onlinePlay.playerIdx then
                Player(v.heldIndex):mem(0x62,FIELD_WORD,2)
            end
        end
    end
end


local function drawBomb(npcID,x,y,frame,fuseTimer,ownerIdx,priority)
    local image = Graphics.sprites.npc[npcID].img
    local config = NPC.config[npcID]

    local x = x + config.gfxoffsetx
    local y = y - config.gfxheight*0.5 + config.gfxoffsety

    local sourceY = frame*config.gfxheight

    local baseColor = battlePlayer.getColor(ownerIdx)
    local baseHue,baseSaturation,baseValue = baseColor:toHSV()

    local flashColor = config.flashColor
    local flashValue = math.abs(math.sin(fuseTimer*0.075*math.pi))*math.clamp(1 - (fuseTimer - (config.flashStartTime - config.flashFadeTime))/config.flashFadeTime)*config.flashExtremity

    local lightColor = Color.fromHSV(
        baseHue + battleBombs.lightColorHueOffset,
        baseSaturation*battleBombs.lightColorSaturationFactor,
        baseValue*battleBombs.lightColorValueFactor
    ):lerp(flashColor,flashValue)

    local darkColor = Color.fromHSV(
        baseHue + battleBombs.darkColorHueOffset,
        baseSaturation*battleBombs.darkColorSaturationFactor,
        baseValue*battleBombs.darkColorValueFactor
    ):lerp(flashColor,flashValue)

    Graphics.drawBox{
        texture = image,priority = priority,
        sceneCoords = true,centred = true,

        x = x,
        y = y,

        sourceWidth = config.gfxwidth,
        sourceHeight = config.gfxheight,
        sourceY = sourceY,

        shader = solidColorShader,uniforms = {
            color = flashColor,
            extremity = flashValue,
        },
    }

    Graphics.drawBox{
        texture = image,priority = priority,
        sceneCoords = true,centred = true,

        x = x,
        y = y,

        sourceWidth = config.gfxwidth,
        sourceHeight = config.gfxheight,
        sourceX = config.gfxwidth,
        sourceY = sourceY,

        shader = bombColorationShader,uniforms = {
            lightColor = lightColor,
            darkColor = darkColor,
        },
    }
end

function battleBombs.onDrawNPC(v)
	if v.despawnTimer <= 0 then
		return
	end

	local config = NPC.config[v.id]
	local data = v.data

	if not data.initialised then
		initialise(v)
	end
    
    local mimicConfig = NPC.config[data.mimicID]

    -- Animation
    v.animationFrame = math.floor(data.animationTimer/mimicConfig.framespeed)%config.frames

    if mimicConfig.framestyle >= 1 and v.direction == DIR_RIGHT then
        v.animationFrame = v.animationFrame + config.frames
    end

    if mimicConfig.framestyle == 2 and (v.heldIndex > 0 or v.isProjectile) then
        v.animationFrame = v.animationFrame + config.frames*2
    end

    if config.isBunch then
        -- Rendering for bunches
        local image = Graphics.sprites.npc[data.mimicID].img

        for bombIndex = data.quantity,1,-1 do
            local offset = battleBombs.getBunchOffset(v,bombIndex)

            drawBomb(data.mimicID,v.x + v.width*0.5 + offset.x,v.y + v.height + offset.y,v.animationFrame,data.fuseTimer,data.ownerIdx,-45)
        end
    else
        -- Rendering for individual bombs
        drawBomb(v.id,v.x + v.width*0.5,v.y + v.height,v.animationFrame,data.fuseTimer,data.ownerIdx,-45)
    end

    npcutils.hideNPC(v)
end


local function isHoldingBunch(p)
    local holdingNPC = p.holdingNPC

    return (holdingNPC ~= nil and battleBombs.idMap[holdingNPC.id] and NPC.config[holdingNPC.id].isBunch)
end

local function canThrowBombs(p)
    if p.forcedState ~= FORCEDSTATE_NONE or p:isClimbing() or p.isTanookiStatue or p.grabTopTimer > 0 then
        return false
    end

    if p.character == CHARACTER_LINK and p:mem(0x160,FIELD_WORD) ~= 0 then
        return false
    end

    if (p.character == CHARACTER_MARIO or p.character == CHARACTER_LUIGI) and p.isDucking then
        return false
    end

    local data = battlePlayer.getPlayerData(p)

    if data.bombs <= 0 then
        return false
    end

    local holdingNPC = p.holdingNPC

    if holdingNPC ~= nil then
        if holdingNPC.id ~= battleBombs.bunchNPCID then
            return false
        end
    end

    return true
end

local function canStashBomb(p)
    if not battleBombs.stashingIsEnabled(p) or p.forcedState ~= FORCEDSTATE_NONE or p.grabTopTimer > 0 then
        return
    end

    local data = battlePlayer.getPlayerData(p)

    if data.bombs >= battleBombs.maxBombs then
        return false
    end

    local holdingNPC = p.holdingNPC

    if holdingNPC == nil then
        return false
    end

    return battleBombs.stashableNPCIDMap[holdingNPC.id]
end


local function unstashBombs(p,newQuantity)
    local data = battlePlayer.getPlayerData(p)
    local holdingNPC = p.holdingNPC

    if not isHoldingBunch(p) then
        -- Drop the currently held NPC
        if holdingNPC ~= nil then
            holdingNPC.heldIndex = 0
        end

        -- Spawn a new bunch NPC to hold
        holdingNPC = NPC.spawn(battleBombs.bunchNPCID,p.x + p.width*0.5,p.y + p.height*0.5,p.section,false,true)
        holdingNPC.data.mimicID = battleBombs.mainNPCID

        holdingNPC.data.playerIdx = p.idx
        holdingNPC.data.ownerIdx = p.idx

        p:mem(0x154,FIELD_WORD,holdingNPC.idx + 1)
        holdingNPC.heldIndex = p.idx

        if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
            onlinePlayNPC.tryMarkAsUnsynchronised(holdingNPC)
        end
    end

    holdingNPC.data.quantity = newQuantity

    if p.powerup == POWERUP_TANOOKI then
        p.tanookiStatueCooldown = math.max(p.tanookiStatueCooldown,battleBombs.unstashThrowCooldown)
    end

    p:mem(0x62,FIELD_WORD,battleBombs.unstashThrowCooldown)

    if onlinePlayPlayers.canMakeSound(p) then
        SFX.play(battleBombs.unstashSounds[math.min(#battleBombs.unstashSounds,holdingNPC.data.quantity)])
    end

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE and onlinePlayPlayers.ownsPlayer(p) then
        unstashCommand:send(0, newQuantity,data.bombs)
    end
end


local function onTickPlayer(p)
    local data = battlePlayer.getPlayerData(p)

    if (data.bombs > 0 or isHoldingBunch(p)) and p.powerup == POWERUP_TANOOKI and not p.isTanookiStatue then
        p.tanookiStatueCooldown = math.max(p.tanookiStatueCooldown,2)
    end

    if onlinePlayPlayers.ownsPlayer(p) and p.keys.altRun == KEYS_PRESSED then
        if canThrowBombs(p) then
            data.bombs = math.max(0,data.bombs - 1)

            if p.character == CHARACTER_LINK then
                local config = NPC.config[battleBombs.mainNPCID]

                local isDrop = (p.isDucking and p:isOnGround())
                local y = (isDrop and (p.y + p.height - config.height*0.5)) or (p.y + config.height*0.5)

                local npc = NPC.spawn(battleBombs.mainNPCID,p.x + p.width*0.5,y,p.section,false,true)

                npc:mem(0x12E,FIELD_WORD,1000)
                npc:mem(0x130,FIELD_WORD,p.idx)
                --npc:mem(0x132,FIELD_WORD,p.idx)

                npc.data.playerIdx = p.idx
                npc.data.ownerIdx = p.idx

                if isDrop then
                    if onlinePlayPlayers.canMakeSound(p) then
                        SFX.play(battleBombs.linkDropSound)
                    end
                else
                    npc.speedX = battleBombs.linkThrowSpeedX*p.direction
                    npc.speedY = battleBombs.linkThrowSpeedY
                    npc.isProjectile = true

                    if p:isOnGround() then
                        p:mem(0x14,FIELD_WORD,-battleBombs.linkThrowCooldown)
                    end

                    if onlinePlayPlayers.canMakeSound(p) then
                        SFX.play(battleBombs.linkThrowSound)
                    end
                end

                p:mem(0x160,FIELD_WORD,battleBombs.linkThrowCooldown)

                if not npc.data.initialised then
                    initialise(npc)
                end

                if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
                    onlinePlayNPC.claimNPCSpawn(npc)
                    linkThrowCommand:send(0, isDrop)
                end
            else
                local currentQuantity = (isHoldingBunch(p) and p.holdingNPC.data.quantity) or 0
                local newQuantity = currentQuantity + 1

                unstashBombs(p,newQuantity)
            end
        elseif canStashBomb(p) then
            local holdingNPC = p.holdingNPC

            if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
                stashCommand:send(0, onlinePlayNPC.getUIDFromNPC(holdingNPC) or -1)
            end

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

            p:mem(0x154,FIELD_WORD,0)
            holdingNPC.heldIndex = 0

            onlinePlayNPC.forceKillNPC(holdingNPC,HARM_TYPE_VANISH)

            data.bombs = math.min(battleBombs.maxBombs,data.bombs + 1)

            if onlinePlayPlayers.canMakeSound(p) then
                SFX.play(battleBombs.stashSound)
            end
        end
    end

    -- Reset Link's bombs
    p:mem(0x08,FIELD_WORD,0)
end

function battleBombs.onTick()
    for _,p in ipairs(Player.get()) do
        onTickPlayer(p)
    end

    -- Update explosions
    local i = 1

	while (battleBombs.explosionEffects[i] ~= nil) do
		local effect = battleBombs.explosionEffects[i]

		effect.timer = effect.timer + 1/16

		if effect.timer >= 1 then
			table.remove(battleBombs.explosionEffects,i)
		else
			i = i + 1
		end
	end
end

function battleBombs.onDraw()    
    -- Draw explosions
    for _,effect in ipairs(battleBombs.explosionEffects) do
		local timer = effect.timer
		local easedTime = easing.outCubic(timer,0,1,1)

		Graphics.drawBox{
			texture = battleBombs.explosionGradient,
			centred = true,sceneCoords = true,
			priority = -5,

			x = effect.x,
			y = effect.y,
			width = effect.radius*2,
			height = effect.radius*2,

			shader = explosionShader,uniforms = {
				noiseTexture = Graphics.sprites.hardcoded["53-1"].img,

				timer = timer,
				easedTime = easedTime/1.01,

				radius = effect.radius*0.5,

				randomValue = effect.randomValue,

				size = vector(effect.radius,effect.radius),

                playerColor = battlePlayer.getColor(effect.playerIdx),
			},
		}
	end
end


local function getLinkSwordHitbox(p)
    -- Based on: https://github.com/smbx/smbx-legacy-source/blob/master/modPlayer.bas#L5030
    if p.character ~= CHARACTER_LINK then
        return nil
    end

    -- Crouching stab
    if p:mem(0x14,FIELD_WORD) > 0 and p.isDucking then
        local swordCollider = Colliders.Box(0,p.y + p.height - 22,38,8)

        if p.direction == DIR_RIGHT then
            swordCollider.x = p.x + p.width
        else
            swordCollider.x = p.x - swordCollider.width
        end

        return swordCollider
    end

    -- Horizontal stab
    if p:mem(0x14,FIELD_WORD) > 0 then
        local swordCollider = Colliders.Box(0,p.y + p.height - 42,38,6)

        if p.direction == DIR_RIGHT then
            swordCollider.x = p.x + p.width
        else
            swordCollider.x = p.x - swordCollider.width
        end

        return swordCollider
    end

    -- Vertical stabs
    if not p:isOnGround()
    and not p:isClimbing()
    and not p.isDucking
    and not p.isTanookiStatue
    and p.mount == MOUNT_NONE
    and p:mem(0x14,FIELD_WORD) == 0
    and p:mem(0x160,FIELD_WORD) == 0
    and not p:mem(0x36,FIELD_BOOL)
    then
        -- Vertical stabs are directly controlled by frame.......
        -- Up stab
        if p.keys.up and p.speedY < 0 and p.frame == 10 then
            local swordCollider = Colliders.Box(0,0,6,14)

            swordCollider.y = p.y - swordCollider.height

            if p.direction == DIR_RIGHT then
                swordCollider.x = p.x + p.width - 4
            else
                swordCollider.x = p.x - swordCollider.width + 4
            end

            return swordCollider
        end

        -- Down stab
        if p.keys.down and p.speedY > 0 and p.frame == 9 then
            local swordCollider = Colliders.Box(p.x + 1,p.y + p.height,p.width - 2,0)

            swordCollider.height = (p.speedY >= 10 and 12) or (p.speedY >= 8 and 10) or 8

            return swordCollider
        end
    end
end

local function getStabbingPlayer(collider)
    for _,p in ipairs(Player.get()) do
        local swordCollider = getLinkSwordHitbox(p)

        if swordCollider ~= nil and swordCollider:collide(collider) then
            return p
        end
    end

    return nil
end


function battleBombs.onNPCHarm(eventObj,v,reason,culprit)
    if battleBombs.stashableNPCIDMap[v.id] and reason == HARM_TYPE_SWORD then
        local p = getStabbingPlayer(v)

        if p ~= nil and onlinePlayPlayers.ownsPlayer(p) then
            if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
                stashCommand:send(0, onlinePlayNPC.getUIDFromNPC(v) or -1)
            end

            local playerData = battlePlayer.getPlayerData(p)

            Effect.spawn(10,v.x + v.width*0.5 - 16,v.y + v.height*0.5 - 16)
            onlinePlayNPC.forceKillNPC(v,HARM_TYPE_VANISH)

            playerData.bombs = math.min(battleBombs.maxBombs,playerData.bombs + 1)
            battleBombs.displayTip(p)

            if onlinePlayPlayers.canMakeSound(p) then
                SFX.play(battleBombs.linkStashSound)
            end
        end

        eventObj.cancelled = true
        return
    end

    if battleBombs.idMap[v.id] then
        if reason == HARM_TYPE_VANISH or reason == HARM_TYPE_LAVA or reason == HARM_TYPE_SPINJUMP then
            return
        end

        local data = v.data

        if not data.initialised then
            initialise(v)
        end

        if reason == HARM_TYPE_TAIL then
            if v.heldIndex ~= 0 then
                eventObj.cancelled = true
                return
            end

            -- Redirect
            local data = v.data

            if type(culprit) == "Player" then
                data.playerIdx = culprit.idx
                v.speedX = 6*math.sign((v.x + v.width*0.5) - (culprit.x + culprit.width*0.5))
            end

            v.speedY = -6

            v.collisionGroup = "battleBombs_thrownBomb"
            v.isProjectile = true

            resetCollisionFlags(v)

            SFX.play(2)

            eventObj.cancelled = true
            return
        end

        -- esplode,
        if onlinePlayNPC.ownsNPC(v) then
            battleBombs.explode(v)
        end

        eventObj.cancelled = true
    end
end


local function grantPoints(p,points)
    local data = battlePlayer.getPlayerData(p)

    if battlePlayer.teamsAreEnabled() then
        local teamIdx = battlePlayer.getTeam(p.idx)

        battlePlayer.teamPoints[teamIdx] = battlePlayer.teamPoints[teamIdx] + points
    else
        data.points = data.points + points
    end

    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or onlinePlayPlayers.ownsPlayer(p) then
        SFX.play(battleBombs.pointSound)
    end

    battlePlayer.addPointEffect(p,points)
    data.pointsHopTimer = 1
end

local function grantPointsFromExplosion(hitPlayer,bomberPlayer)
    local willKill = (hitPlayer.powerup == POWERUP_SMALL and hitPlayer.mount == MOUNT_NONE)
    local points = (willKill and battleBombs.killPoints) or battleBombs.damagePoints

    grantPoints(bomberPlayer,points)

    battleBombs.onExplosionHit(hitPlayer,bomberPlayer)

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        grantPointsCommand:send(0, points,hitPlayer.idx)
    end
end

function battleBombs.onPostExplosion(explosion,bomberPlayer)
    if battleGeneral.mode ~= battleGeneral.gameMode.BOMBS or bomberPlayer == nil or battleMessages.victoryActive then
        return
    end

    -- Grant points if the explosion hit a player
    for _,p in ipairs(Player.get()) do
        if p.idx ~= bomberPlayer.idx
        and p.forcedState == FORCEDSTATE_NONE
        and p.invincibilityTimer == 0
        and not p.hasStarman
        and not battlePlayer.playersAreOnSameTeam(p.idx,bomberPlayer.idx)
        and explosion.collider:collide(p)
        and (onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or p.data._bombInvincibilityEndTime == nil or onlinePlay.localTime >= p.data._bombInvincibilityEndTime)
        then
            if onlinePlayPlayers.ownsPlayer(bomberPlayer) then
                grantPointsFromExplosion(p,bomberPlayer)

                if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
                    p.data._bombInvincibilityEndTime = onlinePlay.localTime + lunatime.toSeconds(battleBombs.explosionHitInvincibilityTime)
                end
            end

            p.invincibilityTimer = battleBombs.explosionHitInvincibilityTime
            SFX.play(76)
        end
    end

    --explosion.collider:debug(true)
end


bombOnlineHandlingConfig = {
    getExtraData = function(v)
        local data = v.data

        if not data.initialised then
            return nil
        end

        return {
            mimicID = data.mimicID,
            fuseTimer = data.fuseTimer,
            playerIdx = data.playerIdx,
            ownerIdx = data.ownerIdx,
        }
    end,
    setExtraData = function(v,receivedData)
        local data = v.data

        if not data.initialised then
            initialise(v)
        end

        data.mimicID = receivedData.mimicID
        data.fuseTimer = receivedData.fuseTimer
        data.playerIdx = receivedData.playerIdx
        data.ownerIdx = receivedData.ownerIdx
    end,
}


function battleBombs.onInitAPI()
    registerEvent(battleBombs,"onTick")
    registerEvent(battleBombs,"onDraw")
    registerEvent(battleBombs,"onTickEnd")
    registerEvent(battleBombs,"onNPCHarm")
    registerEvent(battleBombs,"onPostExplosion")

    registerCustomEvent(battleBombs,"onExplosionHit")


    battleGeneral = require("scripts/battleGeneral")
    battlePlayer = require("scripts/battlePlayer")
    battleMessages = require("scripts/battleMessages")

    onlinePlay = require("scripts/onlinePlay")
    onlinePlayPlayers = require("scripts/onlinePlay_players")
    onlinePlayNPC = require("scripts/onlinePlay_npc")


    -- Lose points when dying
    function battlePlayer.onPostPlayerKillCustom(p)
        if battleGeneral.mode ~= battleGeneral.gameMode.BOMBS or battlePlayer.teamsAreEnabled() then
            return
        end

        -- Lose points
        local data = battlePlayer.getPlayerData(p)

        if data.points > 0 then
            data.points = math.max(0,data.points - battleBombs.deathLosePoints)
            data.pointsShakeFactor = 8
        end
    end

    function battlePlayer.onPlayerRespawn(p)
        if battleGeneral.mode ~= battleGeneral.gameMode.BOMBS or battlePlayer.teamsAreEnabled() then
            return
        end

        -- Display lost points
        battlePlayer.addPointEffect(p,-battleBombs.deathLosePoints)
    end


    -- Prevent damage from explosions
    function battlePlayer.onPlayerHarmCustom(eventObj,p,harmType,causeInfo)
        if battleGeneral.mode ~= battleGeneral.gameMode.BOMBS then
            return
        end

        if causeInfo.playerIdx ~= nil and causeInfo.playerIdx > 0 then
            -- Don't take any damage from player-caused explosions
            if causeInfo.cause == battlePlayer.HARM_CAUSE.EXPLOSION then
                eventObj.cancelled = true
                return
            end

            -- Don't take any damage from the Peach bomb NPC... yes, specifically the NPC, not the explosion
            if causeInfo.cause == battlePlayer.HARM_CAUSE.NPC and causeInfo.npcID == 291 then
                eventObj.cancelled = true
                return
            end
        end
    end


    -- Command for unstashing a bomb
    unstashCommand = onlinePlay.createCommand("battle_bombs_unstash",onlinePlay.IMPORTANCE_MAJOR)

    function unstashCommand.onReceive(sourcePlayerIdx, newQuantity,newBombCount)
        local p = Player(sourcePlayerIdx)
        local data = battlePlayer.getPlayerData(p)

        data.bombs = newBombCount

        unstashBombs(p,newQuantity)
    end

    -- Command for throwing a bunch of bombs
    bunchThrowCommand = onlinePlay.createCommand("battle_bombs_throwBunch",onlinePlay.IMPORTANCE_MAJOR)

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

        if isHoldingBunch(p) then
            onlinePlayNPC.forceKillNPC(p.holdingNPC,HARM_TYPE_VANISH)
        end
    end

    -- Command for Link throwing a bomb
    linkThrowCommand = onlinePlay.createCommand("battle_bombs_linkThrow",onlinePlay.IMPORTANCE_MAJOR)

    function linkThrowCommand.onReceive(sourcePlayerIdx, isDrop)
        local p = Player(sourcePlayerIdx)

        p:mem(0x160,FIELD_WORD,battleBombs.linkThrowCooldown)

        if not isDrop then
            if p:isOnGround() then
                p:mem(0x14,FIELD_WORD,-battleBombs.linkThrowCooldown)
            end
        end

        p:mem(0x160,FIELD_WORD,battleBombs.linkThrowCooldown)

        if onlinePlayPlayers.canMakeSound(p) then
            if isDrop then
                SFX.play(battleBombs.linkDropSound)
            else
                SFX.play(battleBombs.linkThrowSound)
            end
        end
    end

    -- Command for stashing a bomb
    stashCommand = onlinePlay.createCommand("battle_bombs_stash",onlinePlay.IMPORTANCE_MAJOR)

    function stashCommand.onReceive(sourcePlayerIdx, npcUID)
        local p = Player(sourcePlayerIdx)
        local data = battlePlayer.getPlayerData(p)

        -- Make an effect on the NPC
        local npc = onlinePlayNPC.getNPCFromUID(npcUID)

        if npc ~= nil then
            Effect.spawn(10,npc.x + npc.width*0.5 - 16,npc.y + npc.height*0.5 - 16)
        end

        -- Give a bomb back
        data.bombs = math.min(battleBombs.maxBombs,data.bombs + 1)
        battleBombs.displayTip(p)

        -- Play the sound
        if onlinePlayPlayers.canMakeSound(p) then
            if p.character == CHARACTER_LINK then
                SFX.play(battleBombs.linkStashSound)
            else
                SFX.play(battleBombs.stashSound)
            end
        end
    end

    -- Command for a bomb exploding
    bombExplodeCommand = onlinePlayNPC.createNPCCommand("battle_bombs_explode",onlinePlay.IMPORTANCE_MAJOR)

    function bombExplodeCommand.onReceive(npc,sourcePlayerIdx, playerIdx)
        local data = npc.data

        if data.hasExploded then
            return
        end

        if not data.initialised then
            initialise(npc)
        end

        explodeInternal(npc,playerIdx)
    end

    -- Command for a bunch exploding
    bunchExplodeCommand = onlinePlay.createCommand("battle_bombs_bunchExplode",onlinePlay.IMPORTANCE_MAJOR)

    function bunchExplodeCommand.onReceive(sourcePlayerIdx, playerIdx)
        local p = Player(sourcePlayerIdx)

        if isHoldingBunch(p) then
            explodeInternal(p.holdingNPC,playerIdx)
        end
    end

    -- Command to tell another player that they scored points (so generous!)
    -- Not anymore! Command to tell everyone else that WE got points (so greedy!)
    grantPointsCommand = onlinePlay.createCommand("battle_bombs_grantPoints",onlinePlay.IMPORTANCE_MAJOR)

    function grantPointsCommand.onReceive(sourcePlayerIdx, points,hitPlayerIdx)
        if points < 0 then
            return
        end

        if battleGeneral.mode ~= battleGeneral.gameMode.BOMBS then
            return
        end

        local bomberPlayer = Player(sourcePlayerIdx)
        local hitPlayer = Player(hitPlayerIdx)

        grantPoints(bomberPlayer,points)

        battleBombs.onExplosionHit(hitPlayer,bomberPlayer)
    end
end


return battleBombs