///|
pub fn parse_semver(value : String) -> SemVerInfo {
  let text = trim_ascii(value)
  let build_parts = semver_split_once(text, "+")
  let core_and_pre = build_parts[0]
  let build_metadata : String? = if build_parts.length() == 2 &&
    build_parts[1].length() > 0 {
    Some(build_parts[1])
  } else {
    None
  }
  let pre_parts = semver_split_once(core_and_pre, "-")
  let core = pre_parts[0]
  let pre_release : String? = if pre_parts.length() == 2 &&
    pre_parts[1].length() > 0 {
    Some(pre_parts[1])
  } else {
    None
  }
  let parts = semver_split_char(core, 46)
  if parts.length() != 3 {
    return semver_info(false, 0, 0, 0, pre_release, build_metadata)
  }
  if !semver_numeric_identifier(parts[0]) ||
    !semver_numeric_identifier(parts[1]) ||
    !semver_numeric_identifier(parts[2]) {
    return semver_info(false, 0, 0, 0, pre_release, build_metadata)
  }
  if !semver_optional_identifiers(pre_release) ||
    !semver_optional_identifiers(build_metadata) {
    return semver_info(false, 0, 0, 0, pre_release, build_metadata)
  }
  semver_info(
    true,
    semver_to_int(parts[0]),
    semver_to_int(parts[1]),
    semver_to_int(parts[2]),
    pre_release,
    build_metadata,
  )
}

///|
pub fn SemVerInfo::core(self : SemVerInfo) -> String {
  self.major.to_string() +
  "." +
  self.minor.to_string() +
  "." +
  self.patch.to_string()
}

///|
pub fn SemVerInfo::format(self : SemVerInfo) -> String {
  if !self.valid {
    return ""
  }
  let mut out = self.core()
  match self.pre_release {
    None => ()
    Some(pre) => out = out + "-" + pre
  }
  match self.build_metadata {
    None => ()
    Some(build) => out = out + "+" + build
  }
  out
}

///|
pub fn SemVerInfo::is_prerelease(self : SemVerInfo) -> Bool {
  match self.pre_release {
    Some(value) => value.length() > 0
    None => false
  }
}

///|
pub fn SemVerInfo::has_build_metadata(self : SemVerInfo) -> Bool {
  match self.build_metadata {
    Some(value) => value.length() > 0
    None => false
  }
}

///|
pub fn SemVerInfo::is_stable(self : SemVerInfo) -> Bool {
  self.valid && self.major > 0 && !self.is_prerelease()
}

///|
pub fn SemVerInfo::release_channel(self : SemVerInfo) -> String {
  if !self.valid {
    "invalid"
  } else if self.is_prerelease() {
    "prerelease"
  } else if self.major == 0 {
    "initial-development"
  } else {
    "stable"
  }
}

///|
pub fn SemVerInfo::acceptance_note(self : SemVerInfo) -> String {
  if !self.valid {
    "version is not a valid semantic version"
  } else if self.is_prerelease() {
    "pre-release versions are useful for testing but final Mooncakes publication should use a release tag when possible"
  } else if self.major == 0 {
    "0.x versions are acceptable for new libraries, but keep the API boundary clear in README"
  } else {
    "stable semantic version"
  }
}

///|
pub fn SemVerInfo::to_markdown(self : SemVerInfo) -> String {
  let mut out = "## Version Analysis\n\n"
  out = out + "- Version: `" + self.format() + "`\n"
  out = out + "- Valid: " + bool_text(self.valid) + "\n"
  out = out + "- Core: `" + self.core() + "`\n"
  out = out + "- Channel: " + self.release_channel() + "\n"
  out = out + "- Pre-release: " + semver_option_text(self.pre_release) + "\n"
  out = out +
    "- Build metadata: " +
    semver_option_text(self.build_metadata) +
    "\n"
  out = out + "- Note: " + self.acceptance_note() + "\n"
  out
}

///|
pub fn compare_semver(left : String, right : String) -> Int {
  parse_semver(left).compare(parse_semver(right))
}

///|
pub fn SemVerInfo::compare(self : SemVerInfo, other : SemVerInfo) -> Int {
  if !self.valid && !other.valid {
    return 0
  }
  if !self.valid {
    return -1
  }
  if !other.valid {
    return 1
  }
  if self.major != other.major {
    return semver_compare_int(self.major, other.major)
  }
  if self.minor != other.minor {
    return semver_compare_int(self.minor, other.minor)
  }
  if self.patch != other.patch {
    return semver_compare_int(self.patch, other.patch)
  }
  semver_compare_prerelease(self.pre_release, other.pre_release)
}

///|
pub fn classify_version_bump(before : String, after : String) -> VersionBump {
  let left = parse_semver(before)
  let right = parse_semver(after)
  if !left.valid || !right.valid {
    return InvalidVersion
  }
  if left.compare(right) == 0 {
    return SameVersion
  }
  if right.major > left.major {
    Major
  } else if right.major == left.major && right.minor > left.minor {
    Minor
  } else if right.major == left.major &&
    right.minor == left.minor &&
    right.patch > left.patch {
    Patch
  } else {
    InvalidVersion
  }
}

///|
pub fn version_bump_name(bump : VersionBump) -> String {
  match bump {
    Major => "major"
    Minor => "minor"
    Patch => "patch"
    SameVersion => "same"
    InvalidVersion => "invalid"
  }
}

///|
pub fn SemVerInfo::next_patch(self : SemVerInfo) -> String {
  if !self.valid {
    "0.1.0"
  } else {
    self.major.to_string() +
    "." +
    self.minor.to_string() +
    "." +
    (self.patch + 1).to_string()
  }
}

