///|
/// A field that a projection reads from each captured node. The node identity
/// is always part of a capture, so a projection with no fields returns
/// identities alone.
pub(all) enum FieldRequest {
  Kind
  Category
  Text
  Location
  Path
  Depth
  Field
  Attribute(String)
  Extra(String)
} derive(Eq, Debug)

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

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

///|
/// Parses a field name: `kind`, `category`, `text`, `location`, `path`,
/// `depth`, `field`, `attr:` or an adapter extra field such as
/// `id`.
pub fn FieldRequest::parse(name : String) -> FieldRequest {
  match name {
    "kind" => Kind
    "category" => Category
    "text" => Text
    "location" => Location
    "path" => Path
    "depth" => Depth
    "field" => Field
    _ =>
      if name.has_prefix("attr:") {
        Attribute(name.view(start_offset=5).to_owned())
      } else {
        Extra(name)
      }
  }
}

///|
/// A projected value. `Absent` means that the node has no such value; it is
/// different from an empty string.
pub(all) enum Projected {
  Absent
  Text(String)
  Scalar(ScalarValue)
  Name(Name)
  Int(Int)
  Location(QueryLocation)
  Path(ReadOnlyArray[QueryPathStep])
} derive(Eq, Debug)

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

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

///|
priv enum RField {
  RKind
  RCategory
  RText
  RLocation
  RPath
  RDepth
  RField
  RAttribute(Name)
  RExtra(String)
}

///|
/// A checked field selection for one schema.
pub struct Projection {
  schema_id : String
  schema_version : Int
  fields : ReadOnlyArray[FieldRequest]
  priv resolved : ReadOnlyArray[RField]
}

///|
pub impl @debug.Debug for Projection with to_repr(self) {
  @debug.Repr::Repr(self.fields.iter().to_array())
}

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

///|
/// The most fields that one projection can request.
pub let max_projection_fields : Int = 256

///|
/// Checks `fields` against `schema`. An unknown extra field, an unbound
/// attribute prefix or too many fields is an input failure.
pub fn Projection::new(
  schema : Schema,
  fields : ArrayView[FieldRequest],
  namespaces? : ArrayView[(String, String)] = [],
) -> Result[Projection, ReadOnlyArray[QueryDiagnostic]] {
  if fields.length() > max_projection_fields {
    return Err([
      error("query/projection-limit", "A projection requests too many fields.", None),
    ])
  }
  let diagnostics = []
  let resolved = []
  for field in fields {
    let r = match field {
      Kind => Some(RKind)
      Category => Some(RCategory)
      Text => Some(RText)
      Location => Some(RLocation)
      Path => Some(RPath)
      Depth => Some(RDepth)
      Field => Some(RField)
      Extra(name) =>
        if schema.extra_fields.iter().any(n => n == name) {
          Some(RExtra(name))
        } else {
          diagnostics.push(
            error(
              "query/unknown-projection-field",
              "The schema has no projection field \{name}.",
              None,
            ),
          )
          None
        }
      Attribute(name) =>
        match name.find(":") {
          None if name != "" => Some(RAttribute(Name::new("", name)))
          None => {
            diagnostics.push(
              error("query/invalid-name", "An attribute name cannot be empty.", None),
            )
            None
          }
          Some(colon) => {
            let prefix = name.view(end_offset=colon).to_owned()
            let local_name = name.view(start_offset=colon + 1).to_owned()
            match namespaces.iter().find_first(b => b.0 == prefix) {
              Some((_, uri)) if local_name != "" && !local_name.contains(":") =>
                Some(RAttribute(Name::new(uri, local_name)))
              Some(_) => {
                diagnostics.push(
                  error("query/invalid-name", "\{name} is not a valid attribute name.", None),
                )
                None
              }
              None => {
                diagnostics.push(
                  error(
                    "query/unbound-prefix",
                    "The prefix \{prefix} has no namespace binding.",
                    None,
                  ),
                )
                None
              }
            }
          }
        }
    }
    if r is Some(value) {
      resolved.push(value)
    }
  }
  if !diagnostics.is_empty() {
    return Err(ReadOnlyArray::from_array(diagnostics))
  }
  Ok({
    schema_id: schema.id,
    schema_version: schema.version,
    fields: ReadOnlyArray::from_array(fields.to_owned()),
    resolved: ReadOnlyArray::from_array(resolved),
  })
}

///|
/// A projection with no fields: identities only.
pub fn Projection::identity(schema : Schema) -> Projection {
  { schema_id: schema.id, schema_version: schema.version, fields: [], resolved: [] }
}

///|
fn[T : QueryTree] project(
  tree : T,
  node : Int,
  field : RField,
) -> Projected {
  match field {
    RKind =>
      match tree.kind(node) {
        Some(name) => Name(name)
        None => Absent
      }
    RCategory => Text(tree.category(node))
    RText =>
      match tree.text(node) {
        Some(text) => Text(text)
        None => Absent
      }
    RLocation =>
      match tree.location(node) {
        Some(location) => Location(location)
        None => Absent
      }
    RPath => Path(tree.path(node))
    RDepth => Int(tree.depth(node))
    RField =>
      match tree.field(node) {
        Some(name) => Name(name)
        None => Absent
      }
    RAttribute(name) =>
      match tree.attribute(node, name) {
        Some(value) => Scalar(value)
        None => Absent
      }
    RExtra(name) =>
      match tree.extra(node, name) {
        Some(value) => Scalar(value)
        None => Absent
      }
  }
}

///|
/// The output units of a projected value: one, plus its text code units.
fn Projected::units(self : Projected) -> Int {
  1 +
  (match self {
    Absent | Int(_) => 0
    Text(text) => text.length()
    Scalar(value) => value.tag.length() + value.text.length()
    Name(name) => name.namespace_uri.length() + name.local_name.length()
    Location(location) => location.source_id.map(s => s.length()).unwrap_or(0) + 6
    Path(steps) =>
      steps.iter().fold(init=0, (sum, step) => sum +
        step.field.namespace_uri.length() +
        step.field.local_name.length() +
        2)
  })
}