///|
/// PEP 425 tag **computation**: the ordered tag list for a target the caller
/// describes, in the same order as `packaging.tags`.
///
/// The library already *matches* and *ranks* tags -- `parse_wheel_filename`
/// expands a wheel's compressed tag set, `IndexFile::tags` reports it, and the
/// resolver ranks candidates by the smallest index a wheel's tags occupy in the
/// caller's ordered tag list. What it did not do is *produce* that list. This
/// module does, and it is the same boundary as everywhere else in the library:
/// **the caller describes the target, the library computes from it, and nothing
/// is probed**.
///
/// That boundary decides what can be here. `packaging.tags.sys_tags()` is a
/// tuple of four probes -- `sys.version_info`, `sysconfig` config variables for
/// the ABI, `EXT_SUFFIX`, and `platform_tags()`, which on Linux inspects the
/// running libc to decide between `manylinux` and `musllinux`. None of that can
/// be reproduced from a document, and a library that guesses would give
/// different answers on different machines, which is exactly what this one is
/// built not to do. So:
///
/// - `cpython_tags`, `generic_tags`, `pure_python_tags`, `compatible_tags` and
///   `mac_platforms` take their interpreter, ABI list and platform list as
///   arguments, and behave exactly like the reference for those arguments;
/// - `manylinux` / `musllinux` / `linux` platform tags are **not** computed:
///   the caller passes whatever `packaging.tags.platform_tags()` said on the
///   machine it is describing, including the `manylinux` spellings;
/// - a missing argument that `packaging` would fill in from the host (an empty
///   Python version, an empty interpreter name) is a stable error here rather
///   than a silent probe.
///
/// The empty **platform** list is the sharpest edge of that boundary, because it
/// does not look like a probe at all. Every reference function here writes
/// `platforms = list(platforms or platform_tags())`, so an empty list does not
/// mean "no platforms", it means "ask the running machine" -- with the same `[]`
/// a caller would pass for a target that has none. This library reads `[]` the
/// way it is written and refuses the ambiguity: an empty platform list is
/// `TAG_PLATFORMS_REQUIRED`, and the three cases are registered as declared
/// divergences in `fixtures/tag_corpus.mbt`.
///
/// Order matters and is preserved: PEP 425 says a tool must prefer an earlier tag,
/// so the results are ordered arrays, most specific first, never sorted sets.

///|
/// The first two entries of a version, the way Python's `python_version[:2]`
/// behaves: a slice clamps instead of raising, so a major-only version yields
/// one entry.
fn first_two(version : Array[Int]) -> Array[Int] {
  if version.length() <= 2 {
    return version.copy()
  }
  [version[0], version[1]]
}

///|
/// `[3, 10]` becomes `"310"`, `[3]` becomes `"3"`, the way `packaging`'s
/// `_version_nodot` spells an interpreter version.
fn version_nodot(version : Array[Int]) -> String {
  let sb = StringBuilder()
  for part in version {
    sb.write_string(part.to_string())
  }
  sb.to_string()
}

///|
/// The `interpreter-abi-platform` spelling, lowercased.
///
/// `packaging`'s `Tag` lowercases all three components in its constructor, so
/// this is the reference's own normalization rather than a choice of ours: the
/// same tag written in any case is one tag.
fn tag_string(interpreter : String, abi : String, platform : String) -> String {
  "\{interpreter.to_lower()}-\{abi.to_lower()}-\{platform.to_lower()}"
}

///|
/// `packaging`'s `_is_threaded_cpython`: the ABI of a `--disable-gil` build (PEP
/// 703) carries a `t` in its flags, so `cp313t` is threaded and `cp313` is not.
/// The reference reads this off the first ABI, which is the one the interpreter
/// itself reported.
fn is_threaded_cpython(abis : Array[String]) -> Bool {
  if abis.length() == 0 {
    return false
  }
  let chars = abis[0].to_array()
  if chars.length() < 3 || chars[0] != 'c' || chars[1] != 'p' {
    return false
  }
  let mut i = 2
  let mut digits = 0
  while i < chars.length() && chars[i].is_ascii_digit() {
    i = i + 1
    digits = digits + 1
  }
  if digits == 0 {
    return false
  }
  for j = i; j < chars.length(); j = j + 1 {
    if chars[j] == 't' {
      return true
    }
  }
  false
}

