-- Ordered, namespaced content registries used by the native mod API.
-- Each registry stores an op log per id (register/override/patch/remove)
-- folded over the base record at read/merge time, so patches stack across
-- mods in load order and undoing a failed mod is just dropping its ops.
-- The loader merges effective values into the live data only after every
-- enabled mod has initialized successfully.
local Merge = require("src.mods.Merge")

local Registry = {}
Registry.__index = Registry

-- exposed to mods as mod.DELETE: a patch value that unsets a field
Registry.DELETE = Merge.DELETE

local function assertWritable(self, id)
  if self.spec.reservedIds and self.spec.reservedIds[id] then
    error(("%s id is reserved engine metadata: %s"):format(self.name, id))
  end
end

-- spec comes from Schemas.REGISTRIES[name]; bare Registry.new(name) keeps
-- the v1 record behavior for standalone use in tests and tools
function Registry.new(name, spec)
  return setmetatable({
    name = name,
    spec = spec or { semantics = "record" },
    ops = {},      -- id -> ordered { op, value, owner }
    owners = {},   -- id -> last-writing owner (provenance for errors)
    order = {},    -- ids in first-touch order, for array-rebuilding targets
    seen = {},     -- id -> true, keeps order free of duplicates
    cache = {},    -- id -> { value } memoized fold
    base = nil,    -- installed by the loader: fn() -> base table or nil
    frozen = false,
  }, Registry)
end

