-- Hall of Fame induction (engine/movie/hall_of_fame.asm): each party
-- member's back sprite sweeps across the screen and its front sprite then
-- scrolls onto the right side (HoFShowMonOrPlayer's two .ScrollPic passes,
-- #847), then HoFDisplayMonInfo draws the
-- left-side LEVEL/TYPE box, plays the cry, holds, and pops the bottom
-- "HALL OF FAME" text box before fading to the next mon.  After the
-- party, the player pic scrolls in and HoFDisplayPlayerStats shows the
-- name/time/money boxes plus the dex rating.  Plays Music_HallOfFame
-- when the audio data has it.  Calls onDone() after popping itself.

local Assets = require("src.render.Assets")
local Font = require("src.render.Font")
local TextBox = require("src.render.TextBox")
local Music = require("src.core.Music")
local Sound = require("src.core.Sound")
local TypeChart = require("src.battle.TypeChart")
local Strings = require("src.core.Strings")

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

-- SGB: SetPal_PokemonWholeScreen for the mon on display
function HallOfFame:sgbPalettes(game)
  local P = require("src.render.PaletteFX")
  if self.phase == "player" or self.phase == "player_stats"
      or self.phase == "player_dex" or self.phase == "player_rating" then
    return P.wholeNamed(game.data, "MEWMON")
  end
  local mon = game.save.party[self.index or 0]
  if mon then
    local c = P.monPal(game.data, mon.species)
    if c then return { P.whole(c) } end
    return nil
  end
  return P.wholeNamed(game.data, "MEWMON")
end

-- HoFShowMonOrPlayer's .ScrollPic: hSCX is nudged by e = 4px per
-- DelayFrame (doubled on SGB) until it settles.  The front pic rests at
-- hlcoord 12,5 (engine/movie/hall_of_fame.asm HoFLoadMonPlayerPicTileIDs).
local SCROLL_SPEED = 4 -- px/frame @ 60fps
local PIC_X, PIC_Y = 12 * 8, 5 * 8

-- The BACK pic pass that runs in front of every front pic (#847).
-- HoFShowMonOrPlayer loads the back pic (predef LoadMonBackPic, or
-- RedPicBack through HoFLoadPlayerPics) into the same 7x7 window at
-- hlcoord 12,5, points the tilemap at base tile $31, and sweeps hSCX from
-- $c0 to $a0 at e = 4 -- in screen pixels the pic enters at x = 160 and
-- leaves at x = -64, 56 frames.  Only then is the window re-pointed at the
-- front pic (base tile 0) with hSCY back to 0 and e = -4, so the front pic
-- starts at x = -64 rather than at its own width.
-- hSCY = $d0 during the back pass puts the window's top row at y = 88, so
-- its 7 tiles sit flush with the bottom of the screen.  ScaleSpriteByTwo
-- (engine/battle/scale_sprites.asm) doubles the 32x32 back sprite into that
-- 7x7 = 56x56 buffer with the last 4 source rows and the last source column
-- dropped, so what shows is the sprite's top-left 28x28 at 2x.
local BACK_START_X, BACK_END_X = 160, -64
local BACK_Y = 88
local BACK_SCALE = 2
local BACK_CROP = 28
local FRONT_START_X = -64

-- After HoFDisplayAndRecordMonInfo: 80 DelayFrames, then the bottom
-- HALL OF FAME box for 180 DelayFrames, then GBFadeOutToWhite.
local INFO_HOLD = 80
local HOF_HOLD = 180
local FADE_FRAMES = 20

-- HoFPrintTextAndDelay after each dex line
local DEX_HOLD = 120

-- through Assets.resolve so an enabled mod's overrides/ shadows these the
-- same way it shadows every other generated asset
local function tryImage(path)
  if not path then return nil end
  local ok, img = pcall(love.graphics.newImage, Assets.resolve(path))
  return ok and img or nil
end

-- POKéDEX rating tiers (engine/events/pokedex_rating.asm DexRatingsTable)
local function dexRatingKey(owned)
  if owned >= 150 then return "_DexRatingText_Own150To151" end
  local lo = math.floor(owned / 10) * 10
  return ("_DexRatingText_Own%dTo%d"):format(lo, lo + 9)
end

function HallOfFame.new(game, onDone)
  local self = setmetatable({}, HallOfFame)
  self.game = game
  self.onDone = onDone
  self.index = 0
  self.timer = 0
  self.phase = "mons"
  self.sprites = {} -- species -> image or false
  self.spriteTrueColor = {} -- species -> full-color art flag (#637)
  self.backs = {} -- species (or "@player") -> back image or false (#847)
  self.backTrueColor = {}
  local playerPath, playerTrueColor =
    require("src.pokemon.Sprites").playerPath(
      game.data, "front", { kind = "hof" })
  self.playerPic = tryImage(playerPath)
  self.playerTrueColor = self.playerPic and playerTrueColor or false
  self.scrollX = PIC_X
  self.showHofBanner = false
  self.fade = 0
  return self
end

function HallOfFame:enter()
  local data = self.game.data
  if data.audio and data.audio.songs and data.audio.songs.Music_HallOfFame then
    pcall(Music.play, data, "Music_HallOfFame")
  end
  self:nextMon()
end

function HallOfFame:nextMon()
  self.index = self.index + 1
  local mon = self.game.save.party[self.index]
  self.showHofBanner = false
  self.fade = 0
  self.backQuad = nil
  -- HoFShowMonOrPlayer runs the back pic sweep for a party member and for
  -- the player alike (wHoFMonOrPlayer only picks which pics get loaded), so
  -- both enter through the "back" phase and the front pic follows it (#847)
  self.phase = "back"
  self.afterBack = mon and "mons" or "player"
  self.timer = 0
  self.scrollX = BACK_START_X
end

function HallOfFame:spriteFor(species)
  local cached = self.sprites[species]
  if cached == nil then
    local path, trueColor = require("src.pokemon.Sprites").path(
      self.game.data, species, "front", { kind = "hof" })
    cached = tryImage(path) or false
    self.sprites[species] = cached
    self.spriteTrueColor[species] = cached and trueColor or false
  end
  return cached or nil
end

-- The back pic for the pass ahead of the current front pic: the party
-- member's own back sprite (predef LoadMonBackPic), or RedPicBack once the
-- party is done (HoFLoadPlayerPics).  Cached like spriteFor (#847).
function HallOfFame:backPicFor()
  local mon = self.game.save.party[self.index]
  local key = mon and mon.species or "@player"
  local cached = self.backs[key]
  if cached == nil then
    local Sprites = require("src.pokemon.Sprites")
    local path, trueColor
    if mon then
      path, trueColor = Sprites.path(self.game.data, mon.species, "back",
                                     { kind = "hof" })
    else
      path, trueColor = Sprites.playerPath(self.game.data, "back",
                                           { kind = "hof" })
    end
    cached = tryImage(path) or false
    self.backs[key] = cached
    self.backTrueColor[key] = cached and trueColor or false
  end
  return cached or nil, self.backTrueColor[key]
end

-- HoFDisplayPlayerStats' DisplayDexRating tally (also
-- OverworldController:dexRating / PokedexMenu.new's seen+owned counts)
function HallOfFame:dexSeenOwned()
  local dex = self.game.save.pokedex or { seen = {}, owned = {} }
  local seen, owned = 0, 0
  for _ in pairs(dex.seen or {}) do seen = seen + 1 end
  for _ in pairs(dex.owned or {}) do owned = owned + 1 end
  return seen, owned
end

function HallOfFame:advanceMonPhase()
  if self.phase == "mons" and not self.showHofBanner then
    -- 80-frame info hold done: TextBoxBorder at (2,13) + "HALL OF FAME"
    self.showHofBanner = true
    self.timer = HOF_HOLD
  elseif self.phase == "mons" then
    self.phase = "fade"
    self.timer = FADE_FRAMES
    self.fade = 0
  elseif self.phase == "fade" then
    self:nextMon()
  end
end

function HallOfFame:update(dt)
  local input = self.game.input
  local skip = input:wasPressed("a")

  -- .ScrollPic with d = $a0, e = 4: the back pic crosses the screen right to
  -- left and is gone before the front pic starts.  Both scrolls are plain
  -- DelayFrame loops in the ROM, so neither takes a button (#847).
  if self.phase == "back" then
    self.scrollX = self.scrollX - SCROLL_SPEED
    if self.scrollX <= BACK_END_X then
      self.phase = self.afterBack
      self.timer = self.phase == "mons" and INFO_HOLD or 0
      self.scrollX = FRONT_START_X
    end
    return
  end

  if self.phase == "mons" or self.phase == "fade" then
    if self.phase == "mons" and self.scrollX < PIC_X then
      self.scrollX = math.min(PIC_X, self.scrollX + SCROLL_SPEED)
      -- PlayCry is the tail of HoFDisplayMonInfo, which runs only after
      -- HoFShowMonOrPlayer's two scrolls have both settled (#847)
      if self.scrollX >= PIC_X then
        local mon = self.game.save.party[self.index]
        if mon then Sound.playCry(self.game.data, mon.species) end
      end
      return
    end
    if self.phase == "fade" then
      self.timer = self.timer - 1
      self.fade = 1 - math.max(0, self.timer) / FADE_FRAMES
      if self.timer <= 0 or skip then self:advanceMonPhase() end
      return
    end
    self.timer = self.timer - 1
    if skip or self.timer <= 0 then
      self:advanceMonPhase()
    end
  elseif self.phase == "player" then
    if self.scrollX < PIC_X then
      self.scrollX = math.min(PIC_X, self.scrollX + SCROLL_SPEED)
      return
    end
    self.phase = "player_stats"
    self.timer = DEX_HOLD
  elseif self.phase == "player_stats" then
    -- name / play time / money boxes are up; then the dex texts
    self.timer = self.timer - 1
    if skip or self.timer <= 0 then
      self.phase = "player_dex"
      self:showDexTexts()
    end
  end
  -- player_dex / player_rating are driven by the TextBox chain that
  -- showDexTexts pushes; nothing is timed here
end

-- HoFDisplayPlayerStats' three HoFPrintTextAndDelay calls: seen/owned,
-- "POKéDEX Rating:", then the tier text DisplayDexRating copied into
-- wDexRatingText -- each through PrintText (the standard two-row box, so
-- the tier text's \v rows scroll inside it) followed by 120 DelayFrames
-- with no button wait.  Hand-drawing these flattened the \v scrolls and
-- painted the third row over the box's bottom border (#314).
function HallOfFame:showDexTexts()
  local game = self.game
  local text = game.data.text or {}
  local seen, owned = self:dexSeenOwned()
  local seenOwned = (text._DexSeenOwnedText
      or Strings("POKéDEX   Seen:{NUM:wDexRatingNumMonsSeen, 1, 3}\n         Owned:{NUM:wDexRatingNumMonsOwned, 1, 3}"))
    :gsub("{NUM:wDexRatingNumMonsSeen[^}]*}", tostring(seen))
    :gsub("{NUM:wDexRatingNumMonsOwned[^}]*}", tostring(owned))
  local header = (text._DexRatingText or Strings("POKéDEX Rating{COLON}"))
    :gsub("{COLON}", ":")
  local rating = text[dexRatingKey(owned)] or Strings("Keep it up!")
  local function finish()
    -- HoFFadeOutScreenAndMusic -> Credits lead-in (no A wait here)
    game.stack:pop()
    if self.onDone then self.onDone() end
  end
  local function showRating()
    game.stack:push(TextBox.new(game, rating, finish,
                                { auto = { delay = DEX_HOLD } }))
  end
  local function showHeader()
    self.phase = "player_rating"
    game.stack:push(TextBox.new(game, header, showRating,
                                { auto = { delay = DEX_HOLD } }))
  end
  game.stack:push(TextBox.new(game, seenOwned, showHeader,
                              { auto = { delay = DEX_HOLD } }))
end

-- HoFDisplayMonInfo: TextBoxBorder (0,2) b=9,c=10 + LEVEL/TYPE labels
function HallOfFame:drawMonInfo(mon)
  local def = self.game.data.pokemon[mon.species]
  Font.drawBox(0, 2, 12, 11)
  love.graphics.setColor(0, 0, 0, 1)
  local name = mon.nickname or (def and def.name) or mon.species
  Font.draw(name, 1 * 8, 4 * 8)
  -- HoFMonInfoText is placed at (2,6) with "next" separators, and PlaceNextChar
  -- advances 2 * SCREEN_WIDTH per <NEXT> unless BIT_SINGLE_SPACED_LINES is set
  -- (home/text.asm); nothing on the HoF path sets it, so the labels sit on rows
  -- 6/8/10 lined up with their values, not 6/7/8 (#697).
  Font.draw(Strings("LEVEL/"), 2 * 8, 6 * 8)
  Font.draw(Strings("TYPE1/"), 2 * 8, 8 * 8)
  local t1 = def and def.types and def.types[1]
  local t2 = def and def.types and def.types[2]
  local dual = t2 and t2 ~= t1
  if dual then
    -- EraseType2Text blanks 6 tiles at hl+$13 from (3,9), so TYPE2/ is at (2,10)
    Font.draw(Strings("TYPE2/"), 2 * 8, 10 * 8)
  end
  -- PrintLevelCommon at (8,7): bare level digits (no <LV> tile here)
  Font.draw(tostring(mon.level), 8 * 8, 7 * 8)
  -- PrintMonType at (3,9) / +2 rows for type 2
  if t1 then
    Font.draw(TypeChart.displayName(t1), 3 * 8, 9 * 8)
  end
  if dual then
    Font.draw(TypeChart.displayName(t2), 3 * 8, 11 * 8)
  end
end

-- Bottom HALL OF FAME banner: TextBoxBorder (2,13) b=3,c=14
function HallOfFame:drawHofBanner()
  Font.drawBox(2, 13, 16, 5)
  love.graphics.setColor(0, 0, 0, 1)
  Font.draw(Strings("HALL OF FAME"), 4 * 8, 15 * 8)
end

function HallOfFame:drawPic(img, trueColor)
  if not img then return end
  love.graphics.setColor(1, 1, 1, 1)
  local x = self.scrollX or PIC_X
  love.graphics.draw(img, x, PIC_Y)
  -- SET_PAL_POKEMON_WHOLE_SCREEN (HoFShowMonOrPlayer) colors the WHOLE
  -- screen in the mon's palette, so sgbPalettes above hands the blit a
  -- whole-canvas zone -- and a full-color pic has to sit that remap out.
  -- Report the rect the pic covers for the unshaded pass, the way
  -- SummaryMenu does for the status screen pic (#637; #430).
  if trueColor then
    require("src.render.PaletteFX").markTrueColor(x, PIC_Y,
                                                 img:getDimensions())
  end
end

-- The back pic sweep (see the BACK_* constants): the same 7x7 window as the
-- front pic, one screen lower, cropped to the 28x28 ScaleSpriteByTwo keeps.
function HallOfFame:drawBackPic()
  local img, trueColor = self:backPicFor()
  if not img then return end
  local w = math.min(img:getWidth(), BACK_CROP)
  local h = math.min(img:getHeight(), BACK_CROP)
  if not self.backQuad then
    self.backQuad = love.graphics.newQuad(0, 0, w, h, img:getDimensions())
  end
  love.graphics.setColor(1, 1, 1, 1)
  love.graphics.draw(img, self.backQuad, self.scrollX, BACK_Y, 0,
                     BACK_SCALE, BACK_SCALE)
  -- same whole-screen palette exemption the front pic takes (#637)
  if trueColor then
    require("src.render.PaletteFX").markTrueColor(self.scrollX, BACK_Y,
                                                 w * BACK_SCALE,
                                                 h * BACK_SCALE)
  end
end

-- HoFDisplayPlayerStats boxes + labels (player pic already on the right)
function HallOfFame:drawPlayerStats()
  local save = self.game.save
  -- name box: TextBoxBorder (5,0) b=2,c=9 → drawBox(5,0,11,4)
  Font.drawBox(5, 0, 11, 4)
  love.graphics.setColor(0, 0, 0, 1)
  Font.draw(save.player.name or "RED", 7 * 8, 2 * 8)

  -- play time / money box: TextBoxBorder (0,4) b=6,c=10 → drawBox(0,4,12,8)
  Font.drawBox(0, 4, 12, 8)
  love.graphics.setColor(0, 0, 0, 1)
  Font.draw(Strings("PLAY TIME"), 1 * 8, 6 * 8)
  local t = math.floor(save.playTime or 0)
  Font.draw(("%3d:%02d"):format(math.floor(t / 3600), math.floor(t / 60) % 60),
            5 * 8, 7 * 8)
  Font.draw(Strings("MONEY"), 1 * 8, 9 * 8)
  -- PrintBCDNumber with MONEY_SIGN; port uses ¥ like TrainerCard
  Font.draw(("¥%d"):format(save.money or 0), 4 * 8, 10 * 8)
end

function HallOfFame:draw()
  love.graphics.setColor(1, 1, 1, 1)
  love.graphics.rectangle("fill", 0, 0, 160, 144)

  if self.phase == "back" then
    self:drawBackPic()
  elseif self.phase == "mons" or self.phase == "fade" then
    local mon = self.game.save.party[self.index]
    if mon then
      self:drawPic(self:spriteFor(mon.species),
                   self.spriteTrueColor[mon.species])
      if self.scrollX >= PIC_X then
        self:drawMonInfo(mon)
        if self.showHofBanner then
          self:drawHofBanner()
        end
      end
    end
    if self.phase == "fade" and self.fade > 0 then
      love.graphics.setColor(1, 1, 1, self.fade)
      love.graphics.rectangle("fill", 0, 0, 160, 144)
    end
  elseif self.phase == "player" then
    self:drawPic(self.playerPic, self.playerTrueColor)
  elseif self.phase == "player_stats" then
    self:drawPic(self.playerPic, self.playerTrueColor)
    self:drawPlayerStats()
  elseif self.phase == "player_dex" or self.phase == "player_rating" then
    -- the TextBox chain draws the dex texts over the stat boxes
    self:drawPic(self.playerPic, self.playerTrueColor)
    self:drawPlayerStats()
  end

  love.graphics.setColor(1, 1, 1, 1)
end

return HallOfFame
