///|
/// A path to a value in a configuration tree: a non-empty list of non-empty
/// field names. Name matches are exact and case-sensitive. A key path does not
/// select list items.
pub struct KeyPath {
  priv names : Array[String]
} derive(Eq, Hash, Debug)

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

///|
pub extend KeyPath with Hash::{hash, hash_combine}

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

///|
/// Parse the dotted text form, for example `ir.strict_mode`. A name that
/// contains `.` or `"` uses double quotes, for example `servers."eu.west".port`.
/// In a quoted name, `\"` gives `"` and `\\` gives `\`.
pub fn KeyPath::parse(text : String) -> KeyPath raise ConfigError {
  fn invalid(reason : String) -> ConfigError {
    InvalidKeyPath(text~, reason~)
  }

  if text.is_empty() {
    raise invalid("the key path is empty")
  }
  let chars = Array::from_iter(text.iter())
  let names = []
  let mut index = 0
  while true {
    let name = StringBuilder()
    if index < chars.length() && chars[index] == '"' {
      index += 1
      let mut closed = false
      while index < chars.length() {
        let char = chars[index]
        index += 1
        if char == '"' {
          closed = true
          break
        }
        if char == '\\' {
          guard index < chars.length() &&
            (chars[index] == '"' || chars[index] == '\\') else {
            raise invalid("a quoted name allows only the escapes \\\" and \\\\")
          }
          name.write_char(chars[index])
          index += 1
        } else {
          name.write_char(char)
        }
      }
      if !closed {
        raise invalid("a quoted name has no closing quote")
      }
      if index < chars.length() && chars[index] != '.' {
        raise invalid("a quoted name must end at a dot or at the end")
      }
    } else {
      while index < chars.length() && chars[index] != '.' {
        if chars[index] == '"' {
          raise invalid("a quote can only start a name")
        }
        name.write_char(chars[index])
        index += 1
      }
    }
    let name = name.to_string()
    if name.is_empty() {
      raise invalid("a name in the key path is empty")
    }
    names.push(name)
    if index == chars.length() {
      break
    }
    // Skip the dot. A dot at the end gives an empty name in the next pass.
    index += 1
  }
  { names, }
}

///|
/// A key path from field names, from the root down. The list and each name
/// must not be empty.
pub fn KeyPath::from_names(names : Array[String]) -> KeyPath raise ConfigError {
  let path = { names: names.copy() }
  if names.is_empty() {
    raise InvalidKeyPath(text="", reason="the key path is empty")
  }
  if names.contains("") {
    raise InvalidKeyPath(
      text=path.to_string(),
      reason="a name in the key path is empty",
    )
  }
  path
}

///|
/// The field names, from the root down.
pub fn KeyPath::names(self : KeyPath) -> Array[String] {
  self.names.copy()
}

///|
/// The text form, which `KeyPath::parse` reads back to an equal key path.
pub impl Show for KeyPath with output(self, logger) {
  for index, name in self.names {
    if index > 0 {
      logger.write_char('.')
    }
    write_name(logger, name)
  }
}

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

///|
/// Write `name` bare, or in double quotes when it contains `.` or `"`.
fn write_name(logger : &Logger, name : String) -> Unit {
  if !(name.contains(".") || name.contains("\"")) {
    logger.write_string(name)
    return
  }
  logger.write_char('"')
  for char in name {
    if char == '"' || char == '\\' {
      logger.write_char('\\')
    }
    logger.write_char(char)
  }
  logger.write_char('"')
}