local maskCollection,enemies,pathfinding

local tilemap = {}


tilemap.antiSpawnAreaBlockID = 753


tilemap.gridSize = 32

tilemap.spawnSpotConsiderWidth = 8
tilemap.spawnSpotConsiderHeight = 3


tilemap.grid = {}
tilemap.blockGrid = {}
tilemap.gridOriginX = 0
tilemap.gridOriginY = 0
tilemap.gridWidth = 0
tilemap.gridHeight = 0

tilemap.possibleSpawnSpots = {}
tilemap.antiSpawnAreaGrid = {}
tilemap.lankySpawnSpots = {}


local math_min     = math.min
local math_max     = math.max
local math_sin     = math.sin
local math_cos     = math.cos
local math_atan    = math.atan
local math_abs     = math.abs
local math_floor   = math.floor
local math_ceil    = math.ceil
local math_clamp   = math.clamp
local math_invlerp = math.invlerp
local math_huge    = math.huge


local sectionBounds


local function getBlockTop(block, x,width)
    if Block.SLOPE_LR_FLOOR_MAP[block.id] then
        local progress = math_clamp(1 - (x + width - block.x)/block.width,0,1)

        return block.y + block.height*progress
    end

    if Block.SLOPE_RL_FLOOR_MAP[block.id] then
        local progress = math_clamp((x - block.x)/block.width,0,1)

        return block.y + block.height*progress
    end

    return block.y
end


function tilemap.cellIsFull(gridX,gridY)
    if gridX < 1 or gridY < 1 or gridX > tilemap.gridWidth or gridY > tilemap.gridHeight then
        return false
    end

    return tilemap.grid[gridX][gridY]
end

function tilemap.cellIsSlope(gridX,gridY)
    if not tilemap.cellIsFull(gridX,gridY) then
        return false
    end

    local block = tilemap.blockGrid[gridX][gridY]

    return (block ~= nil and block.isValid and Block.SLOPE_MAP[block.id])
end

function tilemap.cellIsSemisolid(gridX,gridY)
    if not tilemap.cellIsFull(gridX,gridY) then
        return false
    end

    local block = tilemap.blockGrid[gridX][gridY]

    return (block ~= nil and block.isValid and Block.SEMISOLID_MAP[block.id])
end

function tilemap.getCellBlock(gridX,gridY)
    if tilemap.cellIsFull(gridX,gridY) then
        return tilemap.blockGrid[gridX][gridY]
    end

    return nil
end

function tilemap.gridAreaIsEmpty(minGridX,minGridY,maxGridX,maxGridY,includeSemisolids)
    for gridX = math_max(1,minGridX),math_min(tilemap.gridWidth,maxGridX) do
        for gridY = math_max(1,minGridY),math_min(tilemap.gridHeight,maxGridY) do
            if tilemap.grid[gridX][gridY] and (includeSemisolids or not tilemap.cellIsSemisolid(gridX,gridY)) then
                return false
            end
        end
    end

    return true
end

function tilemap.gridAreaIsFull(minGridX,minGridY,maxGridX,maxGridY,includeSemisolids)
    for gridX = math_max(1,minGridX),math_min(tilemap.gridWidth,maxGridX) do
        for gridY = math_max(1,minGridY),math_min(tilemap.gridHeight,maxGridY) do
            if not (tilemap.grid[gridX][gridY] and (includeSemisolids or not tilemap.cellIsSemisolid(gridX,gridY))) then
                return false
            end
        end
    end

    return true
end


function tilemap.getTouchingSolidBlock(collider)
    local aabb = Colliders.getAABB(collider)

    local minGridX,minGridY = tilemap.convertWorldPositionToGrid(aabb.x,aabb.y)
    local maxGridX,maxGridY = tilemap.convertWorldPositionToGrid(aabb.x + aabb.width,aabb.y + aabb.height)

    for gridX = math_max(1,minGridX),math_min(tilemap.gridWidth,maxGridX) do
        for gridY = math_max(1,minGridY),math_min(tilemap.gridHeight,maxGridY) do
            if tilemap.grid[gridX][gridY] then
                local block = tilemap.blockGrid[gridX][gridY]

                if Block.SOLID_MAP[block.id] and Colliders.collide(collider,block) then
                    return block
                end
            end
        end
    end

    return nil
