-- STADIUM battles: reading one species' model off disk.
--
-- `NNN.dsm` holds one Pokemon Stadium battle model. It is written by
-- StadiumBuild, out of the player's own copy of that ROM, the first time the
-- mod runs (see StadiumInstall) -- and by tools/stadium_pack.py, which is the
-- oracle that Lua path is tested against. This file is the other half of that
-- format and nothing else: bytes in, tables out. What the tables MEAN is
-- StadiumRig's business (posing a skeleton) and StadiumMon's (which animation
-- a fight is asking for).
--
-- Three things shape it.
--
-- BINARY, NOT LUA. A species is a couple of hundred kilobytes of numbers,
-- most of it animation, and a Lua source file of that is a parse the loader
-- would pay for on every boot whether a battle happened or not. A byte
-- string is read once, on the frame a fight starts, and only for the two
-- species actually fighting.
--
-- LAZY ANIMATIONS. Geometry, bones and textures are decoded on load --
-- they are small, and every one of them is needed the moment the mon
-- appears. The animations are not: a fight uses idle, an entrance and
-- whichever handful of attacks come up, out of the seven to twenty-one a
-- species carries. So the load pass SCANS the animation block, recording
-- where each one starts and skipping the rest, and a track is decoded the
-- first time something plays it. That turns a 200 KB decode into a 20 KB
-- one plus a few milliseconds spread over the fight.
--
-- AN LRU OF FOUR. A model is shared by everything that draws that species
-- -- both sides of a mirror match, both VR eyes -- and kept for a few
-- battles after, because the next fight on the same route is very often
-- the same Pokemon. Four is enough for a wild fight (two) plus the
-- trainer's next two, and it bounds what the mode can hold to a few
-- megabytes.
--
-- Everything is pcall-guarded and every failure answers nil: a missing
-- pack, a truncated file or a driver that will not make an image all end
-- at the same place, which is the flat 2D-3D card this mode falls back to
-- (see Stadium).

-- the mod namespace (see main.lua): V.require loads a sibling module
local V = ...

local StadiumPack = {}

local byte = string.byte
local floor = math.floor

