-- Gold's card flip (engine/games/card_flip.asm _CardFlip), the `special
-- CardFlip` the Goldenrod Game Corner's second machine calls.
--
-- Three coins a go.  A 24 card deck is dealt two at a time; the player picks one
-- of the two face-down cards without seeing it, then bets on a square of a 6x8
-- board, and the card is turned over.  A card is a Pokemon (Pikachu,
-- Jigglypuff, Poliwag, Oddish) and a level (1-6), packed as level * 4 + mon --
-- which is why every win condition in the ASM is a mask on the card byte and
-- why the exact-card squares can just `cp e` against a literal 0-23.
--
-- The payout ladder, from CardFlip_CheckWinCondition:
--
--   6   a PAIR of Pokemon      (Pikachu/Jigglypuff, or Poliwag/Oddish)
--   9   a PAIR of levels       (1-2, 3-4, or 5-6)
--   12  one Pokemon
--   18  one level
--   72  the exact card
--
-- The four squares where a Pokemon pair meets a level pair are .Impossible:
-- they are on the board, the cursor can sit on them, and they always lose.
--
-- Everything that decides an outcome is a pure function here, taking a
-- `random(n) -> 0..n-1` the way src/battle/gen2 does, so a test can shuffle and
-- score thousands of hands.  The screen half is the only part that touches love.
--
-- Layout is transcribed from the ASM, not laid out by eye.  The cursor OAM
-- table is in OAM space, which sits 8px right and 16px down of the screen, so a
-- `cardflip_cursor 13, 5` anchor is screen tile (12, 3):
--
--   CardFlip_InitTilemap    fills the screen with $29 and copies
--                           CardFlipTilemap to hlcoord 9, 0 as 12 rows x 11
--                           columns -- so the odds board is (9,0)-(19,11)
--   .ChooseACard            writes CARDFLIP_LIGHT_ON at hlcoord 9, 0 plus
--                           wCardFlipNumCardsPlayed rows: twelve lights down
--                           column 9, one per hand in the deck
--   CardFlip_InitAttrPals   colours 2x2 boxes at (12,1), (14,1), (16,1) and
--                           (18,1) -- the four Pokemon headers
--   .Level1 .. .Level6      blank a discarded card at hlcoord 13, 3 / 4 / 6 /
--                           7 / 9 / 10 plus two columns per Pokemon, so the
--                           24 card cells are columns 13, 15, 17, 19 on those
--                           six rows
--   GetCoordsOfChosenCard   hlcoord 2, 0 and hlcoord 2, 6, each a 6x5 box; the
--                           level digit lands at +3 +SCREEN_WIDTH and the 3x3
--                           Pokepic one SCREEN_HEIGHT further on
--   CardFlip_PrintCoinBalance
--                           Textbox at (9,15) with a 9x1 interior, "COIN" at
--                           (10,16) and four leading-zero digits at (15,16)
--   CardFlip_UpdateCoinBalanceDisplay
--                           Textbox at (0,12) with an 18x4 interior
--
-- The cart's own art (gfx/card_flip/card_flip_1..3.2bpp.lz and
-- gfx/card_flip/card_flip.tilemap) is extracted into assets/generated/card_flip/
-- when the Gold/Silver manifest carries CardFlip*.  Until those files exist,
-- the board draws as labelled cells.

local Chrome = require("src.ui.gen2.Chrome")
local CoinCase = require("src.core.gen2.CoinCase")
local Sound = require("src.core.Sound")

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

-- constants/misc_constants.asm: CARDFLIP_DECK_SIZE EQU 4 * 6.
CardFlip.DECK_SIZE = 24
CardFlip.NUM_MONS = 4
CardFlip.NUM_LEVELS = 6
-- .DeductCoins: `ld de, -3`.
CardFlip.BET = 3

-- The Pokemon order is the one .Deck's pic anchors and .Pikachu/.Jigglypuff/
-- .Poliwag/.Oddish's `and $3` comparisons agree on.
CardFlip.MONS = { [0] = "PIKACHU", [1] = "JIGGLYPUFF", [2] = "POLIWAG",
  [3] = "ODDISH" }
CardFlip.MON_LABELS = { [0] = "PI", [1] = "JI", [2] = "PO", [3] = "OD" }

-- card = level * 4 + mon.  Everywhere the ASM masks the card byte:
--   and $3   the Pokemon
--   and $1c  the level, times four
--   and $18  the level PAIR, since bit 2 is the odd/even level
function CardFlip.mon(card) return card % 4 end
function CardFlip.level(card) return math.floor(card / 4) end
function CardFlip.levelPair(card) return math.floor(card / 8) end
function CardFlip.card(level, mon) return level * 4 + mon end

-- ------------------------------------------------------------------- deck
--
-- CardFlip_ShuffleDeck, transcribed rather than replaced with a Fisher-Yates:
-- the cart fills the deck with zeroes, then drops the values 23 down to 1 into
-- random empty slots, rejecting a roll of 24-31 (`and $1f / cp
-- CARDFLIP_DECK_SIZE`) and rejecting an occupied slot.  Card 0 is never placed
-- -- the single slot still holding zero IS card 0, which is why "empty" and
-- "card 0" can share a value without breaking anything.
-- `random` is injectable, and the rejection loop is unbounded on the cart
-- because hRandomAdd is never degenerate.  A source that only ever returns one
-- number would spin here forever, so the retries are capped and the fallback is
-- a linear probe for the next empty slot: unreachable with any real RNG, and
-- the difference between a wrong shuffle and a hung game if one is handed in.
local SHUFFLE_RETRIES = 256

function CardFlip.shuffle(random)
  local deck = {}
  for i = 1, CardFlip.DECK_SIZE do deck[i] = 0 end
  local value = CardFlip.DECK_SIZE - 1
  while value > 0 do
    local slot, tries = nil, 0
    repeat
      slot = random(32)
      tries = tries + 1
    until (slot < CardFlip.DECK_SIZE and deck[slot + 1] == 0)
      or tries >= SHUFFLE_RETRIES
    if slot >= CardFlip.DECK_SIZE or deck[slot + 1] ~= 0 then
      slot = 0
      while slot < CardFlip.DECK_SIZE and deck[slot + 1] ~= 0 do
        slot = slot + 1
      end
    end
    deck[slot + 1] = value
    value = value - 1
  end
  return deck
end

-- .CheckTheCard: wDeck + numCardsPlayed * 2 + whichCard.  `which` is 0 or 1,
-- `played` is 0-based, and the deck runs out after twelve hands (.Continue's
-- `cp 12`), at which point the deck is reshuffled.
function CardFlip.dealt(deck, played, which)
  return deck[played * 2 + which + 1]
end

CardFlip.HANDS_PER_DECK = 12

-- ------------------------------------------------------------------ board
--
-- CardFlip_CheckWinCondition's jumptable, read through CollapseCursorPosition
-- (index = cursorY * 6 + cursorX).  Rows and columns are the cart's, 0-based:
--
--   x = 0        the level PAIR column, one entry per two rows
--   x = 1        the single level column
--   x = 2..5     one column per Pokemon
--   y = 0        the Pokemon PAIR row, one entry per two columns
--   y = 1        the single Pokemon row
--   y = 2..7     one row per level
--
-- and the four squares at x < 2, y < 2 are .Impossible.
CardFlip.PAYOUT_MON_PAIR = 6
CardFlip.PAYOUT_LEVEL_PAIR = 9
CardFlip.PAYOUT_MON = 12
CardFlip.PAYOUT_LEVEL = 18
CardFlip.PAYOUT_CARD = 72

CardFlip.BOARD_W = 6
CardFlip.BOARD_H = 8

-- BOARD[y][x], 0-based on both axes.
CardFlip.BOARD = {}
for y = 0, CardFlip.BOARD_H - 1 do
  local row = {}
  for x = 0, CardFlip.BOARD_W - 1 do
    local cell
    if y < 2 and x < 2 then
      cell = { kind = "impossible", payout = 0 }
    elseif y == 0 then
      -- .PikaJiggly / .PoliOddish: `and $2` splits the four Pokemon in half.
      cell = { kind = "monPair", value = math.floor((x - 2) / 2),
        payout = CardFlip.PAYOUT_MON_PAIR }
    elseif y == 1 then
      cell = { kind = "mon", value = x - 2, payout = CardFlip.PAYOUT_MON }
    elseif x == 0 then
      -- .OneTwo / .ThreeFour / .FiveSix, each reached from two rows.
      cell = { kind = "levelPair", value = math.floor((y - 2) / 2),
        payout = CardFlip.PAYOUT_LEVEL_PAIR }
    elseif x == 1 then
      cell = { kind = "level", value = y - 2, payout = CardFlip.PAYOUT_LEVEL }
    else
      cell = { kind = "card", value = CardFlip.card(y - 2, x - 2),
        payout = CardFlip.PAYOUT_CARD }
    end
    row[x] = cell
  end
  CardFlip.BOARD[y] = row
end

function CardFlip.cell(x, y)
  local row = CardFlip.BOARD[y]
  return row and row[x] or nil
end

-- What a square pays against a card, or 0.  Only one square is ever bet on, so
-- there is no stacking to worry about.
function CardFlip.payout(x, y, card)
  local cell = CardFlip.cell(x, y)
  if not cell or not card then return 0 end
  local kind = cell.kind
  if kind == "impossible" then return 0 end
  if kind == "monPair" then
    return math.floor(CardFlip.mon(card) / 2) == cell.value and cell.payout or 0
  end
  if kind == "mon" then
    return CardFlip.mon(card) == cell.value and cell.payout or 0
  end
  if kind == "levelPair" then
    return CardFlip.levelPair(card) == cell.value and cell.payout or 0
  end
  if kind == "level" then
    return CardFlip.level(card) == cell.value and cell.payout or 0
  end
  return card == cell.value and cell.payout or 0
end

-- ---------------------------------------------------------- cursor moves
--
-- ChooseCard_HandleJoypad.  The board is not a plain grid: column 0 spans two
-- rows per entry and row 0 spans two columns, so moving off a paired square
-- SNAPS the other axis even before the step (`and $e`), and moving left or up
-- out of a paired square teleports to a fixed neighbour rather than stepping.
--
-- Returns the new x, y.  Transcribed branch for branch; the `and $e` snaps and
-- the two teleports are the whole reason this is not three lines.
function CardFlip.moveCursor(x, y, direction)
  if direction == "left" then
    if y == 0 then
      -- .mon_pair_left: snap x even, then either teleport or step two.
      x = x - (x % 2)
      if x < 3 then return 1, 2 end -- .left_to_number_gp
      return x - 2, y
    end
    if y == 1 then
      -- .mon_group_left
      if x < 3 then return 1, 2 end
      return x - 1, y
    end
    if x == 0 then return x, y end
    return x - 1, y
  end

  if direction == "right" then
    if y == 0 then
      -- .mon_pair_right
      x = x - (x % 2)
      if x >= 4 then return x, y end
      return x + 2, y
    end
    if x >= 5 then return x, y end
    return x + 1, y
  end

  if direction == "up" then
    if x == 0 then
      -- .num_pair_up
      y = y - (y % 2)
      if y < 3 then return 2, 1 end -- .up_to_mon_group
      return x, y - 2
    end
    if x == 1 then
      -- .num_gp_up
      if y < 3 then return 2, 1 end
      return x, y - 1
    end
    if y == 0 then return x, y end
    return x, y - 1
  end

  if direction == "down" then
    if x == 0 then
      -- .num_pair_down
      y = y - (y % 2)
      if y >= 6 then return x, y end
      return x, y + 2
    end
    if y >= 7 then return x, y end
    return x, y + 1
  end

  return x, y
end

-- ------------------------------------------------------------------ layout
--
-- Screen tile coordinates, derived from the OAM cursor anchors (minus one tile
-- across and two down) and from the discard-blanking hlcoords.
local LIGHT_X = 9                        -- twelve hand lights, rows 0-11
local MON_COL = { [2] = 12, [3] = 14, [4] = 16, [5] = 18 }
local LEVEL_PAIR_COL, LEVEL_COL = 10, 11
local CARD_COL = { [2] = 13, [3] = 15, [4] = 17, [5] = 19 }
-- .Level1 .. .Level6's rows.  The pairs overlap by a row on the cart, which is
-- the stacked-card look; the anchors are what matter here.
local LEVEL_ROW = { [2] = 3, [3] = 4, [4] = 6, [5] = 7, [6] = 9, [7] = 10 }
local MON_PAIR_ROW, MON_ROW = 0, 1

-- GetCoordsOfChosenCard.
local CARD_BOX = { { x = 2, y = 0 }, { x = 2, y = 6 } }
local CARD_BOX_W, CARD_BOX_H = 5, 6

local COIN_BOX_X, COIN_BOX_Y, COIN_BOX_W, COIN_BOX_H = 9, 15, 11, 3
local COIN_LABEL_X, COIN_LABEL_Y = 10, 16
local COIN_VALUE_X, COIN_VALUE_Y = 15, 16

local TEXT_BOX_X, TEXT_BOX_Y, TEXT_BOX_W, TEXT_BOX_H = 0, 12, 20, 6
local TEXT_X, TEXT_Y, TEXT_LINE = 1, 14, 2

-- ------------------------------------------------------------------- text
-- data/text/common_3.asm; none of these are in the cache's text.lua, because no
-- script bytecode the extractor walks points at them.
CardFlip.TEXTS = {
  playWithThree = { "Play with", "3 coins?" },
  notEnough = { "Not enough", "coins." },
  chooseACard = { "Choose a", "card." },
  placeYourBet = { "Place", "your bet" },
  playAgain = { "Play", "again?" },
  shuffled = { "The cards", "shuffled." },
  yeah = { "Yeah!" },
  darn = { "Darn…" },
}

local SFX_TRANSACTION = "Sfx_Transaction"
local SFX_KINESIS = "Sfx_Kinesis"
local SFX_START = "Sfx_SlotMachineStart"
local SFX_CHOOSE = "Sfx_ChooseACard"
local SFX_MOVE = "Sfx_PokeballsPlacedOnTable"
local SFX_WIN = "Sfx_2ndPlace"
local SFX_WRONG = "Sfx_Wrong"
local SFX_PAY_DAY = "Sfx_PayDay"
local SFX_QUIT = "Sfx_QuitSlots"

-- ------------------------------------------------------------------ screen
function CardFlip:wantsFillScale() return true end
function CardFlip:drawsWidescreen() return true end

-- opts: save, random(n), onClose()
function CardFlip.new(game, opts)
  opts = opts or {}
  local self = setmetatable({}, CardFlip)
  self.game = game
  self.save = opts.save or (game and game.save)
  self.onClose = opts.onClose
  self.random = opts.random or function(n)
    if love and love.math and love.math.random then
      return love.math.random(n) - 1
    end
    return math.random(n) - 1
  end
  -- `ld a, $2 / ld [wCardFlipCursorY], a / ld [wCardFlipCursorX], a`.
  self.cursorX, self.cursorY = 2, 2
  self.played = 0
  self.discarded = {}
  self.deck = CardFlip.shuffle(self.random)
  self:playMusic()
  self:enterAsk()
  return self
end

function CardFlip:playMusic()
  local data = self.game and self.game.data
  if not data then return end
  require("src.core.Music").play(data, "Music_GameCorner")
end

function CardFlip:sfx(name)
  local data = self.game and self.game.data
  if data then Sound.play(data, name) end
end

function CardFlip:coins()
  return CoinCase.coins(self.save)
end

-- ---------------------------------------------------------------- phases
--
-- .AskPlayWithThree / .DeductCoins / .ChooseACard / .PlaceYourBet /
-- .CheckTheCard / .TabulateTheResult / .PlayAgain / .Quit.
function CardFlip:enterAsk()
  self.phase = "ask"
  self.choice = 1
  self.lines = CardFlip.TEXTS.playWithThree
end

function CardFlip:deduct()
  -- The check is `wCoins high byte non-zero, or low byte >= 3`, which is just
  -- "at least three coins".
  if self:coins() < CardFlip.BET then
    self.phase = "message"
    self.lines = CardFlip.TEXTS.notEnough
    self.after = function() self:quit() end
    return
  end
  CoinCase.takeCoins(self.save, CardFlip.BET)
  self:sfx(SFX_TRANSACTION)
  self:enterChoose()
end

function CardFlip:enterChoose()
  self.phase = "choose"
  self.which = 0 -- wCardFlipWhichCard
  self.faceUp = nil
  self.lines = CardFlip.TEXTS.chooseACard
end

function CardFlip:enterBet()
  self.phase = "bet"
  self.lines = CardFlip.TEXTS.placeYourBet
end

-- .CheckTheCard: trigger hardware-accurate discrete tile flip sequence
function CardFlip:flip()
  local card = CardFlip.dealt(self.deck, self.played, self.which)
  self.faceUp = card
  self.discarded[card] = true
  local won = CardFlip.payout(self.cursorX, self.cursorY, card)
  self.payoutLeft = won
  self.payoutTick = 0
  self.phase = "flipping"
  self.flipTimer = 0
  self.targetCard = card
end

function CardFlip:updateFlipping()
  self.flipTimer = (self.flipTimer or 0) + 1
  if self.flipTimer == 4 then
    self:sfx(SFX_CHOOSE)
  elseif self.flipTimer >= 12 then
    if (self.payoutLeft or 0) > 0 then
      self.phase = "payout"
      self.lines = CardFlip.TEXTS.yeah
      self:sfx(SFX_WIN)
    else
      self.phase = "result"
      self.lines = CardFlip.TEXTS.darn
      self:sfx(SFX_WRONG)
    end
  end
end

function CardFlip:tabulate()
  local won = CardFlip.payout(self.cursorX, self.cursorY, self.faceUp)
  self.payoutLeft = won
  self.payoutTick = 0
  if won > 0 then
    self.phase = "payout"
    self.lines = CardFlip.TEXTS.yeah
    self:sfx(SFX_WIN)
  else
    self.phase = "result"
    self.lines = CardFlip.TEXTS.darn
    self:sfx(SFX_WRONG)
  end
end

-- .Payout's loop: one coin every two frames, the coin case checked BEFORE each
-- increment so a full case swallows the rest of the win.
function CardFlip:updatePayout()
  self.payoutTick = self.payoutTick + 1
  if self.payoutTick % 2 == 1 then return end
  if self.payoutLeft <= 0 then
    self.phase = "result"
    return
  end
  self.payoutLeft = self.payoutLeft - 1
  if self:coins() < CoinCase.MAX_COINS then
    CoinCase.giveCoins(self.save, 1)
    self:sfx(SFX_PAY_DAY)
  end
end

-- .PlayAgain -> .Continue: the hand counter advances, and only a twelfth hand
-- reshuffles.  Otherwise the card just played is blanked off the board.
function CardFlip:enterAgain()
  self.phase = "again"
  self.choice = 1
  self.lines = CardFlip.TEXTS.playAgain
end

function CardFlip:continue()
  self.played = self.played + 1
  if self.played >= CardFlip.HANDS_PER_DECK then
    self.played = 0
    self.deck = CardFlip.shuffle(self.random)
    self.discarded = {}
    self.phase = "message"
    self.lines = CardFlip.TEXTS.shuffled
    self.after = function() self:deduct() end
    return
  end
  self:deduct()
end

function CardFlip:quit()
  self.phase = "quit"
  self:sfx(SFX_QUIT)
  local data = self.game and self.game.data
  if data then require("src.core.Music").restoreMap(data) end
  if self.onClose then self.onClose() end
end

function CardFlip:update(dt)
  -- Support both fixed-tick 60Hz loop and variable dt accumulator
  if dt and dt > 0 then
    self.dtAccum = (self.dtAccum or 0) + dt
    local TICK = 1 / 60
    while self.dtAccum >= TICK do
      self.dtAccum = self.dtAccum - TICK
      self:tick()
    end
  else
    self:tick()
  end
end

function CardFlip:tick()
  local input = self.game and self.game.input
  if not input then return end
  local phase = self.phase

  if phase == "ask" or phase == "again" then
    if input:wasPressed("up") or input:wasPressed("down") then
      self.choice = self.choice == 1 and 2 or 1
      return
    end
    -- YesNoBox returning carry is .SaidNo on the first question and the
    -- .Increment into .Quit on the second: B leaves either way.
    if input:wasPressed("b") then
      self:quit()
      return
    end
    if input:wasPressed("a") then
      if self.choice ~= 1 then
        self:quit()
      elseif phase == "ask" then
        self:deduct()
      else
        self:continue()
      end
    end
    return
  end

  if phase == "message" then
    if input:wasPressed("a") or input:wasPressed("b") then
      local after = self.after
      self.after = nil
      if after then after() end
    end
    return
  end

  -- .ChooseACard's loop: the highlight alternates between the two face-down
  -- cards on its own and A locks whichever is lit.
  if phase == "choose" then
    if input:wasPressed("a") then
      self:sfx(SFX_START)
      self:enterBet()
      return
    end
    self.blink = (self.blink or 0) + 1
    if self.blink >= 4 then
      self.blink = 0
      self.which = self.which == 0 and 1 or 0
      self:sfx(SFX_KINESIS)
    end
    return
  end

  if phase == "bet" then
    for _, dir in ipairs({ "left", "right", "up", "down" }) do
      if input:wasPressed(dir) then
        local x, y = CardFlip.moveCursor(self.cursorX, self.cursorY, dir)
        if x ~= self.cursorX or y ~= self.cursorY then self:sfx(SFX_MOVE) end
        self.cursorX, self.cursorY = x, y
        return
      end
    end
    if input:wasPressed("a") then self:flip() end
    return
  end

  if phase == "flipping" then
    self:updateFlipping()
    return
  end

  if phase == "payout" then
    self:updatePayout()
    return
  end

  if phase == "result" then
    -- WaitPressAorB_BlinkCursor.
    if input:wasPressed("a") or input:wasPressed("b") then self:enterAgain() end
    return
  end
end

-- ------------------------------------------------------------------- draw
--
-- Authentic Color Game Boy palettes and tile graphics matching pret/pokegold
local TileSheet = require("src.ui.gen2.TileSheet")
local GbcPalette = require("src.render.GbcPalette")

local CARDFLIP_PALS = {
  bg = {
    [0] = { { 255, 255, 255 }, { 140, 57, 255 },  { 49, 156, 66 },  { 0, 0, 0 } },   -- 0: Base / Table green
    [1] = { { 255, 255, 255 }, { 239, 206, 0 },   { 49, 156, 66 },  { 0, 0, 0 } },   -- 1: Pikachu (Yellow)
    [2] = { { 255, 255, 255 }, { 255, 107, 247 }, { 49, 156, 66 },  { 0, 0, 0 } },   -- 2: Jigglypuff (Pink)
    [3] = { { 255, 255, 255 }, { 66, 140, 247 },  { 49, 156, 66 },  { 0, 0, 0 } },   -- 3: Poliwag (Blue)
    [4] = { { 255, 255, 255 }, { 66, 255, 66 },   { 49, 156, 66 },  { 0, 0, 0 } },   -- 4: Oddish (Green)
    [5] = { { 255, 255, 255 }, { 140, 57, 255 },  { 49, 156, 66 },  { 0, 0, 0 } },   -- 5: Level header
    [6] = { { 255, 255, 255 }, { 140, 57, 255 },  { 49, 156, 66 },  { 0, 0, 0 } },   -- 6: Border
    [7] = { { 255, 255, 255 }, { 140, 57, 255 },  { 49, 156, 66 },  { 0, 0, 0 } },   -- 7: Textbox
  },
  obj = {
    [0] = { { 255, 255, 255 }, { 248, 56, 40 },  { 248, 56, 40 },  { 248, 56, 40 } }, -- Authentic GBC Red OAM
  }
}

local TILEMAP = nil
local function getCardFlipTilemap()
  if TILEMAP == nil then
    local path = "assets/generated/card_flip/card_flip.tilemap"
    local data = love and love.filesystem and love.filesystem.read(path)
    if data and #data > 0 then
      TILEMAP = {}
      for i = 1, #data do
        TILEMAP[i] = string.byte(data, i)
      end
    else
      TILEMAP = false
    end
  end
  return TILEMAP or nil
end

function CardFlip:sheets()
  if self.sheet1 == nil then
    self.sheet1 = TileSheet.new({ path = "assets/generated/card_flip/card_flip_1.png", wide = 16, firstTile = 0 })
    self.sheet2 = TileSheet.new({ path = "assets/generated/card_flip/card_flip_2.png", wide = 3, firstTile = 0 })
    self.sheet3 = TileSheet.new({ path = "assets/generated/card_flip/card_flip_3.png", wide = 1, firstTile = 0 })
    self.sheetOn = TileSheet.new({ path = "assets/generated/card_flip/on.png", wide = 1, firstTile = 0 })
    self.sheetOff = TileSheet.new({ path = "assets/generated/card_flip/off.png", wide = 1, firstTile = 0 })
  end
  return self.sheet1, self.sheet2, self.sheet3, self.sheetOn, self.sheetOff
end

function CardFlip:cursorQuads()
  if self.s3Image == nil then
    local _, _, s3 = self:sheets()
    self.s3Image = s3:image()
    if self.s3Image and love and love.graphics then
      local G = love.graphics
      self.quadCorner = G.newQuad(0, 0,  8, 8, 8, 56) -- Tile 0: 1px corner
      self.quadVEdge  = G.newQuad(0, 8,  8, 8, 8, 56) -- Tile 1: 1px vertical edge
      self.quadHEdge  = G.newQuad(0, 16, 8, 8, 8, 56) -- Tile 2: 1px horizontal edge
    end
  end
  return self.s3Image, self.quadCorner, self.quadVEdge, self.quadHEdge
end

local HEADER_TILE_MAP = {
  [0x3E] = 0,  [0x3F] = 1,
  [0x40] = 3,  [0x41] = 4,
  [0x42] = 6,  [0x43] = 7,
  [0x44] = 9,  [0x45] = 10,
  [0x46] = 12, [0x47] = 13,
  [0x48] = 15, [0x49] = 16,
  [0x4A] = 18, [0x4B] = 19,
  [0x4C] = 21, [0x4D] = 22,
}

-- Draw an authentic GBC red cursor bounding frame using OAM sprite tiles.
-- On real GBC hardware OAM color 0 is always transparent, so only the 1px
-- dark edges of tiles 0-2 are visible; the board shows through the middle.
-- We replicate this by drawing in "multiply" blend mode: white (255,255,255)
-- pixels multiply to the board colour unchanged, black (0,0,0) pixels tinted
-- to GBC red draw the border, and nothing fills the interior.
function CardFlip:drawOamBox(px, py, w, h)
  local img, qCorner, qVEdge, qHEdge = self:cursorQuads()
  local G = love.graphics

  if not (img and qCorner and qVEdge and qHEdge) then
    -- Fallback: plain 1px red outline
    G.setColor(248 / 255, 56 / 255, 40 / 255, 1)
    G.rectangle("fill", px, py, w, 1)
    G.rectangle("fill", px, py + h - 1, w, 1)
    G.rectangle("fill", px, py, 1, h)
    G.rectangle("fill", px + w - 1, py, 1, h)
    G.setColor(1, 1, 1, 1)
    return
  end

  local prevBlend, prevAlpha = G.getBlendMode()
  G.setBlendMode("multiply", "premultiplied")
  -- Tint: black (0) → GBC red, white (255) → white (passthrough = transparent)
  G.setColor(248 / 255, 56 / 255, 40 / 255, 1)

  -- 4 Corners
  G.draw(img, qCorner, px,     py,     0,  1,  1)
  G.draw(img, qCorner, px + w, py,     0, -1,  1)
  G.draw(img, qCorner, px,     py + h, 0,  1, -1)
  G.draw(img, qCorner, px + w, py + h, 0, -1, -1)

  -- Top & Bottom horizontal edges
  if w > 16 then
    for x = px + 8, px + w - 16, 8 do
      G.draw(img, qHEdge, x, py,     0, 1,  1)
      G.draw(img, qHEdge, x, py + h, 0, 1, -1)
    end
  end

  -- Left & Right vertical edges
  if h > 16 then
    for y = py + 8, py + h - 16, 8 do
      G.draw(img, qVEdge, px,     y, 0,  1, 1)
      G.draw(img, qVEdge, px + w, y, 0, -1, 1)
    end
  end

  G.setBlendMode(prevBlend, prevAlpha)
  G.setColor(1, 1, 1, 1)
end

function CardFlip:drawBoard()
  local s1, s2, s3, sOn, sOff = self:sheets()
  local tm = getCardFlipTilemap()
  local G = love.graphics

  -- Green background fill
  G.setColor(49 / 255, 156 / 255, 66 / 255, 1)
  G.rectangle("fill", 0, 0, 160, 144)

  if not tm or not s2:available() then
    -- Fallback simple board
    for row = 0, CardFlip.HANDS_PER_DECK - 1 do
      Chrome.print(row == self.played and "o" or ".", LIGHT_X, row)
    end
    for x = 2, 5 do
      Chrome.print(CardFlip.MON_LABELS[x - 2], MON_COL[x], MON_ROW)
      if x % 2 == 0 then Chrome.print("6", MON_COL[x] + 1, MON_PAIR_ROW) end
    end
    for y = 2, 7 do
      local pair = math.floor((y - 2) / 2)
      local isBottom = ((y - 2) % 2 == 1)
      local row = 3 + pair * 3 + (isBottom and 1 or 0)
      Chrome.print(tostring(y - 1), LEVEL_COL, row)
      if y % 2 == 0 then Chrome.print("9", LEVEL_PAIR_COL, row) end
      for x = 2, 5 do
        local card = CardFlip.card(y - 2, x - 2)
        Chrome.print(self.discarded[card] and " " or "?", MON_COL[x], row)
      end
    end
    return
  end

  -- Draw the 11x12 board tilemap at (9, 0)
  for ty = 0, 11 do
    for tx = 0, 10 do
      local idx = ty * 11 + tx + 1
      local tileId = tm[idx]
      local screenX = 9 + tx
      local screenY = ty

      -- Attribute palette
      local pal = 0
      if screenY >= 1 and screenY <= 2 then
        if screenX == 12 or screenX == 13 then pal = 1 -- Pikachu
        elseif screenX == 14 or screenX == 15 then pal = 2 -- Jigglypuff
        elseif screenX == 16 or screenX == 17 then pal = 3 -- Poliwag
        elseif screenX == 18 or screenX == 19 then pal = 4 -- Oddish
        end
      elseif screenX == 9 then
        pal = 1 -- Lights
      end

      local colors = CARDFLIP_PALS.bg[pal]
      s1.palette = colors
      s2.palette = colors
      s3.palette = colors

      if screenX == 9 then
        -- Column 9: Light buttons
        if screenY == self.played then
          sOn.palette = colors
          sOn:draw(0, screenX, screenY)
        else
          sOff.palette = colors
          sOff:draw(0, screenX, screenY)
        end
      elseif tileId >= 0x3e then
        -- Board graphics from card_flip_2 (using accurate 2x2 header tile mapping)
        local mappedId = HEADER_TILE_MAP[tileId] or (tileId - 0x3e)
        s2:draw(mappedId, screenX, screenY)
      elseif tileId < 0x3e then
        -- Graphics from card_flip_1
        s1:draw(tileId, screenX, screenY)
      end
    end
  end

  -- Draw discarded card blanking covers (each 2-tile wide stacked card cell is 16x12 px)
  for y = 2, 7 do
    local level = y - 2
    local pair = math.floor(level / 2)
    local isBottom = (level % 2 == 1)
    local py = 24 + pair * 24 + (isBottom and 12 or 0)
    for x = 2, 5 do
      local mon = x - 2
      local card = CardFlip.card(level, mon)
      if self.discarded[card] then
        -- Discarded cover over full 16x12 stacked card cell
        G.setColor(49 / 255, 156 / 255, 66 / 255, 1)
        G.rectangle("fill", MON_COL[x] * 8, py, 16, 12)
      end
    end
  end
end

function CardFlip:cursorBounds()
  local x, y = self.cursorX, self.cursorY
  if y == 0 then
    -- Pokemon Pair: spans 4 columns (32px), 1 row (8px)
    local px = (MON_COL[x] or 12) * 8
    local py = MON_PAIR_ROW * 8
    return px, py, 32, 8
  elseif y == 1 then
    -- Single Pokemon: 2x2 tiles (16x16 px)
    local px = (MON_COL[x] or 12) * 8
    local py = MON_ROW * 8
    return px, py, 16, 16
  elseif x == 0 then
    -- Level Pair: 1 column (8px), spans 24px across the full pair
    local pair = math.floor((y - 2) / 2)
    local px = LEVEL_PAIR_COL * 8
    local py = 24 + pair * 24
    return px, py, 8, 24
  elseif x == 1 then
    -- Single Level: 1 column (8px), 12px tall for each stacked card
    local pair = math.floor((y - 2) / 2)
    local isBottom = ((y - 2) % 2 == 1)
    local px = LEVEL_COL * 8
    local py = 24 + pair * 24 + (isBottom and 12 or 0)
    return px, py, 8, 12
  else
    -- Exact Card: 2 columns (16px), 12px tall for each stacked card
    local pair = math.floor((y - 2) / 2)
    local isBottom = ((y - 2) % 2 == 1)
    local px = (MON_COL[x] or 12) * 8
    local py = 24 + pair * 24 + (isBottom and 12 or 0)
    return px, py, 16, 12
  end
end

local FACE_DOWN_TILES = {
  { 0x08, 0x09, 0x09, 0x09, 0x0a },
  { 0x0b, 0x28, 0x2b, 0x28, 0x0c },
  { 0x0b, 0x2c, 0x2d, 0x2e, 0x0c },
  { 0x0b, 0x2f, 0x30, 0x31, 0x0c },
  { 0x0b, 0x32, 0x33, 0x34, 0x0c },
  { 0x0d, 0x0e, 0x0e, 0x0e, 0x0f },
}

local FACE_UP_TILES = {
  { 0x18, 0x19, 0x19, 0x19, 0x1a },
  { 0x1b, 0x35, 0x28, 0x28, 0x1c },
  { 0x0b, 0x28, 0x28, 0x28, 0x0c },
  { 0x0b, 0x28, 0x28, 0x28, 0x0c },
  { 0x0b, 0x28, 0x28, 0x28, 0x0c },
  { 0x1d, 0x1e, 0x1e, 0x1e, 0x1f },
}

local MON_ANCHORS = {
  [0] = 24, -- Pikachu (tiles 24..32 in card_flip_2)
  [1] = 33, -- Jigglypuff (tiles 33..41 in card_flip_2)
  [2] = 42, -- Poliwag (tiles 42..50 in card_flip_2)
  [3] = 51, -- Oddish (tiles 51..59 in card_flip_2)
}

-- Draw face-down, flipping, or face-up card at (2, 0) or (2, 6)
function CardFlip:drawCards()
  local s1, s2 = self:sheets()

  for slot = 1, 2 do
    local box = CARD_BOX[slot]
    local bx, by = box.x * 8, box.y * 8
    local chosen = (slot - 1) == self.which
    local isFlipping = (self.phase == "flipping") and chosen
    local isFaceUp = (self.faceUp and chosen)

    if isFaceUp or (isFlipping and (self.flipTimer or 0) >= 4) then
      local activeCard = self.faceUp or self.targetCard or 0
      local lvl = CardFlip.level(activeCard) + 1
      local mon = CardFlip.mon(activeCard)
      local monPal = CARDFLIP_PALS.bg[mon + 1] or CARDFLIP_PALS.bg[1]

      s1.palette = CARDFLIP_PALS.bg[0]
      for cy = 1, 6 do
        for cx = 1, 5 do
          local tid = FACE_UP_TILES[cy][cx]
          s1:draw(tid, box.x + cx - 1, box.y + cy - 1)
        end
      end

      -- Level digit at (box.x + 3, box.y + 1)
      if isFaceUp or (isFlipping and (self.flipTimer or 0) >= 8) then
        Chrome.print(tostring(lvl), box.x + 3, box.y + 1)

        -- Draw 3x3 Pokemon pic from card_flip_2 (s2) at (box.x + 1, box.y + 2)
        s2.palette = monPal
        local anchor = MON_ANCHORS[mon] or 24
        for py = 0, 2 do
          for px = 0, 2 do
            local tid = anchor + py * 3 + px
            s2:draw(tid, box.x + 1 + px, box.y + 2 + py)
          end
        end
      end
    else
      s1.palette = CARDFLIP_PALS.bg[0]
      for cy = 1, 6 do
        for cx = 1, 5 do
          local tid = FACE_DOWN_TILES[cy][cx]
          s1:draw(tid, box.x + cx - 1, box.y + cy - 1)
        end
      end

      if self.phase == "choose" and chosen then
        -- Authentic OAM red selection box around 5x6 card (40x48 px)
        self:drawOamBox(bx, by, 40, 48)
      end
    end
  end
end

function CardFlip:drawPanel()
  self:drawBoard()
  self:drawCards()

  -- Dialogue / Message box at (0, 12), 10 wide, 6 tall (interior 8x4)
  if self.lines then
    Chrome.textbox(TEXT_BOX_X, TEXT_BOX_Y, 8, 4)
    for i, line in ipairs(self.lines) do
      Chrome.print(line, TEXT_X, TEXT_Y + (i - 1) * TEXT_LINE)
    end
  end

  -- Coin box at (9, 15), 11 wide, 3 tall (interior 9x1)
  Chrome.textbox(COIN_BOX_X, COIN_BOX_Y, 9, 1)
  Chrome.print("COIN", COIN_LABEL_X, COIN_LABEL_Y)
  Chrome.print(Chrome.number(self:coins(), 4, true), COIN_VALUE_X, COIN_VALUE_Y)

  if self.phase == "bet" then
    self.betBlink = (self.betBlink or 0) + 1
    if (self.betBlink % 32) < 24 then
      local px, py, w, h = self:cursorBounds()
      self:drawOamBox(px, py, w, h)
    end
  end

  if self.phase == "ask" or self.phase == "again" then
    -- YesNoBox: a 6x5 box at (14,7) with YES at (16,8) and NO at (16,10).
    Chrome.textbox(14, 7, 4, 3)
    Chrome.print("YES", 16, 8)
    Chrome.print("NO", 16, 10)
    Chrome.cursor(15, 8 + (self.choice - 1) * 2)
  end
end

function CardFlip:draw()
  self:drawPanel()
end

function CardFlip:drawWidescreen(winW, winH)
  local G = love.graphics
  Chrome.letterbox(winW, winH, 1, 1, 1)
  local scale = Chrome.fitScale(winW, winH)
  G.push()
  G.translate(Chrome.fitOrigin(winW, winH, scale))
  G.scale(scale, scale)
  self:drawPanel()
  G.pop()
end

return CardFlip

