local battleCamera = require("scripts/battleCamera")
local battleGeneral = require("scripts/battleGeneral")
local battleMap = require("scripts/battleMap")

local blockRespawning = require("scripts/blockRespawning")

local blockutils = require("blocks/blockutils")
local lib3d = require("lib3d")

local basic3DWorld = {}


lib3d.camera.active = false
lib3d.backfaceCulling = false


local SIDE_FRONT  = 0
local SIDE_LEFT   = 1
local SIDE_RIGHT  = 2
local SIDE_TOP    = 3
local SIDE_BOTTOM = 4
local sideList = {SIDE_FRONT,SIDE_LEFT,SIDE_RIGHT,SIDE_TOP,SIDE_BOTTOM}


local IMPORT_SETTINGS = {scale = 32}

local blockData = {
    [3] = {},
    [9] = {}, -- sand
    [7] = { -- Grass block
        textures = {
            [SIDE_TOP] = Graphics.loadImageResolved("block-7-top.png"),
            [SIDE_BOTTOM] = Graphics.loadImageResolved("block-3.png"),
        },
    },
    [277] = { -- Taiga grass block
        textures = {
            [SIDE_TOP] = Graphics.loadImageResolved("block-277-top.png"),
            [SIDE_BOTTOM] = Graphics.loadImageResolved("block-3.png"),
        },
    },

    [1111] = { -- Grass decoration
        models = {
            {lib3d.loadMesh(Misc.resolveFile("block-1111.obj"),IMPORT_SETTINGS)},
        },

        alpha = lib3d.macro.ALPHA_CUTOFF,
    },

    [596] = { -- sandstone
        textures = {
            [SIDE_TOP] = Graphics.loadImageResolved("block-596-top.png"),
            [SIDE_BOTTOM] = Graphics.loadImageResolved("block-596-bottom.png"),
        },
    },

    [598] = { -- cactus
        models = {
            {lib3d.loadMesh(Misc.resolveFile("block-598.obj"),IMPORT_SETTINGS),Graphics.loadImageResolved("block-598-main.png")},
        },

        alpha = lib3d.macro.ALPHA_CUTOFF,
    },

    [11] = { -- fence post
        models = {
            {lib3d.loadMesh(Misc.resolveFile("block-11.obj"),IMPORT_SETTINGS),Graphics.loadImageResolved("block-16.png")},
        },
    },

    [10] = { -- spruce doors
        models = {
            {lib3d.loadMesh(Misc.resolveFile("block-10-top.obj"),IMPORT_SETTINGS),Graphics.loadImageResolved("block-10-top.png")},
            {lib3d.loadMesh(Misc.resolveFile("block-10-bottom.obj"),IMPORT_SETTINGS),Graphics.loadImageResolved("block-10-bottom.png")},
        },
    },

    [14] = { -- smithing table
        textures = {
            [SIDE_TOP] = Graphics.loadImageResolved("block-14-top.png"),
            [SIDE_BOTTOM] = Graphics.loadImageResolved("block-14-bottom.png"),
            [SIDE_LEFT] = Graphics.loadImageResolved("block-14-side.png"),
            [SIDE_RIGHT] = Graphics.loadImageResolved("block-14-side.png"),
        },
    },
    [163] = { -- crafting table
        textures = {
            [SIDE_TOP] = Graphics.loadImageResolved("block-163-top.png"),
            [SIDE_BOTTOM] = Graphics.loadImageResolved("block-16.png"),
            [SIDE_LEFT] = Graphics.loadImageResolved("block-163-side.png"),
            [SIDE_RIGHT] = Graphics.loadImageResolved("block-163-side.png"),
        },
    },
    [165] = { -- furnace
        textures = {
            [SIDE_LEFT] = Graphics.loadImageResolved("block-165-side.png"),
            [SIDE_RIGHT] = Graphics.loadImageResolved("block-165-side.png"),
            [SIDE_TOP] = Graphics.loadImageResolved("block-165-top.png"),
            [SIDE_BOTTOM] = Graphics.loadImageResolved("block-165-top.png"),
        },
    },
    [166] = { -- smoker
        textures = {
            [SIDE_LEFT] = Graphics.loadImageResolved("block-166-side.png"),
            [SIDE_RIGHT] = Graphics.loadImageResolved("block-166-side.png"),
            [SIDE_TOP] = Graphics.loadImageResolved("block-166-top.png"),
            [SIDE_BOTTOM] = Graphics.loadImageResolved("block-166-bottom.png"),
        },
    },
    [167] = { -- blast furnace
        textures = {
            [SIDE_LEFT] = Graphics.loadImageResolved("block-167-side.png"),
            [SIDE_RIGHT] = Graphics.loadImageResolved("block-167-side.png"),
            [SIDE_TOP] = Graphics.loadImageResolved("block-167-top.png"),
            [SIDE_BOTTOM] = Graphics.loadImageResolved("block-167-top.png"),
        },
    },
    [164] = { -- enchanting table
        models = {
            {lib3d.loadMesh(Misc.resolveFile("block-164.obj"),IMPORT_SETTINGS),Graphics.loadImageResolved("block-164-main.png")},
        },
    },
    [162] = { -- bookshelf
        textures = {
            [SIDE_TOP] = Graphics.loadImageResolved("block-16.png"),
            [SIDE_BOTTOM] = Graphics.loadImageResolved("block-16.png"),
        },
    },

    -- Logs
    [6] = {
        textures = {
            [SIDE_TOP] = Graphics.loadImageResolved("block-6-top.png"),
            [SIDE_BOTTOM] = Graphics.loadImageResolved("block-6-top.png"),
        },
    },
    [274] = {
        textures = {
            [SIDE_LEFT] = Graphics.loadImageResolved("block-6-top.png"),
            [SIDE_RIGHT] = Graphics.loadImageResolved("block-6-top.png"),
        },
    },
    [276] = {
        textures = {
            [SIDE_FRONT] = Graphics.loadImageResolved("block-6-top.png"),
        },
    },

    [19] = {
        textures = {
            [SIDE_TOP] = Graphics.loadImageResolved("block-19-top.png"),
            [SIDE_BOTTOM] = Graphics.loadImageResolved("block-19-top.png"),
        },
    },
    [20] = {
        textures = {
            [SIDE_LEFT] = Graphics.loadImageResolved("block-19-top.png"),
            [SIDE_RIGHT] = Graphics.loadImageResolved("block-19-top.png"),
        },
    },
    [279] = {
        textures = {
            [SIDE_FRONT] = Graphics.loadImageResolved("block-19-top.png"),
        },
    },


    [15] = {
        alpha = lib3d.macro.ALPHA_CUTOFF,
    },
    [278] = {
        alpha = lib3d.macro.ALPHA_CUTOFF,
    },
    [16] = {},
    [17] = {},
    [18] = {},
    [275] = {},
    [128] = {},
    [595] = {},
    [127] = {},
    [129] = {},
    [597] = {},
    [12] = {},
    [13] = {},
    [285] = {},
    [286] = {},
    [1116] = {},
    [1117] = {},
    [1118] = {},
}

