local onlinePlay = require("scripts/onlinePlay")
local onlinePlayNPC = require("scripts/onlinePlay_npc")

local bettereffects = require("game/bettereffects")

local maskCollection,waveManager,tilemap,weirdGoop

local enemies = {}


enemies.TIER = {
    BASIC = 1,
    ADVANCED = 2,
    BOSS = 3,
    BIG_BOSS = 4,
}

enemies.tiers = {
    enemies.TIER.BASIC,enemies.TIER.ADVANCED,enemies.TIER.BOSS,enemies.TIER.BIG_BOSS,
}


enemies.mushroomDropID = 9
enemies.mushroomDropChance = 1


enemies.lankyNPCID = 752
enemies.lankyBGOID = 751

enemies.fauxPlayerNormalIDs = {754}
enemies.fauxPlayerBaseID = 754
enemies.fauxPlayerLinkID = 758
enemies.fauxPlayerLinkChance = 1/3


enemies.npcsIDsByTier = {
    [enemies.TIER.BASIC] = {
        1, -- Goomba
        6, -- Koopa
        20, -- Shy Guy
        131, -- Snifit
        165, -- Galoomba
        162, -- Rex
        77, -- Ninji
    },
    [enemies.TIER.ADVANCED] = {
        616, -- Hammer Bro
        311, -- Chargin' Chuck
        --318, -- Puntin' Chuck
        642, -- Spike
    },
    [enemies.TIER.BOSS] = {
        262, -- Mouser
        39, -- Birdo
        15, -- Boom Boom
        267, -- Larry
        280, -- Ludwig
        enemies.fauxPlayerBaseID,
    },
    [enemies.TIER.BIG_BOSS] = {
        86, -- SMB3 Bowser
        201, -- Wart
        enemies.lankyNPCID,
        enemies.lankyNPCID,
    },
}


do
    enemies.npcIDTierMap = {}

    for _,tier in ipairs(enemies.tiers) do
        for _,npcID in ipairs(enemies.npcsIDsByTier[tier]) do
            enemies.npcIDTierMap[npcID] = tier
        end
    end

    for _,npcID in ipairs(enemies.fauxPlayerNormalIDs) do
        enemies.npcIDTierMap[npcID] = enemies.TIER.BOSS
    end

    enemies.npcIDTierMap[enemies.fauxPlayerLinkID] = enemies.TIER.BOSS
end


enemies.tierPower = {
    [enemies.TIER.BASIC] = 1,
    [enemies.TIER.ADVANCED] = 3,
    [enemies.TIER.BOSS] = 5,
    [enemies.TIER.BIG_BOSS] = 7,
}


enemies.config = {}

enemies.deleteEffectIDMap = table.map{
    2,4, -- SMB3 Goomba
    9, -- SMB3 Koopa
    138, -- Mouser
    171,172,175, -- Chucks
    18,66,68, -- Shy guy/Snifit
    84,85,86, -- Rex
    88, -- Galoomba
    49,197, -- Ninji
    29,28, -- Birdo/Eggs
    14, -- Boom Boom
    50, -- SMB3 Bowser
    140, -- Larry
    143, -- Ludwig
    278, -- Spike
    106, -- Wart
    263, -- Hammer Bro
}


enemies.projectileEnemyIDs = {
    6, -- Koopa
    20, -- Shy Guy
    165, -- Galoomba
}

enemies.projectileEnemyIDMap = table.map(enemies.projectileEnemyIDs)


local formationCreateCommand = onlinePlay.createCommand("maskCollection_enemies_formationCreate",onlinePlay.IMPORTANCE_MAJOR)
local formationRemoveCommand = onlinePlay.createCommand("maskCollection_enemies_formationRemove",onlinePlay.IMPORTANCE_MAJOR)

local bossKillCountIncrementCommand = onlinePlay.createCommand("maskCollection_enemies_bossKillCount",onlinePlay.IMPORTANCE_MAJOR)

local formationIDCounter = 0


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 getCollidingLinkSwordPlayer(v)
    for _,p in ipairs(Player.get()) do
        local swordCollider = getLinkSwordHitbox(p)

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

    return nil
