///|
/// A stable, structured error for version and specifier parsing.
pub(all) suberror VersionError {
  InvalidVersion(String, Int)
  InvalidSpecifier(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}"
  }
}

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

///|
/// 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)

///|
/// 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)

///|
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
  }
}

///|
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)
  let mut state = 0

  while cursor.peek() != -1 && cursor.peek() != 43 {
    if state == 0 {
      if cursor.peek() == 45 && is_digit(cursor.peek_at(1)) {
        cursor.advance()
        post_num = cursor.parse_digits()
        has_post = true
        state = 2
      } else {
        ignore(cursor.consume_separator())
        let (kind, len) = cursor.suffix_info()
        if kind == 0 {
          raise VersionError::InvalidVersion("EXPECTED_SUFFIX", cursor.pos)
        }
        cursor.pos += len
        ignore(cursor.consume_separator())
        let num = if is_digit(cursor.peek()) {
          cursor.parse_digits()
        } else {
          @bigint.BigInt::from_int(0)
        }
        if kind <= 3 {
          if has_pre {
            raise VersionError::InvalidVersion(
              "DUPLICATE_PRE_RELEASE",
              cursor.pos,
            )
          }
          has_pre = true
          pre_kind = kind
          pre_num = num
          state = 1
        } else if kind == 4 {
          if has_post {
            raise VersionError::InvalidVersion(
              "DUPLICATE_POST_RELEASE",
              cursor.pos,
            )
          }
          has_post = true
          post_num = num
          state = 2
        } else {
          if has_dev {
            raise VersionError::InvalidVersion(
              "DUPLICATE_DEV_RELEASE",
              cursor.pos,
            )
          }
          has_dev = true
          dev_num = num
          state = 3
        }
      }
    } else {
      if !cursor.consume_separator() {
        raise VersionError::InvalidVersion(
          "EXPECTED_SUFFIX_SEPARATOR",
          cursor.pos,
        )
      }
      let (kind, len) = cursor.suffix_info()
      if kind == 0 {
        raise VersionError::InvalidVersion("EXPECTED_SUFFIX", cursor.pos)
      }
      cursor.pos += len
      ignore(cursor.consume_separator())
      let num = if is_digit(cursor.peek()) {
        cursor.parse_digits()
      } else {
        @bigint.BigInt::from_int(0)
      }
      if kind <= 3 {
        raise VersionError::InvalidVersion("INVALID_SUFFIX_ORDER", cursor.pos)
      } else if kind == 4 {
        if has_post || state >= 2 {
          raise VersionError::InvalidVersion("INVALID_SUFFIX_ORDER", cursor.pos)
        }
        has_post = true
        post_num = num
        state = 2
      } else {
        if has_dev || state >= 3 {
          raise VersionError::InvalidVersion("INVALID_SUFFIX_ORDER", cursor.pos)
        }
        has_dev = true
        dev_num = num
        state = 3
      }
    }
  }

  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()
}

///|
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 {
      l.text.compare(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)
}