-- The live extension-point catalog: every registry, every event name the
-- engine emits, every hook name it calls.
--
-- Registries come from Schemas.REGISTRIES.  Events and hooks are read back
-- out of the source rather than from a hand-kept list, because a hand-kept
-- list is exactly the thing that drifts -- adding a Runtime.emit and
-- forgetting the catalog entry is how a seam ships untested.  The
-- parity-gate meta-test (tests/engine/gate_meta_coverage.lua) walks these
-- three sets, so a new seam is in the coverage requirement the moment its
-- call site exists.

local Schemas = require("src.mods.Schemas")
local FsIo = require("tests.fs_io")

local Catalog = {}

local function luaFilesUnder(dir)
  return FsIo.luaFilesUnder(dir)
end

local function scan(dirs, patterns)
  local found = {}
  for _, dir in ipairs(dirs) do
    for _, path in ipairs(luaFilesUnder(dir)) do
      local handle = io.open(path, "r")
      if handle then
        local body = handle:read("*a")
        handle:close()
        for _, pattern in ipairs(patterns) do
          for name in body:gmatch(pattern) do
            local list = found[name] or {}
            found[name] = list
            list[#list + 1] = path
          end
        end
      end
    end
  end
  return found
end

local function sortedKeys(map)
  local keys = {}
  for key in pairs(map) do keys[#keys + 1] = key end
  table.sort(keys)
  return keys
end

local registries, events, hooks, eventSites, hookSites

function Catalog.registries()
  if not registries then registries = sortedKeys(Schemas.REGISTRIES) end
  return registries
end

-- Runtime.emit is the engine's channel; a bare bus:emit inside src counts
-- too (the loader emits mods.loaded straight off its own bus)
function Catalog.events()
  if not events then
    eventSites = scan({ "src" }, {
      'Runtime%.emit%("([%w%._]+)"',
      'events:emit%("([%w%._]+)"',
    })
    events = sortedKeys(eventSites)
  end
  return events
end

-- A hook whose Runtime.call lives in a shared raiser rather than at the site
-- that decides the value.  Gold's trainer art is not in field.playerPics, so
-- its screens resolve their own path and hand it to Sprites.playerPic
-- (src/pokemon/Sprites.lua); those callers are player.sprite sites and the
-- generation gate has to see them as such.
local INDIRECT_HOOKS = {
  ["player.sprite"] = "playerPic%(",
}

function Catalog.hooks()
  if not hooks then
    hookSites = scan({ "src" }, {
      'Runtime%.call%("([%w%._]+)"',
      'hooks:call%("([%w%._]+)"',
    })
    for name, pattern in pairs(INDIRECT_HOOKS) do
      local list = hookSites[name] or {}
      hookSites[name] = list
      for _, path in ipairs(luaFilesUnder("src")) do
        local handle = io.open(path, "r")
        if handle then
          local body = handle:read("*a")
          handle:close()
          if body:match(pattern) then list[#list + 1] = path end
        end
      end
    end
    hooks = sortedKeys(hookSites)
  end
  return hooks
end

function Catalog.eventSites(name)
  Catalog.events()
  return eventSites[name] or {}
end

function Catalog.hookSites(name)
  Catalog.hooks()
  return hookSites[name] or {}
end

-- mods may only emit under "mod.<id>."; those are not engine seams and
-- carry no coverage requirement
function Catalog.isModEvent(name)
  return name:sub(1, 4) == "mod."
end

return Catalog