end

local function incrementBossKillCount(playerIdx)
    local playerSaveInfo = maskCollection.getPlayerSaveInfo(playerIdx)

    if playerSaveInfo == nil then
        return
    end

    playerSaveInfo.bossKillCount = playerSaveInfo.bossKillCount + 1
end


function enemies.getNecessarySpawnSpace(npcID)
    local enemyConfig = enemies.config[npcID]
    local config = NPC.config[npcID]

    local spawnWidth = (enemyConfig ~= nil and enemyConfig.necessarySpawnWidth) or config.width
    local spawnHeight = (enemyConfig ~= nil and enemyConfig.necessarySpawnHeight) or config.height

    return spawnWidth,spawnHeight
end

function enemies.getNPCMaskCount(npcID)
    local enemyConfig = enemies.config[npcID]

    if enemyConfig ~= nil and enemyConfig.droppedMaskCount ~= nil then
        return enemyConfig.droppedMaskCount
    else
        return 0
    end
end

function enemies.getNPCPower(npcID)
    if enemies.npcIDTierMap[npcID] ~= nil then
        return enemies.tierPower[enemies.npcIDTierMap[npcID]]
    else
        return 0
    end
end


local function getStartFrame(npcID,direction)
    local enemyConfig = enemies.config[npcID]
    local config = NPC.config[npcID]

    if enemyConfig ~= nil and enemyConfig.startFrames ~= nil then
        return enemyConfig.startFrames[direction]
    end

    if config.frames > 0 and config.framestyle >= 1 and direction == DIR_RIGHT then
        return config.frames
    end

    return 0
end


local function createEnemySpawnFormationEffect(formationID,neverRemove,npcID,x,y,direction,finishCallback)
    local enemyConfig = enemies.config[npcID]
    local config = NPC.config[npcID]

    local gfxwidth  = (config.gfxwidth  ~= 0 and config.gfxwidth ) or config.width
    local gfxheight = (config.gfxheight ~= 0 and config.gfxheight) or config.height

    local goopWidth  = enemyConfig.goopWidth  or gfxwidth
    local goopHeight = enemyConfig.goopHeight or gfxheight

    local frame = getStartFrame(npcID,direction)

    weirdGoop.createFormationEffect{
        images = {
            {
                texture = Graphics.sprites.npc[npcID].img,
                x = x + config.gfxoffsetx,
                y = y - gfxheight*0.5 + config.gfxoffsety,
                sourceX = 0,
                sourceY = frame*gfxheight,
                width = gfxwidth,
                height = gfxheight,
            },
        },

        clumps = {
            {
                x = x + config.gfxoffsetx,
                y = y - gfxheight*0.5 + config.gfxoffsety,
                radius = (goopWidth + goopHeight)*0.175,
                eccentricity = (goopWidth/goopHeight - 1)*0.5,
            },
        },

        outlineColor = (enemyConfig ~= nil and enemyConfig.outlineColor) or weirdGoop.OUTLINE_COLOR.YELLOW,

        id = formationID,
        finishCallback = finishCallback,
        neverRemove = neverRemove,
    }
end

function enemies.spawn(npcID,x,y,direction)
    local formationID = formationIDCounter

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        formationCreateCommand:send(0, formationID,npcID,x,y,direction)
    end

    createEnemySpawnFormationEffect(formationID,false,npcID,x,y,direction,function()
        local config = NPC.config[npcID]
        local npc = NPC.spawn(npcID,x,y - config.height*0.5,maskCollection.section,false,true)

        npc.animationFrame = getStartFrame(npcID,direction)

        npc.direction = direction
        npc.spawnDirection = direction

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

            formationRemoveCommand:send(0, formationID)
        end
    end)

    formationIDCounter = formationIDCounter + 1
end


function enemies.onPostNPCHarm(v,reason,culprit)
    if reason == HARM_TYPE_SWORD then
        culprit = getCollidingLinkSwordPlayer(v) or culprit
    end

    local culpritType = type(culprit)

    if culpritType == "Player" then
        v.data._lastPlayerHarmIdx = culprit.idx
    elseif culpritType == "NPC" then
        if culprit:mem(0x132,FIELD_WORD) > 0 then
            v.data._lastPlayerHarmIdx = culprit:mem(0x132,FIELD_WORD)
        end
    end