local blockIDs = {}


local tilemap = {}
local visibleSideMap = {}
local tileMinZ = -2
local tileMaxZ = 5
local tileSize = 32


local sidePositioning = {
    [SIDE_FRONT]  = {offsetX = 0 ,offsetY = 0 ,offsetZ = -1,rotation = vector.quat(0,0,0)},
    [SIDE_LEFT]   = {offsetX = -1,offsetY = 0 ,offsetZ = 0 ,rotation = vector.quat(0,90,0)},
    [SIDE_RIGHT]  = {offsetX = 1 ,offsetY = 0 ,offsetZ = 0 ,rotation = vector.quat(0,-90,0)},
    [SIDE_TOP]    = {offsetX = 0 ,offsetY = -1,offsetZ = 0 ,rotation = vector.quat(-90,0,0)},
    [SIDE_BOTTOM] = {offsetX = 0 ,offsetY = 1 ,offsetZ = 0 ,rotation = vector.quat(90,0,0)},
}


local cameraObjects = {}

local totalMeshes = {}
local meshCount = 0

local quadMeshes = {}
local quadCount = 0

local customMeshes = {}
local customMeshesRenderIDList = {}

local cameraViewMargin = 64


local justChangedAreas = {}


function basic3DWorld.useSimpleMode()
    return battleGeneral.fancyEffectsDisabled()
