require(asset.documents.Craft.UI)
require(asset.documents.Craft.Touches)
require(asset.documents.Craft.Cameras)
require(asset.documents.Block_Inventory)
require(asset.documents.BlockLibrary)

viewer.mode = FULLSCREEN

gameState = "building"

BUILD_STRIP = 4
BUILD_EVERY = 3
BUILD_MAX   = 16*5

buildMode   = nil
buildPhase  = "columns"
buildFeats  = nil
buildIndex  = 1
buildZ      = 0
buildFrame  = 0

PACK_DEFAULT = "Blocks"
PACK_ALT     = "BDCraftBlocks"

worldBlob   = nil
worldIds    = nil
worldStates = nil
worldCarts  = nil
worldSmap   = nil

pendingNewWorld = nil
pendingLoadSlot = nil
pendingRestart = nil

builderMode = false

function currentPack()
  if WorldState.num(readLocalData("BDCraft", 0)) == 1 then
    return PACK_ALT
  end
  return PACK_DEFAULT
end

function currentLeavesSolid()
  return WorldState.num(readLocalData("LeavesSolid", 0)) == 1
end

function currentBuilderMode()
  return WorldState.num(readLocalData("BuilderMode", 0)) == 1
end

function processWorldRequests()
  if pendingRestart then
    pendingRestart = nil
    local ok = pcall(function() viewer.restart() end)
    if not ok then
      WorldBrowser.visible = true
      WorldBrowser.notice = {
        msg = "Reopen the project to apply this change."
      }
    end
    return
  end
  
  if pendingNewWorld then
    local kind = pendingNewWorld
    pendingNewWorld = nil
    newWorld(kind)
  elseif pendingLoadSlot then
    local id = pendingLoadSlot
    pendingLoadSlot = nil
    loadWorldSlot(id)
  end
end

function applyBuilderMode(on)
  builderMode = on
  saveLocalData("BuilderMode", on and 1 or 0)
  
  if gameState == "building" then return end
  
  if on then
    if player and not player.suspended then
      suspendPlayer()
    end
    addPreviewCamera()
    Builder.enter()
    gameState = "builder"
  else
    Builder.exit()
    spawnPlayer()
  end
end

function setup()
  viewer.showWarnings = false
  
  parameter.number("DayNightCycling", 0, 20, 1.5)
  
  scene = craft.scene()
  addPreviewCamera()
  
  scene.sun.rotation = quat.eulerAngles(25, 125, 0)
  scene.ambientColor = color(127, 127, 127, 255)
  
  builderMode = currentBuilderMode()
  allBlocks = blocks(currentPack(), currentLeavesSolid())
  
  DayNight.setup()
  
  scene.voxels:resize(vec3(5,2,5))
  scene.voxels.coordinates = vec3(0,0,0)
  
  local pending = WorldState.num(readLocalData("PendingSlot", 0))
  if pending > 0 then
    saveLocalData("PendingSlot", nil)
    local b, i, st, ca = WorldSlots.load(pending)
    if b then
      worldBlob, worldIds, worldStates, worldCarts = b, i, st, ca
      worldSmap = WorldState.stateMap(st)
    end
  end
  
  if not worldBlob then
    worldBlob, worldIds, worldStates, worldCarts = WorldState.read()
  end
  
  if not worldBlob and WorldState.homesteadExists() then
    worldBlob, worldIds, worldStates, worldCarts = WorldState.readHomestead()
  end
  
  if worldBlob then
    buildMode = "restore"
    WorldState.restoring = true
    worldSmap = WorldState.stateMap(worldStates)
  else
    buildMode = "generate"
    WorldState.restoring = false
    Terrain.prepare()
    buildFeats = Terrain.featureList()
  end
  
  spawnX, spawnZ = loadSpawn()
  cartCrumb("setup:done")
  
  --[[
  parameter.action("bakeHomestead", function()
    bakeHomestead()
  end)
  ]]
end

previewActive = false

function addPreviewCamera()
  if previewActive then return end
  obv = scene.camera:add(BuilderCam,
  vec3(40, 12, 40), 150, 0.5, 400)
  obv.rx = 60
  previewActive = true
end

function removePreviewCamera()
  if not previewActive then return end
  local ok, err = pcall(function()
    scene.camera:remove(BuilderCam)
  end)
  if ok then
    previewActive = false
  else
    print("BuilderCam remove failed: "..tostring(err))
  end
