-- The player's item PC: _PlayersPC and the three item rows behind it
-- (engine/events/pokecenter_pc.asm PlayerWithdrawItemMenu /
-- PlayerDepositItemMenu / PlayerTossItemMenu), plus TossItemFromPC
-- (engine/pokemon/mon_menu.asm).  Both of the cart's callers land here:
--
--   PlayersPC        PLAYERSPC_NORMAL -- the <PLAYER>'s PC row of the
--                    Pokecenter's whose-PC menu (src/ui/gen2/CenterPcMenu.lua):
--                    WITHDRAW ITEM / DEPOSIT ITEM / TOSS ITEM / MAIL BOX /
--                    LOG OFF
--   _PlayersHousePC  PLAYERSPC_HOUSE -- the bedroom PC's whole screen: the
--                    boot sound and PlayersPCTurnOnText first, DECORATION on
--                    the list, TURN OFF instead of LOG OFF, and the answer
--                    carried back out (TRUE only when a decoration moved) so
--                    PlayersHousePCScript can take its `.Warp` arm
--
-- The menu's rows run through the ui.pc.items hook -- the same name and the
-- same (game, items) payload the Gen 1 PC uses
-- (src/world/OverworldController.lua openPC) -- with the exit row appended
-- after it, the way that site appends LOG OFF.
--
-- Items live on save.pcItems, the id -> count map ReceiveItem's PC half
-- already writes (src/core/gen2/MomShopping.lua receiveItemToPc): fifty
-- distinct stacks of at most 99, like wPCItems.  DEPOSIT opens the PACK as a
-- chooser held by this screen, exactly the arrangement the mart's sell flow
-- uses (src/ui/gen2/MartMenu.lua enterSell), because DepositSellPack is the
-- same routine on the cart.

local Bag = require("src.inventory.Bag")
local Chrome = require("src.ui.gen2.Chrome")
local Logger = require("src.core.Logger")
local Runtime = require("src.mods.Runtime")
local Screens = require("src.ui.Screens")
local Sound = require("src.core.Sound")

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

-- MAX_PC_ITEMS stacks of at most MAX_ITEM_STACK (constants/item_constants.asm),
-- the same pair src/core/gen2/MomShopping.lua enforces for Mom's deliveries.
local PC_ITEM_CAPACITY = 50
local MAX_STACK = 99

local function stacksFor(n) return math.ceil((n or 0) / MAX_STACK) end

-- PlayersPCMenuData .PlayersPCMenuPointers strings, verbatim.  .WhichPC picks
-- which rows a caller sees: PLAYERSPC_NORMAL ends on LOG OFF, PLAYERSPC_HOUSE
-- carries DECORATION and ends on TURN OFF.
local ENTRIES = {
  { id = "withdraw", label = "WITHDRAW ITEM" },
  { id = "deposit", label = "DEPOSIT ITEM" },
  { id = "toss", label = "TOSS ITEM" },
  { id = "mailbox", label = "MAIL BOX" },
}
local LOG_OFF = { id = "logoff", label = "LOG OFF" }
local DECORATION = { id = "decoration", label = "DECORATION" }
local TURN_OFF = { id = "turnoff", label = "TURN OFF" }

-- ui.pc.items identity: an unhooked build hands its own list back.
local function sameItems(_, items) return items end

-- PCItemsJoypad's ScrollingMenu is `db 4, 8 ; rows, columns`.
local VISIBLE_ROWS = 4

-- charmap.asm: the quantity glyph.
local TIMES = "\xc3\x97"

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

