///|
pub(all) enum SpecifierOp {
  Equal
  NotEqual
  Less
  LessEqual
  Greater
  GreaterEqual
  Compatible
  ArbitraryEqual
} derive(Eq, @debug.Debug)

///|
pub(all) struct Specifier {
  op : SpecifierOp
  raw_version : String
  version : Version?
  wildcard : Bool
} derive(@debug.Debug)

///|
pub(all) struct SpecifierSet {
  specs : Array[Specifier]
  original : String
} derive(@debug.Debug)

///|
fn split_specifier_list(input : String) -> Array[String] {
  let out : Array[String] = []
  let mut start = 0
  for i = 0; i < input.length(); i = i + 1 {
    if input[i].to_int() == 44 {
      let part = input[start:i].to_owned().trim().to_owned()
      if !part.is_empty() {
        out.push(part)
      }
      start = i + 1
    }
  }
  let part = input[start:input.length()].to_owned().trim().to_owned()
  if !part.is_empty() {
    out.push(part)
  }
  out
}

///|
fn parse_operator(text : String) -> (SpecifierOp, Int) raise VersionError {
  if text.has_prefix("===") {
    (ArbitraryEqual, 3)
  } else if text.has_prefix("~=") {
    (Compatible, 2)
  } else if text.has_prefix("==") {
    (Equal, 2)
  } else if text.has_prefix("!=") {
    (NotEqual, 2)
  } else if text.has_prefix("<=") {
    (LessEqual, 2)
  } else if text.has_prefix(">=") {
    (GreaterEqual, 2)
  } else if text.has_prefix("<") {
    (Less, 1)
  } else if text.has_prefix(">") {
    (Greater, 1)
  } else {
    raise VersionError::InvalidSpecifier("INVALID_OPERATOR", 0)
  }
}

///|
fn parse_specifier(text : String) -> Specifier raise VersionError {
  let s = text.trim().to_owned()
  if s.is_empty() {
    raise VersionError::InvalidSpecifier("EMPTY_SPECIFIER", 0)
  }
  let (op, start) = parse_operator(s)
  let mut version_part = s[start:s.length()].to_owned().trim().to_owned()
  if op == ArbitraryEqual {
    return { op, raw_version: version_part, version: None, wildcard: false, }
  }
  if version_part.is_empty() {
    raise VersionError::InvalidSpecifier("EMPTY_VERSION", start)
  }
  let mut wildcard = false
  if version_part.has_suffix(".*") {
    if op != Equal && op != NotEqual {
      raise VersionError::InvalidSpecifier("WILDCARD_REQUIRES_EQ", start)
    }
    if version_part.length() < 2 {
      raise VersionError::InvalidSpecifier("INVALID_WILDCARD", start)
    }
    version_part = version_part[0:version_part.length() - 2].to_owned()
    wildcard = true
  }
  let v = Version::parse(version_part)
  if v.local_segments.length() > 0 && op != Equal && op != NotEqual {
    raise VersionError::InvalidSpecifier("LOCAL_REQUIRES_EQ", start)
  }
  if wildcard {
    if v.has_pre || v.has_post || v.has_dev || v.local_segments.length() > 0 {
      raise VersionError::InvalidSpecifier("INVALID_WILDCARD_PREFIX", start)
    }
  }
  if op == Compatible && v.release.length() < 2 {
    raise VersionError::InvalidSpecifier(
      "COMPATIBLE_RELEASE_NEEDS_TWO_SEGMENTS", start,
    )
  }
  { op, raw_version: version_part, version: Some(v), wildcard, }
}

///|
pub fn SpecifierSet::parse(input : String) -> SpecifierSet raise VersionError {
  let specs : Array[Specifier] = []
  let parts = split_specifier_list(input)
  for i = 0; i < parts.length(); i = i + 1 {
    specs.push(parse_specifier(parts[i]))
  }
  { specs, original: input, }
}

///|
fn version_is_prerelease(v : Version) -> Bool {
  v.has_pre || v.has_dev
}

///|
fn compatible_upper(v : Version) -> Version {
  let release : Array[@bigint.BigInt] = []
  for i = 0; i < v.release.length(); i = i + 1 {
    release.push(v.release[i])
  }
  let idx = release.length() - 2
  release[idx] = release[idx].add(@bigint.BigInt::from_int(1))
  release[release.length() - 1] = @bigint.BigInt::from_int(0)
  {
    raw: "",
    epoch: v.epoch,
    release,
    has_pre: false,
    pre_kind: 0,
    pre_num: @bigint.BigInt::from_int(0),
    has_post: false,
    post_num: @bigint.BigInt::from_int(0),
    has_dev: false,
    dev_num: @bigint.BigInt::from_int(0),
    local_segments: [],
  }
}

