///|
/// A stable, structured error for version, specifier and packaging-metadata
/// parsing. Every variant carries a stable code and, where a position is
/// meaningful, a UTF-16 offset.
pub(all) suberror VersionError {
  InvalidVersion(String, Int)
  InvalidSpecifier(String, Int)
  InvalidName(String, Int)
  InvalidFilename(String, Int)
  InvalidRequirement(String, Int)
  InvalidMarker(String, Int)
  InvalidToml(String, Int)
  InvalidLicense(String, Int)
  InvalidMetadata(String, Int)
  InvalidIndex(String, Int)
  InvalidPylock(String, Int)
  InvalidTag(String, Int)
} derive(Eq, @debug.Debug)

///|
pub fn VersionError::diagnostic(self : VersionError) -> String {
  match self {
    InvalidVersion(code, pos) => "\{code} at \{pos}"
    InvalidSpecifier(code, pos) => "\{code} at \{pos}"
    InvalidName(code, pos) => "\{code} at \{pos}"
    InvalidFilename(code, pos) => "\{code} at \{pos}"
    InvalidRequirement(code, pos) => "\{code} at \{pos}"
    InvalidMarker(code, pos) => "\{code} at \{pos}"
    InvalidToml(code, pos) => "\{code} at \{pos}"
    InvalidLicense(code, pos) => "\{code} at \{pos}"
    InvalidMetadata(code, pos) => "\{code} at \{pos}"
    InvalidIndex(code, pos) => "\{code} at \{pos}"
    InvalidPylock(code, pos) => "\{code} at \{pos}"
    InvalidTag(code, pos) => "\{code} at \{pos}"
  }
}

///|
pub impl Show for VersionError with fn to_string(self) {
  VersionError::diagnostic(self)
}

///|
pub extend VersionError with Eq::{not_equal, equal}

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

///|
pub extend VersionError with Show::{to_string, output}

///|
/// One normalized local-version segment. Numeric segments are compared as
/// integers; textual segments are compared case-insensitively.
pub(all) struct LocalPart {
  text : String
  numeric : Bool
} derive(Eq, @debug.Debug)

///|
pub extend LocalPart with Eq::{equal, not_equal}

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

///|
/// A parsed PEP 440 version. `raw` keeps the caller's original string, while
/// `to_string` returns the normalized public form.
pub(all) struct Version {
  raw : String
  epoch : @bigint.BigInt
  release : Array[@bigint.BigInt]
  has_pre : Bool
  pre_kind : Int
  pre_num : @bigint.BigInt
  has_post : Bool
  post_num : @bigint.BigInt
  has_dev : Bool
  dev_num : @bigint.BigInt
  local_segments : Array[LocalPart]
} derive(@debug.Debug)

///|
pub extend Version with Compare::{op_lt, op_le, op_ge, op_gt}

///|
pub extend Version with Eq::{not_equal, equal}

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

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

///|
priv struct Cursor {
  source : String
  mut pos : Int
}

///|
fn is_digit(c : Int) -> Bool {
  c >= 48 && c <= 57
}

///|
fn is_ascii_letter(c : Int) -> Bool {
  (c >= 65 && c <= 90) || (c >= 97 && c <= 122)
}

///|
fn is_alnum(c : Int) -> Bool {
  is_digit(c) || is_ascii_letter(c)
}

///|
fn is_separator(c : Int) -> Bool {
  c == 46 || c == 45 || c == 95
}

///|
fn Cursor::peek(self : Cursor) -> Int {
  if self.pos >= self.source.length() {
    -1
  } else {
    self.source[self.pos].to_int()
  }
}

///|
fn Cursor::peek_at(self : Cursor, offset : Int) -> Int {
  let idx = self.pos + offset
  if idx >= self.source.length() {
    -1
  } else {
    self.source[idx].to_int()
  }
}

///|
fn Cursor::advance(self : Cursor) -> Unit {
  self.pos += 1
}

///|
fn Cursor::parse_digits(self : Cursor) -> @bigint.BigInt raise VersionError {
  let start = self.pos
  while is_digit(self.peek()) {
    self.advance()
  }
  if self.pos == start {
    raise VersionError::InvalidVersion("EXPECTED_DIGITS", start)
  }
  @bigint.BigInt::from_string(self.source[start:self.pos].to_owned())
}

