///|
/// PEP 508 requirement lines: `name[extras] specifier ; marker` and
/// `name[extras] @ url ; marker`.
///
/// This is the piece that makes the dependency-inspection scenario work
/// end-to-end: a real `Requires-Dist` line from `METADATA` can be handed
/// straight to `Requirement::parse` instead of the caller having to extract the
/// constraint text by hand.
///
/// Semantics are aligned with PyPA `packaging` 26.3 and verified by the
/// differential corpus (`req` records). Two behaviours are worth calling out
/// because they are easy to get backwards:
///
/// - the *name* and the *extras* keep the spelling the caller used
///   (`Requests[Security]` stays as written), while
/// - the *specifier* is normalized: operands keep their spelling but the
///   clauses are deduplicated and sorted, so `>=2.0,<3.0` renders as
///   `<3.0,>=2.0`.
///
/// The marker is handed to `markers.mbt`, so an unknown marker key or a broken
/// `and`/`or` chain is rejected here exactly where the oracle rejects it. The
/// marker text is also kept verbatim on the parsed value, because re-rendering
/// it canonically belongs to the marker module.

///|
/// A parsed PEP 508 requirement.
pub(all) struct Requirement {
  /// The project name exactly as written; call `canonicalize_name` to compare.
  name : String
  /// Extras exactly as written, deduplicated and sorted.
  extras : Array[String]
  /// The normalized constraint set.
  specifier : SpecifierSet
  /// Present for the `name @ url` form.
  url : String?
  /// The marker text after `;`, verbatim, if there is one.
  marker_text : String?
  /// The caller's original string.
  raw : String
} derive(@debug.Debug)

///|
pub extend Requirement with @debug.Debug::{to_repr}

///|
pub extend Requirement with Show::{output}

///|
fn is_name_alnum(c : Char) -> Bool {
  c.is_ascii_alphabetic() || c.is_ascii_digit()
}

///|
fn is_requirement_name_start(c : Char) -> Bool {
  is_name_alnum(c)
}

///|
fn is_requirement_name_char(c : Char) -> Bool {
  is_name_alnum(c) || c == '.' || c == '_' || c == '-'
}

///|
fn is_requirement_name_end(c : Char) -> Bool {
  is_name_alnum(c) || c == '_'
}

///|
/// PEP 508 names, as `packaging` actually accepts them: the first character is
/// alphanumeric, the last is alphanumeric or `_`, and the middle may also hold
/// `.`, `_` and `-`. This is why `foo_` and `foo._` are accepted while `foo-`,
/// `foo.` and `-foo` are not.
fn is_valid_requirement_name(text : String) -> Bool {
  let chars = text.to_array()
  if chars.length() == 0 {
    return false
  }
  if !is_requirement_name_start(chars[0]) {
    return false
  }
  if !is_requirement_name_end(chars[chars.length() - 1]) {
    return false
  }
  for i = 1; i < chars.length() - 1; i = i + 1 {
    if !is_requirement_name_char(chars[i]) {
      return false
    }
  }
  true
}

///|
fn is_requirement_space(c : Char) -> Bool {
  c == ' ' || c == '\t'
}

///|
/// Sort and deduplicate strings by code point, which is the order
/// `packaging` uses when it renders a set.
fn sorted_unique(items : Array[String]) -> Array[String] {
  let out : Array[String] = []
  for item in items {
    if !out.contains(item) {
      out.push(item)
    }
  }
  out.sort_by(fn(a, b) { String::lexical_compare(a, b) })
  out
}

///|
/// Skip ASCII spaces and tabs starting at `pos`.
fn skip_space(chars : Array[Char], pos : Int) -> Int {
  let mut i = pos
  while i < chars.length() && is_requirement_space(chars[i]) {
    i = i + 1
  }
  i
}

///|
fn slice(chars : Array[Char], start : Int, stop : Int) -> String {
  String::from_array(chars[start:stop].to_owned())
}