///|
/// The tuple comparison `python_version >= (3, 2)` the reference uses for the
/// two stable-ABI decisions. A one-element version (a major version only) never
/// supports them, which is what `len(python_version) > 1` says there.
fn at_least_3_2(python_version : Array[Int]) -> Bool {
  if python_version.length() < 2 {
    return false
  }
  python_version[0] > 3 || (python_version[0] == 3 && python_version[1] >= 2)
}

///|
/// `packaging`'s `_abi3_applies`: PEP 384 landed in 3.2, and a free-threaded
/// build does not support the stable ABI.
fn abi3_applies(python_version : Array[Int], threading : Bool) -> Bool {
  at_least_3_2(python_version) && !threading
}

///|
/// `packaging`'s `_abi3t_applies`: PEP 803's threaded stable ABI, which the
/// reference generates back to 3.2 as well, to leave room for `abi3t` wheels
/// that support older limited-API versions.
fn abi3t_applies(python_version : Array[Int], threading : Bool) -> Bool {
  at_least_3_2(python_version) && threading
}

///|
/// Remove the first occurrence of `value`, the way `list.remove` raises nothing
/// in `packaging`'s `try/except ValueError` around each explicit ABI.
fn remove_first(items : Array[String], value : String) -> Unit {
  for i = 0; i < items.length(); i = i + 1 {
    if items[i] == value {
      ignore(items.remove(i))
      return
    }
  }
}

///|
/// The CPython tags for a target, in `packaging.tags.cpython_tags` order.
///
/// `python_version` is `[3, 12]` or `[3]`; `abis` is what the interpreter
/// reported (`["cp312m"]`, `["cp313t"]`, `["abi3"]`, ...); `platforms` is the
/// platform list, most specific first.
///
/// The order the reference produces is: the explicit ABIs against every
/// platform, then the stable ABI (`abi3`, plus `abi3t` on a free-threaded
/// build) against every platform, then the empty `none` ABI, and finally the
/// same stable ABIs for the previous minor versions down to `cp32` -- older
/// stable-ABI wheels are usable, older version-specific ones are not.
///
/// ```moonbit nocheck
/// cpython_tags([3, 12], ["cp312"], ["linux_x86_64"])
/// // ["cp312-cp312-linux_x86_64", "cp312-abi3-linux_x86_64",
/// //  "cp312-none-linux_x86_64", "cp311-abi3-linux_x86_64", ...]
/// ```
pub fn cpython_tags(
  python_version : Array[Int],
  abis : Array[String],
  platforms : Array[String],
) -> Array[String] raise VersionError {
  if python_version.length() == 0 {
    // `packaging` would fall back to `sys.version_info`, which is the host. The
    // caller has to say which interpreter it is describing.
    raise VersionError::InvalidTag("TAG_VERSION_REQUIRED", 0)
  }
  if platforms.length() == 0 {
    // `platforms or platform_tags()`: an empty list means "probe the host"
    // there, and an empty tag list here. Refusing is the honest reading.
    raise VersionError::InvalidTag("TAG_PLATFORMS_REQUIRED", 0)
  }
  let interpreter = "cp\{version_nodot(first_two(python_version))}"
  let threading = is_threaded_cpython(abis)
  let explicit : Array[String] = if threading {
    ["abi3", "abi3t", "none"]
  } else {
    ["abi3", "none"]
  }
  let remaining : Array[String] = []
  for abi in abis {
    remaining.push(abi)
  }
  for abi in explicit {
    remove_first(remaining, abi)
  }
  let out : Array[String] = []
  for abi in remaining {
    for platform in platforms {
      out.push(tag_string(interpreter, abi, platform))
    }
  }
  let use_abi3 = abi3_applies(python_version, threading)
  let use_abi3t = abi3t_applies(python_version, threading)
  if use_abi3 {
    for platform in platforms {
      out.push(tag_string(interpreter, "abi3", platform))
    }
  }
  if use_abi3t {
    for platform in platforms {
      out.push(tag_string(interpreter, "abi3t", platform))
    }
  }
  for platform in platforms {
    out.push(tag_string(interpreter, "none", platform))
  }
  if use_abi3 || use_abi3t {
    let major = python_version[0]
    let minor = python_version[1]
    for older = minor - 1; older > 1; older = older - 1 {
      let older_interpreter = "cp\{version_nodot([major, older])}"
      for platform in platforms {
        if use_abi3 {
          out.push(tag_string(older_interpreter, "abi3", platform))
        }
        if use_abi3t {
          out.push(tag_string(older_interpreter, "abi3t", platform))
        }
      }
    }
  }
  out
}