end

local function get3DCamera(camIdx)
    local cameraObj = cameraObjects[camIdx]

    if cameraObj == nil then
        cameraObj = lib3d.Camera{renderscale = 0.5,nearclip = 1}
        cameraObjects[camIdx] = cameraObj
    end

    return cameraObj
end

local function getTile(x,y,z)
    local map = tilemap[x]
    if map == nil then
        return nil
    end

    map = map[y]
    if map == nil then
        return
    end

    return map[z]
end


local function blockIsVisible(b,tileZ)
    if not b.isValid or b:mem(0x5A,FIELD_BOOL) then
        return false
    elseif tileZ == 0 and b.isHidden then
        return false
    end

    return true
end

local function canShowSide(ownData,tileX,tileY,tileZ)
    local b = getTile(tileX,tileY,tileZ)

    if b ~= nil and blockIsVisible(b,tileZ) then
        local data = blockData[b.id]

        return (data.models ~= nil or (data.alpha ~= nil and ownData.alpha == nil))
    else
        return true
    end
end


function basic3DWorld.calculateVisibleSides(tileX,tileY,tileZ)
    local b = getTile(tileX,tileY,tileZ)

    if b == nil then
        return
    end

    visibleSideMap[tileX][tileY][tileZ] = nil

    if not blockIsVisible(b,tileZ) then
        return
    end

    local data = blockData[b.id]
    if data == nil or data.models ~= nil then
        return
    end

    local visibleSides

    for _,side in ipairs(sideList) do
        local positioningData = sidePositioning[side]

        if canShowSide(data,tileX + positioningData.offsetX,tileY + positioningData.offsetY,tileZ + positioningData.offsetZ) then
            if visibleSides == nil then
                visibleSides = {}
                visibleSideMap[tileX][tileY][tileZ] = visibleSides
            end

            table.insert(visibleSides,side)
        end
    end
end

function basic3DWorld.calculateVisibleSidesInArea(minX,minY,maxX,maxY,tileMinZ,tileMaxZ)
    local tileMinX = math.floor(minX/tileSize) - 1
    local tileMinY = math.floor(minY/tileSize) - 1
    local tileMaxX = math.ceil(maxX/tileSize) + 1
    local tileMaxY = math.ceil(maxY/tileSize) + 1

    for tileX = tileMinX,tileMaxX do
        for tileY = tileMinY,tileMaxY do
            for tileZ = tileMinZ,tileMaxZ do
                basic3DWorld.calculateVisibleSides(tileX,tileY,tileZ)
            end
        end
    end
end



local shaderPath = Misc.resolveFile("block.glsl")
local blockMaterials = {}

local function createMaterial(texture,alpha,hasModel)
    local loopSize = 0
    if not hasModel then
        loopSize = tileSize
    end

    return lib3d.Material(shaderPath,{texture = texture},{},{ALPHAMODE = alpha,TILE_SIZE = loopSize})
end

local function getMaterial(blockID,side)
    if blockMaterials[blockID] == nil then
        local materials = {}
        local data = blockData[blockID]

        local hasModels 

        if data.textures ~= nil then
            for _,side in ipairs(sideList) do
                local texture = data.textures[side]

                if texture ~= nil then
                    materials[side] = createMaterial(texture,data.alpha,false)
                end
            end
        end

        materials.general = createMaterial(Graphics.sprites.block[blockID].img,data.alpha,false)

        blockMaterials[blockID] = materials
    end

    return blockMaterials[blockID][side] or blockMaterials[blockID].general
end


