-- The clock-setting screens (pokegold engine/rtc/timeset.asm).
--
-- Two screens out of one file, because the cart builds them out of one set of
-- pieces: a Textbox with an up arrow above the value and a down arrow below
-- it, the d-pad walking the value with wraparound, A confirming, and a YES/NO
-- box that either takes the answer or drops back to the picker.
--
--   mode "clock"  InitClock, the first thing OakSpeech does
--                 (engine/menus/intro_menu.asm OakSpeech: `farcall InitClock`).
--                 Oak asks the hour, confirms it, asks the minutes, confirms
--                 them, and reads the whole time back.
--   mode "day"    SetDayOfWeek, the wheel Mom puts up when she hands over the
--                 POKeGEAR (maps/PlayersHouse1F.asm `special SetDayOfWeek`).
--
-- Layout, transcribed from the hlcoord calls:
--   clock hour     Textbox (3,7) 2x15, up arrow (11,7), down (11,10),
--                  "<hour> o'clock" at (4,9)
--   clock minutes  Textbox (11,7) 2x7, up arrow (15,7), down (15,10),
--                  "<mm> min." at (12,9)
--   day            Textbox (9,3) 2x9, up arrow (14,3), down (14,6),
--                  the weekday at (10,5), question in the (0,12) 4x18 box
--
-- The answer is written through src/core/gen2/Clock.lua, which stores the same
-- wStartHour / wStartMinute / wStartDay base the cart does.

local Chrome = require("src.ui.gen2.Chrome")
local Clock = require("src.core.gen2.Clock")
local Strings = require("src.core.Strings")

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

-- data/text/common_1.asm.  None of these are extracted: PrintText reaches them
-- from engine code, so no script pointer walks them and the extractor never
-- sees them.
local TEXT = {
  wokeUp = Strings.source(
    "Zzz... Hm? Wha...?\nYou woke me up!"
    .. "\fWill you check the\nclock for me?"),
  whatTime = Strings.source("What time is it?"),
  whatHours = Strings.source("What?\n%s?"),
  howManyMinutes = Strings.source("How many minutes?"),
  whoaMinutes = Strings.source("Whoa!\n%d min.?"),
  -- OakText_ResponseToSetTime prints the time it has just been given and then
  -- picks its line off the hour: NITE and before MORN_HOUR is "So dark...",
  -- through DAY_HOUR is "I overslept!", and the rest of the day is "Yikes!".
  soDark = Strings.source("%s!\nIt's so dark!"),
  overslept = Strings.source("%s!\nI overslept!"),
  yikes = Strings.source("%s!\nYikes! I over-\nslept!"),
  whatDay = Strings.source("What day is it?"),
  confirmDay = Strings.source("%s, is that right?"),
}
InitClock.TEXT = TEXT

-- constants/misc_constants.asm:37-39.  DAY_HOUR is 10, not 9: it read 9 here,
-- which moved Oak's line an hour early -- 10 o'clock answered "Yikes! I
-- overslept!" where OakText_ResponseToSetTime's `cp DAY_HOUR + 1` still puts
-- it in the plain "I overslept!" arm.  src/world/gen2/Palettes.lua carries the
-- same three and has always had them right.
local MORN_HOUR, DAY_HOUR, NITE_HOUR = 4, 10, 18

-- Clock.DAY_NAMES / Clock.weekdayName is the single translated home for this
-- table: MainMenu's clock box and the Pokegear's clock card read the same
-- weekday off the same save and must never disagree about what it is
-- called.
local DAYS = Clock.DAY_NAMES
InitClock.DAYS = DAYS

function InitClock:wantsFillScale() return true end
function InitClock:drawsWidescreen() return true end

-- PrintHour (engine/rtc/timeset.asm:672) is GetTimeOfDayString + PlaceString,
-- then AdjustHourForAMorPM as a left-aligned two-digit number.  So the cart
-- prints the time-of-day WORD and a 1-12 hour -- "MORN 5" -- and never an
-- AM/PM suffix.  Writing it as "5 AM" was what put the meridiem in the middle
-- of the clock-set line, because InitClock.timeString appends ":mm" to this
-- and Oak came out saying "5 AM:30".
--
-- AdjustHourForAMorPM still governs the number: 0 shows as 12, 13-23 lose 12,
-- so midnight is "NITE 12" and not "NITE 0".
--
-- The word comes from src/world/gen2/Palettes.lua:clockDaytime, which already
-- transcribes GetTimeOfDayString's own ladder (NITE below MORN_HOUR, MORN
-- below DAY_HOUR, DAY below NITE_HOUR, NITE after) off the real constants.
-- One source for it means the clock Oak reads out cannot disagree with the
-- palette the world is lit by.
function InitClock.hourString(hour)
  local h = math.floor(hour or 0) % 24
  local display = h % 12
  if display == 0 then display = 12 end
  -- Clock.daytimeLabel, not Palettes.clockDaytime: the printed word,
  -- translated -- this string reaches the player as-is, unlike the internal
  -- MORN/DAY/NITE key other palette code compares against.
  local word = Clock.daytimeLabel(h)
  return ("%s %d"):format(word, display)