local function append(self, id, op, value, owner)
  if self.frozen then
    error(self.name .. ": content is frozen after load")
  end
  assert(type(id) == "string" and id ~= "", self.name .. " id is required")
  assertWritable(self, id)
  local list = self.ops[id]
  if not list then
    list = {}
    self.ops[id] = list
  end
  -- a rolled-back id keeps its slot: order is registration history, not a
  -- live key set, so a resurrected id stays where it first appeared
  if not self.seen[id] then
    self.seen[id] = true
    self.order[#self.order + 1] = id
  end
  list[#list + 1] = { op = op, value = value, owner = owner }
  self.owners[id] = owner
  self.cache[id] = nil
  return value
end

-- spec.baseAt lets a registry whose ids do not map one-to-one onto target
-- keys (battle_anims routes by id prefix) resolve its own pristine value
local function baseValue(self, id)
  local base = self.base and self.base()
  if base == nil then return nil end
  if self.spec.baseAt then return self.spec.baseAt(base, id) end
  return base[id]
end

-- effective value = base plus the op list; a tombstone folds to nil and a
-- later register may resurrect the id
local function fold(self, value, opList)
  local deep = self.spec.semantics == "deep"
  for _, entry in ipairs(opList or {}) do
    local op = entry.op
    -- a payload that IS the sentinel folds as a delete, never a value;
    -- without this the bare DELETE table would leak into Data as a record
    if entry.value == Merge.DELETE then
      value = nil
    elseif op == "override" or (op == "register" and not deep) then
      value = entry.value
    elseif op == "register" or op == "patch" then
      -- deep registries treat register and patch alike; scalar payloads
      -- (a lone top-level value) replace outright
      if type(entry.value) == "table" then
        value = Merge.deepMerge(Merge.deepCopy(value == nil and {} or value),
          entry.value, self.spec.semantics)
      else
        value = entry.value
      end
    elseif op == "remove" then
      value = nil
    end
  end
  return value
end

function Registry:register(id, value, owner, replace)
  if replace then return self:override(id, value, owner) end -- v1 signature
  assert(value ~= nil, self.name .. " value is required for " .. tostring(id))
  -- duplicates collide against the base table too, forcing an explicit
  -- override; compose chains accumulate and deep keys merge instead
  if self.spec.semantics == "record" and self:get(id) ~= nil then
    error(("%s already registered: %s"):format(self.name, id))
  end
  return append(self, id, "register", value, owner)
end

function Registry:override(id, value, owner)
  assert(value ~= nil, self.name .. " value is required for " .. tostring(id))
  return append(self, id, "override", value, owner)
end

function Registry:patch(id, partial, owner)
  assert(partial ~= nil, self.name .. " patch value is required for " .. tostring(id))
  if self.spec.semantics == "compose" then
    error(self.name .. ": patch is not supported on compose registries")
  end
  return append(self, id, "patch", partial, owner)
end

-- tombstone: consumers treat the id as absent after the merge
function Registry:remove(id, owner)
  return append(self, id, "remove", nil, owner)
end

function Registry:get(id)
  if self.spec.semantics == "compose" then
    -- chain() sorts top priority first, so the head is the effective value
    local chain = self:chain(id)
    return chain[1]
  end
  local hit = self.cache[id]
  if hit then return hit.value end
  local value = fold(self, baseValue(self, id), self.ops[id])
  self.cache[id] = { value = value }
  return value
end

function Registry:has(id)
  return self:get(id) ~= nil
end

-- compose fold: the ordered entry list for an id.  Override is the
-- total-conversion escape hatch (09 4.4) -- it clears the whole chain, every
-- owner's entries alike, and installs itself as the only contribution;
-- remove tombstones the whole entry the same way but installs nothing.
-- Order is priority (higher first) then registration order.  The second
-- return says the chain was cleared, which is how a consumer holding an
-- out-of-band base contribution (MapScripts) knows to leave it out.
local function composed(self, id)
  local entries, replacesBase = {}, false
  for seq, entry in ipairs(self.ops[id] or {}) do
    if entry.op == "register" then
      entries[#entries + 1] = { value = entry.value, owner = entry.owner, seq = seq }
    elseif entry.op == "override" then
      for i = #entries, 1, -1 do entries[i] = nil end
      entries[1] = { value = entry.value, owner = entry.owner, seq = seq }
      replacesBase = true
    elseif entry.op == "remove" then
      -- owner-scoped removal would leave the consumer's own base
      -- contribution standing, so the map would still dispatch; 09 4.4
      -- makes remove a whole-entry tombstone.  A later register still
      -- resurrects the id, ops after this one survive the clear
      for i = #entries, 1, -1 do entries[i] = nil end
      replacesBase = true
    end
  end
  table.sort(entries, function(a, b)
    local pa = type(a.value) == "table" and a.value.priority or 0
    local pb = type(b.value) == "table" and b.value.priority or 0
    if pa ~= pb then return pa > pb end
    return a.seq < b.seq
  end)
  return entries, replacesBase
end

-- compose only: the ordered value list for an id
function Registry:chain(id)
  assert(self.spec.semantics == "compose",
    self.name .. ": chain is compose-only")
  local entries = composed(self, id)
  local values = {}
  for i = 1, #entries do values[i] = entries[i].value end
  return values
end

-- chain()'s owners, index-aligned with its values: consumers that
-- attribute dispatch (map_scripts runner sources) read both sides of the
-- same fold
function Registry:chainOwners(id)
  assert(self.spec.semantics == "compose",
    self.name .. ": chainOwners is compose-only")
  local entries = composed(self, id)
  local owners = {}
  for i = 1, #entries do owners[i] = entries[i].owner end
  return owners
end

-- compose only: true once an override has cleared this id's chain, so a
-- consumer that keeps its own base contribution outside the registry
-- (MapScripts' engine scripts) knows the total conversion excluded it
function Registry:chainReplacesBase(id)
  assert(self.spec.semantics == "compose",
    self.name .. ": chainReplacesBase is compose-only")
  local _, replacesBase = composed(self, id)
  return replacesBase
end

-- iterator over the merged view: base ids first, then op-only ids;
-- tombstoned ids are skipped.  No ordering guarantee.
function Registry:each()
  local ids, seen = {}, {}
  local base = self.base and self.base()
  if base then
    -- spec.baseIds names the ids hiding inside a structured target; without
    -- it the target's own keys are the id space
    if self.spec.baseIds then
      for _, id in ipairs(self.spec.baseIds(base)) do
        seen[id] = true
        ids[#ids + 1] = id
      end
    else
      for id in pairs(base) do
        seen[id] = true
        ids[#ids + 1] = id
      end
    end
  end
  for id in pairs(self.ops) do
    if not seen[id] then ids[#ids + 1] = id end
  end
  local i = 0
  return function()
    while true do
      i = i + 1
      local id = ids[i]
      if id == nil then return nil end
      local value = self:get(id)
      if value ~= nil then return id, value end
    end
  end
end

-- v1 compat: the values mods contributed, folded to their effective form
function Registry:items()
  local out = {}
  for id in pairs(self.ops) do out[id] = self:get(id) end
  return out
end

-- deletes every op an owner appended, in one pass; the loader calls this
-- before the merge so a failed mod leaves zero trace in Data
function Registry:rollback(owner)
  if owner == nil then return end
  for id, list in pairs(self.ops) do
    local touched = false
    for i = #list, 1, -1 do
      if list[i].owner == owner then
        table.remove(list, i)
        touched = true
      end
    end
    if touched then
      if #list == 0 then
        self.ops[id] = nil
        self.owners[id] = nil
      else
        self.owners[id] = list[#list].owner
      end
      self.cache[id] = nil
    end
  end
end

-- set once the boot merge has run; unlike the event/hook buses, content
-- stays deterministic by refusing registration after that point
function Registry:freeze()
  self.frozen = true
end

return Registry
