-- Voxel world mode: the camera angle and its tween.
--
-- Deliberately the same shape as src/render/Tilt.lua -- level 0 is off and
-- 1..3 are the same 15/35/50 degree ladder, eased the same way. What
-- differs is what the renderer does with the angle: tilt projects the flat
-- world canvas as one rigid plane, voxel mode drives a real 3D camera over
-- extruded terrain and voxel character models.
--
-- And because it is a real camera over real geometry, it has a rung tilt
-- could never have: 75 degrees, low enough to read as a diorama shot from
-- table height. Tilt's flat plane degenerates into a horizon line there,
-- but geometry only gets more of itself to show.
--
-- The LEVEL is not ours. The engine's render_pipelines plumbing owns it --
-- the options row, the hotkey, the ladder labels, persistence in
-- save.options.pipelines.voxel, and the mutual exclusion with tilt -- and
-- hands it to update() every frame. All this module keeps is the eased
-- ANGLE that level implies, because the tween is renderer state and only
-- the renderer knows what to do with a half-raised camera.
--
-- Purely presentational, like tilt and survey zoom: nothing here reaches
-- collision, movement, triggers or scripts.

local Voxel = {}

-- FULL is a PRESET, not another angle: one rung that puts the whole mode in
-- its intended state at once -- this camera, the miniature blur at full, the
-- horizon flat, the view fitted -- so a player who wants "the diorama" picks
-- it rather than assembling it from four rows. It sits directly after OFF
-- because that is the order those two get used in.
--
-- Its ANGLE is 35 degrees, the same as the rung of that name. The duplicate
-- in the table is deliberate: the ladder is a list of what each rung LOOKS
-- like, and two rungs may look the same while meaning different things.
--
-- 1ST and 3RD are the other rungs that are more than an angle: the camera
-- steps off its orbit entirely and stands with the player -- in their eyes
-- (lib/FirstPerson.lua), or on a boom behind their shoulder
-- (lib/ThirdPerson.lua) -- with free look and free movement on both. Their
-- ANGLE entries are 75 -- the orbit rung they hand over from -- because the
-- tween in and out starts from whatever the orbit shows, and the lowest rung
-- is the one a dive into a head should start from. Everything angle-derived
-- (the sky's fade, the billboard lean the blend eases away) reads that 75
-- while the free-roam rig owns the actual camera.
Voxel.ANGLES_DEG = { 0, 35, 15, 35, 50, 75, 75, 75 }
Voxel.ANGLE_LABELS = { "OFF", "FULL", "15", "35", "50", "75",
                       "1ST (EXPERIMENTAL)", "3RD (EXPERIMENTAL)" }
Voxel.MAX_LEVEL = #Voxel.ANGLES_DEG - 1

-- the rung FULL sits on, so nothing has to hunt for it by label
Voxel.FULL_LEVEL = 1

function Voxel.isFull(level)
  return (level or Voxel.level) == Voxel.FULL_LEVEL
end

-- the rung the first-person camera sits on, likewise
Voxel.FP_LEVEL = 6

function Voxel.isFirstPerson(level)
  return (level or Voxel.level) == Voxel.FP_LEVEL
end

-- and the third-person one, which is the same rig with the eye boomed off
-- the back of the head (lib/ThirdPerson.lua)
Voxel.TP_LEVEL = 7

function Voxel.isThirdPerson(level)
  return (level or Voxel.level) == Voxel.TP_LEVEL
end

-- The two of them together: the rungs where the camera stands WITH the
-- player rather than orbiting the view centre, which is what decides that
-- the look inputs are read, the walk goes free and the cards turn to face
-- the eye. Everything that used to ask isFirstPerson for those asks this.
function Voxel.isFreeCam(level)
  level = level or Voxel.level
  return Voxel.isFirstPerson(level) or Voxel.isThirdPerson(level)
end

-- ------- what the hotkey walks
--
-- The ANGLE rungs only, with FULL left out. The key is a display-mode
-- cycler: pressing it should change the camera and nothing else, and FULL
-- reaches in and rewrites four other settings. Landing on it by accident,
-- mid-walk, would silently turn the blur to maximum and flatten the horizon
-- with no indication that a keypress had done so. FULL stays on the OPTIONS
-- row, which is where a preset that changes other rows belongs.
--
-- 1ST and 3RD are on the path: they change the camera and only the camera,
-- which is exactly what the key promises -- and the key is also the way back
-- OUT of them on a keyboard, where the mouse is captured and the OPTIONS
-- menu is a trip.
Voxel.HOTKEY_ORDER = { 0, 2, 3, 4, 5, 6, 7 }  -- OFF,15,35,50,75,1ST,3RD

-- The rung a press moves to from `level`.
--
-- A level that is not on the key's path -- FULL, reached from the menu --
-- steps on from whichever rung shows the SAME camera it does. FULL is 35
-- degrees, so a press from it goes to 50 rather than back to 35, and the key
-- never appears to do nothing. Matched by ANGLE rather than by a hardcoded
-- rung, so retuning FULL moves the key's answer with it.
function Voxel.nextHotkeyLevel(level)
  level = level or Voxel.level
  local order = Voxel.HOTKEY_ORDER
  local at = nil
  for i, rung in ipairs(order) do
    if rung == level then at = i break end
  end
  if not at then
    local deg = Voxel.ANGLES_DEG[level + 1]
    for i, rung in ipairs(order) do
      if Voxel.ANGLES_DEG[rung + 1] == deg then at = i break end
    end
  end
  if not at then return order[1] end
  return order[at % #order + 1]
end

Voxel.level = 0
Voxel.angle = 0
Voxel.from = 0
Voxel.goal = 0
Voxel.t = 1

-- Whether the scene has terrain to show for the current map. VoxelScene
-- maintains it every frame; while the first mesh of a fresh toggle is
-- still building, update() holds the camera tween at flat -- the 2D
-- fallback IS the flat pose, so the switch waits invisibly instead of
-- tilting an empty stage (or, before builds went asynchronous, freezing
-- the whole frame for seconds).
Voxel.ready = true

Voxel.TWEEN_TIME = 0.25
-- Camera distance as a multiple of the view height, and the matching field
-- of view. Kept equal to Tilt.FOCAL so a given angle frames the world the
-- same way in both modes; Voxel3D derives the FOV from it.
Voxel.FOCAL = 1.0

local function ease(t)
  return t * t * (3 - 2 * t)
end

local function goalFor(level)
  return math.rad(Voxel.ANGLES_DEG[level + 1] or 0)
end

function Voxel.setLevel(level)
  level = math.floor(tonumber(level) or 0)
  if level < 0 then level = 0 end
  if level > Voxel.MAX_LEVEL then level = Voxel.MAX_LEVEL end
  local goal = goalFor(level)
  if goal ~= Voxel.goal or level ~= Voxel.level then
    Voxel.from = Voxel.angle
    Voxel.goal = goal
    Voxel.t = 0
    -- leaving flat: presume no terrain until the scene reports some, so
    -- the tween's very first frame already waits instead of easing over
    -- an empty stage (render() confirms readiness the same frame when
    -- the meshes are already cached)
    if Voxel.angle == 0 and goal > 0 then Voxel.ready = false end
  end
  Voxel.level = level
end

function Voxel.reset()
  Voxel.level, Voxel.angle = 0, 0
  Voxel.from, Voxel.goal, Voxel.t = 0, 0, 1
end

function Voxel.levelLabel(level)
  return Voxel.ANGLE_LABELS[(level or Voxel.level) + 1] or "OFF"
end

-- The pipeline's per-frame tick. `level` is what the engine currently has
-- the mode set to, so a hotkey press or an options row lands here as a new
-- goal to ease toward rather than as a jump.  dt is real frame time, so
-- fast-forward does not speed the camera up.
function Voxel.update(dt, level)
  if level ~= nil and level ~= Voxel.level then Voxel.setLevel(level) end
  -- hold at flat until there is geometry to tilt over; easing OUT (goal
  -- below the current angle) never waits
  if Voxel.angle == 0 and Voxel.goal > 0 and not Voxel.ready then
    return
  end
  if Voxel.t < 1 then
    Voxel.t = math.min(1, Voxel.t + dt / Voxel.TWEEN_TIME)
    Voxel.angle = Voxel.from + (Voxel.goal - Voxel.from) * ease(Voxel.t)
  else
    Voxel.angle = Voxel.goal
  end
end

-- True while voxel mode is on *or* still easing out -- i.e. whenever the
-- renderer must take the 3D path instead of the flat blit. Mirrors
-- Tilt.active so the two gate the same way.
function Voxel.active()
  return Voxel.level > 0 or Voxel.angle > 0
end

return Voxel
