-- The Magnet Train ride (pokegold engine/events/magnet_train.asm).
--
-- `special MagnetTrain` is a self-contained cutscene: it takes the whole frame
-- loop away from the overworld, redraws the background out of the train
-- station tileset, and runs a seven-entry jumptable until it sets
-- JUMPTABLE_EXIT.  Everything below is that routine with no love calls in it,
-- so the timing, the scroll and the frameset can be asserted headless; the
-- screen that draws it is src/ui/gen2/MagnetTrainRide.lua.
--
-- The illusion is one 32x18 background and three horizontal SCX bands:
--
--   scanlines   0-46    wMagnetTrainOffset * 2   bushes, always moving
--   scanlines  47-94    wMagnetTrainPosition     the train body
--   scanlines  95-143   wMagnetTrainOffset * 2   bushes again
--
-- MagnetTrain_UpdateLYOverrides writes those three runs into
-- wLYOverridesBackup every frame and then advances the offset, so the scenery
-- never stops even while the jumptable is parked on a .WaitScene.  The train
-- band is what the jumptable actually moves, and the player sprite rides it
-- through wGlobalAnimXOffset, which is why the two stay locked together.
--
-- Everything here is 8-bit and wraps, exactly as the ASM's `add` does: the
-- forward trip runs wMagnetTrainPosition from 96 down past 0 to -96, and it is
-- the byte wrap that keeps SCX legal on the way.

local MagnetTrain = {}
MagnetTrain.__index = MagnetTrain

-- constants/gfx_constants.asm
local TILE_WIDTH = 8
local SCREEN_WIDTH, SCREEN_HEIGHT = 20, 18
local TILEMAP_WIDTH = 32
local SCREEN_HEIGHT_PX = 144

-- PAL_BG_* (constants/gfx_constants.asm) as 1-based palette slots, the way
-- src/world/gen2/Palettes.lua indexes a bgSet.
MagnetTrain.PAL_BG_GRAY = 1
MagnetTrain.PAL_BG_GREEN = 3
MagnetTrain.PAL_BG_YELLOW = 5

-- SetMagnetTrainPals paints the attribute map in four ByteFills: four rows of
-- green, ten of gray, four more of green, and then six tiles of yellow at
-- (7, 8) for the window the player is framed in.
local BUSH_ROWS_TOP = 4      -- hlbgcoord 0, 0  / bc = 4 * TILEMAP_WIDTH
local TRAIN_ROWS = 10        -- hlbgcoord 0, 4  / bc = 10 * TILEMAP_WIDTH
local WINDOW_ROW = 8         -- hlbgcoord 7, 8  / bc = 6
local WINDOW_COL, WINDOW_WIDTH = 7, 6

-- DrawMagnetTrain lays MagnetTrainTilemap over BG rows 6-9.
local FG_ROW = 6
local FG_ROWS = 4

-- Every value in this file is a hardware byte.
local function b(value) return value % 256 end
MagnetTrain.byte = b

--------------------------------------------------------------------------
-- The player in the window
--------------------------------------------------------------------------

-- data/sprite_anims/framesets.asm .Frameset_MagnetTrainRed: two OAM sets on an
-- eight frame beat, the fourth of them mirrored, then `oamrestart`.  The
-- object's own sequence is SPRITE_ANIM_FUNC_NULL (data/sprite_anims/
-- objects.asm), so nothing ever moves the struct: the only motion the player
-- has is wGlobalAnimXOffset, which the two MoveTrain states advance.
local FRAMESET = {
  { oamset = 1, duration = 8, xflip = false },
  { oamset = 2, duration = 8, xflip = false },
  { oamset = 1, duration = 8, xflip = false },
  { oamset = 2, duration = 8, xflip = true },
  "restart",
}

