///|
/// What a bare version means when no operator is given.
pub(all) enum DefaultOp {
  /// Exact: `1.2.3` means `=1.2.3`, which is how npm and Ruby read it.
  Exact
  /// Caret: `1.2.3` means `^1.2.3`, Cargo's default.
  Caret
  /// Tilde: `1.2.3` means `~1.2.3`.
  Tilde
}

///|
/// How `~` and `~>` work out their upper bound.
pub(all) enum TildeBump {
  /// npm and Cargo: bump the minor if one was written, otherwise bump the major.
  ///
  /// - `~1.2.3` → `>=1.2.3 <1.3.0`
  /// - `~1.2`   → `>=1.2.0 <1.3.0`
  /// - `~1`     → `>=1.0.0 <2.0.0`
  Patch
  /// Ruby's `~>`: drop the last segment, then bump whatever is left.
  ///
  /// - `~> 1.2.3` → `>=1.2.3 <1.3.0`
  /// - `~> 1.2`   → `>=1.2.0 <2.0.0`
  /// - `~> 1`     → `>=1.0.0 <2.0.0`
  LastSpecified
}

///|
/// How prereleases are matched against a range.
///
/// The spec only orders prereleases against each other; it says nothing about how a
/// range should treat them, so every ecosystem settled on its own convention.
pub(all) enum PrereleasePolicy {
  /// The npm and Cargo convention: a prerelease can only satisfy a set when some
  /// comparator carries a prerelease with the same `major.minor.patch`.
  ///
  /// So `>=1.2.3-alpha` takes `1.2.3-beta` but not `1.2.4-beta`, and `*` takes no
  /// prerelease at all.
  SameTuple
  /// Treat prereleases as ordinary versions (npm's `includePrerelease`).
  Always
  /// Prereleases never match anything, for when only stable releases will do.
  Never
}

///|
/// Range grammar settings.
///
/// A version has one spelling in SemVer 2.0.0, but ranges are not in the spec, and
/// npm, Cargo and RubyGems each wrote their own grammar. The differences are pulled
/// out as settings here: one parser, several grammars, the same idea as moonregex.
///
/// Switches that govern version literals:
/// - `allow_v_prefix`: accept `v1.2.3`
/// - `allow_equals_prefix`: accept `=1.2.3` as a version literal
/// - `allow_build_metadata`: accept a `+build` segment
/// - `trim`: allow whitespace around a version literal
///
/// Switches that govern range syntax:
/// - `default_op`: what a bare version means (npm `Exact`, Cargo `Caret`)
/// - `tilde_bump`: how `~` finds its upper bound (npm/Cargo `Patch`, Ruby `LastSpecified`)
/// - `allow_caret` / `allow_tilde`: accept `^` / `~` (`~>` only under `LastSpecified`)
/// - `allow_ne`: accept `!=`
/// - `allow_or`: accept `||`
/// - `allow_hyphen_range`: accept `1.2.3 - 2.0.0`
/// - `allow_xrange`: accept the shorthand forms `1.x`, `1.2.*` and bare `1.2`
/// - `allow_empty`: whether an empty string means "any version"
/// - `prerelease_policy`: how prereleases are matched
pub(all) struct Syntax {
  allow_v_prefix : Bool
  allow_equals_prefix : Bool
  allow_build_metadata : Bool
  trim : Bool
  default_op : DefaultOp
  tilde_bump : TildeBump
  allow_caret : Bool
  allow_tilde : Bool
  allow_ne : Bool
  allow_or : Bool
  allow_hyphen_range : Bool
  allow_xrange : Bool
  allow_empty : Bool
  prerelease_policy : PrereleasePolicy
}

///|
/// Strict SemVer 2.0.0 spelling: only `= > >= < <=`, joined by commas or spaces,
/// with no `^`, `~`, `||`, hyphen ranges, `1.x` shorthand or `v` prefix.
///
/// The one exception is `*`, which always means "any version": it is what
/// `Range::to_string` prints for an empty range, so refusing it would break the round trip.
pub fn Syntax::strict() -> Syntax {
  {
    allow_v_prefix: false,
    allow_equals_prefix: false,
    allow_build_metadata: true,
    trim: false,
    default_op: Exact,
    tilde_bump: Patch,
    allow_caret: false,
    allow_tilde: false,
    allow_ne: false,
    allow_or: false,
    allow_hyphen_range: false,
    allow_xrange: false,
    allow_empty: false,
    prerelease_policy: SameTuple,
  }
}

///|
/// npm (node-semver) grammar: a bare version is exact, with `^`, `~`, `||`, hyphen
/// ranges, `1.x` shorthand, a `v` prefix, and the empty string for "any".
pub fn Syntax::npm() -> Syntax {
  {
    allow_v_prefix: true,
    allow_equals_prefix: true,
    allow_build_metadata: true,
    trim: true,
    default_op: Exact,
    tilde_bump: Patch,
    allow_caret: true,
    allow_tilde: true,
    allow_ne: false,
    allow_or: true,
    allow_hyphen_range: true,
    allow_xrange: true,
    allow_empty: true,
    prerelease_policy: SameTuple,
  }
}