end

function enemies.onPostNPCKill(v,reason)
    -- Drop masks
    local droppedMaskCount = enemies.getNPCMaskCount(v.id)

    if droppedMaskCount > 0 and maskCollection.gameState == maskCollection.GAME_STATE.WAVE then
        if onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
            local sourceX = v.x + v.width*0.5
            local sourceY = v.y + v.height*0.5
            local baseX = sourceX
            local baseY = sourceY

            local masksX = baseX
            local masksY = tilemap.snapDown(baseX,baseY) or tilemap.snapDown(baseX - 32,baseY) or tilemap.snapDown(baseX + 32,sourceY)

            if masksY == nil then
                baseX = v.spawnX + v.spawnWidth*0.5
                baseY = v.spawnY + v.spawnHeight*0.5

                masksX = baseX
                masksY = tilemap.snapDown(baseX,baseY) or (v.spawnY + v.spawnHeight)
            end

            for maskIndex = 1,droppedMaskCount do
                local indexOffset = maskIndex - (droppedMaskCount + 1)*0.5
                local angle = (droppedMaskCount > 0 and indexOffset*60/((droppedMaskCount - 1)*0.5)) or 0

                local offset = vector(0,-48):rotate(angle)

                local goalX = masksX + offset.x
                local goalY = masksY + offset.y - 48
                local hit,hitPointX,hitPointY,_,_ = tilemap.lineCast(baseX,baseY - 48,goalX,goalY)

                local maskNPC = NPC.spawn(751,sourceX,sourceY,v.section,false,true)

                maskNPC.direction = v.direction

                if hit then
                    maskNPC.data.goalX = hitPointX
                    maskNPC.data.goalY = hitPointY
                else
                    maskNPC.data.goalX = goalX
                    maskNPC.data.goalY = goalY
                end

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

        if v.data._lastPlayerHarmIdx ~= nil and (onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or v.data._lastPlayerHarmIdx == onlinePlay.playerIdx) then
            -- Chance to spawn a mushroom
            local killerPlayer = Player(v.data._lastPlayerHarmIdx)

            if reason ~= HARM_TYPE_VANISH and killerPlayer.powerup == POWERUP_SMALL and RNG.random() < enemies.mushroomDropChance then
                local powerupNPC = NPC.spawn(enemies.mushroomDropID,v.x + v.width*0.5,v.y + v.height*0.5,maskCollection.section,false,true)

                if (killerPlayer.x + killerPlayer.width*0.5) > (powerupNPC.x + powerupNPC.width*0.5) then
                    powerupNPC.direction = DIR_RIGHT
                else
                    powerupNPC.direction = DIR_LEFT
                end

                powerupNPC.speedY = -6
                powerupNPC.isProjectile = true

                powerupNPC:mem(0x12E,FIELD_WORD,30)
                powerupNPC:mem(0x130,FIELD_WORD,killerPlayer.idx)

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

                SFX.play(7)
            end

            -- We got a kill! So increment our count
            if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
                bossKillCountIncrementCommand:send(onlinePlay.hostPlayerIdx)
            end

            incrementBossKillCount(v.data._lastPlayerHarmIdx)
        end

        maskCollection.progressGuide(maskCollection.GUIDE_STATE.BOSSES,maskCollection.GUIDE_STATE.PHANTOS)

        SFX.play(20)
    end

    -- Death goop
    local enemyConfig = enemies.config[v.id]

    if enemyConfig ~= nil and enemyConfig.createDeathGoop and reason ~= HARM_TYPE_VANISH then
        local config = NPC.config[v.id]

        local gfxwidth  = (config.gfxwidth  ~= 0 and config.gfxwidth ) or config.width
        local gfxheight = (config.gfxheight ~= 0 and config.gfxheight) or config.height

        local goopWidth  = enemyConfig.goopWidth  or gfxwidth
        local goopHeight = enemyConfig.goopHeight or gfxheight

        weirdGoop.createDestructionEffect{
            images = {
                {
                    texture = Graphics.sprites.npc[v.id].img,
                    x = v.x + v.width*0.5 + config.gfxoffsetx,
                    y = v.y + v.height - gfxheight*0.5 + config.gfxoffsety,
                    sourceX = 0,
                    sourceY = v.animationFrame*gfxheight,
                    width = gfxwidth,
                    height = gfxheight,
                },
            },

            clumps = {
                {
                    x = v.x + v.width*0.5 + config.gfxoffsetx,
                    y = v.y + v.height - gfxheight*0.5 + config.gfxoffsety,
                    radius = (goopWidth + goopHeight)*0.175,
                    eccentricity = (goopWidth/goopHeight - 1)*0.5,
                },
            },

            speedDirection = ((reason == HARM_TYPE_JUMP or reason == HARM_TYPE_SPINJUMP) and vector(0,1)) or vector(0,-1),
            countFactor = 0.05*((enemyConfig ~= nil and enemyConfig.deathGoopCountMultiplier) or 1),

            outlineColor = enemyConfig.outlineColor or enemies.OUTLINE_COLOR.YELLOW,
        }
    end