end

function buildTick()
  if buildMode == "restore" then
    if buildPhase == "columns" then
      local z2 = math.min(buildZ + BUILD_STRIP - 1, BUILD_MAX - 1)
      WorldState.paintStrip(worldBlob, worldIds, buildZ, z2, worldSmap)
      buildZ = z2 + 1
      if buildZ >= BUILD_MAX then buildPhase = "states" end
      return false
    end
    
    if buildPhase == "states" then
      WorldState.applyStates(worldStates, worldIds)
      buildPhase = "carts"
      return false
    end
    
    if buildPhase == "carts" then
      WorldState.applyCarts(worldCarts)
      buildPhase = "tracks"
      buildZ = 0
      return false
    end
    
    local z2 = math.min(buildZ + BUILD_STRIP - 1, BUILD_MAX - 1)
    fixConnectedStrip(buildZ, z2)
    buildZ = z2 + 1
    if buildZ < BUILD_MAX then return false end
    -- Restore is fully done: the volume, its states, its
    -- carts, and now every track and fence's real
    -- connectivity all exist, so it's safe to let fences and
    -- tracks react to live edits again.
    WorldState.restoring = false
    return true
  end
  
  if buildPhase == "columns" then
    local z2 = math.min(buildZ + BUILD_STRIP - 1, BUILD_MAX - 1)
    Terrain.paintStrip(buildZ, z2)
    buildZ = z2 + 1
    if buildZ >= BUILD_MAX then buildPhase = "features" end
    return false
  end
  
  local f = buildFeats[buildIndex]
  if not f then return true end
  f()
  buildIndex = buildIndex + 1
  return buildFeats[buildIndex] == nil
end

function loadSpawn()
  local x = tonumber(readLocalData("PlayerX"))
  local z = tonumber(readLocalData("PlayerZ"))
  
  if x and z and x >= 0 and x <= BUILD_MAX and z >= 0 and z <= BUILD_MAX then
    return x, z
  end
  
  return 40, 40
end

function spawnPlayer()
  removePreviewCamera()
  
  if player then
    resumePlayer()
  else
    player = scene:entity():add(Player,
    scene.camera:get(craft.camera), spawnX, 26, spawnZ)
  end
  
  gameState = "playing"
end

function resetForNewWorld()
  WorldBrowser.hide()
  Builder.exit()
  
  if player and player.cart then
    pcall(function() player.cart:setRider(nil) end)
    player.cart = nil
  end
  
  if player then suspendPlayer() end
  
  WorldState.clearCarts()
  
  scene.voxels:resize(vec3(5,2,5))
  scene.voxels.coordinates = vec3(0,0,0)
  
  buildPhase = "columns"
  buildIndex = 1
  buildZ = 0
  buildFrame = 0
  
  if not player then
    addPreviewCamera()
  end
  
  gameState = "building"
end

-- "flat" is always the generator. Anything else prefers the
-- baked homestead in project storage and falls back to the
-- procedural generator if none is baked.
function newWorld(kind)
  resetForNewWorld()
  
  if kind ~= "flat" and WorldState.homesteadExists() then
    local blob, ids, states, carts = WorldState.readHomestead()
    if blob then
      worldBlob, worldIds, worldStates, worldCarts = blob, ids, states, carts
      worldSmap = WorldState.stateMap(states)
      buildMode = "restore"
      WorldState.restoring = true
      return
    end
  end
  
  if kind == "flat" then
    Terrain.prepareFlat(11)
    buildFeats = {}
  else
    Terrain.prepare()
    buildFeats = Terrain.featureList()
  end
  
  buildMode = "generate"
  WorldState.restoring = false
end

function loadWorldSlot(id)
  local blob, ids, states, carts = WorldSlots.load(id)
  if not blob then
    print("slot "..tostring(id).." could not be read")
    return
  end
  
  resetForNewWorld()
  
  worldBlob, worldIds, worldStates, worldCarts = blob, ids, states, carts
  worldSmap = WorldState.stateMap(states)
  buildMode = "restore"
  WorldState.restoring = true
end

-- entity.active = false already stops the entity, and
-- player.cart = {} already stops Player:update, which is
-- the only thing touching self.rb. Tearing the rigidbody out
-- is what has crashed before, so this does nothing more.
function suspendPlayer()
  player.suspended = true
  player.cart = {}
  player.camera.entity.parent = nil
  
  if player.rb then
    player.rb.linearVelocity = vec3(0, 0, 0)
  end
  
  player.entity.active = false