end

function InitClock.oclockString(hour)
  return Strings("%s o'clock", InitClock.hourString(hour))
end

function InitClock.timeString(hour, minute)
  return ("%s:%02d"):format(InitClock.hourString(hour),
    math.floor(minute or 0) % 60)
end

-- .OakTimeSoDarkText / .OakTimeOversleptText / .OakTimeYikesText, in the ladder
-- OakText_ResponseToSetTime walks them in.
function InitClock.responseKey(hour)
  local h = math.floor(hour or 0) % 24
  if h < MORN_HOUR then return "soDark" end
  if h <= DAY_HOUR then return "overslept" end
  if h < NITE_HOUR then return "yikes" end
  return "soDark"
end

-- opts: mode ("clock" | "day"), save, onDone(hour, minute) / onDone(day),
-- autoConfirm (a driver's deterministic path: every step takes its default and
-- answers YES).
function InitClock.new(game, opts)
  opts = opts or {}
  local self = setmetatable({}, InitClock)
  self.game = game
  self.mode = opts.mode == "day" and "day" or "clock"
  self.save = opts.save or (game and game.save)
  self.onDone = opts.onDone
  self.autoConfirm = opts.autoConfirm or false
  self.hour = opts.hour or Clock.DEFAULT_HOUR
  self.minute = opts.minute or Clock.DEFAULT_MINUTE
  -- `xor a / ld [wTempDayOfWeek], a`: the wheel opens on SUNDAY.
  self.day = opts.day or 0
  -- YesNoBox's cursor, which opens on YES.
  self.yesNo = 1
  -- The cart opens on the "you woke me up" page and only then starts asking;
  -- the day wheel has no preamble.
  self.phase = self.mode == "day" and "day" or "intro"
  -- Which page of the current question is showing.  PrintText pages a `para`
  -- (a \f here) on a button press like any other text box, and Oak's opening
  -- is three lines long over two of them.
  self.page = 1
  return self
end

