local battleGeneral,battlePlayer,battleHUD,onlinePlay

local paralx2 = require("paralx2")

local battleCamera = {}

local CAMERA_MODE_ADDR = 0x00B25130
local SPLIT_SCREEN_MODE_ADDR = 0x00B25132


battleCamera.SPLIT_MODE = {
    DISABLED = 0,
    VERTICAL = 1,
    HORIZONTAL = 2,
}

battleCamera.useSeamlessWrap = false


battleCamera.splitMode = battleCamera.SPLIT_MODE.DISABLED

battleCamera.hasUpdatedCameras = false

battleCamera.seamlessWrapActive = false
battleCamera.wrapSeamIsVisible = false

battleCamera.onlineFollowedPlayerIdx = 0
battleCamera.isSpectating = false


function battleCamera.isOnScreen(x,y,width,height)
    if x < (camera.x + camera.width) and (x + width) > camera.x and y < (camera.y + camera.height) and (y + height) > camera.y then
        return true
    end

    if camera2.isSplit and x < (camera2.x + camera2.width) and (x + width) > camera2.x and y < (camera2.y + camera2.height) and (y + height) > camera2.y then
        return true
    end

    return false
end

function battleCamera.isSplitScreen()
    return (battleCamera.splitMode ~= battleCamera.SPLIT_MODE.DISABLED)
end


function battleCamera.useHorizontalSeamlessWrap(sectionIdx)
    if not battleCamera.useSeamlessWrap or Player.count() < 2 or battleCamera.isSplitScreen() then
        return false
    end

    local sectionObj = Section(sectionIdx)

    return sectionObj.wrapH and not sectionObj.wrapV
end

function battleCamera.useVerticalSeamlessWrap(sectionIdx)
    if not battleCamera.useSeamlessWrap or Player.count() < 2 or battleCamera.isSplitScreen() then
        return false
    end

    local sectionObj = Section(sectionIdx)

    return sectionObj.wrapV and not sectionObj.wrapH
end


function battleCamera.setCameraSizes()
    local screenWidth,screenHeight = battleGeneral.getScreenSize()

    if battleCamera.splitMode == battleCamera.SPLIT_MODE.VERTICAL then
        camera.width = math.floor(screenWidth*0.5 + 0.5)
        camera.height = screenHeight
        camera.renderX = 0
        camera.renderY = 0

        camera2.width = screenWidth - camera.width
        camera2.height = screenHeight
        camera2.renderX = screenWidth - camera2.width
        camera2.renderY = 0
        camera2.isSplit = true
    elseif battleCamera.splitMode == battleCamera.SPLIT_MODE.HORIZONTAL then
        camera.width = screenWidth
        camera.height = math.floor(screenHeight*0.5 + 0.5)
        camera.renderX = 0
        camera.renderY = 0

        camera2.width = screenWidth
        camera2.height = screenHeight - camera.height
        camera2.renderX = 0
        camera2.renderY = screenHeight - camera2.height
        camera2.isSplit = true
    else
        camera.width = screenWidth
        camera.height = screenHeight
        camera.renderX = 0
        camera.renderY = 0

        camera2.width = screenWidth
        camera2.height = screenHeight
        camera2.renderX = 0
        camera2.renderY = 0
        camera2.isSplit = false
    end
end

function battleCamera.initCameras()
    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE and battleGeneral.gameData.playerCount == 2 and not battleGeneral.disableSplitScreen then
        -- Split screen
        local fullRuleset = battleOptions.getFullRuleset()

        battleCamera.splitMode = fullRuleset.general.splitScreenStyle
    else
        battleCamera.splitMode = battleCamera.SPLIT_MODE.DISABLED
    end

    local firstCameraSection = battleCamera.getCameraSection(1)

    battleCamera.seamlessWrapActive = (battleCamera.useHorizontalSeamlessWrap(firstCameraSection) or battleCamera.useVerticalSeamlessWrap(firstCameraSection))

    if battleCamera.isSplitScreen() or battleCamera.seamlessWrapActive then
        mem(CAMERA_MODE_ADDR,FIELD_WORD,4)
    else
        mem(CAMERA_MODE_ADDR,FIELD_WORD,0)
    end

    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
        battleCamera.onlineFollowedPlayerIdx = 0
    elseif battleCamera.onlineFollowedPlayerIdx == 0 then
        battleCamera.onlineFollowedPlayerIdx = onlinePlay.playerIdx
    end

    mem(SPLIT_SCREEN_MODE_ADDR,FIELD_WORD,0)

    battleCamera.setCameraSizes()