///|
fn Cursor::match_label(self : Cursor, label : String) -> Bool {
  if self.pos + label.length() > self.source.length() {
    false
  } else {
    self.source[self.pos:self.pos + label.length()].to_owned().to_lower() ==
    label
  }
}

///|
/// Returns `(kind, length)` where kind is 1..3 for a/b/rc pre-releases,
/// 4 for post-releases, and 5 for dev-releases.
fn Cursor::suffix_info(self : Cursor) -> (Int, Int) {
  if self.match_label("alpha") {
    (1, 5)
  } else if self.match_label("beta") {
    (2, 4)
  } else if self.match_label("preview") {
    (3, 7)
  } else if self.match_label("pre") {
    (3, 3)
  } else if self.match_label("post") {
    (4, 4)
  } else if self.match_label("rev") {
    (4, 3)
  } else if self.match_label("rc") {
    (3, 2)
  } else if self.match_label("dev") {
    (5, 3)
  } else if self.match_label("a") {
    (1, 1)
  } else if self.match_label("b") {
    (2, 1)
  } else if self.match_label("c") {
    (3, 1)
  } else if self.match_label("r") {
    (4, 1)
  } else {
    (0, 0)
  }
}

///|
fn Cursor::consume_separator(self : Cursor) -> Bool {
  if is_separator(self.peek()) {
    self.advance()
    true
  } else {
    false
  }
}

///|
/// Match a suffix label `offset` characters ahead of the cursor without
/// moving it. Returns `(kind, length)` with the same meaning as
/// `Cursor::suffix_info`.
fn Cursor::label_at(self : Cursor, offset : Int) -> (Int, Int) {
  let saved = self.pos
  self.pos += offset
  let (kind, len) = self.suffix_info()
  self.pos = saved
  (kind, len)
}

///|
/// PEP 440 allows `[-_.]?label[-_.]?number?`, so a suffix may be glued to the
/// previous component (`1.0post1`) or introduced by any single separator
/// (`1.0.post1`, `1.0-post1`). The separator is part of the match only when a
/// label follows it; otherwise it stays available for the next suffix group.
fn Cursor::peek_suffix(self : Cursor) -> (Int, Int) {
  let (direct_kind, direct_len) = self.label_at(0)
  if direct_kind != 0 {
    return (direct_kind, direct_len)
  }
  if is_separator(self.peek()) {
    let (kind, len) = self.label_at(1)
    if kind != 0 {
      return (kind, len + 1)
    }
  }
  (0, 0)
}

///|
fn all_digits(text : String) -> Bool {
  for i = 0; i < text.length(); i = i + 1 {
    if !is_digit(text[i].to_int()) {
      return false
    }
  }
  true
}

///|
fn normalize_numeric_local(text : String) -> String {
  @bigint.BigInt::from_string(text).to_string()
}

