-- Where does this species live?  engine/overworld/wildmons.asm FindNest, which
-- is the data behind the Pokedex's AREA page and the Pokegear MAP card's
-- "<MON>'S NEST" overlay.
--
-- FindNest takes a species and a region (e: 0 Johto, 1 Kanto) and fills the
-- tilemap with LANDMARK indices -- one per map whose wild data contains the
-- species.  It reads exactly three sources, in this order:
--
--   .FindGrass   JohtoGrassWildMons / KantoGrassWildMons, all NUM_GRASSMON * 3
--                slots, so morning, day AND night count
--   .FindWater   JohtoWaterWildMons / KantoWaterWildMons
--   .RoamMon1/2/3  the three roamers' CURRENT map, Johto only
--
-- and nothing else.  Headbutt trees, fishing groups, the Bug Contest and swarms
-- are all absent from it, so a HEADBUTT-only species legitimately has no nest
-- and the page stays blank -- that is the cart's answer, not a gap.
--
-- Region is decided here by LANDMARK INDEX rather than by which of the two
-- tables a map came from: the extractor emits one `grass`/`water` table keyed
-- by map, and Johto's landmarks are the run below LANDMARK_PALLET_TOWN with
-- Kanto's above it (constants/landmark_constants.asm).  Same split, different
-- spelling.
local Nests = {}

-- constants/landmark_constants.asm.  LANDMARK_SPECIAL is 0 and never a nest.
Nests.LANDMARK_PALLET_TOWN = 0x2e
Nests.LANDMARK_FAST_SHIP = 0x5e

function Nests.regionOf(landmark)
  if not landmark or landmark <= 0 then return nil end
  if landmark >= Nests.LANDMARK_FAST_SHIP then return nil end
  return (landmark < Nests.LANDMARK_PALLET_TOWN) and "johto" or "kanto"
end

-- ---------------------------------------------------------- the landmarks
--
-- data/maps/landmarks.asm, which on Gold is one index space shared by every
-- map header's `landmark` byte, the Pokegear MAP card and the #DEX AREA page
-- this file feeds.  It is the `landmarks` registry (src/mods/Schemas.lua), one
-- of the Gen 2-only six: Red's town map is a different table with a different
-- id space, so the name is gated under Gen 1 and routed to
-- gen2Landmarks.landmarks under Gen 2 -- straight onto the cache's own table,
-- which means the merge lands in the very table the map card draws from and no
-- Builtins seeding is needed (the same arrangement gen2Maps has).
--
-- Two reads go through here rather than through landmarks.order, which is a
-- plain ordered list the extractor writes and a registered landmark is
-- therefore absent from: src/core/Game2.lua:currentLandmark and
-- src/ui/gen2/MapRadio.lua's region test.  `index` on a record is the byte the
-- map header carries, so the lookup is by that and the order list stays the
-- fallback for a dataset whose records predate it.

-- memoized per landmark table (weak keys, so a second dataset in one process
-- does not pin the first); built on first read, which is after the merge --
-- nothing asks for a landmark before the overworld exists
local byIndex = setmetatable({}, { __mode = "k" })

-- Two records may claim one index -- a mod that registers a landmark at a byte
-- the cart already uses -- and pairs() would decide which one answers per
-- process.  The cache's own row wins its own slot (landmarks.order is that
-- list), and between two newcomers the lower id wins, so the answer is the
-- same on every boot.  A mod that means to MOVE a vanilla landmark patches
-- that record rather than shadowing its index.
local function indexTable(landmarks)
  local hit = byIndex[landmarks]
  if hit then return hit end
  local map, order = {}, landmarks.order or {}
  for id, record in pairs(landmarks.landmarks or {}) do
    local index = type(record) == "table" and record.index
    if index then
      local held = map[index]
      if held == nil or order[index + 1] == id
          or (order[index + 1] ~= held and id < held) then
        map[index] = id
      end
    end
  end
  byIndex[landmarks] = map
  return map
end

-- The LANDMARK_* id at a map header's landmark byte, or nil.
function Nests.landmarkId(data, index)
  local landmarks = data and data.gen2Landmarks
  if not (landmarks and index) then return nil end
  local hit = indexTable(landmarks)[index]
  if hit then return hit end
  return landmarks.order and landmarks.order[index + 1] or nil
end

-- The record behind that byte: the two-line name and the map-card position.
function Nests.landmark(data, index)
  local landmarks = data and data.gen2Landmarks
  local id = Nests.landmarkId(data, index)
  return id and landmarks.landmarks and landmarks.landmarks[id] or nil
end

local function landmarkOfMap(data, mapId)
  local def = data and data.gen2Maps and data.gen2Maps[mapId]
  return def and def.landmark
end

-- Every slot of one encounter table entry, across all times of day: the cart
-- walks `NUM_GRASSMON * 3` bytes without caring which third it is in.
local function tableHasSpecies(entry, species)
  if type(entry) ~= "table" then return false end
  local slots = entry.slots
  if type(slots) ~= "table" then return false end
  for _, list in pairs(slots) do
    if type(list) == "table" then
      for _, slot in ipairs(list) do
        if slot and slot.species == species then return true end
      end
    end
  end
  return false
end

-- The landmark indices where `species` can be met, in ascending order.
--
-- `region` is "johto" or "kanto"; nil means both, which no cart screen asks
-- for but is the useful answer for a test.
function Nests.find(data, species, region, save)
  local out, seen = {}, {}
  local function add(landmark)
    if not landmark or landmark <= 0 or seen[landmark] then return end
    local where = Nests.regionOf(landmark)
    if not where then return end
    if region and where ~= region then return end
    seen[landmark] = true
    out[#out + 1] = landmark
  end

  local enc = data and data.gen2Encounters
  for _, key in ipairs({ "grass", "water" }) do
    for mapId, entry in pairs((enc and enc[key]) or {}) do
      if tableHasSpecies(entry, species) then
        add(landmarkOfMap(data, mapId))
      end
    end
  end

  -- .RoamMon1/2/3: the roamer's CURRENT map, and only while it is still out
  -- there -- a caught or defeated one keeps its slot but loses its species and
  -- map. Johto-only on the cart, which the region filter above enforces.
  --
  -- The test is Roamers.active, NOT "has HP": a roamer starts life at hp 0
  -- (`xor a ; generate new stats`) and only gets a real value once you have met
  -- it, so an HP test would hide all three until first contact -- exactly the
  -- ones the page is most useful for.
  local Roamers = require("src.core.gen2.Roamers")
  for _, slot in ipairs((save and save.roamers) or {}) do
    if Roamers.active(slot) and slot.species == species then
      add(landmarkOfMap(data, slot.map))
    end
  end

  table.sort(out)
  return out
end

return Nests