end


function tilemap.areaIsEmpty(minX,minY,maxX,maxY,includeSemisolids)
    local minGridX,minGridY = tilemap.convertWorldPositionToGrid(minX,minY)
    local maxGridX,maxGridY = tilemap.convertWorldPositionToGrid(maxX,maxY)

    return tilemap.gridAreaIsEmpty(minGridX,minGridY,maxGridX,maxGridY,includeSemisolids)
end

function tilemap.areaIsFull(minX,minY,maxX,maxY,includeSemisolids)
    local minGridX,minGridY = tilemap.convertWorldPositionToGrid(minX,minY)
    local maxGridX,maxGridY = tilemap.convertWorldPositionToGrid(maxX,maxY)

    return tilemap.gridAreaIsFull(minGridX,minGridY,maxGridX,maxGridY,includeSemisolids)
end


function tilemap.snapDown(x,y)
    local gridX,gridY = tilemap.convertWorldPositionToGrid(x,y)

    for checkGridY = math_max(1,gridY),tilemap.gridHeight do
        if tilemap.cellIsFull(gridX,checkGridY) then
            local block = tilemap.blockGrid[gridX][checkGridY]
            local config = Block.config[block.id]

            local topY = getBlockTop(block, x,0)

            if topY >= y then
                return topY
            end

            --return (checkGridY - 1)*tilemap.gridSize + tilemap.gridOriginY
        end
    end

    return nil
end

function tilemap.boxCastDown(x,width, startY,destY, includeSemisolids)
    local minGridX = math_floor((x - tilemap.gridOriginX)/tilemap.gridSize) + 1
    local maxGridX = math_floor((x + width - tilemap.gridOriginX)/tilemap.gridSize) + 1

    local minGridY = math_floor((startY - tilemap.gridOriginY)/tilemap.gridSize) + 1
    local maxGridY = math_floor((destY - tilemap.gridOriginY)/tilemap.gridSize) + 1

    for checkGridY = math_max(1,minGridY),math_min(tilemap.gridHeight,maxGridY) do
        local highestHitY

        for checkGridX = minGridX,maxGridX do
            if tilemap.cellIsFull(checkGridX,checkGridY) then
                local block = tilemap.blockGrid[checkGridX][checkGridY]
                local config = Block.config[block.id]

                local topY = getBlockTop(block, x,width)

                if (includeSemisolids or not config.semisolid) and topY >= startY and (highestHitY == nil or topY < highestHitY) then
                    highestHitY = topY
                end
            end
        end

        if highestHitY ~= nil then
            return highestHitY
        end
    end

    return nil
end