-- The current question, split into its pages.
function InitClock:pages()
  local out = {}
  for page in (self:question() .. "\f"):gmatch("(.-)\f") do
    if page ~= "" then out[#out + 1] = page end
  end
  if #out == 0 then out[1] = "" end
  return out
end

function InitClock:pageText()
  local pages = self:pages()
  return pages[math.min(self.page, #pages)] or ""
end

-- True while there is another page of the same question to show.
function InitClock:morePages()
  return self.page < #self:pages()
end

-- The value the picker is walking right now, and its wrap limit.
function InitClock:value()
  if self.phase == "hour" then return self.hour, 23 end
  if self.phase == "minute" then return self.minute, 59 end
  return self.day, 6
end

function InitClock:step(delta)
  local value, last = self:value()
  -- .AdvanceThroughMidnight / .DecreaseThroughMidnight: both ends wrap.
  value = (value + delta) % (last + 1)
  if self.phase == "hour" then self.hour = value
  elseif self.phase == "minute" then self.minute = value
  else self.day = value end
end

-- The line printed above the picker.
function InitClock:question()
  if self.phase == "intro" then return Strings(TEXT.wokeUp) end
  if self.phase == "hour" then return Strings(TEXT.whatTime) end
  if self.phase == "minute" then return Strings(TEXT.howManyMinutes) end
  if self.phase == "day" then return Strings(TEXT.whatDay) end
  if self.phase == "confirm-hour" then
    return Strings(TEXT.whatHours, InitClock.oclockString(self.hour))
  end
  if self.phase == "confirm-minute" then
    return Strings(TEXT.whoaMinutes, self.minute)
  end
  if self.phase == "confirm-day" then
    return Strings(TEXT.confirmDay, Clock.weekdayName(self.day + 1) or "?")
  end
  if self.phase == "response" then
    return Strings(TEXT[InitClock.responseKey(self.hour)],
      InitClock.timeString(self.hour, self.minute))
  end
  return ""
end

-- data/text/common_1.asm's "@MIN." suffix (DisplayMinutesWithMinString),
-- separate from TEXT.whoaMinutes' own "%d min.?" confirmation line above.
local MINUTES = Strings.source("%d min.")

-- The value the picker box shows, or nil while a page is up with no picker.
function InitClock:display()
  if self.phase == "hour" then return InitClock.oclockString(self.hour) end
  if self.phase == "minute" then return Strings(MINUTES, self.minute) end
  if self.phase == "day" then return Clock.weekdayName(self.day + 1) or "?" end
  return nil
end

function InitClock:confirming()
  return self.phase == "confirm-hour" or self.phase == "confirm-minute"
    or self.phase == "confirm-day"
end

function InitClock:finish()
  if self.mode == "day" then
    Clock.setWeekday(self.save, self.day)
    if self.onDone then self.onDone(self.day) end
    return
  end
  Clock.setTime(self.save, self.hour, self.minute)
  if self.onDone then self.onDone(self.hour, self.minute) end
end

-- A on a picker confirms it, YES on a confirmation takes it, NO drops back to
-- the picker it came from (`jr c, .loop` / `jr nc, .HourIsSet`).
function InitClock:accept()
  -- A on a page that has more behind it turns the page, the way `para` does.
  if self:morePages() then
    self.page = self.page + 1
    return
  end
  self.page = 1
  if self.phase == "intro" then
    self.phase = "hour"
  elseif self.phase == "hour" then
    self.phase = "confirm-hour"
  elseif self.phase == "confirm-hour" then
    self.phase = "minute"
  elseif self.phase == "minute" then
    self.phase = "confirm-minute"
  elseif self.phase == "confirm-minute" then
    self.phase = "response"
  elseif self.phase == "response" then
    self:finish()
  elseif self.phase == "day" then
    self.phase = "confirm-day"
  elseif self.phase == "confirm-day" then
    self:finish()
  end
end

function InitClock:decline()
  if self.phase == "confirm-hour" then
    self.phase = "hour"
  elseif self.phase == "confirm-minute" then
    self.phase = "minute"
  elseif self.phase == "confirm-day" then
    self.phase = "day"
  end
end

function InitClock:update(_dt)
  -- The driver path: no screen this new may be allowed to stall a scripted
  -- run, so it walks itself to the end taking every default.
  if self.autoConfirm then
    self:accept()
    return
  end
  local input = self.game and self.game.input
  if not input then return end
  if self:confirming() and not self:morePages() then
    -- YesNoBox: the cursor walks two rows and B is NO.
    if input:wasPressed("up") or input:wasPressed("down") then
      self.yesNo = self.yesNo == 1 and 2 or 1
    elseif input:wasPressed("a") then
      if self.yesNo == 1 then self:accept() else self:decline() end
      self.yesNo = 1
    elseif input:wasPressed("b") then
      self:decline()
      self.yesNo = 1
    end
    return
  end
  if input:wasPressed("up") then
    self:step(1)
  elseif input:wasPressed("down") then
    self:step(-1)
  elseif input:wasPressed("a") then
    self:accept()
  end
end

-- ------------------------------------------------------------------- drawing

-- The picker box: a Textbox with the value inside it, TIMESET_UP_ARROW on its
-- top border and TIMESET_DOWN_ARROW three rows below.
function InitClock:pickerBox()
  if self.phase == "hour" then return 3, 7, 15, 2, 11, 4, 9 end
  if self.phase == "minute" then return 11, 7, 7, 2, 15, 12, 9 end
  if self.phase == "day" then return 9, 3, 9, 2, 14, 10, 5 end
  return nil
end

-- TimeSetUpArrowGFX / TimeSetDownArrowGFX are two 1bpp tiles the cart loads
-- OVER the ♂ and ♀ font cells for this screen only; the extractor carries the
-- font page, not the replacements, so the pair are drawn rather than printed.
-- Four pixel rows, widest at the base, which is what the two 1bpp tiles are.
local ARROW_ROWS = { 1, 3, 5, 7 }

local function arrow(tx, ty, up)
  local G = love.graphics
  local x, y = tx * 8, ty * 8
  -- The arrow tile REPLACES the border tile it lands on (hlcoord 11, 7 is the
  -- box's own top row), so the cell is cleared before it is drawn.
  G.setColor(1, 1, 1, 1)
  G.rectangle("fill", x, y, 8, 8)
  G.setColor(0, 0, 0, 1)
  for i, width in ipairs(ARROW_ROWS) do
    local row = up and (i - 1) or (#ARROW_ROWS - i)
    G.rectangle("fill", x + math.floor((8 - width) / 2), y + 2 + row, width, 1)
  end
  G.setColor(1, 1, 1, 1)
end

function InitClock:drawPanel()
  Chrome.clear()
  local value = self:display()
  if value then
    local bx, by, bw, bh, arrowX, tx, ty = self:pickerBox()
    Chrome.textbox(bx, by, bw, bh)
    -- The two arrows sit ON the border rows, which is why they are placed
    -- after the box rather than inside it.
    arrow(arrowX, by, true)
    arrow(arrowX, by + bh + 1, false)
    Chrome.print(value, tx, ty)
  end
  -- The question (and the confirmations) share the bottom textbox every other
  -- Gold prompt uses.
  Chrome.textbox(0, 12, 18, 4)
  Chrome.printWrapped(self:pageText(), 1, 14, 18, 3)
  if self:confirming() then
    Chrome.box(14, 6, 6, 5)
    Chrome.print("YES", 16, 7)
    Chrome.print("NO", 16, 9)
    Chrome.cursor(15, self.yesNo == 1 and 7 or 9)
  end
end

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

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

return InitClock