end


function battleCamera.cameraIsFocusedOnPlayer(camIdx,playerIdx)
    if battleCamera.isSplitScreen() then
        return (playerIdx == camIdx)
    end

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        return (playerIdx == battleCamera.onlineFollowedPlayerIdx)
    end

    if battleGeneral.isInHub or battleGeneral.disableSplitScreen then
        return battlePlayer.getPlayerIsActive(Player(playerIdx))
    end

    return (playerIdx == 1)
end

function battleCamera.getCamerasPlayers(camIdx)
    if battleCamera.isSplitScreen() then
        return {Player(camIdx)}
    end

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        return {Player(battleCamera.onlineFollowedPlayerIdx)}
    end

    if battleGeneral.isInHub or battleGeneral.disableSplitScreen then
        return battlePlayer.getActivePlayers()
    end

    return {player}
end

function battleCamera.getCameraSection(camIdx)
    if battleCamera.isSplitScreen() then
        return Player(camIdx).section
    end

    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        return Player(battleCamera.onlineFollowedPlayerIdx).section
    end

    return player.section
end


function battleCamera.onStart()
    battleCamera.initCameras()
    Graphics.activateHud(false)
end


local function getNextActivePlayer(playerIdx)
    local players = battlePlayer.getActivePlayers()

    for _,p in ipairs(players) do
        -- Find first active player that has an index that is higher the provided one 
        if p.idx > playerIdx then
            return p
        end
    end

    -- Wrap back around to the first active player
    return players[1] or player
end