function tilemap.lineCast(startX,startY,destX,destY)
    local dx = destX - startX
    local dy = destY - startY

    local startGridX,startGridY = tilemap.convertWorldPositionToGrid(startX,startY)
    local destGridX,destGridY = tilemap.convertWorldPositionToGrid(destX,destY)

    local gridDiffX = destGridX - startGridX
    local gridDiffY = destGridY - startGridY
    local stepCount = math_max(math_abs(gridDiffX),math_abs(gridDiffY))*4 + 1

    for stepIndex = 1,stepCount do
        local checkGridX = startGridX
        local checkGridY = startGridY

        if stepCount > 1 then
            local t = (stepIndex - 1)/(stepCount - 1)

            checkGridX = startGridX + math_floor(t*gridDiffX + 0.5)
            checkGridY = startGridY + math_floor(t*gridDiffY + 0.5)
        end
 
        --local checkX = startX + dx*t
        --local checkY = startY + dy*t

        --local checkGridX,checkGridY = tilemap.convertWorldPositionToGrid(checkX,checkY)

        if tilemap.cellIsFull(checkGridX,checkGridY) then
            local block = tilemap.blockGrid[checkGridX][checkGridY]
            local config = Block.config[block.id]

            -- Vertical hits
            if config.floorslope ~= 0 then
                -- Hitting the top edge of a slope
                local blockSlope = block.height/block.width*config.floorslope       
                local lineSlope = dy/dx

                local topX = block.x + block.width*(1 - config.floorslope)*0.5
                local topY = block.y

                local hitX = (dx == 0 and startX) or ((topY - blockSlope*topX) - (startY - lineSlope*startX))/(lineSlope - blockSlope)

                if hitX >= block.x and hitX <= (block.x + block.width) then
                    local angle = math_atan(blockSlope)
                    local angleSin = math_sin(angle)
                    local angleCos = math_cos(angle)

                    --if vector(destX - topX,destY - topY):rotate(-angleDeg).y >= 0 and vector(startX - topX,startY - topY):rotate(-angleDeg).y <= 0 then
                    if (destY - topY)*angleCos >= (destX - topX)*angleSin and (startY - topY)*angleCos <= (startX - topX)*angleSin then
                        local slopeProgress = (hitX - block.x)/block.width
                        if config.floorslope < 0 then
                            slopeProgress = 1 - slopeProgress
                        end

                        local hitY = block.y + block.height*slopeProgress

                        return true,hitX,hitY,angleSin,-angleCos
                    end
                end
            elseif dy > 0 then
                -- Hitting the top edge
                local edgeY = block.y

                if destY >= edgeY and startY <= edgeY then
                    local edgeHitX = startX + dx*math_invlerp(startY,destY,edgeY)

                    if edgeHitX >= block.x and edgeHitX <= (block.x + block.width) then
                        return true,edgeHitX,edgeY,0,-1
                    end
                end
            end

            if not config.semisolid then
                if config.ceilingslope ~= 0 then
                    -- Hitting the bottom edge of a slope
                    local blockSlope = block.height/block.width*config.ceilingslope                
                    local lineSlope = dy/dx

                    local topX = block.x + block.width*(1 - config.ceilingslope)*0.5
                    local topY = block.y

                    local hitX = (dx == 0 and startX) or ((topY - blockSlope*topX) - (startY - lineSlope*startX))/(lineSlope - blockSlope)

                    if hitX >= block.x and hitX <= (block.x + block.width) then
                        local angle = math_atan(blockSlope)
                        local angleSin = math_sin(angle)
                        local angleCos = math_cos(angle)

                        --if vector(destX - topX,destY - topY):rotate(-angleDeg).y >= 0 and vector(startX - topX,startY - topY):rotate(-angleDeg).y <= 0 then
                        if (destY - topY)*angleCos <= (destX - topX)*angleSin and (startY - topY)*angleCos >= (startX - topX)*angleSin then
                            local slopeProgress = (hitX - block.x)/block.width
                            if config.ceilingslope < 0 then
                                slopeProgress = 1 - slopeProgress
                            end

                            local hitY = block.y + block.height*slopeProgress

                            return true,hitX,hitY,-angleSin,angleCos
                        end
                    end
                elseif dy < 0 then
                    -- Hitting the bottom edge
                    local edgeY = block.y + block.height

                    if destY <= edgeY and startY >= edgeY then
                        local edgeHitX = startX + dx*math_invlerp(startY,destY,edgeY)

                        if edgeHitX >= block.x and edgeHitX <= (block.x + block.width) then
                            return true,edgeHitX,edgeY,0,1
                        end
                    end
                end

                -- Horizontal hits
                if dx > 0 and config.floorslope ~= -1 and config.ceilingslope ~= 1 then
                    -- Hitting the left edge
                    local edgeX = block.x

                    if destX >= edgeX and startX <= edgeX then
                        local edgeHitY = startY + dy*math_invlerp(startX,destX,edgeX)

                        if edgeHitY >= block.y and edgeHitY <= (block.y + block.height) then
                            return true,edgeX,edgeHitY,-1,0
                        end
                    end
                end

                if dx < 0 and config.floorslope ~= 1 and config.ceilingslope ~= -1 then
                    -- Hitting the right edge
                    local edgeX = block.x + block.width

                    if destX <= edgeX and startX >= edgeX then
                        local edgeHitY = startY + dy*math_invlerp(startX,destX,edgeX)

                        if edgeHitY >= block.y and edgeHitY <= (block.y + block.height) then
                            return true,edgeX,edgeHitY,1,0
                        end
                    end
                end
            end
        end
    end

    return false
end