///|
/// Cargo grammar: a bare version carries a caret (`1.2.3` is `^1.2.3`), with `,` or
/// space for conjunctions and `*` / `1.*` wildcards; no `||`, no hyphen ranges, no `v`.
pub fn Syntax::cargo() -> Syntax {
  {
    allow_v_prefix: false,
    allow_equals_prefix: true,
    allow_build_metadata: true,
    trim: true,
    default_op: Caret,
    tilde_bump: Patch,
    allow_caret: true,
    allow_tilde: true,
    allow_ne: false,
    allow_or: false,
    allow_hyphen_range: false,
    allow_xrange: true,
    allow_empty: false,
    prerelease_policy: SameTuple,
  }
}

///|
/// RubyGems grammar: `~>` is the pessimistic operator (drop the last segment, then bump),
/// and `!=` is allowed; no `^`, `||`, hyphen ranges or shorthand.
pub fn Syntax::ruby() -> Syntax {
  {
    allow_v_prefix: false,
    allow_equals_prefix: true,
    allow_build_metadata: true,
    trim: true,
    default_op: Exact,
    tilde_bump: LastSpecified,
    allow_caret: false,
    allow_tilde: true,
    allow_ne: true,
    allow_or: false,
    allow_hyphen_range: false,
    allow_xrange: false,
    allow_empty: false,
    prerelease_policy: SameTuple,
  }
}

///|
/// The default grammar is npm's.
pub extend Syntax with Default::{default}

///|
pub impl Default for Syntax with fn default() -> Syntax {
  Syntax::npm()
}

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

///|
pub impl Show for Syntax with fn to_string(self : Syntax) -> String {
  let sb = StringBuilder()
  sb.write_string("Syntax{")
  sb.write_string("default_op=")
  sb.write_string(
    match self.default_op {
      Exact => "exact"
      Caret => "caret"
      Tilde => "tilde"
    },
  )
  sb.write_string(", tilde_bump=")
  sb.write_string(
    match self.tilde_bump {
      Patch => "patch"
      LastSpecified => "last-specified"
    },
  )
  sb.write_string(", or=")
  sb.write_string(self.allow_or.to_string())
  sb.write_string(", hyphen=")
  sb.write_string(self.allow_hyphen_range.to_string())
  sb.write_string(", xrange=")
  sb.write_string(self.allow_xrange.to_string())
  sb.write_string(", prerelease=")
  sb.write_string(
    match self.prerelease_policy {
      SameTuple => "same-tuple"
      Always => "always"
      Never => "never"
    },
  )
  sb.write_string("}")
  sb.to_string()
}

///|
/// Look up a preset by name, for the CLI and config files.
pub fn Syntax::from_name(name : String) -> Syntax? {
  match name {
    "npm" => Some(Syntax::npm())
    "cargo" => Some(Syntax::cargo())
    "ruby" => Some(Syntax::ruby())
    "strict" | "semver" => Some(Syntax::strict())
    _ => None
  }
}

///|
/// The preset name for this syntax, or `None` for a custom mix.
pub fn Syntax::name(self : Syntax) -> String? {
  if self == Syntax::npm() {
    Some("npm")
  } else if self == Syntax::cargo() {
    Some("cargo")
  } else if self == Syntax::ruby() {
    Some("ruby")
  } else if self == Syntax::strict() {
    Some("strict")
  } else {
    None
  }
}

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

///|
pub impl Eq for Syntax with fn equal(self : Syntax, other : Syntax) -> Bool {
  self.allow_v_prefix == other.allow_v_prefix &&
  self.allow_equals_prefix == other.allow_equals_prefix &&
  self.allow_build_metadata == other.allow_build_metadata &&
  self.trim == other.trim &&
  self.default_op == other.default_op &&
  self.tilde_bump == other.tilde_bump &&
  self.allow_caret == other.allow_caret &&
  self.allow_tilde == other.allow_tilde &&
  self.allow_ne == other.allow_ne &&
  self.allow_or == other.allow_or &&
  self.allow_hyphen_range == other.allow_hyphen_range &&
  self.allow_xrange == other.allow_xrange &&
  self.allow_empty == other.allow_empty &&
  self.prerelease_policy == other.prerelease_policy
}

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

///|
pub impl Eq for DefaultOp with fn equal(self : DefaultOp, other : DefaultOp) -> Bool {
  match (self, other) {
    (Exact, Exact) | (Caret, Caret) | (Tilde, Tilde) => true
    _ => false
  }
}

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

///|
pub impl Eq for TildeBump with fn equal(self : TildeBump, other : TildeBump) -> Bool {
  match (self, other) {
    (Patch, Patch) | (LastSpecified, LastSpecified) => true
    _ => false
  }
}

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

///|
pub impl @debug.Debug for PrereleasePolicy with fn to_repr(
  self : PrereleasePolicy,
) -> @debug.Repr {
  @debug.Repr(
    match self {
      SameTuple => "SameTuple"
      Always => "Always"
      Never => "Never"
    },
  )
}

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

///|
pub impl Eq for PrereleasePolicy with fn equal(
  self : PrereleasePolicy,
  other : PrereleasePolicy,
) -> Bool {
  match (self, other) {
    (SameTuple, SameTuple) | (Always, Always) | (Never, Never) => true
    _ => false
  }
}