///|
pub fn SemVerInfo::next_minor(self : SemVerInfo) -> String {
  if !self.valid {
    "0.1.0"
  } else {
    self.major.to_string() + "." + (self.minor + 1).to_string() + ".0"
  }
}

///|
pub fn SemVerInfo::next_major(self : SemVerInfo) -> String {
  if !self.valid {
    "1.0.0"
  } else {
    (self.major + 1).to_string() + ".0.0"
  }
}

///|
pub fn recommend_release_version(
  current : String,
  bump : VersionBump,
) -> String {
  let info = parse_semver(current)
  match bump {
    Major => info.next_major()
    Minor => info.next_minor()
    Patch => info.next_patch()
    SameVersion => info.format()
    InvalidVersion => if info.valid { info.next_patch() } else { "0.1.0" }
  }
}

///|
pub fn semver_accepts_mooncakes_release(version : String) -> Bool {
  let info = parse_semver(version)
  info.valid && !info.is_prerelease()
}

///|
pub fn semver_release_table(versions : Array[String]) -> String {
  let mut out = "| Version | Valid | Channel | Next patch |\n| --- | --- | --- | --- |\n"
  let mut index = 0
  while index < versions.length() {
    let info = parse_semver(versions[index])
    out = out + "| `" + versions[index] + "`"
    out = out + " | " + bool_text(info.valid)
    out = out + " | " + info.release_channel()
    out = out + " | `" + info.next_patch() + "` |\n"
    index += 1
  }
  out
}

///|
fn semver_split_once(value : String, marker : String) -> Array[String] {
  let parts : Array[String] = []
  match index_of(value, marker) {
    None => parts.push(value)
    Some(pos) => {
      parts.push(slice(value, 0, pos))
      parts.push(slice(value, pos + marker.length(), value.length()))
    }
  }
  parts
}

///|
fn semver_split_char(value : String, code : Int) -> Array[String] {
  let parts : Array[String] = []
  let mut start = 0
  let mut index = 0
  while index < value.length() {
    if code_at(value, index) == code {
      parts.push(slice(value, start, index))
      start = index + 1
    }
    index += 1
  }
  parts.push(slice(value, start, value.length()))
  parts
}

///|
fn semver_numeric_identifier(value : String) -> Bool {
  if value.length() == 0 {
    return false
  }
  if value.length() > 1 && code_at(value, 0) == 48 {
    return false
  }
  semver_all_digits(value)
}

///|
fn semver_all_digits(value : String) -> Bool {
  let mut index = 0
  while index < value.length() {
    if !is_digit(code_at(value, index)) {
      return false
    }
    index += 1
  }
  true
}

///|
fn semver_optional_identifiers(value : String?) -> Bool {
  match value {
    None => true
    Some(text) => semver_identifier_list(text)
  }
}

///|
fn semver_identifier_list(value : String) -> Bool {
  if value.length() == 0 {
    return false
  }
  let parts = semver_split_char(value, 46)
  let mut index = 0
  while index < parts.length() {
    if !semver_identifier(parts[index]) {
      return false
    }
    index += 1
  }
  true
}

///|
fn semver_identifier(value : String) -> Bool {
  if value.length() == 0 {
    return false
  }
  let mut index = 0
  while index < value.length() {
    let code = code_at(value, index)
    if !(is_ident_char(code) || code == 46) {
      return false
    }
    index += 1
  }
  true
}

///|
fn semver_to_int(value : String) -> Int {
  let mut total = 0
  let mut index = 0
  while index < value.length() {
    total = total * 10 + code_at(value, index) - 48
    index += 1
  }
  total
}

///|
fn semver_compare_int(left : Int, right : Int) -> Int {
  if left < right {
    -1
  } else if left > right {
    1
  } else {
    0
  }
}

///|
fn semver_compare_prerelease(left : String?, right : String?) -> Int {
  match left {
    None =>
      match right {
        None => 0
        Some(_) => 1
      }
    Some(left_text) =>
      match right {
        None => -1
        Some(right_text) =>
          semver_compare_identifier_lists(left_text, right_text)
      }
  }
}

///|
fn semver_compare_identifier_lists(left : String, right : String) -> Int {
  let left_parts = semver_split_char(left, 46)
  let right_parts = semver_split_char(right, 46)
  let mut index = 0
  while index < left_parts.length() && index < right_parts.length() {
    let result = semver_compare_identifier_value(
      left_parts[index],
      right_parts[index],
    )
    if result != 0 {
      return result
    }
    index += 1
  }
  semver_compare_int(left_parts.length(), right_parts.length())
}

///|
fn semver_compare_identifier_value(left : String, right : String) -> Int {
  let left_numeric = semver_all_digits(left)
  let right_numeric = semver_all_digits(right)
  if left_numeric && right_numeric {
    return semver_compare_int(semver_to_int(left), semver_to_int(right))
  }
  if left_numeric {
    return -1
  }
  if right_numeric {
    return 1
  }
  semver_compare_ascii(left, right)
}

///|
fn semver_compare_ascii(left : String, right : String) -> Int {
  let mut index = 0
  while index < left.length() && index < right.length() {
    let left_code = code_at(left, index)
    let right_code = code_at(right, index)
    if left_code != right_code {
      return semver_compare_int(left_code, right_code)
    }
    index += 1
  }
  semver_compare_int(left.length(), right.length())
}

///|
fn semver_option_text(value : String?) -> String {
  match value {
    None => "(none)"
    Some(text) => "`" + text + "`"
  }
}