-- Which games a mod is for.  One derivation for the manifest's `games` key,
-- the legacy `gen2compat` flag, and the labels both mod surfaces draw:
-- src/mods/LauncherMods.lua and src/mods/ManagerState.lua read this rather
-- than each keeping its own copy of the rule.
local GameVersion = require("src.core.GameVersion")

local ModTargets = {}

-- every version of one generation, in launcher order (GameVersion.ORDER)
function ModTargets.generationVersions(gen)
  local out = {}
  for _, id in ipairs(GameVersion.ORDER) do
    if GameVersion.generation(id) == gen then out[#out + 1] = id end
  end
  return out
end

-- the generations this engine has games for, ascending
local function generations()
  local seen, out = {}, {}
  for _, id in ipairs(GameVersion.ORDER) do
    local gen = GameVersion.generation(id)
    if not seen[gen] then
      seen[gen] = true
      out[#out + 1] = gen
    end
  end
  table.sort(out)
  return out
end

-- one manifest token -> the version ids it covers, or nil when it names no
-- game this engine knows: "red" | "gen1" | "all"
function ModTargets.expand(token)
  if type(token) ~= "string" then return nil end
  local key = token:lower():match("^%s*(.-)%s*$")
  if key == "all" then return GameVersion.ORDER end
  if GameVersion.VERSIONS[key] then return { key } end
  local gen = key:match("^gen%s*(%d+)$")
  if gen then
    local list = ModTargets.generationVersions(tonumber(gen))
    if #list > 0 then return list end
  end
  return nil
end

-- version-id lists are always ORDER-sorted and deduped, so two manifests that
-- say the same thing different ways encode and compare the same
local function fromSet(set)
  local out = {}
  for _, id in ipairs(GameVersion.ORDER) do
    if set[id] then out[#out + 1] = id end
  end
  return out
end

-- normalize a manifest `games` array; second return is the tokens that named
-- no game, which Manifest reports at its own api level
function ModTargets.normalize(list)
  local set, unknown = {}, {}
  for _, token in ipairs(type(list) == "table" and list or {}) do
    local ids = ModTargets.expand(token)
    if ids then
      for _, id in ipairs(ids) do set[id] = true end
    else
      unknown[#unknown + 1] = tostring(token)
    end
  end
  return fromSet(set), unknown
end

function ModTargets.union(a, b)
  local set = {}
  for _, list in ipairs({ a or {}, b or {} }) do
    for _, id in ipairs(list) do set[id] = true end
  end
  return fromSet(set)
end

-- The pre-`games` reading of a manifest: Gen 1 always, Gen 2 only where the
-- author claimed gen2compat (src/mods/Manifest.lua).
function ModTargets.legacy(gen2compat)
  local out = ModTargets.generationVersions(1)
  if gen2compat then
    return ModTargets.union(out, ModTargets.generationVersions(2))
  end
  return out
end

-- does a version-id list hold any game of that generation
function ModTargets.covers(versions, gen)
  for _, id in ipairs(versions or {}) do
    if GameVersion.generation(id) == gen then return true end
  end
  return false
end

-- the version ids a validated manifest targets; Manifest.validate resolves
-- `games` at load, so this is the same answer everywhere
function ModTargets.versions(manifest)
  local games = manifest and manifest.games
  if type(games) == "table" and #games > 0 then return games end
  return ModTargets.legacy(manifest and manifest.gen2compat)
end

-- Does the mod target this game?  `version` is a version id; pass nil with a
-- generation to ask about a whole generation (the loader's injected seam).
function ModTargets.supports(manifest, version, generation)
  local versions = ModTargets.versions(manifest)
  if version and GameVersion.VERSIONS[version] then
    for _, id in ipairs(versions) do
      if id == version then return true end
    end
    return false
  end
  return ModTargets.covers(versions, generation or GameVersion.generation())
end

-- What will actually happen here: the loader gates on this same answer, per
-- version (Loader:_gateGeneration), and the player's override forces past it,
-- so the two UIs report a run, not a claim.
function ModTargets.runsHere(manifest, version, generation, forced)
  if forced then return true end
  return ModTargets.supports(manifest, version, generation)
end

-- "Gen 1" / "Gen 1+2" while a mod takes whole generations, the version names
-- ("Red/Gold") once it takes only some of one
function ModTargets.label(manifest)
  local versions = ModTargets.versions(manifest)
  local set = {}
  for _, id in ipairs(versions) do set[id] = true end
  local whole, partial = {}, false
  for _, gen in ipairs(generations()) do
    local all, any = true, false
    for _, id in ipairs(ModTargets.generationVersions(gen)) do
      if set[id] then any = true else all = false end
    end
    if any and all then whole[#whole + 1] = tostring(gen)
    elseif any then partial = true end
  end
  if #whole > 0 and not partial then
    return "Gen " .. table.concat(whole, "+")
  end
  local names = {}
  for _, id in ipairs(versions) do
    names[#names + 1] = GameVersion.info(id).label or id
  end
  if #names == 0 then return "No game" end
  return table.concat(names, "/")
end

-- the same label as an all-caps chip, for the launcher tag and the GB font
function ModTargets.chip(manifest)
  return ModTargets.label(manifest):upper()
end

-- one game's own name, for a line that has to say which one this is
function ModTargets.gameLabel(version)
  local info = version and GameVersion.info(version)
  return (info and info.label) or tostring(version)
end

-- One launcher-voice line for a mod that does not target `version`
-- (src/mods/LauncherMods.lua statusFor).
function ModTargets.detail(manifest, version)
  return ("For %s, not %s"):format(ModTargets.label(manifest),
    ModTargets.gameLabel(version))
end

-- Does a dependency spec apply to this game / version?
-- If spec.games is provided, it must match version / generation.
-- If spec.game_version is provided, the engine version must satisfy it.
function ModTargets.specApplies(spec, version, generation)
  if not spec then return false end
  if type(spec.games) == "table" and #spec.games > 0 then
    if version or generation then
      local match = false
      for _, id in ipairs(spec.games) do
        if version and id == version then
          match = true
          break
        end
        if generation and GameVersion.generation(id) == generation then
          match = true
          break
        end
      end
      if not match then return false end
    end
  end
  if spec.game_version then
    local ok = pcall(function()
      local Semver = require("src.mods.Semver")
      local Version = require("src.core.Version")
      if Version and Version.engine and Version.engine:match("^0%.0%.0%-") == nil then
        return Semver.satisfies(Version.engine, spec.game_version)
      end
      return true
    end)
    if not ok then return false end
  end
  return true
end

return ModTargets