///|
/// Read an extras list at `pos`, where `chars[pos]` is the opening bracket.
/// Returns the extras and the position just past the closing bracket.
///
/// `name[]` and `name[  ]` are accepted and contribute no extras, but a list
/// that has started must be closed, and a comma may not be followed by the
/// bracket: `name[a,]` is not a requirement.
fn parse_extras(
  chars : Array[Char],
  pos : Int,
) -> (Array[String], Int) raise VersionError {
  let extras : Array[String] = []
  let mut cursor = pos + 1
  cursor = skip_space(chars, cursor)
  if cursor < chars.length() && chars[cursor] != ']' {
    let mut done = false
    while !done {
      cursor = skip_space(chars, cursor)
      let start = cursor
      while cursor < chars.length() && is_requirement_name_char(chars[cursor]) {
        cursor = cursor + 1
      }
      let extra = slice(chars, start, cursor)
      if !is_valid_requirement_name(extra) {
        raise VersionError::InvalidRequirement("INVALID_EXTRA", start)
      }
      extras.push(extra)
      cursor = skip_space(chars, cursor)
      if cursor < chars.length() && chars[cursor] == ',' {
        cursor = cursor + 1
      } else if cursor < chars.length() && chars[cursor] == ']' {
        done = true
      } else {
        raise VersionError::InvalidRequirement("EXPECTED_EXTRAS_END", cursor)
      }
    }
  }
  if cursor < chars.length() && chars[cursor] == ']' {
    (extras, cursor + 1)
  } else {
    raise VersionError::InvalidRequirement("UNCLOSED_EXTRAS", cursor)
  }
}

///|
/// Read the marker that follows a `;`: everything up to the end of the line,
/// trimmed, then validated by the marker parser.
fn parse_marker_text(
  chars : Array[Char],
  pos : Int,
) -> String raise VersionError {
  let text = slice(chars, pos, chars.length()).trim().to_owned()
  if text.is_empty() {
    raise VersionError::InvalidRequirement("EMPTY_MARKER", pos)
  }
  validate_marker_text(text)
  text
}

///|
/// True for the characters a version specifier can start with. Characters that
/// cannot (a comma, say) mean the line had no specifier at all, which the
/// grammar reports differently from a malformed one.
fn is_specifier_start(c : Char) -> Bool {
  c == '(' || c == '=' || c == '<' || c == '>' || c == '!' || c == '~'
}

///|
/// Split a specifier body on commas, keeping empty segments.
fn split_keeping_empty(body : String) -> Array[String] {
  let chars = body.to_array()
  let out : Array[String] = []
  let mut start = 0
  for i = 0; i < chars.length(); i = i + 1 {
    if chars[i] == ',' {
      out.push(slice(chars, start, i))
      start = i + 1
    }
  }
  out.push(slice(chars, start, chars.length()))
  out
}

///|
/// The requirement grammar allows a trailing comma but no empty clause, so
/// `>=1.0,` is a requirement while `>=1.0,,` and `>=1.0, ,<2` are not, even
/// though the bare specifier parser tolerates all three. Returns the offset of
/// the offending comma, or nothing when the layout is fine.
fn specifier_layout_error(body : String) -> Int? {
  let segments = split_keeping_empty(body)
  if segments[0].trim().is_empty() {
    return Some(0)
  }
  let mut offset = 0
  for i = 0; i < segments.length(); i = i + 1 {
    if i > 0 && segments[i].trim().is_empty() && i != segments.length() - 1 {
      return Some(offset - 1)
    }
    offset = offset + segments[i].length() + 1
  }
  None
}

///|
/// Index of the first `;` that ends a version specifier.
///
/// The specifier may be wrapped in parentheses, which is a spelling PEP 508
/// dropped but the oracle still accepts; a `;` inside those parentheses does
/// not separate a marker.
fn specifier_end(chars : Array[Char], pos : Int) -> Int {
  let mut cursor = pos
  let mut depth = 0
  while cursor < chars.length() {
    let c = chars[cursor]
    if c == '(' {
      depth = depth + 1
    } else if c == ')' {
      if depth > 0 {
        depth = depth - 1
      } else {
        return cursor
      }
    } else if c == ';' && depth == 0 {
      return cursor
    }
    cursor = cursor + 1
  }
  chars.length()
}