-- data/sprite_anims/oam.asm: SPRITE_ANIM_OAMSET_MAGNET_TRAIN_RED_1 and _2 are
-- vtile $00 and $04 over the same .OAMData_MagnetTrainRed 2x2 block.  Those
-- two vtiles are the two four-tile requests MagnetTrain_LoadGFX_PlayMusic
-- makes: ChrisSpriteGFX at vTiles0 $00, and ChrisSpriteGFX + 12 tiles at
-- vTiles0 $04.  A walking overworld sprite is six 16x16 frames, so those are
-- sheet frame 0 (standing down) and sheet frame 3 (the down walk step).
local OAMSET_VTILE = { 0x00, 0x04 }
MagnetTrain.SHEET_FRAME = { [0x00] = 0, [0x04] = 3 }

-- .OAMData_MagnetTrainRed, `dbsprite x tile, y tile, x px, y px, vtile, attr`.
-- Every entry carries OAM_PRIO, so on the cart the four tiles sit BEHIND
-- background colours 1-3 and only show through the window's colour 0.
local OAM_DATA = {
  { x = b(-1 * TILE_WIDTH), y = b(-1 * TILE_WIDTH), tile = 0x00 },
  { x = b(0 * TILE_WIDTH), y = b(-1 * TILE_WIDTH), tile = 0x01 },
  { x = b(-1 * TILE_WIDTH), y = b(0 * TILE_WIDTH), tile = 0x02 },
  { x = b(0 * TILE_WIDTH), y = b(0 * TILE_WIDTH), tile = 0x03 },
}

-- AddOrSubtractX: a mirrored object flips around its own 8-pixel cell.
local function mirror(value, flip)
  if not flip then return value end
  return b(-(value + TILE_WIDTH))
end

--------------------------------------------------------------------------
-- The ride
--------------------------------------------------------------------------

-- `toGoldenrod` is the wScriptVar the script left behind: Goldenrod's officer
-- writes `setval FALSE` and Saffron's writes `setval TRUE`, and MagnetTrain
-- reads it as "and a / jr nz, .ToGoldenrod".
--
-- opts.bgTiles is MagnetTrainBGTiles (a 2x18 tilemap) and opts.fgTilemap is
-- MagnetTrainTilemap (20x4); both come from the extracted cache and either may
-- be missing, in which case :tilemap() answers nil and the ride still runs.
function MagnetTrain.new(opts)
  opts = opts or {}
  local self = setmetatable({}, MagnetTrain)
  self.toGoldenrod = opts.toGoldenrod and true or false
  if self.toGoldenrod then
    -- .ToGoldenrod: `ld a, -1` / `lb bc, -8 tiles, -12 tiles` /
    -- `lb de, (11 tiles) + (11 tiles + 4), 12 tiles`.
    self.direction = b(-1)
    self.holdPosition = b(-8 * TILE_WIDTH)                       -- b
    self.initPosition = b(-12 * TILE_WIDTH)                      -- c
    self.finalPosition = b(12 * TILE_WIDTH)                      -- e
    self.playerSpriteInitX =
      b((11 * TILE_WIDTH) + (11 * TILE_WIDTH + 4))               -- d
  else
    -- forwards: `ld a, 1` / `lb bc, 8 tiles, 12 tiles` /
    -- `lb de, (11 tiles) - (11 tiles + 4), -12 tiles`.
    self.direction = 1
    self.holdPosition = b(8 * TILE_WIDTH)
    self.initPosition = b(12 * TILE_WIDTH)
    self.finalPosition = b(-12 * TILE_WIDTH)
    self.playerSpriteInitX = b((11 * TILE_WIDTH) - (11 * TILE_WIDTH + 4))
  end

  -- MagnetTrain_LoadGFX_PlayMusic's tail writes wJumptableIndex and the three
  -- bytes after it, so the wait counter starts life holding the init position.
  -- State 0 overwrites it before any .WaitScene reads it.
  self.index = 0
  self.offset = self.initPosition
  self.position = self.initPosition
  self.waitCounter = self.initPosition
  self.exited = false
  self.globalX = 0

  -- The sprite struct does not exist until .InitPlayerSpriteAnim runs.
  self.spriteX, self.spriteY = nil, nil
  self.frame, self.frameDuration = -1, 0
  self.oamFrame = nil

  self.bgTiles = opts.bgTiles
  self.fgTilemap = opts.fgTilemap
  self:updateLYOverrides(true)
  return self
end

function MagnetTrain:done() return self.exited end

