///|
/// One target-specific reference with its annotation's source location.
/// The module and function strings are opaque. Consumers interpret them.
pub(all) struct ExternalBinding {
  target : Target
  external : External
} derive(Eq, Debug)

///|
/// A repeated external binding. The existing collection is unchanged.
pub(all) suberror BindingError {
  DuplicateBinding(Target)
} derive(Eq, Debug)

///|
/// An immutable collection with at most one external binding per target.
/// Use `with_binding` to add a binding and `get` to look one up.
///
/// ```mbt check
/// test {
///   let target = @ast.Target::extension("orbit")
///   let external : @ast.External = {
///     module_: "space/list",
///     function: "length",
///     location: @span.SrcSpan::from_ints(0, 40).unwrap(),
///   }
///   let bindings = @ast.ExternalBindings::empty().with_binding(target, external)
///   assert_eq(bindings.get(target), Some(external))
///   assert_eq(bindings.get(Erlang), None)
/// }
/// ```
pub struct ExternalBindings {
  bindings : ArrayView[ExternalBinding]
} derive(Eq, Debug)

///|
/// No external bindings.
pub fn ExternalBindings::empty() -> ExternalBindings {
  { bindings: [][:], }
}

///|
/// Add a binding in a new collection. Raise `DuplicateBinding` when that
/// target already has a binding. No registration or backend selection occurs.
pub fn ExternalBindings::with_binding(
  self : ExternalBindings,
  target : Target,
  external : External,
) -> ExternalBindings raise BindingError {
  if self.get(target) is Some(_) {
    raise DuplicateBinding(target)
  }
  let bindings = self.bindings.to_owned()
  bindings.push({ target, external, })
  bindings.sort_by((a, b) => {
    match (a.target, b.target) {
      (Erlang, _) => -1
      (_, Erlang) => 1
      (JavaScript, _) => -1
      (_, JavaScript) => 1
      _ => byte_order(a.target.name(), b.target.name())
    }
  })
  { bindings: bindings[:], }
}

///|
/// Find a binding by target identity. Return `None` when none is present.
pub fn ExternalBindings::get(
  self : ExternalBindings,
  target : Target,
) -> External? {
  for binding in self.bindings {
    if binding.target == target {
      return Some(binding.external)
    }
  }
  None
}

///|
/// Return a copy of all bindings in canonical order, with standard targets first.
pub fn ExternalBindings::all(self : ExternalBindings) -> Array[ExternalBinding] {
  self.bindings.to_owned()
}

///|
/// Whether the definition has no external binding for any target.
pub fn ExternalBindings::is_empty(self : ExternalBindings) -> Bool {
  self.bindings.is_empty()
}

///|
/// Import the compiler's two optional external fields into a generic collection.
/// Standard compatibility decoders use this boundary. New code can use
/// `empty().with_binding(target, external)` for any registered target.
pub fn ExternalBindings::from_standard(
  erlang : External?,
  javascript : External?,
) -> ExternalBindings {
  let bindings = []
  if erlang is Some(external) {
    bindings.push({ target: Erlang, external, })
  }
  if javascript is Some(external) {
    bindings.push({ target: JavaScript, external, })
  }
  { bindings: bindings[:], }
}

///|
/// Find this function's binding for a target, including standard targets.
/// A binding does not imply a target restriction on the definition.
pub fn Function::external_for(self : Function, target : Target) -> External? {
  self.externals.get(target)
}

///|
/// Find this external type's binding for a target, including standard targets.
pub fn CustomType::external_for(
  self : CustomType,
  target : Target,
) -> External? {
  self.externals.get(target)
}