-- The Magnet Train ride, drawn (pokegold engine/events/magnet_train.asm).
--
-- src/core/gen2/MagnetTrain.lua is the routine: the jumptable, the three SCX
-- bands and the player's frameset, all love-free.  This is the presentation,
-- and it is the cart's presentation rather than an approximation of it:
--
--   * MagnetTrain_LoadGFX_PlayMusic loads NO background tiles.  The train, the
--     bushes and the window are all drawn out of whatever the map's tileset
--     already had in VRAM, which for both stations is TILESET_TRAIN_STATION --
--     so the sheet this screen bakes from is the same one the overworld was
--     drawing a frame earlier.
--   * The two tilemaps behind DrawMagnetTrain (MagnetTrainBGTiles, a 2x18
--     vertical strip repeated across all 32 columns, and MagnetTrainTilemap,
--     the 20x4 train laid over rows 6-9) come from the extracted cache at
--     data.gen2Field.magnetTrain.  A cache built before the extractor
--     learned to follow them has neither, and then the ride runs with a
--     blank screen rather than with invented art.
--   * The background is baked into a 256x144 canvas once, because the only
--     thing that changes per frame is the per-band SCX.
--   * SetMagnetTrainPals gives the four bush rows and the four bottom rows
--     PAL_BG_GREEN, the ten train rows PAL_BG_GRAY and the six window tiles
--     PAL_BG_YELLOW, out of a TOWN palette set at the current time of day
--     (the routine pushes wEnvironment, forces TOWN, and pops it back).
--
-- Two hardware details from the cart are faithfully reproduced:
-- 1. Viewport wrapping: the 256x144 background scrolls in 3 LY override bands,
--    wrapped strictly within the 160x144 GB screen so pixels do not bleed into
--    the widescreen surround.
-- 2. OAM Priority (OAM_PRIO): the four player OBJs sit behind background
--    colours 1-3 and only show through the window's colour 0.  This is drawn
--    via a base background pass, the player sprite, and an overlay pass where
--    shade 0 is transparent and solid train tiles mask the sprite.

local Assets = require("src.render.Assets")
local Chrome = require("src.ui.gen2.Chrome")
local FieldMoves = require("src.world.gen2.FieldMoves")
local GbcPalette = require("src.render.GbcPalette")
local MagnetTrain = require("src.core.gen2.MagnetTrain")
local Music = require("src.core.Music")
local Palettes = require("src.world.gen2.Palettes")
local Sound = require("src.core.Sound")

local MagnetTrainRide = {}
MagnetTrainRide.__index = MagnetTrainRide
MagnetTrainRide.isOpaque = true

local SCREEN_W, SCREEN_H = 160, 144
local BG_W = 256           -- TILEMAP_WIDTH * 8
local TILES_PER_ROW = 16   -- every 2bpp sheet the importer writes
local BLACK = { 0, 0, 0 }

-- Overlay shader for OAM_PRIO: shade 0 is transparent; shades 1-3 are solid
-- palette colors, masking any sprites beneath solid background tiles.
local OVERLAY_SHADER_SOURCE = [[
extern vec3 pal0;
extern vec3 pal1;
extern vec3 pal2;
extern vec3 pal3;

vec4 effect(vec4 tint, Image tex, vec2 uv, vec2 screen) {
  vec4 px = Texel(tex, uv);
  float shade = floor((1.0 - px.r) * 3.0 + 0.5);
  vec3 rgb = pal0;
  if (shade > 2.5) {
    rgb = pal3;
  } else if (shade > 1.5) {
    rgb = pal2;
  } else if (shade > 0.5) {
    rgb = pal1;
  }
  float alpha = shade < 0.5 ? 0.0 : px.a;
  return vec4(rgb, alpha) * tint;
}
]]

function MagnetTrainRide:wantsFillScale() return true end