end


function enemies.deleteEffects()
    for _,effect in ipairs(Effect.get()) do
        if enemies.deleteEffectIDMap[effect.id] then
            effect.animationFrame = -999
            effect.timer = 0
            effect.x = 0
            effect.y = 0
        end
    end

    for _,effect in ipairs(bettereffects.getEffectObjects()) do
        if enemies.deleteEffectIDMap[effect.id] then
            effect.x = 0
            effect.y = 0
            effect:kill()
        end
    end

    for _,effect in ipairs(bettereffects.getEffectSpawners()) do
        if enemies.deleteEffectIDMap[effect.id] then
            effect:kill()
        end
    end
end


function enemies.initConfigs()
    -- Some regular enemies
    for _,npcID in ipairs{1,6,7,161,20,131,162,163,165,166,77} do
        enemies.config[npcID] = {
            createDeathGoop = true,
            outlineColor = weirdGoop.OUTLINE_COLOR.YELLOW,
        }
    end

    for _,npcID in ipairs{616,311,318,642} do
        enemies.config[npcID] = {
            createDeathGoop = true,
            outlineColor = weirdGoop.OUTLINE_COLOR.PURPLE,
        }
    end

    for _,npcID in ipairs{321,40} do
        enemies.config[npcID] = {
            createDeathGoop = true,
            outlineColor = weirdGoop.OUTLINE_COLOR.CYAN,
            deathGoopCountMultiplier = 0.5,
        }
    end

    -- Proper start frames
    enemies.config[6].startFrames   = {[DIR_LEFT] = 0,[DIR_RIGHT] = 2}
    enemies.config[161].startFrames = {[DIR_LEFT] = 0,[DIR_RIGHT] = 4}
    enemies.config[20].startFrames  = {[DIR_LEFT] = 0,[DIR_RIGHT] = 2}
    enemies.config[131].startFrames = {[DIR_LEFT] = 0,[DIR_RIGHT] = 2}
    enemies.config[162].startFrames = {[DIR_LEFT] = 0,[DIR_RIGHT] = 2}
    enemies.config[163].startFrames = {[DIR_LEFT] = 0,[DIR_RIGHT] = 2}
    enemies.config[165].startFrames = {[DIR_LEFT] = 0,[DIR_RIGHT] = 2}
    enemies.config[77].startFrames  = {[DIR_LEFT] = 0,[DIR_RIGHT] = 2}

    -- Snifit bullet
    enemies.config[133] = {
        createDeathGoop = false,
        outlineColor = weirdGoop.OUTLINE_COLOR.CYAN,
    }

    -- Mouser
    enemies.config[262] = {
        createDeathGoop = true,
        outlineColor = weirdGoop.OUTLINE_COLOR.PINK,

        droppedMaskCount = 3,

        necessarySpawnWidth = 160,

        startFrames = {[DIR_LEFT] = 0,[DIR_RIGHT] = 7},
    }

    -- Birdo
    enemies.config[39] = {
        createDeathGoop = true,
        outlineColor = weirdGoop.OUTLINE_COLOR.PINK,

        droppedMaskCount = 3,

        necessarySpawnWidth = 160,

        startFrames = {[DIR_LEFT] = 0,[DIR_RIGHT] = 5},
    }

    -- Boom Boom
    enemies.config[15] = {
        createDeathGoop = true,
        outlineColor = weirdGoop.OUTLINE_COLOR.PINK,

        droppedMaskCount = 3,

        necessarySpawnWidth = 192,

        startFrames = {[DIR_LEFT] = 0,[DIR_RIGHT] = 5},
    }

    -- SMB3 Bowser
    enemies.config[86] = {
        createDeathGoop = true,
        outlineColor = weirdGoop.OUTLINE_COLOR.PINK,

        droppedMaskCount = 5,

        startFrames = {[DIR_LEFT] = 0,[DIR_RIGHT] = 5},
    }

    -- Larry
    enemies.config[267] = {
        createDeathGoop = true,
        outlineColor = weirdGoop.OUTLINE_COLOR.PINK,

        droppedMaskCount = 3,

        startFrames = {[DIR_LEFT] = 0,[DIR_RIGHT] = 10},
    }

    enemies.config[268] = {
        createDeathGoop = true,
        outlineColor = weirdGoop.OUTLINE_COLOR.PINK,

        droppedMaskCount = 3,
    }

    -- Ludwig
    enemies.config[280] = {
        createDeathGoop = true,
        outlineColor = weirdGoop.OUTLINE_COLOR.PINK,

        droppedMaskCount = 3,

        startFrames = {[DIR_LEFT] = 0,[DIR_RIGHT] = 10},
    }

    enemies.config[281] = {
        createDeathGoop = true,
        outlineColor = weirdGoop.OUTLINE_COLOR.PINK,

        droppedMaskCount = 3,
    }

    -- Wart
    enemies.config[201] = {
        createDeathGoop = true,
        outlineColor = weirdGoop.OUTLINE_COLOR.PINK,

        droppedMaskCount = 5,

        startFrames = {[DIR_LEFT] = 0,[DIR_RIGHT] = 5},
    }