-- MagnetTrain's .loop, one pass: the exit bit, PlaySpriteAnimations, the
-- jumptable, then the LY overrides.  Returns the sfx label the frame played,
-- which is only ever SFX_TRAIN_ARRIVED on the last one.
function MagnetTrain:update()
  if self.exited then return nil end
  self:stepSpriteFrame()
  local sfx = self:runJumptable()
  self:updateLYOverrides()
  return sfx
end

-- MagnetTrain_Jumptable.Next
function MagnetTrain:next()
  self.index = self.index + 1
end

-- .WaitScene: zero means "advance", anything else counts down.  A counter of
-- 128 therefore holds for 129 frames, the last of which is the one that reads
-- zero and moves on.
function MagnetTrain:waitScene()
  if self.waitCounter == 0 then
    self:next()
    return
  end
  self.waitCounter = self.waitCounter - 1
end

function MagnetTrain:runJumptable()
  local index = self.index
  if index == 0 then
    -- .InitPlayerSpriteAnim: InitSpriteAnimStruct at d = (8 + 2) * 8 + 5,
    -- e = wMagnetTrainPlayerSpriteInitX, then SPRITEANIMSTRUCT_TILE_ID = 0.
    self.spriteY = b((8 + 2) * TILE_WIDTH + 5)
    self.spriteX = self.playerSpriteInitX
    self.frame, self.frameDuration, self.oamFrame = -1, 0, nil
    self:next()
    self.waitCounter = 128
  elseif index == 1 or index == 3 or index == 5 then
    self:waitScene()
  elseif index == 2 then
    -- .MoveTrain1: one pixel a frame until the train reaches its hold
    -- position, then park for another 128 frames.
    if self.position == self.holdPosition then
      self:next()
      self.waitCounter = 128
      return nil
    end
    self.position = b(self.position - self.direction)
    self.globalX = b(self.globalX + self.direction)
  elseif index == 4 then
    -- .MoveTrain2: the same, at double speed, until it leaves the screen.
    if self.position == self.finalPosition then
      self:next()
      return nil
    end
    self.position = b(self.position - 2 * self.direction)
    self.globalX = b(self.globalX + 2 * self.direction)
  elseif index >= 6 then
    -- .TrainArrived: JUMPTABLE_EXIT and SFX_TRAIN_ARRIVED, and the loop reads
    -- the exit bit at the top of the next pass.
    self.exited = true
    return "Sfx_TrainArrived"
  end
  return nil
end

-- MagnetTrain_UpdateLYOverrides.  The three runs are 6*8-1, 6*8 and 6*8+1
-- entries, which is 144 scanlines exactly; hSCX takes the first band's value
-- because line 0 is drawn before the LCD interrupt has fired.  The offset
-- advances by two per frame (`add d` twice) AFTER the overrides are written.
--
-- `initial` builds the first frame's table without advancing, matching
-- MagnetTrain_InitLYOverrides, which ByteFills the whole array with the init
-- position before the loop starts.
function MagnetTrain:updateLYOverrides(initial)
  local ly = self.ly or {}
  if initial then
    for line = 1, SCREEN_HEIGHT_PX do ly[line] = self.initPosition end
    self.ly = ly
    self.scx = self.initPosition
    return ly
  end
  local scx = b(self.offset * 2)
  self.scx = scx
  local line = 1
  for _ = 1, 6 * TILE_WIDTH - 1 do ly[line] = scx; line = line + 1 end
  for _ = 1, 6 * TILE_WIDTH do ly[line] = self.position; line = line + 1 end
  for _ = 1, 6 * TILE_WIDTH + 1 do ly[line] = scx; line = line + 1 end
  self.ly = ly
  self.offset = b(self.offset + 2 * self.direction)
  return ly
end