///|
/// The tags for an interpreter that needs no specialization
/// (`packaging.tags.generic_tags`): every ABI against every platform, with the
/// empty `none` ABI appended when the caller did not list it.
///
/// The `none` test is exact, exactly as it is there: `["NONE"]` does not contain
/// `none`, so `none` is appended and the resulting list carries the same tag
/// twice. That is a duplicate in the reference too, and a corpus that silently
/// deduplicated it would be testing a different function.
pub fn generic_tags(
  interpreter : String,
  abis : Array[String],
  platforms : Array[String],
) -> Array[String] raise VersionError {
  if interpreter.is_empty() {
    // `packaging` would ask the running interpreter what it is.
    raise VersionError::InvalidTag("TAG_INTERPRETER_REQUIRED", 0)
  }
  if platforms.length() == 0 {
    raise VersionError::InvalidTag("TAG_PLATFORMS_REQUIRED", 0)
  }
  let all : Array[String] = []
  for abi in abis {
    all.push(abi)
  }
  if !all.contains("none") {
    all.push("none")
  }
  let out : Array[String] = []
  for abi in all {
    for platform in platforms {
      out.push(tag_string(interpreter, abi, platform))
    }
  }
  out
}

///|
/// `py310`, `py3`, `py39`, ... `py30`: the interpreter versions a project
/// supports without a compiled component, in `packaging`'s
/// `_py_interpreter_range` order -- the exact version, then the major-only one,
/// then every earlier minor of that major.
fn py_interpreter_range(python_version : Array[Int]) -> Array[String] {
  let out : Array[String] = []
  if python_version.length() > 1 {
    out.push("py\{version_nodot(first_two(python_version))}")
  }
  out.push("py\{python_version[0]}")
  if python_version.length() > 1 {
    let major = python_version[0]
    for minor = python_version[1] - 1; minor >= 0; minor = minor - 1 {
      out.push("py\{version_nodot([major, minor])}")
    }
  }
  out
}

///|
/// The pure-Python tags (`packaging.tags.pure_python_tags`): `none` ABI, `any`
/// platform, one per interpreter version in `py_interpreter_range` order. These
/// are the only tags whose computation does not depend on the target platform
/// at all, which is why a single-platform library can still ship them.
pub fn pure_python_tags(
  python_version : Array[Int],
) -> Array[String] raise VersionError {
  if python_version.length() == 0 {
    raise VersionError::InvalidTag("TAG_VERSION_REQUIRED", 0)
  }
  let out : Array[String] = []
  for version in py_interpreter_range(python_version) {
    out.push(tag_string(version, "none", "any"))
  }
  out
}

///|
/// The tags for a non-CPython interpreter that is compatible with the given
/// Python version (`packaging.tags.compatible_tags`): every `py*` version
/// against every platform, then `-none-any` when the caller named
/// an interpreter, then the pure-Python tags.
///
/// `interpreter` may be `None` (an unnamed interpreter) or an empty string: the
/// reference tests truthiness, so both mean "do not emit that line".
pub fn compatible_tags(
  python_version : Array[Int],
  interpreter : String?,
  platforms : Array[String],
) -> Array[String] raise VersionError {
  if python_version.length() == 0 {
    raise VersionError::InvalidTag("TAG_VERSION_REQUIRED", 0)
  }
  if platforms.length() == 0 {
    raise VersionError::InvalidTag("TAG_PLATFORMS_REQUIRED", 0)
  }
  let out : Array[String] = []
  for version in py_interpreter_range(python_version) {
    for platform in platforms {
      out.push(tag_string(version, "none", platform))
    }
  }
  match interpreter {
    Some(name) =>
      if !name.is_empty() {
        out.push(tag_string(name, "none", "any"))
      }
    None => ()
  }
  for tag in pure_python_tags(python_version) {
    out.push(tag)
  }
  out
}