function tilemap.convertWorldPositionToGrid(worldX,worldY)
    return math_floor((worldX - tilemap.gridOriginX)/tilemap.gridSize) + 1,math_floor((worldY - tilemap.gridOriginY)/tilemap.gridSize) + 1
end

function tilemap.convertGridPositionToWorldCentred(gridX,gridY)
    return (gridX - 0.5)*tilemap.gridSize + tilemap.gridOriginX,(gridY - 0.5)*tilemap.gridSize + tilemap.gridOriginY
end


local function cellIsPossibleSpawnSpot(gridX,gridY)
    if tilemap.cellIsFull(gridX,gridY) or not tilemap.cellIsFull(gridX,gridY + 1) then
        return false
    end

    if tilemap.antiSpawnAreaGrid[gridX] ~= nil and tilemap.antiSpawnAreaGrid[gridX][gridY] then
        return false
    end

    if ((gridY - 0.5)*tilemap.gridSize + tilemap.gridOriginY) < sectionBounds.top then
        return false
    end

    return true
end

local function addPossibleSpawnSpot(gridX,gridY)
    local spawnSpot = {}

    spawnSpot.gridX = gridX
    spawnSpot.gridY = gridY

    spawnSpot.x,spawnSpot.y = tilemap.convertGridPositionToWorldCentred(gridX,gridY)
    spawnSpot.y = spawnSpot.y + tilemap.gridSize*0.5

    -- See what height would fit
    spawnSpot.height = 0

    while (
        spawnSpot.height < tilemap.spawnSpotConsiderHeight
        and (not tilemap.cellIsFull(spawnSpot.gridX,spawnSpot.gridY - spawnSpot.height - 1) or tilemap.cellIsSemisolid(spawnSpot.gridX,spawnSpot.gridY - spawnSpot.height - 1))
    ) do
        spawnSpot.height = spawnSpot.height + 1
    end

    -- See what width would fit
    spawnSpot.width = 0

    while (
        spawnSpot.width < tilemap.spawnSpotConsiderWidth
        and tilemap.gridAreaIsEmpty(spawnSpot.gridX - spawnSpot.width - 1,spawnSpot.gridY - spawnSpot.height,spawnSpot.gridX + spawnSpot.width + 1,spawnSpot.gridY,false)
        and tilemap.gridAreaIsFull(spawnSpot.gridX - spawnSpot.width - 1,spawnSpot.gridY + 1,spawnSpot.gridX + spawnSpot.width + 1,spawnSpot.gridY + 1,false)
    ) do
        spawnSpot.width = spawnSpot.width + 1
    end

    table.insert(tilemap.possibleSpawnSpots,spawnSpot)
end

function tilemap.findPossibleSpawnSpots()
    -- Responsible for finding positions in which enemies could possibly spawn. For each, also figures out the widths/heights that would fit in the spot
    tilemap.possibleSpawnSpots = {}

    for gridX = 1,tilemap.gridWidth do
        for gridY = 0,tilemap.gridHeight do
            if cellIsPossibleSpawnSpot(gridX,gridY) then
                addPossibleSpawnSpot(gridX,gridY)
            end
        end
    end

    -- Lanky is very cool and special and gets its own spawn spots
    tilemap.lankySpawnSpots = {}

    for _,bgo in BGO.iterate(enemies.lankyBGOID) do
        if not bgo.isHidden then
            table.insert(tilemap.lankySpawnSpots,{
                x = bgo.x + bgo.width*0.5,
                y = bgo.y + 64,
            })            
        end
    end
end


function tilemap.getFittingSpawnSpots(spawnWidth,spawnHeight)
    local necessarySpotWidth = math_ceil((spawnWidth/tilemap.gridSize - 1)*0.5)
    local necessarySpotHeight = math_ceil(spawnHeight/tilemap.gridSize) - 1

    local viableSpawnSpots = {}

    for _,spawnSpot in ipairs(tilemap.possibleSpawnSpots) do
        if spawnSpot.width >= necessarySpotWidth and spawnSpot.height >= necessarySpotHeight then
            table.insert(viableSpawnSpots,spawnSpot)
        end
    end

    return viableSpawnSpots
end