-- ------- where a pack comes from
--
-- Two places, asked in this order.
--
-- CACHE_DIR is in the save directory and is what actually ships: the mod
-- carries no models (they are Pokemon Stadium's data), so StadiumInstall
-- builds them out of the player's own ROM on first run and writes them here.
--
-- DIR is inside the mod, and exists for a developer checkout that has run
-- tools/stadium_pack.py -- which is also how the oracle the Lua extractor is
-- tested against gets built. It is second because a locally built cache
-- should win over whatever a checkout happens to have lying around.
StadiumPack.CACHE_DIR = "dramatic_shape/stadium"
StadiumPack.DIR = "assets/stadium"

local function readPack(species)
  local rel = ("%s/%03d.dsm"):format(StadiumPack.CACHE_DIR, species)
  if love and love.filesystem and love.filesystem.getInfo then
    local okInfo, info = pcall(love.filesystem.getInfo, rel, "file")
    if okInfo and info then
      local ok, bytes = pcall(love.filesystem.read, rel)
      if ok and type(bytes) == "string" and #bytes > 4 then return bytes end
    end
  end
  local mod = V.mod
  if not (mod and mod.read) then return nil end
  local ok, bytes = pcall(mod.read, mod,
                          ("%s/%03d.dsm"):format(StadiumPack.DIR, species))
  if ok and type(bytes) == "string" and #bytes > 4 then return bytes end
  return nil
end

-- The battle system's context slots, in the order tools/stadium_pack.py
-- writes them -- slot 165 upward (see model_extract/manifest.json's
-- animationSlots). Indexed by POSITION, so this list is the format's
-- contract and the packer's CONTEXTS must stay identical to it.
-- Position 2 was called "hit" until the move table was read against it: it
-- is the animation most of a species' MOVES play, which makes it the default
-- attack and not a damage reaction (see StadiumMon's STATES). The name is a
-- label on a position -- the format is the ORDER -- so renaming it changes no
-- bytes, but it has to match tools/stadium_pack.py's CONTEXTS.
StadiumPack.CONTEXT = {
  "idle", "attack_default", "faint", "entrance", "reaction_169", "reaction_170",
  "reaction_171", "reaction_172", "reaction_173", "reaction_174",
  "struggle", "idle_alt", "faint_alt", "flinch", "reaction_179",
  "reaction_180", "reaction_181", "reaction_182", "entrance_alt",
  "idle_return",
}

-- name -> slot position, for callers that ask by name
StadiumPack.SLOT = {}
for i, name in ipairs(StadiumPack.CONTEXT) do StadiumPack.SLOT[name] = i end

StadiumPack.N_MOVES = 165
StadiumPack.NONE = 0xFFFF

-- The frame rate every animation in the set is authored at
-- (model_extract/README.md: keyframe times are frame / 30).
StadiumPack.FPS = 30

-- ------- readers
--
-- One cursor threaded through by hand rather than an object: this runs over
-- a couple of hundred thousand values on the frame a battle starts, and a
-- method call per value is the difference between a hitch and no hitch.

local function u8(s, p) return byte(s, p), p + 1 end

local function u16(s, p)
  local a, b = byte(s, p, p + 1)
  return a + b * 256, p + 2
end

local function i16(s, p)
  local a, b = byte(s, p, p + 1)
  local v = a + b * 256
  if v >= 32768 then v = v - 65536 end
  return v, p + 2
end

local function u32(s, p)
  local a, b, c, d = byte(s, p, p + 3)
  return a + b * 256 + c * 65536 + d * 16777216, p + 4
end

local function i32(s, p)
  local v
  v, p = u32(s, p)
  if v >= 2147483648 then v = v - 4294967296 end
  return v, p
end

-- IEEE 754 single, by hand. LOVE has love.data.unpack, but this file reads
-- exactly four floats per model (the header's extents) and a hand decode
-- costs nothing while removing a version floor from the mod's whole
-- STADIUM path.
local function f32(s, p)
  local b1, b2, b3, b4 = byte(s, p, p + 3)
  local sign = 1
  if b4 >= 128 then sign, b4 = -1, b4 - 128 end
  local expo = b4 * 2 + floor(b3 / 128)
  local mant = (b3 % 128) * 65536 + b2 * 256 + b1
  if expo == 255 then
    if mant == 0 then return sign * math.huge, p + 4 end
    return 0, p + 4
  end
  if expo == 0 then return sign * mant * 2 ^ -149, p + 4 end
  return sign * (1 + mant / 8388608) * 2 ^ (expo - 127), p + 4
end

-- 16.16 fixed point, which is how bone scales are stored (they run from
-- about -31 to 100 across the set and a float would cost twice the bytes
-- for precision nothing can see).
local function fixed(s, p)
  local v
  v, p = i32(s, p)
  return v / 65536, p
end

-- ------- the load

local function readHeader(s, p, model)
  model.species, p = u16(s, p)
  model.boneCount, p = u16(s, p)
  model.primCount, p = u16(s, p)
  model.texCount, p = u16(s, p)
  model.animCount, p = u16(s, p)
  model.auxCount, p = u16(s, p)
  model.rootScale, p = f32(s, p)
  -- a species whose standby loop is corrupt in the source extraction, and
  -- which the mod therefore holds at its bind pose (see the packer's
  -- idle_is_broken). Three of the 151.
  local static
  static, p = u8(s, p)
  model.staticPose = static ~= 0
  model.height, p = f32(s, p)
  model.floor, p = f32(s, p)
  model.radius, p = f32(s, p)

  local moveAnim, moveAux, ctx = {}, {}, {}
  for i = 1, StadiumPack.N_MOVES do moveAnim[i], p = u16(s, p) end
  for i = 1, StadiumPack.N_MOVES do moveAux[i], p = i16(s, p) end
  for i = 1, #StadiumPack.CONTEXT do ctx[i], p = u16(s, p) end
  model.moveAnim, model.moveAux, model.ctx = moveAnim, moveAux, ctx
  return p
end

-- The bone tree, as flat parallel arrays: a rig walk touches every bone
-- every frame and an array of little tables would be a cache miss per bone
-- and a table per bone to collect.
local function readBones(s, p, model)
  local n = model.boneCount
  local parent, t, r, sc = {}, {}, {}, {}
  for i = 1, n do
    -- 0-based in the file, 1-based here, and 0 for "no parent" so the rig's
    -- walk can test it without a sentinel comparison
    local par
    par, p = i16(s, p)
    parent[i] = par + 1
    local b = (i - 1) * 3
    t[b + 1], p = i16(s, p)
    t[b + 2], p = i16(s, p)
    t[b + 3], p = i16(s, p)
    r[b + 1], p = i16(s, p)
    r[b + 2], p = i16(s, p)
    r[b + 3], p = i16(s, p)
    sc[b + 1], p = fixed(s, p)
    sc[b + 2], p = fixed(s, p)
    sc[b + 3], p = fixed(s, p)
  end
  model.parent, model.restT, model.restR, model.restS = parent, t, r, sc
  return p
end

-- One drawable piece: the triangles that share a texture and a cull mode.
--
-- Positions and normals stay in BONE-LOCAL space, exactly as the display
-- list had them, because that is what makes the skinning a single matrix
-- multiply per vertex (every vertex in the set is rigidly bound to one bone
-- -- see model_extract/README.md) rather than a weighted blend.
local function readPrims(s, p, model)
  local prims = {}
  for i = 1, model.primCount do
    local prim = {}
    prim.tex, p = u16(s, p)
    prim.tex = prim.tex + 1
    local cull, blend
    cull, p = u8(s, p)
    blend, p = u8(s, p)
    prim.cull = cull ~= 0
    prim.additive = blend ~= 0
    prim.texAnim, p = i16(s, p)

    -- the texture-animation channel's value -> which texture to swap in.
    -- Keyed by the stream's own byte, so the rig can look one up without
    -- searching.
    local mapN
    mapN, p = u8(s, p)
    if mapN > 0 then
      local map = {}
      for _ = 1, mapN do
        local key, tex
        key, p = u8(s, p)
        tex, p = u16(s, p)
        map[key] = tex + 1
      end
      prim.texMap = map
    end

    local fxN
    fxN, p = u16(s, p)
    if fxN > 0 then
      local frames = {}
      for k = 1, fxN do
        frames[k], p = u16(s, p)
        frames[k] = frames[k] + 1
      end
      prim.fxFrames = frames
    end

    local nv, ni
    nv, p = u16(s, p)
    ni, p = u16(s, p)
    prim.vertCount, prim.indexCount = nv, ni

    -- five arrays rather than one array of vertices, for the same reason
    -- the bones are flat: the skinning loop reads them in step and writes
    -- one LOVE vertex row out
    local px, py, pz = {}, {}, {}
    local uv = {}
    local nx, ny, nz = {}, {}, {}
    local bone = {}
    for k = 1, nv do
      px[k], p = i16(s, p)
      py[k], p = i16(s, p)
      pz[k], p = i16(s, p)
      local u, v
      u, p = i16(s, p)
      v, p = i16(s, p)
      uv[k * 2 - 1], uv[k * 2] = u / 512, v / 512
      local a, b, c
      a, p = u8(s, p)
      b, p = u8(s, p)
      c, p = u8(s, p)
      if a >= 128 then a = a - 256 end
      if b >= 128 then b = b - 256 end
      if c >= 128 then c = c - 256 end
      nx[k], ny[k], nz[k] = a / 127, b / 127, c / 127
      bone[k], p = u8(s, p)
      bone[k] = bone[k] + 1
    end
    prim.px, prim.py, prim.pz = px, py, pz
    prim.uv = uv
    prim.nx, prim.ny, prim.nz = nx, ny, nz
    prim.bone = bone

    local idx = {}
    for k = 1, ni do
      idx[k], p = u16(s, p)
      idx[k] = idx[k] + 1
    end
    prim.index = idx
    prims[i] = prim
  end
  model.prims = prims
  return p
end

-- The textures, kept as the raw RGBA8 they arrived as and turned into
-- images on first use. A species carries every frame of every blink and
-- every dizzy swirl; a fight that never shows one should not pay to
-- upload it.
--
-- Raw rather than PNG, which is what DSM3 changed: an ImageData over these
-- bytes is a memcpy where a PNG is a decode on the frame a battle starts,
-- and -- the reason it was actually done -- uncompressed pixels are the same
-- pixels whichever side wrote them, so the Lua extractor's output can be
-- diffed against the Python packer's byte for byte. Two deflate
-- implementations need not agree; two arrays of pixels do.
local function readTextures(s, p, model)
  local tex = {}
  for i = 1, model.texCount do
    local w, h, len
    w, p = u16(s, p)
    h, p = u16(s, p)
    len, p = u32(s, p)
    tex[i] = { w = w, h = h, rgba = s:sub(p, p + len - 1) }
    p = p + len
  end
  model.textures = tex
  return p
end

-- How many bytes one animation's track block occupies, without decoding
-- any of it. This is the scan that makes lazy animations possible: nine
-- components a bone, each either one value or one a frame, and the only
-- thing that has to be READ is the byte that says which.
local COMP_BYTES = { 2, 2, 2, 2, 2, 2, 4, 4, 4 }   -- t t t r r r s s s

local function skipTracks(s, p, boneCount, frames)
  for _ = 1, boneCount do
    local present
    present, p = u8(s, p)
    if present ~= 0 then
      for c = 1, 9 do
        local kind
        kind, p = u8(s, p)
        p = p + COMP_BYTES[c] * (kind == 0 and 1 or frames)
      end
    end
  end
  return p
end

local function readAnims(s, p, model)
  local anims = {}
  for i = 1, model.animCount do
    local len
    len, p = u8(s, p)
    local name = s:sub(p, p + len - 1)
    p = p + len
    local frames, loopStart, aux
    frames, p = u16(s, p)
    loopStart, p = u16(s, p)
    aux, p = i16(s, p)
    anims[i] = {
      name = name, frames = frames, loopStart = loopStart,
      aux = aux >= 0 and (aux + 1) or nil,
      seconds = frames / StadiumPack.FPS,
      offset = p,                    -- where its tracks start; decoded later
    }
    p = skipTracks(s, p, model.boneCount, frames)
  end
  model.anims = anims
  return p
end

local function readAux(s, p, model)
  local aux = {}
  for i = 1, model.auxCount do
    local frames, loopStart, chanN
    frames, p = u16(s, p)
    loopStart, p = u16(s, p)
    chanN, p = u16(s, p)
    local chans = {}
    for c = 1, chanN do
      local n
      n, p = u16(s, p)
      local stream = {}
      for k = 1, n do stream[k], p = u16(s, p) end
      chans[c] = stream
    end
    aux[i] = { frames = frames, loopStart = loopStart, channels = chans }
  end
  model.auxAnims = aux
  return p
end

-- ------- a track block, decoded on demand
--
-- The shape a pose walk wants: `tracks[bone]` is either nil (this bone
-- holds its rest transform for the whole animation) or nine entries, each
-- either a number (constant) or an array of one value per frame.
--
-- That fold is the source data's own, not something imposed here: a bone
-- that only rotates costs two bytes for each of its six other components,
-- and across the 151 species it is most of the reason the whole set is 24
-- megabytes rather than a hundred.
function StadiumPack.tracks(model, index)
  local anim = model.anims and model.anims[index]
  if not anim then return nil end
  if anim.tracks then return anim.tracks end
  local s, p = model.bytes, anim.offset
  if not (s and p) then return nil end
  local frames = anim.frames
  local out = {}
  for b = 1, model.boneCount do
    local present
    present, p = u8(s, p)
    if present ~= 0 then
      local comps = {}
      for c = 1, 9 do
        local kind
        kind, p = u8(s, p)
        local read = (c >= 7) and fixed or i16
        if kind == 0 then
          comps[c], p = read(s, p)
        else
          local arr = {}
          for k = 1, frames do arr[k], p = read(s, p) end
          comps[c] = arr
        end
      end
      out[b] = comps
    end
  end
  anim.tracks = out
  return out
end

-- One texture as a LOVE image, decoded on first ask.
function StadiumPack.image(model, index)
  local slot = model.textures and model.textures[index]
  if not slot then return nil end
  if slot.image ~= nil then return slot.image or nil end
  local ok, img = pcall(function()
    local data = love.image.newImageData(slot.w, slot.h, "rgba8", slot.rgba)
    local image = love.graphics.newImage(data)
    -- N64 art at N64 resolution: nearest keeps the texels the size the
    -- artist drew them, exactly as every other texture in this mode
    image:setFilter("nearest", "nearest")
    return image
  end)
  slot.image = (ok and img) or false
  return slot.image or nil
end

-- ------- the cache

local cache = {}          -- species -> model
local order = {}          -- species, least recently used first
StadiumPack.KEEP = 4

local function touch(species)
  for i = #order, 1, -1 do
    if order[i] == species then table.remove(order, i) end
  end
  order[#order + 1] = species
  while #order > StadiumPack.KEEP do
    local drop = table.remove(order, 1)
    local model = cache[drop]
    cache[drop] = nil
    if model and model.textures then
      for _, slot in ipairs(model.textures) do
        if slot.image and slot.image.release then
          pcall(slot.image.release, slot.image)
        end
        -- CLEARED, not just released. A released Image is still a truthy
        -- value, and `image()` below hands back whatever is in this field
        -- without looking at it -- so leaving the corpse here meant the next
        -- ask returned a dead object, which reached mesh:setTexture and threw
        -- "Cannot use object after it has been released" from inside the
        -- scene pass. Nil means the next ask decodes it again, which is the
        -- whole point of the slot being lazy.
        slot.image = nil
      end
    end
  end
end

-- Say that this species is IN USE, so the cache does not evict it.
--
-- The eviction order above is a least-recently-LOADED list, not a
-- least-recently-used one: `touch` runs from `load`, and `load` is only
-- reached when a side's species CHANGES (StadiumMon.setSpecies returns early
-- otherwise). A Pokemon that stands on the field for several turns therefore
-- never refreshes its position, drifts to the front of the queue, and is
-- evicted -- its textures released -- while it is still being drawn sixty
-- times a second. That is what a fifth species entering a battle did: call
-- out a Clefairy and whatever had been standing longest lost its textures
-- mid-fight.
--
-- So the mode says, every frame, which two species are actually standing
-- there (see Stadium.update). With KEEP at 4 and two sides, the two in use
-- are always the two most recent and cannot reach the front of the queue.
function StadiumPack.keep(species)
  if species and cache[species] then touch(species) end
end

-- Whether a pack for this species is on disk at all. Cheap enough to ask
-- before a battle commits to the mode, and the honest test: a mod
-- installed without its assets folder must decline rather than error.
function StadiumPack.available(species)
  if cache[species] then return true end
  return readPack(species) ~= nil
end

-- The model for a National Dex number (1..151), or nil.
function StadiumPack.load(species)
  if not (species and species >= 1 and species <= 151) then return nil end
  local hit = cache[species]
  if hit ~= nil then
    touch(species)
    return hit or nil
  end

  local bytes = readPack(species)
  if not bytes then
    cache[species] = false
    return nil
  end

  local ok, model = pcall(function()
    if bytes:sub(1, 4) ~= "DSM3" then
      error("not a DSM3 pack -- delete it and let the mod rebuild it", 0)
    end
    local m = { bytes = bytes }
    local p = 5
    p = readHeader(bytes, p, m)
    p = readBones(bytes, p, m)
    p = readPrims(bytes, p, m)
    p = readTextures(bytes, p, m)
    p = readAnims(bytes, p, m)
    readAux(bytes, p, m)
    return m
  end)
  if not ok then
    V.mod.log:warn("stadium: %03d.dsm did not read: %s -- that Pokemon "
                   .. "falls back to its flat pic", species, tostring(model))
    cache[species] = false
    return nil
  end

  cache[species] = model
  touch(species)
  return model
end

-- Drop everything (hot reload, or a graphics context that went away).
function StadiumPack.invalidate()
  for _, model in pairs(cache) do
    if model and model.textures then
      for _, slot in ipairs(model.textures) do
        if slot.image and slot.image.release then
          pcall(slot.image.release, slot.image)
        end
        slot.image = nil
      end
    end
  end
end

function StadiumPack.forget()
  StadiumPack.invalidate()
  cache, order = {}, {}
end

return StadiumPack
