-- Builder mode: orbit the world and edit it, with no player
-- in the scene.
--
-- Digging and placing exist because BasicPlayer:init runs
--   self.camera.entity:add(VoxelToucher, self.camera, fn)
-- and self.camera.entity IS scene.camera -- the toucher was
-- never on the player, it was on the camera. So builder mode
-- adds the same component to the same entity with no player
-- anywhere near it.
--
-- Builder.selected is the single source of truth for "what am
-- I holding": the VoxelToucher closure reads it, MegaBar's
-- onSelect writes it, and the fill tool reads it too.

function VOXEL_LIMITS(c)
  local x = math.floor(c.x)
  local y = math.floor(c.y)
  local z = math.floor(c.z)
  return x >= 0 and x < WorldState.SX
  and y >= 0 and y < WorldState.SY
  and z >= 0 and z < WorldState.SZ
end

function VOXEL_REFUSED(c)
  if not Builder.active then return end
  if math.floor(c.y) >= WorldState.SY then
    Builder.say("Cannot place voxels above the ceiling set for these worlds.")
  else
    Builder.say("Cannot place voxels outside current world settings.")
  end
end

function VOXEL_EDITED(kind)
  Builder.undo = nil
end

Builder = {}

Builder.active = false
Builder.blocks = nil
Builder.selected = nil
Builder.toucher = nil
Builder.bar = nil
Builder.laidOutW = nil
Builder.laidOutH = nil

Builder.REACH = 520

Builder.fillOn = false
Builder.fillBtn = nil
Builder.undoBtn = nil
Builder.undo = nil
Builder.pendingFill = nil
Builder.pendingUndo = nil
Builder.btnTouchId = nil
Builder.btnWhich = nil

Builder.FILL_GLYPH = "\240\159\154\176"  -- water faucet
Builder.UNDO_GLYPH = "\226\134\169"      -- leftwards hook arrow

Builder.FILL_D = 52
Builder.FILL_GAP = -12
Builder.FILL_MARGIN = 18
Builder.BTN_LABEL_H = 16

Builder.FILL_MAX = 20000

Builder.msg = nil
Builder.msgT = 0
Builder.MSG_LIFE = 3.0

-- Every one of these derives its look or behaviour from how
-- it was placed, and a bulk voxels:set calls none of the
-- hooks that would supply that.
Builder.NO_FILL = {
  ["Track Straight"] = true,
  ["Track Corner"] = true,
  ["Minecart"] = true,
  ["Piston"] = true,
  ["Pusher"] = true,
  ["Wooden Fence"] = true,
  ["Stone Fence"] = true,
  ["Sign"] = true,
  ["Chest"] = true,
  ["Tree Generator"] = true,
  ["Orange Tree Generator"] = true,
}

function Builder.fillable(block)
  if not block then return false, "No block selected" end
  
  local name = tostring(block.name or "")
  if Builder.NO_FILL[name] then
    return false, name.." cannot be used with the fill tool"
  end
  
  return true
end

local function hitRect(r, x, y)
  return r and x >= r.x and x <= r.x + r.w
  and y >= r.y and y <= r.y + r.h
end

function Builder.say(s)
  Builder.msg = s
  Builder.msgT = ElapsedTime
end

function Builder.enter()
  if Builder.active then return end
  
  Builder.blocks = Builder.blocks or placeableBlocks(allBlocks)
  if not Builder.selected then
    Builder.selected = Builder.blocks[1]
  end
  
  local ok, err = pcall(function()
    Builder.toucher = scene.camera:add(VoxelToucher,
    scene.camera:get(craft.camera),
    function() return Builder.selected end)
  end)
  
  if not ok then
    print("Builder: VoxelToucher failed: "..tostring(err))
    return
  end
  
  Builder.toucher.reach = Builder.REACH
  Builder.toucher.onTap = Builder.onTap
  
  Builder.active = true
  Builder.layoutBar()
end