local function addPlane(material)
    quadCount = quadCount + 1
    meshCount = meshCount + 1

    local mesh = quadMeshes[quadCount]

    if mesh == nil or not mesh.isValid then
        mesh = lib3d.Quad{material = material,width = tileSize,height = tileSize}
        quadMeshes[quadCount] = mesh
    else
        mesh.materials[1] = material
        mesh.active = true
    end

    totalMeshes[meshCount] = mesh

    return mesh
end


local function getBlockMeshes(blockID)
    local meshList = customMeshes[blockID]
    if meshList == nil then
        meshList = {count = 0}
        customMeshes[blockID] = meshList
    end

    if meshList.count == 0 then
        table.insert(customMeshesRenderIDList,blockID)
    end

    meshList.count = meshList.count + 1

    local meshes = meshList[meshList.count]

    if meshes == nil then
        local data = blockData[blockID]

        meshes = {}

        for i,modelData in ipairs(data.models) do
            local material = createMaterial(modelData[2] or Graphics.sprites.block[blockID].img,data.alpha,true)

            meshes[i] = lib3d.Mesh{meshdata = modelData[1],material = material}
        end

        meshList[meshList.count] = meshes
    end

    return meshes
end


local cameraCentreX
local cameraCentreY

local function sideIsVisible(b,side)
    if side == SIDE_LEFT then
        return (b.x > cameraCentreX)
    elseif side == SIDE_RIGHT then
        return ((b.x + b.width) < cameraCentreX)
    elseif side == SIDE_TOP then
        return (b.y > cameraCentreY)
    elseif side == SIDE_BOTTOM then
        return ((b.y + b.height) < cameraCentreY)
    end

    return true
end

local function addBlock(b,tileX,tileY,tileZ,previousFrontMesh,previousTopMesh,previousBottomMesh,previousID)
    if not b.isValid then
        return
    end

    local data = blockData[b.id]

    local x = b.x + b.width*0.5
    local y = b.y + b.height*0.5
    local z = tileZ*tileSize

    if data.models ~= nil then
        if not blockIsVisible(b,tileZ) then
            return
        end

        local meshes = getBlockMeshes(b.id)

        for _,mesh in ipairs(meshes) do
            if mesh.isValid then
                -- Position it
                local position = mesh.transform.position

                position.x = x
                position.y = y
                position.z = z
                mesh.active = true

                -- Add to total meshes list
                meshCount = meshCount + 1
                totalMeshes[meshCount] = mesh
            end
        end

        return
    end

    local visibleSides = visibleSideMap[tileX][tileY][tileZ]
    if visibleSides == nil then
        return
    end

    local halfTileSize = tileSize*0.5
    local frontMesh,topMesh,bottomMesh

    for _,side in ipairs(visibleSides) do
        local positioningData = sidePositioning[side]

        if sideIsVisible(b,side) then
            if side == SIDE_FRONT and previousFrontMesh ~= nil and previousID == b.id then
                frontMesh = previousFrontMesh

                frontMesh.position.x = frontMesh.position.x + halfTileSize
                frontMesh.scale.x = frontMesh.scale.x + 1
            elseif side == SIDE_TOP and previousTopMesh ~= nil and previousID == b.id then
                topMesh = previousTopMesh

                topMesh.position.x = topMesh.position.x + halfTileSize
                topMesh.scale.x = topMesh.scale.x + 1
            elseif side == SIDE_BOTTOM and previousBottomMesh ~= nil and previousID == b.id then
                bottomMesh = previousBottomMesh

                bottomMesh.position.x = bottomMesh.position.x + halfTileSize
                bottomMesh.scale.x = bottomMesh.scale.x + 1
            else
                local mesh = addPlane(getMaterial(b.id,side))

                local transform = mesh.transform
                local position = mesh.position
                local rotation = mesh.rotation
                local scale = mesh.scale

                position.x = x + positioningData.offsetX*halfTileSize
                position.y = y + positioningData.offsetY*halfTileSize
                position.z = z + positioningData.offsetZ*halfTileSize

                for i = 1,3 do
                    scale[i] = 1
                end

                for i = 1,4 do
                    rotation[i] = positioningData.rotation[i]
                end

                if side == SIDE_FRONT then
                    frontMesh = mesh
                elseif side == SIDE_TOP then
                    topMesh = mesh
                elseif side == SIDE_BOTTOM then
                    bottomMesh = mesh
                end
            end
        end
    end

    return frontMesh,topMesh,bottomMesh,b.id