///|
/// Parse a PEP 508 requirement line.
///
/// ```moonbit nocheck
/// let r = try! Requirement::parse("Requests[Security]>=2.0,<3.0 ; python_version >= \"3.8\"")
/// r.name                      // "Requests"
/// r.extras                    // ["Security"]
/// r.specifier.to_string()     // "<3.0,>=2.0"
/// r.marker_text               // Some("python_version >= \"3.8\"")
/// ```
///
/// The two operand forms have different whitespace rules, which is why the URL
/// form is parsed before any `;` is looked for: the URL runs to the next space,
/// so a `;` can be part of it.
pub fn Requirement::parse(input : String) -> Requirement raise VersionError {
  let chars = input.to_array()
  let end = chars.length()
  let mut pos = skip_space(chars, 0)
  if pos >= end {
    raise VersionError::InvalidRequirement("EMPTY_REQUIREMENT", pos)
  }
  // name: the first run of name characters, validated as a whole
  let name_start = pos
  while pos < end && is_requirement_name_char(chars[pos]) {
    pos = pos + 1
  }
  let name = slice(chars, name_start, pos)
  if !is_valid_requirement_name(name) {
    raise VersionError::InvalidRequirement("INVALID_NAME", name_start)
  }
  // extras
  pos = skip_space(chars, pos)
  let mut extras : Array[String] = []
  if pos < end && chars[pos] == '[' {
    let (parsed, next) = parse_extras(chars, pos)
    extras = parsed
    pos = next
  }
  pos = skip_space(chars, pos)
  let mut url : String? = None
  let mut marker_text : String? = None
  let mut specifier = SpecifierSet::parse("")
  if pos < end && chars[pos] == '@' {
    // `name @ url (whitespace requirement_marker?)?`
    pos = pos + 1
    pos = skip_space(chars, pos)
    let start = pos
    while pos < end && !is_requirement_space(chars[pos]) {
      pos = pos + 1
    }
    if pos == start {
      raise VersionError::InvalidRequirement("EMPTY_URL", start)
    }
    url = Some(slice(chars, start, pos))
    if pos < end {
      let before = pos
      pos = skip_space(chars, pos)
      if pos == before {
        raise VersionError::InvalidRequirement(
          "EXPECTED_WHITESPACE_AFTER_URL", pos,
        )
      }
      if pos < end {
        if chars[pos] != ';' {
          raise VersionError::InvalidRequirement(
            "EXPECTED_SEMICOLON_AFTER_URL", pos,
          )
        }
        pos = pos + 1
        marker_text = Some(parse_marker_text(chars, pos))
      }
    }
  } else if pos < end && chars[pos] == ';' {
    // `name ; marker`: the specifier is empty, and the separator follows the
    // name directly, as in `BeautifulSoup4; extra == "htmlsoup"`.
    pos = pos + 1
    marker_text = Some(parse_marker_text(chars, pos))
  } else if pos < end {
    // `specifier (whitespace requirement_marker)?`
    if !is_specifier_start(chars[pos]) {
      raise VersionError::InvalidRequirement("EXPECTED_SPECIFIER", pos)
    }
    let paren = chars[pos] == '('
    let stop = if paren {
      let mut cursor = pos + 1
      while cursor < end && chars[cursor] != ')' {
        cursor = cursor + 1
      }
      if cursor >= end {
        raise VersionError::InvalidRequirement("UNCLOSED_VERSION_SPEC", pos)
      }
      cursor
    } else {
      specifier_end(chars, pos)
    }
    let body = if paren {
      slice(chars, pos + 1, stop)
    } else {
      slice(chars, pos, stop)
    }
    let body_start = if paren { pos + 1 } else { pos }
    match specifier_layout_error(body) {
      Some(offset) =>
        raise VersionError::InvalidRequirement(
          "EXPECTED_SPECIFIER",
          body_start + offset,
        )
      None => ()
    }
    specifier = SpecifierSet::parse(body.trim().to_owned())
    pos = if paren { stop + 1 } else { stop }
    pos = skip_space(chars, pos)
    if pos < end {
      if chars[pos] != ';' {
        raise VersionError::InvalidRequirement("TRAILING_INPUT", pos)
      }
      pos = pos + 1
      marker_text = Some(parse_marker_text(chars, pos))
    }
  }
  {
    name,
    extras: sorted_unique(extras),
    specifier,
    url,
    marker_text,
    raw: input,
  }
}

///|
/// The normalized rendering: name, sorted extras, normalized specifier, URL and
/// marker, in that order.
pub fn Requirement::to_string(self : Requirement) -> String {
  let b = StringBuilder()
  b.write_string(self.name)
  if self.extras.length() > 0 {
    b.write_string("[")
    b.write_string(self.extras.join(","))
    b.write_string("]")
  }
  b.write_string(self.specifier.to_string())
  match self.url {
    Some(u) => b.write_string(" @ \{u}")
    None => ()
  }
  match self.marker_text {
    Some(m) =>
      if self.url is Some(_) {
        b.write_string(" ; \{m}")
      } else {
        b.write_string("; \{m}")
      }
    None => ()
  }
  b.to_string()
}

///|
pub impl Show for Requirement with fn to_string(self) {
  self.to_string()
}