-- opts: toGoldenrod (the wScriptVar the officer's script set), onDone()
function MagnetTrainRide.new(game, opts)
  opts = opts or {}
  local self = setmetatable({}, MagnetTrainRide)
  self.game = game
  self.data = game and game.data
  self.onDone = opts.onDone
  self.finished = false
  local save = opts.save or (game and game.save)
  self.gender = save and save.player and save.player.gender or nil

  local field = self.data and self.data.gen2Field
  local gfx = field and field.magnetTrain
  self.ride = MagnetTrain.new({
    toGoldenrod = opts.toGoldenrod,
    bgTiles = gfx and gfx.bgTiles,
    fgTilemap = gfx and gfx.tilemap,
  })

  self.tileset = self.data and self.data.gen2Tilesets
    and self.data.gen2Tilesets.TILESET_TRAIN_STATION
  self.palettes = self:bgPalettes()
  self.spriteSheet = self:playerSheet()

  -- PlayMusic2 MUSIC_MAGNET_TRAIN, the last thing the GFX load does.
  local songs = self.data and self.data.audio and self.data.audio.songs
  if songs and songs.Music_MagnetTrain then
    Music.stop()
    Music.play(self.data, "Music_MagnetTrain", true, { reason = "magnettrain" })
  end
  return self
end

-- GetSGBLayout with wEnvironment forced to TOWN and wTimeOfDayPal taken from
-- the real clock, so the bushes are the colour the outside world is right now
-- even though both stations are INDOOR maps.
function MagnetTrainRide:bgPalettes()
  local data = self.data
  local palettes = data and data.gen2Palettes
  if not palettes then return nil end
  return Palettes.bgSet(palettes, { environment = "TOWN" },
    Palettes.clockDaytime())
end

function MagnetTrainRide:palette(slot)
  local set = self.palettes
  local colors = set and (set[slot] or set[1])
  return colors
end

--------------------------------------------------------------------------
-- Sheets & Shaders
--------------------------------------------------------------------------

local function sheetFor(path)
  if not path then return nil end
  -- `and` would truncate pcall's second return, so this cannot fold into one
  -- expression.
  local ok, image = pcall(Assets.image, path)
  if not (ok and image) then return nil end
  image:setFilter("nearest", "nearest")
  local width, height = image:getDimensions()
  local quads = {}
  for tile = 0, math.floor(width / 8) * math.floor(height / 8) - 1 do
    quads[tile] = love.graphics.newQuad(
      tile % TILES_PER_ROW * 8, math.floor(tile / TILES_PER_ROW) * 8,
      8, 8, width, height)
  end
  return { image = image, quads = quads, width = width, height = height }
end

function MagnetTrainRide:tileSheet()
  if self.sheet == nil then
    self.sheet = sheetFor(self.tileset and self.tileset.image) or false
  end
  return self.sheet or nil
end

-- The player's own overworld sheet: six 16x16 frames stacked vertically, of
-- which MagnetTrain.SHEET_FRAME names the two the cutscene requests.  Quads
-- are cut per 8x8 sub-tile rather than by the sheet's 16-pixel width, because
-- the OAM data addresses the four tiles of a frame individually.
function MagnetTrainRide:playerSheet()
  local def = self:playerSpriteDef()
  local path = def and def.image
  if not path then return nil end
  local ok, image = pcall(Assets.image, path)
  if not (ok and image) then return nil end
  image:setFilter("nearest", "nearest")
  return { image = image }
end

-- The quad for one vtile of the two loaded blocks.  A 2x2 frame is stored
-- top-left, top-right, bottom-left, bottom-right, which is the order
-- .OAMData_MagnetTrainRed's four `dbsprite` rows walk.
function MagnetTrainRide:playerQuad(vtile)
  local sheet = self.spriteSheet
  if not sheet then return nil end
  local frame = MagnetTrain.SHEET_FRAME[vtile - (vtile % 4)]
  if not frame then return nil end
  self.playerQuads = self.playerQuads or {}
  local quad = self.playerQuads[vtile]
  if not quad then
    local sub = vtile % 4
    local w, h = sheet.image:getDimensions()
    quad = love.graphics.newQuad(
      (sub % 2) * 8, frame * 16 + math.floor(sub / 2) * 8, 8, 8, w, h)
    self.playerQuads[vtile] = quad
  end
  return quad
end

function MagnetTrainRide:overlayShader()
  if self.ovShader ~= nil then return self.ovShader or nil end
  if not (love and love.graphics and love.graphics.newShader) then
    self.ovShader = false
    return nil
  end
  local ok, result = pcall(love.graphics.newShader, OVERLAY_SHADER_SOURCE)
  self.ovShader = ok and result or false
  return self.ovShader or nil
end

local function palChannel(colors, index)
  local c = colors and colors[index]
  if not c then return 0, 0, 0 end
  return (c[1] or 0) / 255, (c[2] or 0) / 255, (c[3] or 0) / 255
end

function MagnetTrainRide:useOverlayShader(colors)
  local sh = self:overlayShader()
  if not sh then return false end
  local resolved = GbcPalette.resolve(colors)
  for i = 0, 3 do
    local u = "pal" .. i
    if sh:hasUniform(u) then
      local r, g, b = palChannel(resolved, i + 1)
      sh:send(u, { r, g, b })
    end
  end
  love.graphics.setShader(sh)
  return true
end


--------------------------------------------------------------------------
-- The baked backgrounds (Base + OAM_PRIO Overlay)
--------------------------------------------------------------------------

-- DrawMagnetTrain plus SetMagnetTrainPals, rendered once into 256x144 canvases.
-- Base canvas contains the full opaque background; Overlay canvas contains
-- solid shades 1-3 with shade 0 transparent for OAM_PRIO window masking.
function MagnetTrainRide:backgrounds()
  if self.bgCanvas ~= nil then
    return self.bgCanvas or nil, self.bgOverlayCanvas or nil
  end
  self.bgCanvas = false
  self.bgOverlayCanvas = false
  local rows = self.ride:tilemap()
  local sheet = self:tileSheet()
  if not (rows and sheet) then return nil, nil end

  local ok1, baseCanvas = pcall(love.graphics.newCanvas, BG_W, SCREEN_H)
  if not ok1 then return nil, nil end
  baseCanvas:setFilter("nearest", "nearest")

  local ok2, overlayCanvas = pcall(love.graphics.newCanvas, BG_W, SCREEN_H)
  if ok2 and overlayCanvas then
    overlayCanvas:setFilter("nearest", "nearest")
  else
    overlayCanvas = nil
  end

  local groups = {}
  for row = 1, #rows do
    for col = 1, #rows[row] do
      local slot = MagnetTrain.paletteSlot(col - 1, row - 1)
      groups[slot] = groups[slot] or {}
      local list = groups[slot]
      list[#list + 1] = { rows[row][col], (col - 1) * 8, (row - 1) * 8 }
    end
  end

  local G = love.graphics
  local previous = G.getCanvas()
  G.push("all")
  G.origin()
  G.setScissor()

  -- 1. Base background canvas (shades 0-3)
  G.setCanvas(baseCanvas)
  G.clear(0, 0, 0, 1)
  G.setColor(1, 1, 1, 1)
  local shader = GbcPalette.available()
  for slot, list in pairs(groups) do
    local colors = self:palette(slot)
    if shader and colors then GbcPalette.use(colors) end
    for _, cell in ipairs(list) do
      local quad = sheet.quads[cell[1]]
      if quad then G.draw(sheet.image, quad, cell[2], cell[3]) end
    end
  end
  if shader then GbcPalette.clear() end

  -- 2. Overlay canvas for OAM_PRIO (shades 1-3 solid, shade 0 transparent)
  if overlayCanvas then
    G.setCanvas(overlayCanvas)
    G.clear(0, 0, 0, 0)
    G.setColor(1, 1, 1, 1)
    local ovShader = self:overlayShader()
    for slot, list in pairs(groups) do
      local colors = self:palette(slot)
      if ovShader and colors then self:useOverlayShader(colors) end
      for _, cell in ipairs(list) do
        local quad = sheet.quads[cell[1]]
        if quad then G.draw(sheet.image, quad, cell[2], cell[3]) end
      end
    end
    if ovShader then G.setShader() end
  end

  G.setCanvas(previous)
  G.pop()

  self.bgCanvas = baseCanvas
  self.bgOverlayCanvas = overlayCanvas
  return baseCanvas, overlayCanvas
end

function MagnetTrainRide:background()
  local base = self:backgrounds()
  return base
end

--------------------------------------------------------------------------
-- Frame
--------------------------------------------------------------------------

function MagnetTrainRide:playSfx(name)
  local audio = self.data and self.data.audio
  local sfx = audio and audio.sfx
  if sfx and sfx[Sound.resolve(self.data, name)] then
    Sound.play(self.data, name)
  end
end

function MagnetTrainRide:finish()
  if self.finished then return end
  self.finished = true
  if self.onDone then self.onDone() end
end

function MagnetTrainRide:update(_dt)
  if self.finished then return end
  local sfx = self.ride:update()
  if sfx then self:playSfx(sfx) end
  if self.ride:done() then
    -- MagnetTrain's .done tears the screen back down and returns to the
    -- script, which then runs `warpcheck` and `newloadmap MAPSETUP_TRAIN`.
    self:finish()
  end
end

-- Draws the 3 LY override bands strictly wrapped inside the 160px GB screen.
function MagnetTrainRide:drawBands(canvas)
  if not canvas then return end
  local G = love.graphics
  G.setColor(1, 1, 1, 1)
  self.bandQuad = self.bandQuad
    or love.graphics.newQuad(0, 0, SCREEN_W, 1, BG_W, SCREEN_H)
  for _, band in ipairs(self.ride:bands()) do
    local top, bottom, scx = band[1], band[2], band[3] % BG_W
    local height = bottom - top + 1
    local w1 = math.min(SCREEN_W, BG_W - scx)
    self.bandQuad:setViewport(scx, top, w1, height, BG_W, SCREEN_H)
    G.draw(canvas, self.bandQuad, 0, top)
    if w1 < SCREEN_W then
      local w2 = SCREEN_W - w1
      self.bandQuad:setViewport(0, top, w2, height, BG_W, SCREEN_H)
      G.draw(canvas, self.bandQuad, w1, top)
    end
  end
end

-- GetPlayerIcon's sheet, which .InitPlayerSpriteAnim pairs with
-- SPRITE_ANIM_OBJ_MAGNET_TRAIN_RED or _BLUE
-- (../pokecrystal/engine/events/magnet_train.asm:131-141, :291-305).
function MagnetTrainRide:playerSpriteDef()
  local sprites = self.data and self.data.gen2Sprites
  if not sprites then return nil end
  return sprites[FieldMoves.playerSprite(self.gender)]
    or sprites.SPRITE_CHRIS or sprites.SPRITE_KRIS
end

-- MapObjectPals' PAL_OW_RED, the palette every .OAMData_MagnetTrainRed entry
-- names, or PAL_OW_BLUE for the _BLUE object Kris rides under.
function MagnetTrainRide:playerPalette()
  local palettes = self.data and self.data.gen2Palettes
  if not palettes then return nil end
  return Palettes.spritePalette(palettes, Palettes.clockDaytime(),
    self:playerSpriteDef())
end

function MagnetTrainRide:drawPlayer()
  local sheet = self.spriteSheet
  if not sheet then return end
  local oam = self.ride:playerOam()
  if #oam == 0 then return end
  local G = love.graphics
  local shader = GbcPalette.available()
  local colors = shader and self:playerPalette()
  if colors then GbcPalette.use(colors) end
  G.setColor(1, 1, 1, 1)
  for _, entry in ipairs(oam) do
    if entry.x >= -8 and entry.x < SCREEN_W and entry.y >= -16 and entry.y < SCREEN_H then
      local quad = self:playerQuad(entry.tile)
      if quad then
        G.draw(sheet.image, quad,
          entry.x + (entry.xflip and 8 or 0), entry.y, 0,
          entry.xflip and -1 or 1, 1)
      end
    end
  end
  if colors then GbcPalette.clear() end
end


-- The backdrop is BG colour 0 of the gray palette the train body uses; on the
-- cart it is what shows wherever nothing was drawn.
function MagnetTrainRide:backdrop()
  return GbcPalette.color(self:palette(MagnetTrain.PAL_BG_GRAY), 1) or BLACK
end

function MagnetTrainRide:drawPanel()
  local G = love.graphics
  local backdrop = self:backdrop()
  G.setColor(backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255, 1)
  G.rectangle("fill", 0, 0, SCREEN_W, SCREEN_H)
  G.setColor(1, 1, 1, 1)

  local base, overlay = self:backgrounds()
  -- 1. Base background (bushes, train body, window aperture)
  self:drawBands(base)
  -- 2. Player sprite (OAM_PRIO: sits behind train body, visible through window)
  self:drawPlayer()
  -- 3. Train body / frame overlay (colors 1-3 mask sprite; color 0 is transparent)
  self:drawBands(overlay)

  G.setColor(1, 1, 1, 1)
end

function MagnetTrainRide:draw()
  local G = love.graphics
  G.push("all")
  G.setScissor(0, 0, SCREEN_W, SCREEN_H)
  self:drawPanel()
  G.pop()
end

-- MagnetTrain_LoadGFX_PlayMusic opens on ClearBGPalettes / ClearSprites
-- (engine/events/magnet_train.asm:101-103), so nothing of the station is left
-- behind the ride.  The surround has to be the same gray backdrop the panel
-- computes: a white letterbox would frame the train in a colour the cart never
-- puts on this screen.
function MagnetTrainRide:drawsWidescreen() return true end

function MagnetTrainRide:drawWidescreen(winW, winH)
  local G = love.graphics
  local backdrop = self:backdrop()
  Chrome.letterbox(winW, winH, backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255)
  G.setColor(1, 1, 1, 1)
  local scale = Chrome.fitScale(winW, winH)
  local ox, oy = Chrome.fitOrigin(winW, winH, scale)
  G.push("all")
  G.setScissor(ox, oy, SCREEN_W * scale, SCREEN_H * scale)
  G.translate(ox, oy)
  G.scale(scale, scale)
  self:drawPanel()
  G.pop()
end

return MagnetTrainRide