-- opts: save, items (items.lua), house (PLAYERSPC_HOUSE: boot text,
--       DECORATION row, TURN OFF), events (wEventFlags, for the decoration
--       menu), onClose(changedDecorations)
function ItemPcMenu.new(game, opts)
  opts = opts or {}
  local self = setmetatable({}, ItemPcMenu)
  self.game = game
  self.save = opts.save or (game and game.save)
  self.items = opts.items or (game and game.data and game.data.items)
  self.data = game and game.data
  self.onClose = opts.onClose
  self.house = opts.house and true or false
  self.events = opts.events
  -- wChangedDecorations, carried out so `special PlayersHousePC` can answer
  -- TRUE and PlayersHousePCScript can take its `.Warp` arm.
  self.changedDecorations = false
  if self.save then self.save.pcItems = self.save.pcItems or {} end
  local entries = {}
  for i, entry in ipairs(ENTRIES) do entries[i] = entry end
  if self.house then entries[#entries + 1] = DECORATION end
  -- Same hook name and same (game, items) payload as the Gen 1 PC menu
  -- (src/world/OverworldController.lua openPC), so one mod source can add a
  -- row to both generations' PCs.  Unguarded, like that site: the list is
  -- built once per session, not per frame.  A hook that answers with anything
  -- but a table is degraded to the vanilla list.
  local hooked = Runtime.call("ui.pc.items", sameItems, game, entries)
  if type(hooked) == "table" then
    entries = hooked
  else
    Logger.error("ui.pc.items returned %s; keeping the vanilla items",
                 type(hooked))
  end
  -- LOG OFF / TURN OFF is appended AFTER the hook, exactly as the Gen 1 site
  -- appends LOG OFF: a mod cannot orphan the way out of the PC.
  entries[#entries + 1] = self.house and TURN_OFF or LOG_OFF
  self.entries = entries
  self.index = 1
  self.phase = "menu"
  self.rows = {}
  self.listIndex = 1
  self.scroll = 0
  self.message = nil
  self.qtyState = nil
  self.confirm = nil
  if self.house then
    -- _PlayersHousePC: PC_PlayBootSound, then PlayersPCTurnOnText.
    self:playSfx("Sfx_BootPc")
    self:say({ { "{PLAYER} turned on", "the PC." } })
  end
  return self
end

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

function ItemPcMenu:playerName()
  local player = self.save and self.save.player
  return (player and player.name) or "GOLD"
end

-- A queue of text pages, each a list of lines; A or B turns them, and the last
-- one runs onDone.  The item PC's messages never log off by themselves, which
-- is _PlayersPC's `.loop`: a refusal drops back into the same menu.
function ItemPcMenu:say(pages, onDone)
  self.message = { pages = pages, page = 1, onDone = onDone }
end

function ItemPcMenu:close()
  -- _PlayersHousePC plays PC_PlayShutdownSound only on the unchanged arm;
  -- `.changed_deco_tiles` leaves for the map reload without it.
  if self.house and not self.changedDecorations then
    self:playSfx("Sfx_ShutDownPc")
  end
  if self.onClose then self.onClose(self.changedDecorations) end
end

-- ---------------------------------------------------------------- the items

function ItemPcMenu:def(id)
  return self.items and self.items[id]
end

-- _CheckTossableItem: KEY ITEMs and HMs answer non-zero.  The extractor
-- carries that as `canToss = false`; an id with no attributes row counts as
-- tossable, the way the PACK's sell gate treats it.
function ItemPcMenu:cantToss(id)
  local def = self:def(id)
  return def ~= nil and def.canToss == false
end

function ItemPcMenu:rebuild()
  local pc = (self.save and self.save.pcItems) or {}
  local rows = {}
  for id, count in pairs(pc) do
    if (count or 0) > 0 then
      local def = self:def(id)
      local remaining = count
      while remaining > 0 do
        local n = math.min(remaining, MAX_STACK)
        rows[#rows + 1] = {
          id = id, count = n,
          name = (def and def.name) or id,
          index = def and def.index or math.huge,
        }
        remaining = remaining - n
      end
    end
  end
  -- wPCItems keeps acquisition order; without that recorded, item id order is
  -- the stable choice, the same sort the PACK uses.
  table.sort(rows, function(a, b)
    if a.index ~= b.index then return a.index < b.index end
    if a.id ~= b.id then return a.id < b.id end
    return a.count > b.count
  end)
  self.rows = rows
  if self.listIndex > #rows + 1 then self.listIndex = #rows + 1 end
  if self.listIndex < 1 then self.listIndex = 1 end
  self:ensureVisible()
end

function ItemPcMenu:listTotal()
  return #self.rows + 1 -- CANCEL
end

function ItemPcMenu:ensureVisible()
  if self.listIndex <= self.scroll then
    self.scroll = self.listIndex - 1
  elseif self.listIndex > self.scroll + VISIBLE_ROWS then
    self.scroll = self.listIndex - VISIBLE_ROWS
  end
  self.scroll = math.max(0, math.min(self.scroll,
    math.max(0, self:listTotal() - VISIBLE_ROWS)))
end

-- ReceiveItem over wPCItems: the add tops up every existing stack of that id
-- and spills the rest into a new one, so it only needs a free stack when the
-- room in place is short.  False is the no-carry the deposit turns into
-- _PlayersPCNoRoomDepositText.
-- engine/items/items.asm:156 PutItemInPocket
function ItemPcMenu:pcAdd(id, qty)
  local pc = self.save.pcItems
  local held = pc[id] or 0
  local used = 0
  for _, count in pairs(pc) do used = used + stacksFor(count) end
  local need = stacksFor(held + qty) - stacksFor(held)
  if used + need > PC_ITEM_CAPACITY then return false end
  pc[id] = held + qty
  return true
end

function ItemPcMenu:pcRemove(id, qty)
  local pc = self.save.pcItems
  local held = (pc[id] or 0) - qty
  pc[id] = held > 0 and held or nil
end

-- HasNoItems (engine/pokemon/mon_menu.asm): every pocket, TM/HMs included --
-- which the flat inventory answers in one walk.  Badges share the table but
-- are not bag items.
local function bagIsEmpty(save)
  for id, count in pairs((save and save.inventory) or {}) do
    if (count or 0) > 0 and not Bag.isBadge(id) then return false end
  end
  return true
end

-- BuySellToss_InterpretJoypad (engine/items/buy_sell_toss.asm): up and down
-- wrap through the ends, left and right step by ten and clamp.
local function qtyStep(qty, max, delta)
  local n = qty + delta
  if delta == 1 then
    if n > max then n = 1 end
  elseif delta == -1 then
    if n < 1 then n = max end
  elseif delta > 0 then
    if n > max then n = max end
  else
    if n <= 0 then n = 1 end
  end
  return n
end

-- prompt is the two lines under the selector; onAccept(qty) commits.
function ItemPcMenu:askQuantity(max, prompt, onAccept)
  self.qtyState = { qty = 1, max = max, prompt = prompt, onAccept = onAccept }
end

-- ------------------------------------------------------------ the three rows

function ItemPcMenu:withdraw(row, qty)
  -- PlayerWithdrawItemMenu .withdraw: ReceiveItem into the bag first; only a
  -- carry tosses the stack out of the PC.
  if not Bag.add(self.save, row.id, qty, self.data) then
    self:say({ { "There's no room", "for more items." } })
    return
  end
  self:pcRemove(row.id, qty)
  self:rebuild()
  self:say({ { ("Withdrew %d"):format(qty), row.name .. "(S)." } })
end

function ItemPcMenu:chooseWithdraw()
  local row = self.rows[self.listIndex]
  if not row then
    self.phase = "menu"
    return
  end
  -- .Submenu: an item without a quantity attribute (a KEY ITEM in the PC) is
  -- always x1; everything else asks _PlayersPCHowManyWithdrawText.
  if self:cantToss(row.id) then
    self:withdraw(row, 1)
    return
  end
  self:askQuantity(row.count,
    { "How many do you", "want to withdraw?" },
    function(qty) self:withdraw(row, qty) end)
end

function ItemPcMenu:deposit(id, name, qty)
  if not self:pcAdd(id, qty) then
    self:say({ { "There's no room to", "store items." } })
    return
  end
  Bag.remove(self.save, id, qty)
  if self.pack then self.pack:rebuild() end
  self:say({ { ("Deposited %d"):format(qty), name .. "(S)." } })
end

function ItemPcMenu:enterDeposit()
  -- .CheckItemsInBag: an empty bag never opens the PACK.
  if bagIsEmpty(self.save) then
    self:say({ { "No items here!" } })
    return
  end
  self.phase = "deposit"
  -- DepositSellPack: the PACK as a chooser, held and drawn by this screen the
  -- way the mart holds its sell PACK.  `world = {}` keeps field items inert.
  self.pack = Screens.build(self.game, "Gen2PackMenu", {
    save = self.save,
    items = self.items,
    world = {},
    onChoose = function(id, count) self:offerToDeposit(id, count) end,
    onClose = function() self:leaveDeposit() end,
  })
end

function ItemPcMenu:leaveDeposit()
  self.pack = nil
  self.phase = "menu"
end

function ItemPcMenu:offerToDeposit(id, count)
  if (count or 0) < 1 then return end
  local def = self:def(id)
  local name = (def and def.name) or id
  -- .DepositItem (engine/events/pokecenter_pc.asm:504): an item with no
  -- quantity is always x1 and never reaches .AskQuantity.
  if self:cantToss(id) then
    self:deposit(id, name, 1)
    return
  end
  self:askQuantity(count,
    { "How many do you", "want to deposit?" },
    function(qty) self:deposit(id, name, qty) end)
end

function ItemPcMenu:chooseToss()
  local row = self.rows[self.listIndex]
  if not row then
    self.phase = "menu"
    return
  end
  -- TossItemFromPC .key_item -> .CantToss.
  if self:cantToss(row.id) then
    self:say({ { "That's too impor-", "tant to toss out!" } })
    return
  end
  self:askQuantity(row.count,
    { "Toss out how many", row.name .. "(S)?" },
    function(qty)
      -- .ItemsThrowAwayText's yes/no sits between the count and the toss.
      self.confirm = {
        prompt = { ("Throw away %d"):format(qty), row.name .. "(S)?" },
        choice = 1,
        onYes = function()
          self:pcRemove(row.id, qty)
          self:rebuild()
          self:say({ { "Discarded", row.name .. "(S)." } })
        end,
      }
    end)
end

-- ------------------------------------------------------------------ the menu

function ItemPcMenu:choose()
  local entry = self.entries[self.index]
  if not entry then return end
  local game = self.game
  if entry.id == "withdraw" or entry.id == "toss" then
    self.phase = entry.id
    self.listIndex = 1
    self.scroll = 0
    self:rebuild()
    return
  end
  if entry.id == "deposit" then
    self:enterDeposit()
    return
  end
  if entry.id == "mailbox" then
    if not (game and game.stack) then return end
    Screens.push(game, "Gen2MailboxMenu", {
      save = self.save,
      onClose = function() game.stack:pop() end,
    })
    return
  end
  if entry.id == "decoration" then
    if not (game and game.stack) then return end
    Screens.push(game, "Gen2DecorationMenu", {
      save = self.save,
      events = self.events,
      onDone = function(changed)
        self.changedDecorations = self.changedDecorations or changed or false
        game.stack:pop()
      end,
    })
    return
  end
  -- logoff / turnoff: PlayerLogOffMenu serves both rows.
  self:close()
end

-- ------------------------------------------------------------------- update

function ItemPcMenu:update(_dt)
  local input = self.game and self.game.input
  if not input then return end

  if self.message then
    if input:wasPressed("a") or input:wasPressed("b") then
      local m = self.message
      if m.page < #m.pages then
        m.page = m.page + 1
        return
      end
      self.message = nil
      if m.onDone then m.onDone() end
    end
    return
  end

  if self.qtyState then
    local q = self.qtyState
    if input:wasPressed("up") then
      q.qty = qtyStep(q.qty, q.max, 1)
    elseif input:wasPressed("down") then
      q.qty = qtyStep(q.qty, q.max, -1)
    elseif input:wasPressed("right") then
      q.qty = qtyStep(q.qty, q.max, 10)
    elseif input:wasPressed("left") then
      q.qty = qtyStep(q.qty, q.max, -10)
    elseif input:wasPressed("b") then
      self.qtyState = nil
    elseif input:wasPressed("a") then
      self.qtyState = nil
      q.onAccept(q.qty)
    end
    return
  end

  if self.confirm then
    local c = self.confirm
    if input:wasPressed("up") or input:wasPressed("down") then
      c.choice = c.choice == 1 and 2 or 1
    elseif input:wasPressed("b") then
      self.confirm = nil
      if c.onNo then c.onNo() end
    elseif input:wasPressed("a") then
      self.confirm = nil
      if c.choice == 1 then
        if c.onYes then c.onYes() end
      elseif c.onNo then
        c.onNo()
      end
    end
    return
  end

  if self.phase == "deposit" then
    if self.pack then
      self.pack:update(_dt)
    else
      self.phase = "menu"
    end
    return
  end

  if self.phase == "withdraw" or self.phase == "toss" then
    if input:wasPressed("up") then
      self.listIndex = self.listIndex > 1 and self.listIndex - 1
        or self:listTotal()
      self:ensureVisible()
    elseif input:wasPressed("down") then
      self.listIndex = self.listIndex < self:listTotal() and self.listIndex + 1
        or 1
      self:ensureVisible()
    elseif input:wasPressed("b") then
      self.phase = "menu"
    elseif input:wasPressed("a") then
      if self.phase == "withdraw" then
        self:chooseWithdraw()
      else
        self:chooseToss()
      end
    end
    return
  end

  if input:wasPressed("up") then
    self.index = self.index > 1 and self.index - 1 or #self.entries
  elseif input:wasPressed("down") then
    self.index = self.index < #self.entries and self.index + 1 or 1
  elseif input:wasPressed("a") then
    self:choose()
  elseif input:wasPressed("b") then
    -- DoNthMenu's carry is `.turn_off`.
    self:close()
  end
end

-- --------------------------------------------------------------------- draw

function ItemPcMenu:drawBottomLines(lines)
  Chrome.box(0, 12, 20, 6)
  if not lines then return end
  local name = self:playerName()
  local startY = #lines >= 3 and 13 or 14
  for i, line in ipairs(lines) do
    Chrome.print((line:gsub("{PLAYER}", name)), 1, startY + (i - 1) * 2)
  end
end

function ItemPcMenu:drawList()
  Chrome.box(0, 0, 20, 12)
  for row = 1, VISIBLE_ROWS do
    local i = row + self.scroll
    local ty = row * 2
    if i <= #self.rows then
      local entry = self.rows[i]
      if i == self.listIndex then Chrome.cursor(5, ty) end
      Chrome.print(entry.name, 6, ty)
      -- PlaceMenuItemQuantity (engine/menus/menu_2.asm:18, :24)
      if not self:cantToss(entry.id) then
        Chrome.print(TIMES .. Chrome.number(entry.count, 2), 7, ty + 1)
      end
    elseif i == self:listTotal() then
      if i == self.listIndex then Chrome.cursor(5, ty) end
      Chrome.print("CANCEL", 6, ty)
    end
  end
  -- UpdateItemDescription under the list.
  local row = self.rows[self.listIndex]
  local def = row and self:def(row.id)
  local description = def and def.description
  if description then
    local first, second = description:match("^(.-)<NEXT>(.*)$")
    if not first then first, second = description:match("^(.-)\n(.*)$") end
    Chrome.box(0, 12, 20, 6)
    Chrome.print(first or description, 1, 14)
    if second then Chrome.print(second, 1, 16) end
  else
    Chrome.box(0, 12, 20, 6)
  end
end

function ItemPcMenu:drawPanel()
  Chrome.clear()

  if self.phase == "deposit" and self.pack then
    self.pack:drawPanel()
  elseif self.phase == "withdraw" or self.phase == "toss" then
    self:drawList()
  else
    -- _PlayersPCAskWhatDoText, printed under the list the whole time.  The
    -- box goes down first: the menu window overlays it where the house's
    -- six-row list runs past row 12, the way the cart's windows stack.
    self:drawBottomLines({ "What do you want", "to do?" })
    -- PlayersPCMenuData is menu_coords 0, 0, 15, 12; the house list is one
    -- row taller than that box, so size it to the entries.
    Chrome.box(0, 0, 16, math.max(12, #self.entries * 2 + 2))
    for i, entry in ipairs(self.entries) do
      local ty = i * 2
      if i == self.index then Chrome.cursor(1, ty) end
      Chrome.print(entry.label, 2, ty)
    end
  end

  if self.qtyState then
    local q = self.qtyState
    self:drawBottomLines(q.prompt)
    Chrome.box(7, 15, 13, 3)
    Chrome.print(TIMES, 8, 16)
    Chrome.print(Chrome.number(q.qty, 2, true), 9, 16)
  elseif self.confirm then
    self:drawBottomLines(self.confirm.prompt)
    Chrome.box(14, 7, 6, 5)
    Chrome.print("YES", 16, 8)
    Chrome.print("NO", 16, 10)
    Chrome.cursor(15, self.confirm.choice == 1 and 8 or 10)
  elseif self.message then
    self:drawBottomLines(self.message.pages[self.message.page])
  end

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

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

function ItemPcMenu: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

ItemPcMenu.ENTRIES = ENTRIES
ItemPcMenu.PC_ITEM_CAPACITY = PC_ITEM_CAPACITY

return ItemPcMenu
