///|
pub(all) suberror SyncError {
  SyncError(String)
}

///|
priv struct Version {
  major : Int
  minor : Int
  patch : Int
} derive(Eq, Compare)

///|
fn Version::to_string(self : Version) -> String {
  "\{self.major}.\{self.minor}.\{self.patch}"
}

///|
fn[T] invalid_version(
  kind : StringView,
  input : StringView,
) -> T raise SyncError {
  raise SyncError("invalid \{kind} version `\{input}`")
}

///|
fn parse_numeric_component(
  kind : StringView,
  input : StringView,
  component : StringView,
) -> Int raise SyncError {
  if component.is_empty() ||
    (component.length() > 1 && component[0] == '0') ||
    !component.all(char => char is ('0'..='9')) {
    return invalid_version(kind, input)
  }
  (@strconv.from_str(component) : Int) catch {
    _ => invalid_version(kind, input)
  }
}

///|
fn valid_identifier(
  identifier : StringView,
  numeric_leading_zero~ : Bool,
) -> Bool {
  if identifier.is_empty() {
    return false
  }
  let mut numeric = true
  for char in identifier {
    if !(char is ('0'..='9' | 'a'..='z' | 'A'..='Z' | '-')) {
      return false
    }
    if !(char is ('0'..='9')) {
      numeric = false
    }
  }
  !(numeric_leading_zero &&
  numeric &&
  identifier.length() > 1 &&
  identifier[0] == '0')
}

///|
fn valid_identifiers(value : StringView, numeric_leading_zero~ : Bool) -> Bool {
  for identifier in value.split(".") {
    if !valid_identifier(identifier, numeric_leading_zero~) {
      return false
    }
  }
  true
}

///|
fn parse_semver_core(
  kind : StringView,
  input : StringView,
  allow_suffix~ : Bool,
) -> Version raise SyncError {
  let (without_build, build) = match input.split_once("+") {
    Some((core, build)) => (core, Some(build))
    None => (input, None)
  }
  if build is Some(value) &&
    (
      !allow_suffix ||
      value.contains("+") ||
      !valid_identifiers(value, numeric_leading_zero=false)
    ) {
    return invalid_version(kind, input)
  }
  let (core, prerelease) = match without_build.split_once("-") {
    Some((core, prerelease)) => (core, Some(prerelease))
    None => (without_build, None)
  }
  if prerelease is Some(value) &&
    (!allow_suffix || !valid_identifiers(value, numeric_leading_zero=true)) {
    return invalid_version(kind, input)
  }
  let parts = core.split(".").to_array()
  if parts.length() != 3 {
    return invalid_version(kind, input)
  }
  {
    major: parse_numeric_component(kind, input, parts[0]),
    minor: parse_numeric_component(kind, input, parts[1]),
    patch: parse_numeric_component(kind, input, parts[2]),
  }
}

///|
fn parse_target_version(input : StringView) -> Version raise SyncError {
  parse_semver_core("target", input, allow_suffix=false)
}

///|
fn parse_current_version(input : StringView) -> Version raise SyncError {
  parse_semver_core("current", input, allow_suffix=true)
}