end


local baseBossHealth = {}

function enemies.updateBigBossHealth()
    local multiplier = waveManager.calculateBigBossHealthMultiplier(maskCollection.getActivePlayerInSectionCount())

    for _,npcID in ipairs(enemies.npcsIDsByTier[enemies.TIER.BIG_BOSS]) do
        if baseBossHealth[npcID] == nil then
            baseBossHealth[npcID] = NPC.config[npcID].health
        end

        NPC.config[npcID].health = math.floor(baseBossHealth[npcID]*multiplier + 0.5)
    end
end


function formationCreateCommand.onReceive(sourcePlayerIdx, formationID,npcID,x,y,direction)
    if sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
        return
    end

    createEnemySpawnFormationEffect(formationID,true,npcID,x,y,direction)
end

function formationRemoveCommand.onReceive(sourcePlayerIdx, formationID)
    if sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
        return
    end

    for _,formationEffect in ipairs(weirdGoop.formationEffects) do
        if formationEffect.id == formationID then
            formationEffect.toRemove = true
            return
        end
    end
end

function bossKillCountIncrementCommand.onReceive(sourcePlayerIdx)
    incrementBossKillCount(sourcePlayerIdx)
end


function enemies.onInitAPI()
    registerEvent(enemies,"onPostNPCHarm")
    registerEvent(enemies,"onPostNPCKill")

    registerEvent(enemies,"onTickEnd","deleteEffects")
    registerEvent(enemies,"onDraw","deleteEffects")

    maskCollection = require("maskCollection/scripts/maskCollection")
    waveManager = require("maskCollection/scripts/waveManager")
    tilemap = require("maskCollection/scripts/tilemap")
    weirdGoop = require("maskCollection/scripts/weirdGoop")
    
    enemies.initConfigs()


    function maskCollection.onGameStateChange()
        if maskCollection.gameState == maskCollection.GAME_STATE.WAVE then
            enemies.updateBigBossHealth()
        end
    end
end


return enemies