///|
fn matches_wildcard_prefix(v : Version, prefix : Version) -> Bool {
  if v.epoch.compare(prefix.epoch) != 0 {
    return false
  }
  for i = 0; i < prefix.release.length(); i = i + 1 {
    let segment = if i < v.release.length() {
      v.release[i]
    } else {
      @bigint.BigInt::from_int(0)
    }
    if segment.compare(prefix.release[i]) != 0 {
      return false
    }
  }
  true
}

///|
fn compatible_contains(v : Version, spec_v : Version) -> Bool {
  let lower_ok = compare_without_local(v, spec_v) >= 0
  if !lower_ok {
    return false
  }
  let upper = compatible_upper(spec_v)
  let upper_ok = compare_without_local(v, upper) < 0
  if !upper_ok {
    return false
  }
  if version_is_prerelease(v) &&
    compare_bigint_arrays(v.release, upper.release) == 0 {
    return false
  }
  true
}

///|
fn compare_without_local(left : Version, right : Version) -> Int {
  let l = { ..left, local_segments: [], }
  let r = { ..right, local_segments: [], }
  l.compare(r)
}

///|
fn specifier_contains(spec : Specifier, version : Version) -> Bool {
  if spec.op == ArbitraryEqual {
    return version.to_string().to_lower() == spec.raw_version.to_lower()
  }
  let spec_v = match spec.version {
    Some(v) => v
    None => return false
  }
  if spec.wildcard {
    let matched = matches_wildcard_prefix(version, spec_v)
    return if spec.op == Equal { matched } else { !matched }
  }
  let equal_cmp = if spec_v.local_segments.length() == 0 {
    compare_without_local(version, spec_v) == 0
  } else {
    version.compare(spec_v) == 0
  }
  let basic = match spec.op {
    Equal => equal_cmp
    NotEqual => !equal_cmp
    Less => compare_without_local(version, spec_v) < 0
    LessEqual => compare_without_local(version, spec_v) <= 0
    Greater => compare_without_local(version, spec_v) > 0
    GreaterEqual => compare_without_local(version, spec_v) >= 0
    Compatible => compatible_contains(version, spec_v)
    ArbitraryEqual => false
  }
  if !basic {
    return false
  }
  if spec.op == Less &&
    version_is_prerelease(version) &&
    !version_is_prerelease(spec_v) {
    let earliest = {
      ..spec_v,
      has_dev: true,
      dev_num: @bigint.BigInt::from_int(0),
      local_segments: [],
    }
    if compare_without_local(version, earliest) >= 0 {
      return false
    }
  }
  if spec.op == Greater && !spec_v.has_post && version.has_post {
    let without_post = {
      ..version,
      has_post: false,
      has_dev: false,
      local_segments: [],
    }
    if compare_without_local(without_post, spec_v) == 0 {
      return false
    }
  }
  true
}

///|
fn SpecifierSet::allows_prereleases(self : SpecifierSet) -> Bool {
  for spec in self.specs {
    if spec.op != NotEqual {
      let candidate = match spec.version {
        Some(v) => Some(v)
        None => Some(Version::parse(spec.raw_version)) catch { _ => None }
      }
      if candidate is Some(v) && version_is_prerelease(v) {
        return true
      }
    }
  }
  false
}

///|
pub fn SpecifierSet::contains(
  self : SpecifierSet,
  version : Version,
  prereleases? : Bool? = None,
) -> Bool {
  // packaging >=26: a single candidate has no stable alternative, so the
  // automatic policy permits prereleases. filter has the full candidate set.
  let allow_pre = prereleases.unwrap_or(true)
  if version_is_prerelease(version) && !allow_pre {
    return false
  }
  for i = 0; i < self.specs.length(); i = i + 1 {
    if !specifier_contains(self.specs[i], version) {
      return false
    }
  }
  true
}

///|
pub fn SpecifierSet::filter(
  versions : Array[Version],
  spec : SpecifierSet,
  prereleases? : Bool? = None,
) -> Array[Version] {
  let out : Array[Version] = []
  let pre : Array[Version] = []
  let explicit = prereleases.unwrap_or(spec.allows_prereleases())
  for i = 0; i < versions.length(); i = i + 1 {
    if spec.contains(versions[i], prereleases=Some(true)) {
      if version_is_prerelease(versions[i]) && !explicit {
        pre.push(versions[i])
      } else {
        out.push(versions[i])
      }
    }
  }
  if out.is_empty() && prereleases is None {
    pre
  } else {
    out
  }
}