///|
/// One step of a decode path after the key path: a list index or a field name.
pub(all) enum DecodeStep {
  Field(String)
  Index(Int)
} derive(Eq, Debug)

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

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

///|
/// The path of a value that failed to decode. `key` holds the leading names up
/// to the first list index, and `None` is the root of the bound tree. `suffix`
/// is empty or starts with an `Index` step.
pub(all) struct DecodePath {
  key : KeyPath?
  suffix : Array[DecodeStep]
} derive(Eq, Debug)

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

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

///|
/// The text form, for example `targets[1].level` or `servers."eu.west".port`.
/// A field name uses the quote rule of `KeyPath`. A path with no key and no
/// suffix gives `(root)`.
pub impl Show for DecodePath with output(self, logger) {
  match self.key {
    Some(key) => key.output(logger)
    None => if self.suffix.is_empty() { logger.write_string("(root)") }
  }
  for index, step in self.suffix {
    match step {
      Index(item) => logger.write_string("[\{item}]")
      Field(name) => {
        if self.key is Some(_) || index > 0 {
          logger.write_char('.')
        }
        write_name(logger, name)
      }
    }
  }
}

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

///|
/// The sources to blame for a value that failed to decode.
pub(all) enum Blame {
  /// The sources that set this leaf or list, in merge order. Never empty.
  Set(Array[Origin])
  /// The key is a table. The sources that set keys in it, in merge order.
  Table(Array[Origin])
  /// The value came from the bind default.
  Default
  /// No source sets this key.
  Unset
} derive(Eq, Debug)

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

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

///|
/// A decode path and the sources to blame for it.
pub(all) struct Located {
  path : DecodePath
  blame : Blame
} derive(Eq, Debug)

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

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

///|
/// A binding failure: the decoder or validation detail and the paths that it
/// names. `at` holds one or more paths and is never empty.
pub(all) struct BindFailure {
  at : Array[Located]
  detail : String
} derive(Eq, Debug)

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

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

///|
/// Text for people: one line for each path, `:  ()`.
fn BindFailure::lines(self : BindFailure) -> String {
  if self.at.is_empty() {
    return self.detail
  }
  self.at
  .map(located => "\{located.path}: \{self.detail} (\{blame_text(located.blame)})")
  .join("\n")
}

///|
/// Text for people that names the sources to blame.
fn blame_text(blame : Blame) -> String {
  match blame {
    Set(origins) => "set in \{origins_text(origins)}"
    Table([]) => "empty table, no source sets keys in it"
    Table(origins) => "table set in \{origins_text(origins)}"
    Default => "from the bind default"
    Unset => "not set by any source"
  }
}

///|
fn origins_text(origins : Array[Origin]) -> String {
  origins.map(origin_text).join(", ")
}