///|
fn parse_version(input : String) -> Version raise VersionError {
  let source = input.trim().to_owned()
  if source.is_empty() {
    raise VersionError::InvalidVersion("EMPTY_VERSION", 0)
  }
  let cursor = { source, pos: 0, }

  if cursor.peek() == 118 || cursor.peek() == 86 {
    cursor.advance()
    if !is_digit(cursor.peek()) {
      raise VersionError::InvalidVersion("EXPECTED_DIGIT_AFTER_V", cursor.pos)
    }
  }

  let mut epoch = @bigint.BigInt::from_int(0)
  let release : Array[@bigint.BigInt] = []
  let first = cursor.parse_digits()
  if cursor.peek() == 33 {
    cursor.advance()
    epoch = first
    release.push(cursor.parse_digits())
  } else {
    release.push(first)
  }

  while cursor.peek() == 46 && is_digit(cursor.peek_at(1)) {
    cursor.advance()
    release.push(cursor.parse_digits())
  }

  let mut has_pre = false
  let mut pre_kind = 0
  let mut pre_num = @bigint.BigInt::from_int(0)
  let mut has_post = false
  let mut post_num = @bigint.BigInt::from_int(0)
  let mut has_dev = false
  let mut dev_num = @bigint.BigInt::from_int(0)
  // PEP 440 fixes the order of the optional suffixes: pre, then post, then
  // dev. Each label accepts an optional separator on either side, and the
  // `-N` spelling is an alternative form of `.postN`.
  let (pre_candidate, pre_len) = cursor.peek_suffix()
  if pre_candidate >= 1 && pre_candidate <= 3 {
    cursor.pos += pre_len
    ignore(cursor.consume_separator())
    has_pre = true
    pre_kind = pre_candidate
    if is_digit(cursor.peek()) {
      pre_num = cursor.parse_digits()
    }
  }
  if cursor.peek() == 45 && is_digit(cursor.peek_at(1)) {
    cursor.advance()
    has_post = true
    post_num = cursor.parse_digits()
  } else {
    let (post_candidate, post_len) = cursor.peek_suffix()
    if post_candidate == 4 {
      cursor.pos += post_len
      ignore(cursor.consume_separator())
      has_post = true
      if is_digit(cursor.peek()) {
        post_num = cursor.parse_digits()
      }
    }
  }
  let (dev_candidate, dev_len) = cursor.peek_suffix()
  if dev_candidate == 5 {
    cursor.pos += dev_len
    ignore(cursor.consume_separator())
    has_dev = true
    if is_digit(cursor.peek()) {
      dev_num = cursor.parse_digits()
    }
  }

  let local_segments : Array[LocalPart] = []
  if cursor.peek() == 43 {
    cursor.advance()
    if cursor.peek() == -1 {
      raise VersionError::InvalidVersion("EMPTY_LOCAL_VERSION", cursor.pos)
    }
    while cursor.peek() != -1 {
      let start = cursor.pos
      while is_alnum(cursor.peek()) {
        cursor.advance()
      }
      if cursor.pos == start {
        raise VersionError::InvalidVersion("INVALID_LOCAL_SEGMENT", cursor.pos)
      }
      let raw_segment = cursor.source[start:cursor.pos].to_owned().to_lower()
      let numeric = all_digits(raw_segment)
      let text = if numeric {
        normalize_numeric_local(raw_segment)
      } else {
        raw_segment
      }
      local_segments.push({ text, numeric, })
      if is_separator(cursor.peek()) {
        cursor.advance()
        if cursor.peek() == -1 || !is_alnum(cursor.peek()) {
          raise VersionError::InvalidVersion(
            "INVALID_LOCAL_SEGMENT",
            cursor.pos,
          )
        }
      } else {
        break
      }
    }
  }

  if cursor.peek() != -1 {
    raise VersionError::InvalidVersion("INVALID_VERSION_END", cursor.pos)
  }

  {
    raw: input,
    epoch,
    release,
    has_pre,
    pre_kind,
    pre_num,
    has_post,
    post_num,
    has_dev,
    dev_num,
    local_segments,
  }
}

///|
pub fn Version::parse(input : String) -> Version raise VersionError {
  parse_version(input)
}

///|
fn format_version(v : Version) -> String {
  let b = StringBuilder()
  if v.epoch.compare(@bigint.BigInt::from_int(0)) > 0 {
    b.write_string(v.epoch.to_string())
    b.write_string("!")
  }
  for i = 0; i < v.release.length(); i = i + 1 {
    if i > 0 {
      b.write_string(".")
    }
    b.write_string(v.release[i].to_string())
  }
  if v.has_pre {
    if v.pre_kind == 1 {
      b.write_string("a")
    } else if v.pre_kind == 2 {
      b.write_string("b")
    } else {
      b.write_string("rc")
    }
    b.write_string(v.pre_num.to_string())
  }
  if v.has_post {
    b.write_string(".post")
    b.write_string(v.post_num.to_string())
  }
  if v.has_dev {
    b.write_string(".dev")
    b.write_string(v.dev_num.to_string())
  }
  if v.local_segments.length() > 0 {
    b.write_string("+")
    for i = 0; i < v.local_segments.length(); i = i + 1 {
      if i > 0 {
        b.write_string(".")
      }
      b.write_string(v.local_segments[i].text)
    }
  }
  b.to_string()
}

///|

///|
/// ```mbt check
/// test {
///   let v = @moon_pyversion.Version::parse("1.0rc1")
///   inspect(@moon_pyversion.Version::is_prerelease(v), content="true")
/// }
/// ```
pub fn Version::is_prerelease(self : Version) -> Bool {
  self.has_pre || self.has_dev
}

///|
/// True when the version carries a post-release marker.
pub fn Version::is_postrelease(self : Version) -> Bool {
  self.has_post
}

///|
/// True when the version carries a dev marker.
pub fn Version::is_devrelease(self : Version) -> Bool {
  self.has_dev
}