end


local function renderCamera(camIdx,cameraObj,priority)
    local renderscale = cameraObj.renderscale
    local cam = Camera(camIdx)

    local width  = cam.width *renderscale
    local height = cam.height*renderscale

    cameraObj:draw(priority,false)

    Graphics.drawBox{
        texture = cameraObj.target,priority = priority,
        x = 0,y = 0,width = cam.width,height = cam.height,
        sourceWidth = width,sourceHeight = height,
        sourceX = (cameraObj.target.width  - width )*0.5,
        sourceY = (cameraObj.target.height - height)*0.5,
    }

    -- Reset things for next render
    --Text.print(meshCount,32,32)
    
    for i = 1,meshCount do
        local mesh = totalMeshes[i]

        if mesh.isValid then
            mesh.active = false
        end

        totalMeshes[i] = nil
    end

    for i = 1,#customMeshesRenderIDList do
        local blockID = customMeshesRenderIDList[i]
        local meshList = customMeshes[blockID]

        meshList.count = 0

        customMeshesRenderIDList[i] = nil
    end

    meshCount = 0
    quadCount = 0
end

local function addBlocksInArea(tileMinX,tileMaxX,tileMinY,tileMaxY,tileMinZ,tileMaxZ)
    for tileZ = tileMinZ,tileMaxZ do
        for tileY = tileMinY,tileMaxY do
            local previousFrontMesh,previousTopMesh,previousBottomMesh
            local previousID

            for tileX = tileMinX,tileMaxX do
                local b = getTile(tileX,tileY,tileZ)

                if b ~= nil then
                    previousFrontMesh,previousTopMesh,previousBottomMesh,previousID = addBlock(b,tileX,tileY,tileZ,previousFrontMesh,previousTopMesh,previousBottomMesh,previousID)
                else
                    previousID = nil
                end
            end
        end
    end
end



function basic3DWorld.onStart()
    if not basic3DWorld.useSimpleMode() then
        local bounds = Section(0).origBoundary

        for _,b in Block.iterate() do
            if blockData[b.id] ~= nil then
                local layerModeMap = battleGeneral.getLayerModes(b.layerName)

                if layerModeMap == nil or layerModeMap[battleGeneral.mode] then
                    local tileX = math.floor(b.x/tileSize)
                    local tileY = math.floor(b.y/tileSize)
                    local tileZ = tonumber(b.layerName:match("Depth ([%d-]+)$")) or 0

                    tilemap[tileX] = tilemap[tileX] or {}
                    tilemap[tileX][tileY] = tilemap[tileX][tileY] or {}
                    tilemap[tileX][tileY][tileZ] = b

                    visibleSideMap[tileX] = visibleSideMap[tileX] or {}
                    visibleSideMap[tileX][tileY] = visibleSideMap[tileX][tileY] or {}
                end
            end
        end

        
        basic3DWorld.calculateVisibleSidesInArea(bounds.left - cameraViewMargin,bounds.top - cameraViewMargin,bounds.right + cameraViewMargin,bounds.bottom + cameraViewMargin,tileMinZ,tileMaxZ)


        for blockID,data in pairs(blockData) do
            table.insert(blockIDs,blockID)
        end
    end
end


function basic3DWorld.onPostBlockRemove(b,makeEffects,delete)
    battleMap.queueTerrainRedraw(0)

    if basic3DWorld.useSimpleMode() then
        return
    end

    table.insert(justChangedAreas,{
        minX = b.x,minY = b.y,maxX = b.x + b.width,maxY = b.y + b.height,
    })

    battleMap.queueTerrainRedraw(0)
end