local function getPreviousActivePlayer(playerIdx)
    local players = battlePlayer.getActivePlayers()

    for i = #players,1,-1 do
        -- Find first active player that has an index that is lower the provided one 
        local p = players[i]

        if p.idx < playerIdx then
            return p
        end
    end

    -- Wrap back around to the last active player
    return players[#players] or player
end


function battleCamera.handleSpectating()
    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
        return
    end

    -- Spectating
    local followedPlayer = Player(battleCamera.onlineFollowedPlayerIdx)
    local followedPlayerData = battlePlayer.getPlayerData(followedPlayer)

    if not followedPlayerData.isActive --[[and not battleGeneral.isInHub]] then
        -- Currently player is inactive, find someone else to follow
        local newPlayer

        if battleCamera.isSpectating then
            newPlayer = getNextActivePlayer(followedPlayer.idx)
        else
            newPlayer = battlePlayer.getActivePlayers()[1]
        end

        if newPlayer ~= nil then
            battleCamera.onlineFollowedPlayerIdx = newPlayer.idx
            battleCamera.isSpectating = true
            SFX.play(13)
        end
    elseif battleCamera.isSpectating then
        -- Changing spectated player manually
        local ownPlayer = Player(onlinePlay.playerIdx)
        local ownPlayerData = battlePlayer.getPlayerData(ownPlayer)

        local newPlayer

        if ownPlayerData.rawKeys.right == KEYS_PRESSED then
            newPlayer = getNextActivePlayer(followedPlayer.idx)
        elseif ownPlayerData.rawKeys.left == KEYS_PRESSED then
            newPlayer = getPreviousActivePlayer(followedPlayer.idx)
        end

        if newPlayer ~= nil and newPlayer.idx ~= battleCamera.onlineFollowedPlayerIdx then
            battleCamera.onlineFollowedPlayerIdx = newPlayer.idx
            SFX.play(26)
        end
    end
end

function battleCamera.preventPlayersGoingOffScreen()
    -- Mainly for the hub
    if not battleGeneral.isInHub or onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE or battleCamera.isSplitScreen() or not battleCamera.hasUpdatedCameras then
        return
    end

    for _,p in ipairs(Player.get()) do
        if p.forcedState == FORCEDSTATE_NONE then
            if (p.x + p.width) > (camera.x + camera.width) then
                p.x = camera.x + camera.width - p.width
                p.speedX = -4
            elseif p.x < camera.x then
                p.x = camera.x
                p.speedX = 4
            end
        end
    end
end


function battleCamera.getPlayerFocusPosition(p)
    local data = battlePlayer.getPlayerData(p)

    -- Inside an ice block
    if data.iceBlockNPC ~= nil and data.iceBlockNPC.isValid then
        local iceBlock = data.iceBlockNPC

        if iceBlock.heldIndex > 0 then
            return battleCamera.getPlayerFocusPosition(iceBlock.heldPlayer)
        end

        return iceBlock.x + iceBlock.width*0.5,iceBlock.y + iceBlock.height
    end

    return p.x + p.width*0.5,p.y + p.height
end


local originalPlayerSections = {}
local lastCameraSections = {}

local seamlessWrapFirstCameraBuffer = Graphics.CaptureBuffer()
local seamlessWrapSecondCameraBuffer = Graphics.CaptureBuffer()

local seamlessWrapCompositionLayers = {
    {startPriority = -100,endPriority = -99},
    {startPriority = -99,endPriority = -75},
    {startPriority = -75,endPriority = -60},
    {startPriority = -60,endPriority = -40},
    {startPriority = -40,endPriority = -25},
    {startPriority = -25,endPriority = -0.1},
    {startPriority = -0.1,endPriority = 5},
}

function battleCamera.onCameraUpdate(camIdx)
    local cameraSection = battleCamera.getCameraSection(camIdx)
    local cameraSectionObj = Section(cameraSection)

    local horizontalWrap = battleCamera.useHorizontalSeamlessWrap(cameraSection)
    local verticalWrap = battleCamera.useVerticalSeamlessWrap(cameraSection)

    if battleCamera.seamlessWrapActive ~= (horizontalWrap or verticalWrap) then
        battleCamera.initCameras()
    end

    -- The section displayed by a camera will always be the section of Player(camIdx).
    -- This little hack makes sure that the right section is rendered for this camera.
    originalPlayerSections[camIdx] = Player(camIdx).section
    Player(camIdx).section = cameraSection


    -- Set size of cameras
    -- You'd think that this wouldn't be necessary, but for some reason
    -- cameras sometimes get their sizes reset when refocusing the game window?
    if camIdx == 1 then
        battleCamera.setCameraSizes()
    end

    -- Set camera position
    local cam = Camera(camIdx)

    local focusPlayers = battleCamera.getCamerasPlayers(camIdx)
    local playerCount = #focusPlayers

    local bounds = cameraSectionObj.boundary
    local sumX = 0
    local sumY = 0

    for _,p in ipairs(focusPlayers) do
        local focusX,focusY = battleCamera.getPlayerFocusPosition(p)

        sumX = sumX + focusX
        sumY = sumY + focusY
    end

    cam.x = math.floor(sumX/playerCount - cam.width*0.5 + 0.5)
    cam.y = math.floor(sumY/playerCount - cam.height*0.5 + 0.5)

    if not horizontalWrap then
        cam.x = math.clamp(cam.x,bounds.left,bounds.right - camera.width)
    elseif camIdx == 2 then
        if camera.x < bounds.left then
            battleCamera.wrapSeamIsVisible = true
            cam.x = camera.x - bounds.left + bounds.right
            cam.y = camera.y
            cam.isSplit = true
        elseif (camera.x + camera.width) > bounds.right then
            battleCamera.wrapSeamIsVisible = true
            cam.x = camera.x + bounds.left - bounds.right
            cam.y = camera.y
            cam.isSplit = true
        else
            battleCamera.wrapSeamIsVisible = false
            cam.width = 0
            cam.height = 0
            cam.x = 0
            cam.y = 0
            cam.isSplit = false
        end
    end

    if not verticalWrap then
        cam.y = math.clamp(cam.y,bounds.top,bounds.bottom - camera.height)
    elseif camIdx == 2 then
        if camera.y < bounds.top then
            battleCamera.wrapSeamIsVisible = true
            cam.x = camera.x
            cam.y = camera.y - bounds.top + bounds.bottom
            cam.isSplit = true
        elseif (camera.y + camera.height) > bounds.bottom then
            battleCamera.wrapSeamIsVisible = true
            cam.x = camera.x
            cam.y = camera.y + bounds.top - bounds.bottom
            cam.isSplit = true
        else
            battleCamera.wrapSeamIsVisible = false
            cam.width = 0
            cam.height = 0
            cam.x = 0
            cam.y = 0
            cam.isSplit = false
        end
    end
end


local function shouldDrawHUD(camIdx,cameraSection)
    if battleCamera.useHorizontalSeamlessWrap(cameraSection) or battleCamera.useVerticalSeamlessWrap(cameraSection) then
        if battleCamera.wrapSeamIsVisible then
            return (camIdx == 2)
        else
            return (camIdx == 1)
        end
    end

    return true
end

function battleCamera.onCameraDraw(camIdx)
    battleCamera.hasUpdatedCameras = true
    
    local cam = Camera(camIdx)
    local cameraSection = battleCamera.getCameraSection(camIdx)

    if shouldDrawHUD(camIdx,cameraSection) then
        battleHUD.draw(camIdx)
    end

    -- Split screen divider
    local screenWidth,screenHeight = battleGeneral.getScreenSize()

    if battleCamera.splitMode == battleCamera.SPLIT_MODE.VERTICAL then
        Graphics.drawBox{
            color = Color.black,priority = 1,
            x = screenWidth*0.5 - cam.renderX - 1,
            y = -camera.renderY,
            width = 2,
            height = screenHeight,
        }
    elseif battleCamera.splitMode == battleCamera.SPLIT_MODE.HORIZONTAL then
        Graphics.drawBox{
            color = Color.black,priority = 1,
            x = -camera.renderX,
            y = screenHeight*0.5 - cam.renderY - 1,
            width = screenWidth,
            height = 2,
        }
    end


    if battleCamera.wrapSeamIsVisible then
        for _,layer in ipairs(seamlessWrapCompositionLayers) do
            if layer.buffer == nil then
                layer.buffer = Graphics.CaptureBuffer()
            end

            if camIdx == 1 then
                layer.buffer:clear(-100)
                Graphics.redirectCameraFB(layer.buffer, layer.startPriority,layer.endPriority)
            else
                Graphics.drawScreen{
                    texture = layer.buffer,priority = layer.endPriority,
                }
            end
        end
    end
end



function battleCamera.onDrawEnd()
    for idx = 1,2 do
        if originalPlayerSections[idx] ~= nil then
            Player(idx).section = originalPlayerSections[idx]
            originalPlayerSections[idx] = nil
        end
    end
end


local function wrapPosition(x,y,width,height,section,horizontalWrap,verticalWrap)
    local bounds = Section(section).boundary

    if horizontalWrap then
        x = (x + width*0.5 - bounds.left)%(bounds.right - bounds.left) + bounds.left - width*0.5
    end

    if verticalWrap then
        y = (y + height*0.5 - bounds.top)%(bounds.bottom - bounds.top) + bounds.top - height*0.5
    end

    return x,y
end

local function wrapObjectsInSection(section,horizontalWrap,verticalWrap)
    local bounds = section.boundary

    local x1 = bounds.left - 64
    local y1 = bounds.top - 64
    local x2 = bounds.right + 64
    local y2 = bounds.bottom + 64

    for _,npc in NPC.iterateIntersecting(x1,y1,x2,y2) do
        if npc.section == section.idx and npc.heldIndex == 0 then
            npc.x,npc.y = wrapPosition(npc.x,npc.y,npc.width,npc.height,npc.section,horizontalWrap,verticalWrap)
        end
    end
end

function battleCamera.onTickEnd()
    for _,p in ipairs(Player.get()) do
        local data = battlePlayer.getPlayerData(p)

        local horizontalWrap = battleCamera.useHorizontalSeamlessWrap(p.section)
        local verticalWrap = battleCamera.useVerticalSeamlessWrap(p.section)

        local bounds = player.sectionObj.boundary

        if horizontalWrap then
            local boundsWidth = bounds.right - bounds.left

            if (p.x + p.width*0.5) > bounds.right then
                data.seamlessWrapOffsetX = data.seamlessWrapOffsetX - boundsWidth
                p.x = p.x - boundsWidth
            elseif (p.x + p.width*0.5) < bounds.left then
                data.seamlessWrapOffsetX = data.seamlessWrapOffsetX + boundsWidth
                p.x = p.x + boundsWidth
            end
        end

        if verticalWrap then
            local boundsHeight = bounds.bottom - bounds.top

            if (p.y + p.height*0.5) > bounds.bottom then
                data.seamlessWrapOffsetY = data.seamlessWrapOffsetY - boundsHeight
                p.y = p.y - boundsHeight
            elseif (p.y + p.height*0.5) < bounds.top then
                data.seamlessWrapOffsetY = data.seamlessWrapOffsetY + boundsHeight
                p.y = p.y + boundsHeight
            end
        end
    end

    for _,section in ipairs(Section.getActive()) do
        local horizontalWrap = battleCamera.useHorizontalSeamlessWrap(section.idx)
        local verticalWrap = battleCamera.useVerticalSeamlessWrap(section.idx)

        if horizontalWrap or verticalWrap then
            wrapObjectsInSection(section,horizontalWrap,verticalWrap)
        end
    end
end


local originalParalx2Draw = paralx2.onCameraDraw

function paralx2.onCameraDraw(camIdx)
    if not battleCamera.seamlessWrapActive then
        originalParalx2Draw(camIdx)
        return
    end

    if battleCamera.wrapSeamIsVisible == (camIdx == 1) then
        return
    end

    -- This code adjusts the camera's position and the section's boundaries such that everything looks right for seamless wrap
    local cameraSectionObj = Section(battleCamera.getCameraSection(1))
    local cameraPlayer = battleCamera.getCamerasPlayers(1)[1]

    local cam = Camera(camIdx)

    local playerData = battlePlayer.getPlayerData(cameraPlayer)

    local originalBounds = cameraSectionObj.boundary
    local newBounds = cameraSectionObj.boundary

    local originalX = cam.x
    local originalY = cam.y

    newBounds.left = newBounds.left - cam.width - playerData.seamlessWrapOffsetX
    newBounds.right = newBounds.right + cam.width - playerData.seamlessWrapOffsetX
    newBounds.top = newBounds.top - cam.height - playerData.seamlessWrapOffsetY
    newBounds.bottom = newBounds.bottom + cam.height - playerData.seamlessWrapOffsetY
    cameraSectionObj.boundary = newBounds

    cam.x = camera.x - playerData.seamlessWrapOffsetX
    cam.y = camera.y - playerData.seamlessWrapOffsetY


    originalParalx2Draw(camIdx)


    cam.x = originalX
    cam.y = originalY
    cameraSectionObj.boundary = originalBounds
end


function battleCamera.onInitAPI()
    battleGeneral = require("scripts/battleGeneral")
    battlePlayer = require("scripts/battlePlayer")
    battleHUD = require("scripts/battleHUD")
    battleOptions = require("scripts/battleOptions")
    onlinePlay = require("scripts/onlinePlay")

    registerEvent(battleCamera,"onStart")
    registerEvent(battleCamera,"onTick","handleSpectating")
    registerEvent(battleCamera,"onTick","preventPlayersGoingOffScreen")
    registerEvent(battleCamera,"onTickEnd")
    registerEvent(battleCamera,"onCameraUpdate")
    registerEvent(battleCamera,"onCameraDraw")
    registerEvent(battleCamera,"onDraw")
    registerEvent(battleCamera,"onDrawEnd")
end


return battleCamera