///|
/// The selection of a source location or input. `Discover` uses the values
/// that a host or a source provider supplies, `Explicit` uses only the given
/// value, and `Skip` loads nothing.
pub(all) enum SourceSelection[T] {
  Discover
  Explicit(T)
  Skip
} derive(Eq, Debug)

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

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

///|
/// An immutable configuration source: a kind, a display name, an optional
/// display priority, the list merge policy, key-path policies, the input, and
/// the key paths to exclude.
pub struct ConfigSource {
  priv kind : SourceKind
  priv name : String
  priv priority : Int?
  priv policy : MergePolicy
  priv key_policies : Array[(KeyPath, MergePolicy)]
  priv input : SourceInput
  priv excluded : Array[(KeyPath, Bool)]
}

///|
priv enum SourceInput {
  Value(ConfigValue)
  Path(String)
  Candidates(Array[String], String)
  Env(SourceSelection[Map[String, String]], String, Array[String])
  Parsed(String, ConfigValue)
  NoInput
}

///|
/// The kind of the source.
pub fn ConfigSource::kind(self : ConfigSource) -> SourceKind {
  self.kind
}

///|
/// The display name of the source.
pub fn ConfigSource::name(self : ConfigSource) -> String {
  self.name
}

///|
/// An in-memory source from a value tree. The root must be a struct.
pub fn in_memory(
  value : ConfigValue,
  name? : String = "in-memory",
  priority? : Int,
) -> ConfigSource raise ConfigError {
  value.check_root(name)
  value_source(InMemory, name, priority, value)
}

///|
/// An in-memory source from flat key-path pairs, for example
/// `("ir.layout", @config.string("tree"))`. Pairs apply in list order with the
/// merge rules, so a later pair wins. Pair values keep their type.
pub fn in_memory_pairs(
  pairs : Array[(String, ConfigValue)],
  name? : String = "in-memory",
  priority? : Int,
) -> ConfigSource raise ConfigError {
  let mut value = fields([])
  for pair in pairs {
    let (text, leaf) = pair
    value = merge(value, nest(KeyPath::parse(text), leaf))
  }
  value_source(InMemory, name, priority, value)
}

///|
/// A defaults source from a JSON value, for example `Defaults::default().to_json()`.
/// The root must be an object.
pub fn ConfigSource::defaults(
  json : Json,
  name? : String = "defaults",
  priority? : Int,
) -> ConfigSource raise ConfigError {
  value_source(Defaults, name, priority, ConfigValue::from_json(json, origin=name))
}

///|
/// A defaults source from a value tree. The root must be a struct.
pub fn ConfigSource::defaults_value(
  value : ConfigValue,
  name? : String = "defaults",
  priority? : Int,
) -> ConfigSource raise ConfigError {
  value.check_root(name)
  value_source(Defaults, name, priority, value)
}

///|
/// A file source at an explicit path. A relative path joins the working
/// directory of the builder. The extension selects the registered format. The
/// load raises `FileNotFound` when the file does not exist. The name is the
/// path when no name is given.
pub fn ConfigSource::file(
  path : String,
  role? : FileRole = Explicit,
  name? : String,
  priority? : Int,
) -> ConfigSource {
  {
    kind: File(role),
    name: name.unwrap_or(path),
    priority,
    policy: Replace,
    key_policies: [],
    input: Path(path),
    excluded: [],
  }
}

///|
/// A file source whose text the caller already read and turned into a value
/// tree, for example a legacy file with its own format. The report and the
/// origins show `path`, which joins the working directory when it is relative.
/// The root must be a struct.
pub fn ConfigSource::parsed_file(
  path : String,
  value : ConfigValue,
  role? : FileRole = Explicit,
  name? : String,
  priority? : Int,
) -> ConfigSource raise ConfigError {
  value.check_root(path)
  {
    kind: File(role),
    name: name.unwrap_or(path),
    priority,
    policy: Replace,
    key_policies: [],
    input: Parsed(path, value),
    excluded: [],
  }
}

///|
/// A file source over an explicit list of candidate files at one location.
/// No existing candidate gives the status `NotFound`, one loads it, and more
/// than one raises `Ambiguous` with `display`. A candidate exists when the
/// listing of its directory holds its exact name as a file. The name is
/// `display` when no name is given.
pub fn ConfigSource::candidates(
  paths : Array[String],
  display~ : String,
  role? : FileRole = Explicit,
  name? : String,
  priority? : Int,
) -> ConfigSource {
  {
    kind: File(role),
    name: name.unwrap_or(display),
    priority,
    policy: Replace,
    key_policies: [],
    input: Candidates(paths.copy(), display),
    excluded: [],
  }
}

///|
/// A source that loads nothing and reports the status `Skipped`. The name is
/// the report name of `kind` when no name is given.
pub fn ConfigSource::skipped(
  kind : SourceKind,
  name? : String,
  priority? : Int,
) -> ConfigSource {
  {
    kind,
    name: name.unwrap_or(kind.report_name()),
    priority,
    policy: Replace,
    key_policies: [],
    input: NoInput,
    excluded: [],
  }
}

///|
/// A new source that removes the subtrees at `paths` after the parse and
/// before the merge. When `warn` is true, each removed key that the source held
/// adds a `KeyRemoved` warning to its report.
pub fn ConfigSource::exclude(
  self : ConfigSource,
  paths : Array[KeyPath],
  warn~ : Bool,
) -> ConfigSource {
  { ..self, excluded: [..self.excluded, ..paths.map(path => (path, warn))] }
}

///|
/// A new source with the list merge policy `policy` for every list that has no
/// key-path policy.
pub fn ConfigSource::with_policy(
  self : ConfigSource,
  policy : MergePolicy,
) -> ConfigSource {
  { ..self, policy, }
}

///|
/// A new source with the list merge policy `policy` at `key`. A key-path
/// policy beats the source policy.
pub fn ConfigSource::with_key_policy(
  self : ConfigSource,
  key : KeyPath,
  policy : MergePolicy,
) -> ConfigSource {
  let policies = self.key_policies.filter(entry => entry.0 != key)
  policies.push((key, policy))
  { ..self, key_policies: policies }
}

///|
fn value_source(
  kind : SourceKind,
  name : String,
  priority : Int?,
  value : ConfigValue,
) -> ConfigSource {
  {
    kind,
    name,
    priority,
    policy: Replace,
    key_policies: [],
    input: Value(value),
    excluded: [],
  }
}

///|
/// A tree that holds `leaf` at `key`.
fn nest(key : KeyPath, leaf : ConfigValue) -> ConfigValue {
  key.names.rev_fold(init=leaf, (value, name) => fields([(name, value)]))
}

///|
/// `value` without the subtree at `names`, or `None` when it holds no value
/// there.
fn remove_key(value : ConfigValue, names : ArrayView[String]) -> ConfigValue? {
  guard value.ion.kind() is Struct(fields) && names is [name, .. rest] else {
    return None
  }
  guard fields.search_by(field => field.name().text() == Some(name))
    is Some(index) else {
    return None
  }
  let kept = fields.copy()
  if rest.is_empty() {
    kept.remove(index) |> ignore
  } else {
    guard remove_key({ ion: fields[index].value() }, rest) is Some(child) else {
      return None
    }
    kept[index] = @ion.Field::from_token(fields[index].name(), child.ion)
  }
  Some({
    ion: @ion.Ion::from_fields(kept).with_annotations(value.ion.annotations()),
  })
}