function blockRespawning.onBlockRespawn(b)
    battleMap.queueTerrainRedraw(0)

    if basic3DWorld.useSimpleMode() then
        return
    end

    table.insert(justChangedAreas,{
        minX = b.x,minY = b.y,maxX = b.x + b.width,maxY = b.y + b.height,
    })

    battleMap.queueTerrainRedraw(0)
end


function basic3DWorld.onDraw()
    if basic3DWorld.useSimpleMode() then
        return
    end

    local i = 1

    while (justChangedAreas[i] ~= nil) do
        local v = justChangedAreas[i]

        basic3DWorld.calculateVisibleSidesInArea(v.minX,v.minY,v.maxX,v.maxY,-1,1)

        justChangedAreas[i] = nil
        i = i + 1
    end

    for _,blockID in ipairs(blockIDs) do
        blockutils.setBlockFrame(blockID,-999)
    end
end


function basic3DWorld.onCameraDraw(camIdx)
    local cam = Camera(camIdx)

    if basic3DWorld.useSimpleMode() then
        return
    end

    local sectionIdx = battleCamera.getCameraSection(camIdx)
    local bounds = Section(sectionIdx).origBoundary

    local cameraObj = get3DCamera(camIdx)

    cameraCentreX = cam.x + cam.width*0.5
    cameraCentreY = cam.y + cam.height*0.5

    --cameraObj.transform.rotation = vector.quat(-20,-20,0)
    cameraObj.transform.rotation = vector.quatid
    cameraObj.transform.position = vector(cameraCentreX,cameraCentreY,0) + cameraObj.transform.rotation*vector(0,0,-cameraObj.flength - tileSize*0.5)

    -- Add main depth stuff
    local tileMinX = math.floor((cam.x - cameraViewMargin)/tileSize)
    local tileMinY = math.floor((cam.y - cameraViewMargin)/tileSize)
    local tileMaxX = math.ceil((cam.x + cam.width  + cameraViewMargin)/tileSize)
    local tileMaxY = math.ceil((cam.y + cam.height + cameraViewMargin)/tileSize)

    addBlocksInArea(tileMinX,tileMaxX,tileMinY,tileMaxY,0,tileMaxZ)

    basic3DWorld.onPreDraw(camIdx,cameraObj)

    -- Draw it to the screen
    renderCamera(camIdx,cameraObj,-67)

    basic3DWorld.onPostDraw(camIdx,cameraObj)

    -- Add foreground stuff
    addBlocksInArea(tileMinX,tileMaxX,tileMinY,tileMaxY,tileMinZ,-1)

    -- Draw it to the screen
    renderCamera(camIdx,cameraObj,-4)
end


function basic3DWorld.getBlocksInSphere(x,y,z,radius)
    local minTileX = math.floor((x - radius)/tileSize)
    local minTileY = math.floor((y - radius)/tileSize)
    local minTileZ = math.floor((z - radius)/tileSize)
    local maxTileX = math.ceil((x + radius)/tileSize)
    local maxTileY = math.ceil((y + radius)/tileSize)
    local maxTileZ = math.ceil((z + radius)/tileSize)

    local blocks = {}

    for tileX = minTileX,maxTileX do
        for tileY = minTileY,maxTileY do
            for tileZ = minTileZ,maxTileZ do
                local b = getTile(tileX,tileY,tileZ)

                if b ~= nil and (tileZ == 0 or not b.isHidden) then
                    local distX = (b.x + tileSize*0.5) - x
                    local distY = (b.y + tileSize*0.5) - y
                    local distZ = tileZ*tileSize - z

                    if (distX*distX + distY*distY + distZ*distZ) < radius*radius then
                        table.insert(blocks,b)
                    end
                end
            end
        end
    end

    return blocks
end


function basic3DWorld.onInitAPI()
    registerEvent(basic3DWorld,"onStart")
    registerEvent(basic3DWorld,"onDraw")
    registerEvent(basic3DWorld,"onCameraDraw")
    registerEvent(basic3DWorld,"onPostBlockRemove")

    registerCustomEvent(basic3DWorld,"onPreDraw")
    registerCustomEvent(basic3DWorld,"onPostDraw")
end


return basic3DWorld