function Builder.exit()
  if not Builder.active then return end
  
  local ok, err = pcall(function()
    scene.camera:remove(VoxelToucher)
  end)
  if not ok then
    print("Builder: VoxelToucher remove failed: "..tostring(err))
  end
  
  if Builder.bar then Builder.bar:setActive(false) end
  
  Builder.undo = nil
  Builder.pendingUndo = nil
  Builder.pendingFill = nil
  
  Builder.toucher = nil
  Builder.btnTouchId = nil
  Builder.btnWhich = nil
  Builder.active = false
end

function Builder.layoutBar()
  if not Builder.blocks or #Builder.blocks == 0 then return end
  
  if not Builder.bar then
    Builder.bar = MegaBar(Builder.blocks,
    {
      position = "bottom",
      onSelect = function(bar, block, index)
        if not block then return end
        Builder.selected = block
        if Builder.fillOn then
          local ok, why = Builder.fillable(block)
          if not ok then Builder.say(why) end
        end
      end
    })
    
    local start = 1
    for i, b in ipairs(Builder.blocks) do
      if b == Builder.selected then start = i break end
    end
    Builder.bar:setSelected(start)
  else
    Builder.bar:relayout()
  end
  
  Builder.bar:setActive(true)
  Builder.layoutFill()
  Builder.laidOutW, Builder.laidOutH = WIDTH, HEIGHT
end

function Builder.checkOrientation()
  if Builder.laidOutW ~= WIDTH or Builder.laidOutH ~= HEIGHT then
    Builder.layoutBar()
  end
end

function Builder.barTop()
  local bar = Builder.bar
  if not bar or not bar.barH then return 0 end
  return bar:baseY() + bar.barH + MegaBar.LABEL_H
end

function Builder.layoutFill()
  local left = Builder.FILL_MARGIN
  pcall(function()
    if layout.safeArea.left > 0 then
      left = left + layout.safeArea.left
    end
  end)
  
  local d = Builder.FILL_D
  local y = Builder.barTop() + Builder.FILL_GAP
  + Builder.BTN_LABEL_H
  
  Builder.fillBtn = {x = left, y = y, w = d, h = d}
  Builder.undoBtn = {x = left + d + Builder.FILL_GAP,
  y = y, w = d, h = d}
end