-- The SCX each of the three bands is scrolled by this frame, as
-- { first scanline, last scanline (inclusive), scx }.  A band is a run of
-- equal LY overrides, so this is the same information the table holds and the
-- shape a renderer wants.
function MagnetTrain:bands()
  local ly = self.ly
  if not ly then return {} end
  local out = {}
  local start, value = 0, ly[1]
  for line = 1, SCREEN_HEIGHT_PX do
    if ly[line] ~= value then
      out[#out + 1] = { start, line - 2, value }
      start, value = line - 1, ly[line]
    end
  end
  out[#out + 1] = { start, SCREEN_HEIGHT_PX - 1, value }
  return out
end

--------------------------------------------------------------------------
-- The background
--------------------------------------------------------------------------

-- DrawMagnetTrain.  Rows 0-17 are MagnetTrainBGTiles' two-tile pair for that
-- row repeated across all 32 columns (`.FillAlt`, TILEMAP_WIDTH / 2 times),
-- and then MagnetTrainTilemap's four 20-tile lines are laid over rows 6-9.
--
-- Answers nil when the cache carries no tilemaps, which is what a cache built
-- before the extractor learned about them looks like.
function MagnetTrain:tilemap()
  local bg = self.bgTiles
  if not (bg and #bg >= SCREEN_HEIGHT * 2) then return nil end
  local rows = {}
  for row = 0, SCREEN_HEIGHT - 1 do
    local even, odd = bg[row * 2 + 1], bg[row * 2 + 2]
    local line = {}
    for col = 0, TILEMAP_WIDTH - 1 do
      line[col + 1] = (col % 2 == 0) and even or odd
    end
    rows[row + 1] = line
  end
  local fg = self.fgTilemap
  if fg and #fg >= SCREEN_WIDTH * FG_ROWS then
    for line = 0, FG_ROWS - 1 do
      local row = rows[FG_ROW + line + 1]
      for col = 0, SCREEN_WIDTH - 1 do
        row[col + 1] = fg[line * SCREEN_WIDTH + col + 1]
      end
    end
  end
  return rows
end

-- SetMagnetTrainPals, read back as "which palette does this cell use".
-- `col` and `row` are 0-based BG map coordinates.
function MagnetTrain.paletteSlot(col, row)
  if row == WINDOW_ROW and col >= WINDOW_COL
      and col < WINDOW_COL + WINDOW_WIDTH then
    return MagnetTrain.PAL_BG_YELLOW
  end
  if row < BUSH_ROWS_TOP then return MagnetTrain.PAL_BG_GREEN end
  if row < BUSH_ROWS_TOP + TRAIN_ROWS then return MagnetTrain.PAL_BG_GRAY end
  return MagnetTrain.PAL_BG_GREEN
end

--------------------------------------------------------------------------
-- The sprite
--------------------------------------------------------------------------

-- GetSpriteAnimFrame, cut down to one frameset that never waits, ends or
-- changes sequence.  A frame with duration 8 is therefore shown nine times:
-- the pass that sets the duration, then eight that decrement it.
function MagnetTrain:stepSpriteFrame()
  if not self.spriteX then return end
  if self.frameDuration ~= 0 then
    self.frameDuration = self.frameDuration - 1
    return
  end
  self.frame = self.frame + 1
  local entry = FRAMESET[self.frame + 1]
  if entry == "restart" then
    self.frame = 0
    entry = FRAMESET[1]
  end
  self.frameDuration = entry.duration
  self.oamFrame = entry
end

-- The four OAM entries the player is drawn as this frame, each
-- { x, y, tile, xflip } in SCREEN pixels (the hardware's byte minus the 8 and
-- 16 pixel OAM origins).  `tile` is the vtile the OAM set resolves to, which
-- MagnetTrain.SHEET_FRAME turns into a 16x16 frame of the walking sheet.
--
-- Empty before .InitPlayerSpriteAnim has run.
function MagnetTrain:playerOam()
  local entry = self.oamFrame
  if not (entry and self.spriteX) then return {} end
  local vtile = OAMSET_VTILE[entry.oamset]
  local out = {}
  for _, sprite in ipairs(OAM_DATA) do
    local x = b(self.spriteX + self.globalX + mirror(sprite.x, entry.xflip))
    local y = b(self.spriteY + sprite.y)
    out[#out + 1] = {
      x = x - 8,
      y = y - 16,
      tile = vtile + sprite.tile,
      xflip = entry.xflip,
    }
  end
  return out
end

return MagnetTrain
