///|
/// A validated package stored under the identifier used in dependency lists.
pub(all) struct Package {
  id : String
  document : Document
} derive(Eq, @debug.Debug)

///|
/// An in-memory package collection. File discovery belongs to adapters and the
/// CLI; the core resolver accepts parsed documents so it works on every target.
pub(all) struct PackageSet {
  packages : Map[String, Package]
  package_ids : Array[String]
}

///|
pub fn PackageSet::new() -> PackageSet {
  { packages: Map([]), package_ids: [], }
}

///|
/// Add one package after checking its identifier and required metadata.
/// Nothing is inserted when diagnostics are returned.
pub fn PackageSet::add(
  self : PackageSet,
  id : String,
  document : Document,
  source? : String = "package.pc",
) -> Array[Diagnostic] {
  if id.is_empty() || id.iter().any(whitespace) {
    return [
      diag(
        "PC013",
        "Package identifier must not be empty or contain whitespace",
        { source, line: 1, column: 1, },
      ),
    ]
  }
  if self.packages.get(id) is Some(_) {
    return [
      diag("PC014", "Duplicate package identifier: " + id, {
        source,
        line: 1,
        column: 1,
      }),
    ]
  }
  let diagnostics = document.validate_metadata(source~)
  if !diagnostics.is_empty() {
    return diagnostics
  }
  self.packages[id] = { id, document, }
  self.package_ids.push(id)
  []
}

///|
pub fn PackageSet::get(self : PackageSet, id : String) -> Package? {
  self.packages.get(id)
}

///|
/// Find the first package, in insertion order, whose `Provides` field can
/// satisfy a virtual dependency. Exact package identifiers are handled by the
/// caller and always take precedence. Versioned rules follow pkgconf 3.0.7's
/// constraint-pair semantics, including its deliberately asymmetric ranges.
fn PackageSet::find_provider(
  self : PackageSet,
  requirement : Requirement,
) -> Package? {
  for id in self.package_ids {
    let pkg = self.packages.get(id).unwrap()
    guard pkg.document.field("Provides") is Some(field) else { continue }
    guard parse_requirements(field.value, source=field.location.source)
      is Ok(provides) else {
      continue
    }
    for provided in provides {
      if provided.name == requirement.name &&
        virtual_requirement_matches(requirement, provided) {
        return Some(pkg)
      }
    }
  }
  None
}

///|
/// Compare a requested virtual dependency with one `Provides` declaration.
///
/// This is not ordinary interval intersection. It mirrors the comparator-pair
/// table used by pkgconf 3.0.7 so range boundaries behave the same way.
fn virtual_requirement_matches(
  requirement : Requirement,
  provided : Requirement,
) -> Bool {
  if requirement.op == Any {
    return provided.op != Any
  }
  if provided.op == Any {
    return false
  }
  let order = compare_versions(requirement.version, provided.version)
  match requirement.op {
    Any => abort("handled above")
    Less =>
      match provided.op {
        Any => false
        Less => order < 0
        Greater => order > 0
        LessEqual => order <= 0
        GreaterEqual => order > 0
        Equal => order > 0
        NotEqual => order <= 0
      }
    Greater =>
      match provided.op {
        Any => false
        Less => order < 0
        Greater => order > 0
        LessEqual => order < 0
        GreaterEqual => order >= 0
        Equal => order < 0
        NotEqual => order >= 0
      }
    LessEqual =>
      match provided.op {
        Any => false
        Less => order < 0
        Greater => order > 0
        LessEqual => order <= 0
        GreaterEqual => order >= 0
        Equal => order >= 0
        NotEqual => order < 0
      }
    GreaterEqual =>
      match provided.op {
        Any => false
        Less => order < 0
        Greater => order > 0
        LessEqual => order <= 0
        GreaterEqual => order >= 0
        Equal => order <= 0
        NotEqual => order > 0
      }
    Equal =>
      match provided.op {
        Any => false
        Less => order < 0
        Greater => order > 0
        LessEqual => order <= 0
        GreaterEqual => order >= 0
        Equal => order == 0
        NotEqual => order != 0
      }
    NotEqual =>
      match provided.op {
        Any => false
        Less => order >= 0
        Greater => order <= 0
        LessEqual => order > 0
        GreaterEqual => order < 0
        Equal => order != 0
        NotEqual => order == 0
      }
  }
}

///|
/// Check a direct dependency list against the packages currently in the set.
pub fn PackageSet::check_requirements(
  self : PackageSet,
  requirements : Array[Requirement],
  source? : String = "Requires",
) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for requirement in requirements {
    match self.packages.get(requirement.name) {
      None =>
        if self.find_provider(requirement) is None {
          diagnostics.push(
            diag("PC015", "Required package not found: " + requirement.name, {
              source,
              line: 1,
              column: 1,
            }),
          )
        }
      Some(pkg) => {
        let version = pkg.document.field("Version").unwrap().value
        if !requirement.matches(version) {
          diagnostics.push(
            diag(
              "PC016",
              "Package " +
              requirement.name +
              " has version " +
              version +
              ", which does not satisfy " +
              requirement_text(requirement),
              { source, line: 1, column: 1, },
            ),
          )
        }
      }
    }
  }
  diagnostics
}

///|
fn requirement_text(requirement : Requirement) -> String {
  match requirement.op {
    Any => "(any)"
    Equal => "= " + requirement.version
    NotEqual => "!= " + requirement.version
    Less => "< " + requirement.version
    LessEqual => "<= " + requirement.version
    Greater => "> " + requirement.version
    GreaterEqual => ">= " + requirement.version
  }
}