end

function resumePlayer()
  player.entity.active = true
  player.entity.position = vec3(spawnX, 26, spawnZ)
  
  player.camera.entity.parent = player.entity
  player.camera.entity.position = vec3(0, 0.85, 0)
  
  player.cart = nil
  player.suspended = nil
  if player.rb then
    player.rb.linearVelocity = vec3(0, 0, 0)
  end
end

spawnTimer = 0

function saveSpawn(dt)
  if not player or player.suspended then return end
  
  spawnTimer = spawnTimer + dt
  if spawnTimer < 0.5 then return end
  spawnTimer = 0
  
  local p = player.entity.position
  saveLocalData("PlayerX", p.x)
  saveLocalData("PlayerZ", p.z)
end

function orientationChanged(o)
  if player then player:relayout() end
  WorldBrowser.layoutGrid()
end

lastW, lastH = nil, nil

function checkLiveLayout()
  if lastW == WIDTH and lastH == HEIGHT then return end
  lastW, lastH = WIDTH, HEIGHT
  
  if player and not player.suspended then
    pcall(function() player:relayout() end)
  end
  pcall(function() WorldBrowser.layoutGrid() end)
end

function drawOverlay(label, spin)
  pushStyle()
  
  if spin then
    local dots, radius = 8, 26
    noStroke()
    for i = 1, dots do
      local a = ElapsedTime * 220 + i * 360 / dots
      fill(255, 255, 255, 255 * (i / dots))
      ellipse(WIDTH/2 + radius * math.cos(math.rad(a)),
      140 + radius * math.sin(math.rad(a)), 9)
    end
  end
  
  fill(255)
  fontSize(24)
  text(label, WIDTH/2, 70)
  
  popStyle()
end

function cartCrumb(s)
  saveLocalData("cartcrumb", s)
end

function update(dt)
  processWorldRequests()
  checkLiveLayout()
  
  DayNight.update(dt)
  
  scene:update(dt)
  
  if gameState == "building" then
    buildFrame = buildFrame + 1
    if buildFrame % BUILD_EVERY == 0 then
      if buildTick() then
        if builderMode then
          Builder.enter()
          gameState = "builder"
        else
          gameState = "waiting"
        end
      end
    end
  elseif gameState == "builder" then
    Builder.update()
  elseif gameState == "playing" then
    if player and not player.suspended then
      player:cartTick()
    end
    saveSpawn(dt)
  end
  
  if WorldBrowser.visible and player and player.joystick then
    player.joystick.touchId = nil
    player.joystick.offsetX = 0
    player.joystick.offsetY = 0
  end
  
  if player and player.voxelToucher then
    player.voxelToucher.disabled =
    WorldBrowser.visible or (player.isInventoryOpen == true)
  end
end

function draw()
  update(DeltaTime)
  
  scene:draw()
  
  WorldBrowser.processPending()
  
  if gameState == "building" then
    drawOverlay("Building world...", true)
  elseif gameState == "waiting" then
    drawOverlay("Tap to begin", false)
  elseif gameState == "playing" and not WorldBrowser.visible then
    player:draw()
  elseif gameState == "builder" and not WorldBrowser.visible then
    Builder.draw()
  end
  
  WorldBrowser.draw()
end

tapStart = nil
tapMoved = false

function touched(t)
  trackRecordTouch(t)
  
  if WorldBrowser.touched(t) then return end
  
  if gameState == "builder" and Builder.touched(t) then return end
  
  if gameState == "playing" and player and not player.suspended
  and player.joystick and player.joystick:touched(t) then
    return
  end
  
  touches.touched(t)
  
  if gameState == "waiting" then
    if t.state == BEGAN then
      tapStart = vec2(t.x, t.y)
      tapMoved = false
    elseif t.state == MOVING then
      if tapStart and vec2(t.x, t.y):dist(tapStart) > 20 then
        tapMoved = true
      end
    elseif t.state == ENDED then
      if tapStart and not tapMoved then
        spawnPlayer()
      end
      tapStart = nil
    end
    return
  end
  
  if (gameState == "playing" or gameState == "builder")
  and t.state == ENDED then
    WorldState.save()
  end
end