///|
/// `packaging`'s `_mac_binary_formats`: the binary formats a macOS version
/// supports for an architecture, oldest first. The empty list for a version that
/// predates the architecture is not an error -- it is how the reference says
/// "no such wheel can exist", and it is what keeps `mac_platforms("10.3",
/// "x86_64")` from inventing tags.
fn mac_binary_formats(version : (Int, Int), arch : String) -> Array[String] {
  let formats : Array[String] = [arch]
  if arch == "x86_64" {
    if version < (10, 4) {
      return []
    }
    formats.push("intel")
    formats.push("fat64")
    formats.push("fat3")
  } else if arch == "i386" {
    if version < (10, 4) {
      return []
    }
    formats.push("intel")
    formats.push("fat3")
    formats.push("fat")
  } else if arch == "ppc64" {
    if version > (10, 5) || version < (10, 4) {
      return []
    }
    formats.push("fat64")
  } else if arch == "ppc" {
    if version > (10, 6) {
      return []
    }
    formats.push("fat3")
    formats.push("fat")
  }
  if arch == "arm64" || arch == "x86_64" {
    formats.push("universal2")
  }
  if arch == "x86_64" ||
    arch == "i386" ||
    arch == "ppc64" ||
    arch == "ppc" ||
    arch == "intel" {
    formats.push("universal")
  }
  formats
}

///|
/// The macOS platform tags for a version and architecture
/// (`packaging.tags.mac_platforms`), most specific first.
///
/// Both arguments are required, unlike the reference, where either may be left
/// out and read from the running machine. The rules are the reference's own:
/// before macOS 11 every release bumped the minor version under major 10; from
/// 11 on the major version is the release and the minor one is a midyear update;
/// and on 11+ the compatible 10.x range is replayed for `x86_64` (with the
/// `universal2` spelling only for other architectures).
pub fn mac_platforms(version : (Int, Int), arch : String) -> Array[String] {
  let out : Array[String] = []
  if version >= (10, 0) && version < (11, 0) {
    for minor = version.1; minor >= 0; minor = minor - 1 {
      for format in mac_binary_formats((10, minor), arch) {
        out.push("macosx_10_\{minor}_\{format}")
      }
    }
  }
  if version >= (11, 0) {
    for major = version.0; major > 10; major = major - 1 {
      for format in mac_binary_formats((major, 0), arch) {
        out.push("macosx_\{major}_0_\{format}")
      }
    }
    let major = 10
    for minor = 16; minor > 3; minor = minor - 1 {
      if arch == "x86_64" {
        for format in mac_binary_formats((major, minor), arch) {
          out.push("macosx_10_\{minor}_\{format}")
        }
      } else {
        out.push("macosx_10_\{minor}_universal2")
      }
    }
  }
  out
}

///|
/// The rank of a file's tag set against an ordered `supported` list: the
/// smallest index any of `tags` occupies, or `supported.length()` when none
/// does.
///
/// This is the selector rule `packaging.tags.create_compatible_tags_selector`
/// applies -- a thing matched by several tags is ranked by its *best* one, and
/// `supported` is ordered most specific first -- and the same rule the resolver
/// uses to order candidates. It is public here so callers who built their tag
/// list with the functions above can rank wheels with it.
pub fn tag_rank(tags : Array[String], supported : Array[String]) -> Int {
  let mut best = supported.length()
  for i = 0; i < tags.length(); i = i + 1 {
    for j = 0; j < supported.length(); j = j + 1 {
      if supported[j] == tags[i] && j < best {
        best = j
      }
    }
  }
  best
}