///|
/// The public form: the version with local segments removed, e.g.
/// 1.0+abc -> 1.0. Mirrors the spec's "public version" notion used for
/// equality against local-ignoring constraints.
pub fn Version::public(self : Version) -> String {
  if self.local_segments.length() == 0 {
    return self.to_string()
  }
  let stripped : Version = { ..self, local_segments: [], }
  stripped.to_string()
}

///|
/// ```mbt check
/// test {
///   let v = @moon_pyversion.Version::parse("1.0rc1.post2+local")
///   inspect(@moon_pyversion.Version::base_version(v), content="1.0")
/// }
/// ```
pub fn Version::base_version(self : Version) -> String {
  let stripped : Version = {
    ..self,
    has_pre: false,
    has_post: false,
    has_dev: false,
    local_segments: [],
  }
  stripped.to_string()
}

///|
/// The epoch component as an integer string ("0" for the common case).
pub fn Version::epoch_string(self : Version) -> String {
  self.epoch.to_string()
}

///|
pub fn Version::to_string(self : Version) -> String {
  format_version(self)
}

///|
pub impl Show for Version with fn to_string(self) {
  format_version(self)
}

///|
pub fn Version::normalize(input : String) -> String raise VersionError {
  let v = parse_version(input)
  format_version(v)
}

///|
fn compare_bigint_arrays(
  left : Array[@bigint.BigInt],
  right : Array[@bigint.BigInt],
) -> Int {
  let len = Int::max(left.length(), right.length())
  for i = 0; i < len; i = i + 1 {
    let l = if i < left.length() {
      left[i]
    } else {
      @bigint.BigInt::from_int(0)
    }
    let r = if i < right.length() {
      right[i]
    } else {
      @bigint.BigInt::from_int(0)
    }
    let c = l.compare(r)
    if c != 0 {
      return c
    }
  }
  0
}

///|
fn compare_numeric_local(left : String, right : String) -> Int {
  if left.length() != right.length() {
    left.length().compare(right.length())
  } else {
    left.compare(right)
  }
}

///|
fn compare_local(left : Array[LocalPart], right : Array[LocalPart]) -> Int {
  let len = Int::min(left.length(), right.length())
  for i = 0; i < len; i = i + 1 {
    let l = left[i]
    let r = right[i]
    let c = if l.numeric && r.numeric {
      compare_numeric_local(l.text, r.text)
    } else if l.numeric != r.numeric {
      if l.numeric {
        1
      } else {
        -1
      }
    } else {
      // PEP 440 compares textual local segments as byte strings. MoonBit's
      // `Compare` for `String` orders by length first, which would place `x`
      // below `a1`, so the lexical comparison is requested explicitly.
      String::lexical_compare(l.text, r.text)
    }
    if c != 0 {
      return c
    }
  }
  left.length().compare(right.length())
}

///|
pub fn Version::compare(left : Version, right : Version) -> Int {
  let c_epoch = left.epoch.compare(right.epoch)
  if c_epoch != 0 {
    return c_epoch
  }
  let c_release = compare_bigint_arrays(left.release, right.release)
  if c_release != 0 {
    return c_release
  }
  // A bare development release precedes alpha; a post-development release
  // stays with its post-release, not before every prerelease.
  let left_pre = if left.has_pre {
    0
  } else if left.has_dev && !left.has_post {
    -1
  } else {
    1
  }
  let right_pre = if right.has_pre {
    0
  } else if right.has_dev && !right.has_post {
    -1
  } else {
    1
  }
  if left_pre != right_pre {
    return left_pre.compare(right_pre)
  }
  if left.has_pre {
    let c_kind = left.pre_kind.compare(right.pre_kind)
    if c_kind != 0 {
      return c_kind
    }
    let c_pre = left.pre_num.compare(right.pre_num)
    if c_pre != 0 {
      return c_pre
    }
  }
  if left.has_post != right.has_post {
    return if left.has_post { 1 } else { -1 }
  }
  if left.has_post {
    let c_post = left.post_num.compare(right.post_num)
    if c_post != 0 {
      return c_post
    }
  }
  if left.has_dev != right.has_dev {
    return if left.has_dev { -1 } else { 1 }
  }
  if left.has_dev {
    let c_dev = left.dev_num.compare(right.dev_num)
    if c_dev != 0 {
      return c_dev
    }
  }
  compare_local(left.local_segments, right.local_segments)
}

///|
pub impl Eq for Version with fn equal(self, other) {
  Version::compare(self, other) == 0
}

///|
pub impl Compare for Version with fn compare(self, other) {
  Version::compare(self, other)
}