function tilemap.refresh()
    sectionBounds = Section(maskCollection.section).origBoundary

    -- Collect the blocks that will be relevant
    local relevantBlocks = {}
    local minX = math_huge
    local minY = math_huge
    local maxX = -math_huge
    local maxY = -math_huge

    for _,block in Block.iterateIntersecting(sectionBounds.left - 64,sectionBounds.top - 64,sectionBounds.right + 64,sectionBounds.bottom + 64) do
        if not block.isHidden and (Block.SOLID_MAP[block.id] or Block.SEMISOLID_MAP[block.id]) then
            minX = math_min(minX,block.x)
            minY = math_min(minY,block.y)
            maxX = math_max(maxX,block.x + block.width)
            maxY = math_max(maxY,block.y + block.height)

            table.insert(relevantBlocks,block)
        end
    end

    -- Decide the size and positioning of the grid
    tilemap.gridOriginX = minX
    tilemap.gridOriginY = minY
    tilemap.gridWidth = math_ceil((maxX - minX)/tilemap.gridSize)
    tilemap.gridHeight = math_ceil((maxY - minY)/tilemap.gridSize)

    -- Fill in an empty grid
    tilemap.grid = {}
    tilemap.blockGrid = {}

    for gridX = 1,tilemap.gridWidth do
        tilemap.grid[gridX] = {}
        tilemap.blockGrid[gridX] = {}

        for gridY = 1,tilemap.gridHeight do
            tilemap.grid[gridX][gridY] = false
        end
    end

    -- Set the relevant positions
    for _,block in ipairs(relevantBlocks) do
        local minGridX = math_floor((block.x - tilemap.gridOriginX)/tilemap.gridSize) + 1
        local minGridY = math_floor((block.y - tilemap.gridOriginY)/tilemap.gridSize) + 1
        local maxGridX = math_floor((block.x + block.width - tilemap.gridOriginX)/tilemap.gridSize)
        local maxGridY = math_floor((block.y + block.height - tilemap.gridOriginY)/tilemap.gridSize)

        for gridX = minGridX,maxGridX do
            for gridY = minGridY,maxGridY do
                tilemap.grid[gridX][gridY] = true
                tilemap.blockGrid[gridX][gridY] = block
            end
        end
    end

    -- Set up anti-spawn areas
    tilemap.antiSpawnAreaGrid = {}

    for _,block in Block.iterate(tilemap.antiSpawnAreaBlockID) do
        if not block.isHidden then
            local minGridX = math_floor((block.x - tilemap.gridOriginX)/tilemap.gridSize) + 1
            local minGridY = math_floor((block.y - tilemap.gridOriginY)/tilemap.gridSize) + 1
            local maxGridX = math_floor((block.x + block.width - tilemap.gridOriginX)/tilemap.gridSize)
            local maxGridY = math_floor((block.y + block.height - tilemap.gridOriginY)/tilemap.gridSize)

            for gridX = math_max(1,minGridX),math_min(tilemap.gridWidth,maxGridX) do
                tilemap.antiSpawnAreaGrid[gridX] = tilemap.antiSpawnAreaGrid[gridX] or {}

                for gridY = math_max(1,minGridY),math_min(tilemap.gridHeight,maxGridY) do
                    tilemap.antiSpawnAreaGrid[gridX][gridY] = true
                end
            end
        end
    end

    tilemap.findPossibleSpawnSpots()

    pathfinding.refreshGraph()
end


function tilemap.onStart()
    tilemap.refresh()
end

function tilemap.onDraw()
    for _,spawnSpot in ipairs(tilemap.possibleSpawnSpots) do
        local width = (spawnSpot.width*2 + 1)*tilemap.gridSize
        local height = (spawnSpot.height + 1)*tilemap.gridSize

        Graphics.drawBox{
            color = Color.green.. 0.5,
            sceneCoords = true,

            x = spawnSpot.x - width*0.5,
            y = spawnSpot.y - height,
            width = width,
            height = height,
        }
    end
end


function tilemap.onInitAPI()
    registerEvent(tilemap,"onStart")
    --registerEvent(tilemap,"onDraw")

    maskCollection = require("maskCollection/scripts/maskCollection")
    enemies = require("maskCollection/scripts/enemies")
    pathfinding = require("maskCollection/scripts/pathfinding")
end


return tilemap