-- Four-connected on purpose: a fill that could step
-- diagonally would leak through a diagonal-chain wall.
-- Reaching the volume edge is a failure, not a boundary.
function Builder.fillCells(x, y, z)
  local vox = scene.voxels
  local SX, SY, SZ = WorldState.SX, WorldState.SY, WorldState.SZ
  
  local unbounded =
  "Can only use horizontal fill on fully bounded areas"
  
  if y < 0 or y >= SY then
    return nil,
    "Cannot place voxels above the ceiling set for these worlds"
  end
  
  if x < 0 or x >= SX or z < 0 or z >= SZ then
    return nil, "Can't fill outside the current world settings"
  end
  
  local sid = vox:get(x, y, z, BLOCK_ID)
  if sid and sid ~= 0 then return nil, "That cell is not empty" end
  
  local seen, out, stack = {}, {}, {}
  local edge = false
  local n = 0
  
  local function push(nx, nz)
    if nx < 0 or nx >= SX or nz < 0 or nz >= SZ then
      edge = true
      return
    end
    local k = nx + SX * nz
    if not seen[k] then
      seen[k] = true
      stack[#stack + 1] = k
    end
  end
  
  seen[x + SX * z] = true
  stack[1] = x + SX * z
  
  while #stack > 0 do
    local k = table.remove(stack)
    local cx = k % SX
    local cz = math.floor(k / SX)
    
    local id = vox:get(cx, y, cz, BLOCK_ID)
    if not id or id == 0 then
      n = n + 1
      if n > Builder.FILL_MAX then return nil, unbounded end
      out[n] = k
      
      push(cx - 1, cz)
      push(cx + 1, cz)
      push(cx, cz - 1)
      push(cx, cz + 1)
      if edge then return nil, unbounded end
    end
  end
  
  return out
end

-- Deferred out of the touch callback: a fill can create
-- thousands of blocks, and dynamic ones build child entities
-- in created() -- entity construction from inside touched()
-- is the pattern this project has already crashed on.
function Builder.doFill(x, y, z)
  local block = Builder.selected
  local ok, why = Builder.fillable(block)
  if not ok then
    Builder.say(why)
    return
  end
  
  local cells, err = Builder.fillCells(x, y, z)
  if not cells then
    Builder.say(err)
    return
  end
  
  local vox = scene.voxels
  local SX = WorldState.SX
  local id = block.id
  
  for i = 1, #cells do
    local k = cells[i]
    vox:set(k % SX, y, math.floor(k / SX), id)
  end
  
  Builder.undo = {y = y, id = id, cells = cells}
  
  WorldState.save()
  
  sound(SOUND_POWERUP, 26734)
  Builder.say("Filled "..#cells.." blocks")
end

function Builder.doUndo()
  local u = Builder.undo
  if not u then return end
  Builder.undo = nil
  
  local vox = scene.voxels
  local SX = WorldState.SX
  
  for i = 1, #u.cells do
    local k = u.cells[i]
    vox:set(k % SX, u.y, math.floor(k / SX), 0)
  end
  
  WorldState.save()
  sound(SOUND_HIT, 26744)
  Builder.say("Undid "..#u.cells.." blocks")
end

function Builder.update()
  if not Builder.active then return end
  
  local p = Builder.pendingFill
  if p then
    Builder.pendingFill = nil
    Builder.doFill(p.x, p.y, p.z)
    return
  end
  
  if Builder.pendingUndo then
    Builder.pendingUndo = nil
    Builder.doUndo()
  end
end

function Builder.onTap(tx, ty)
  if not Builder.fillOn then return false end
  
  local okBlock, why = Builder.fillable(Builder.selected)
  if not okBlock then
    Builder.say(why)
    return true
  end
  
  local t = Builder.toucher
  if not t then return true end
  
  local hc, hf = nil, nil
  t:castToBlock(tx, ty, function(coord, face, b)
    hc, hf = coord, face
  end)
  
  if not hc then return true end
  
  local c = hc + hf
  Builder.pendingFill = vec3(math.floor(c.x),
  math.floor(c.y), math.floor(c.z))
  return true
end

function Builder.drawGlyph(g, cx, cy, size, a)
  pushStyle()
  font("HelveticaNeue")
  fontSize(size)
  textMode(CENTER)
  textAlign(CENTER)
  textWrapWidth(0)
  fill(255, 255, 255, a or 255)
  text(g, cx, cy - size * 0.06)
  popStyle()
end

function Builder.drawBtnLabel(r, label)
  pushStyle()
  font("HelveticaNeue-Bold")
  fontSize(12)
  textMode(CENTER)
  textAlign(CENTER)
  textWrapWidth(0)
  local cx = r.x + r.w * 0.5
  local cy = r.y - Builder.BTN_LABEL_H * 0.5
  fill(0, 0, 0, 190)
  text(label, cx + 1, cy - 1)
  fill(255, 255, 255, 255)
  text(label, cx, cy)
  popStyle()
end

function Builder.drawFillBtn()
  local r = Builder.fillBtn
  if not r then return end
  
  local cx, cy = r.x + r.w * 0.5, r.y + r.h * 0.5
  pushStyle()
  noStroke()
  if Builder.fillOn then
    fill(70, 170, 235, 235)
  else
    fill(120, 120, 128, 64)
  end
  ellipse(cx, cy, r.w)
  popStyle()
  
  Builder.drawGlyph(Builder.FILL_GLYPH, cx-3, cy, r.w * 0.8,
  Builder.fillOn and 255 or 210)
  Builder.drawBtnLabel(r, Builder.fillOn and "Fill On" or "Fill")
end

function Builder.drawUndoBtn()
  if not Builder.undo then return end
  local r = Builder.undoBtn
  if not r then return end
  
  local cx, cy = r.x + r.w * 0.5, r.y + r.h * 0.5
  pushStyle()
  noStroke()
  fill(200, 90, 60, 200)
  ellipse(cx, cy, r.w)
  popStyle()
  
  Builder.drawGlyph(Builder.UNDO_GLYPH, cx, cy, r.w * 0.48)
  Builder.drawBtnLabel(r, "Undo")
end

function Builder.drawMsg()
  if not Builder.msg then return end
  local age = ElapsedTime - Builder.msgT
  if age > Builder.MSG_LIFE then
    Builder.msg = nil
    return
  end
  
  local a = 255
  if age > Builder.MSG_LIFE - 0.6 then
    a = 255 * (Builder.MSG_LIFE - age) / 0.6
  end
  
  local y = Builder.barTop() + Builder.FILL_GAP
  + Builder.BTN_LABEL_H + Builder.FILL_D + 26
  
  pushStyle()
  font("HelveticaNeue-Bold")
  fontSize(18)
  textMode(CENTER)
  textAlign(CENTER)
  textWrapWidth(math.min(420, WIDTH * 0.8))
  fill(0, 0, 0, a * 0.8)
  text(Builder.msg, WIDTH * 0.5 + 1, y - 2)
  fill(255, 255, 255, a)
  text(Builder.msg, WIDTH * 0.5, y)
  popStyle()
end

function Builder.drawBar()
  if Builder.bar then Builder.bar:draw() end
end

function Builder.draw()
  if not Builder.active then return end
  Builder.checkOrientation()
  
  if Builder.toucher then
    pcall(function() Builder.toucher:draw() end)
  end
  
  Builder.drawBar()
  Builder.drawFillBtn()
  Builder.drawUndoBtn()
  Builder.drawMsg()
end

-- Both buttons sit above barTop, outside the band Builder
-- touched swallows wholesale, so without holding the id a
-- drag off one would reach the orbit viewer mid-gesture.
function Builder.touchedButtons(t)
  local fr, ur = Builder.fillBtn, Builder.undoBtn
  if not fr then return false end
  
  if Builder.btnTouchId and t.id == Builder.btnTouchId then
    if t.state == ENDED or t.state == CANCELLED then
      local which = Builder.btnWhich
      Builder.btnTouchId, Builder.btnWhich = nil, nil
      
      if t.state == ENDED and which == "fill"
      and hitRect(fr, t.x, t.y) then
        Builder.fillOn = not Builder.fillOn
        if Builder.fillOn then
          local ok, why = Builder.fillable(Builder.selected)
          if not ok then Builder.say(why) end
        end
      elseif t.state == ENDED and which == "undo"
      and Builder.undo and hitRect(ur, t.x, t.y) then
        Builder.pendingUndo = true
      end
    end
    return true
  end
  
  if t.state == BEGAN then
    if hitRect(fr, t.x, t.y) then
      Builder.btnTouchId, Builder.btnWhich = t.id, "fill"
      return true
    end
    if Builder.undo and hitRect(ur, t.x, t.y) then
      Builder.btnTouchId, Builder.btnWhich = t.id, "undo"
      return true
    end
  end
  
  return false
end

-- Anything swallowed here is a touch BuilderCam may already
-- be tracking: a finger that began on the world and drifted
-- into the HUD band gets claimed from this point on, and its
-- ENDED is never forwarded. Releasing the id is the fix.
function Builder.touched(t)
  if not Builder.active then return false end
  
  local function claim()
    if obv and obv.release then obv:release(t.id) end
    return true
  end
  
  if Builder.touchedButtons(t) then return claim() end
  
  local bar = Builder.bar
  if not bar then return false end
  
  if bar:touched(t) then return claim() end
  
  if t.y <= Builder.barTop() then return claim() end
  
  return false
end
