///|
/// Application-level value shape; sparse and dense composites share a shape.
pub(all) enum BindingKind {
  TextValue
  ListValue
  AssociationValue
} derive(Eq)

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

///|
pub(all) struct BindingRule {
  name : String
  kind : BindingKind
  required : Bool
}

///|
pub(all) enum ContractError {
  UnknownRule(String)
  DuplicateRule(String)
  IncompatibleRule(String)
  MissingRequired(String)
  WrongKind(String, BindingKind)
  InvalidBindings(ExpandError)
}

///|
/// Opt-in application contract. Unspecified variables retain RFC semantics.
pub struct ContractTemplate {
  priv template : Template
  priv rules : Array[BindingRule]
  priv reject_unused : Bool
  priv names : Map[String, Bool]
  priv scalar_names : Array[String]
}

///|
pub fn Template::with_contract(
  self : Template,
  rules : Array[BindingRule],
  reject_unused~ : Bool = true,
) -> Result[ContractTemplate, ContractError] {
  let profiles : Map[String, VariableProfile] = Map([])
  let names : Map[String, Bool] = Map([])
  let scalar_names : Array[String] = []
  for profile in self.variable_profiles() {
    profiles[profile.name] = profile
    names[profile.name] = true
    if profile.scalar_only {
      scalar_names.push(profile.name)
    }
  }
  let seen : Map[String, Bool] = Map([])
  for rule in rules {
    if seen.contains(rule.name) {
      return Err(DuplicateRule(rule.name))
    }
    seen[rule.name] = true
    let profile = match profiles.get(rule.name) {
      Some(value) => value
      None => return Err(UnknownRule(rule.name))
    }
    if profile.scalar_only && rule.kind != TextValue {
      return Err(IncompatibleRule(rule.name))
    }
  }
  Ok({
    template: self,
    rules: rules.copy(),
    reject_unused,
    names,
    scalar_names,
  })
}

///|
fn Value::binding_kind(self : Value) -> BindingKind {
  match self {
    Scalar(_) => TextValue
    List(_) | SparseList(_) => ListValue
    Assoc(_) | SparseAssoc(_) => AssociationValue
  }
}

///|
fn Value::has_defined_content(self : Value) -> Bool {
  match self {
    Scalar(_) => true
    List(items) => !items.is_empty()
    Assoc(items) => !items.is_empty()
    SparseList(items) => items.any(fn(item) { item is Some(_) })
    SparseAssoc(items) => items.any(fn(item) { item.1 is Some(_) })
  }
}

///|
/// Validate shape, presence and ordinary expansion limits without rendering.
/// Empty composites are undefined; an empty scalar is defined, as in RFC 6570.
pub fn ContractTemplate::validate(
  self : ContractTemplate,
  bindings : Array[(String, Value)],
  max_bindings~ : Int32 = 4096,
  max_value_length~ : Int32 = 1048576,
  max_items~ : Int32 = 4096,
) -> Result[Unit, ContractError] {
  if max_value_length < 0 {
    return Err(InvalidBindings(InvalidLimit("max_value_length")))
  }
  if max_items < 0 {
    return Err(InvalidBindings(InvalidLimit("max_items")))
  }
  let values = match
    prepare_bindings(bindings, max_bindings, max_value_length, max_items) {
    Ok(value) => value
    Err(error) => return Err(InvalidBindings(error))
  }
  if self.reject_unused {
    for (name, _) in bindings {
      if !self.names.contains(name) {
        return Err(InvalidBindings(UnusedVariable(name)))
      }
    }
  }
  for rule in self.rules {
    match values.get(rule.name) {
      None => if rule.required { return Err(MissingRequired(rule.name)) }
      Some(value) => {
        if value.binding_kind() != rule.kind {
          return Err(WrongKind(rule.name, rule.kind))
        }
        if rule.required && !value.has_defined_content() {
          return Err(MissingRequired(rule.name))
        }
      }
    }
  }
  // Also check restrictions on variables without an explicit application rule.
  for name in self.scalar_names {
    if values.get(name) is Some(value) && value.binding_kind() != TextValue {
      return Err(InvalidBindings(InvalidValue(name)))
    }
  }
  Ok(())
}

///|
pub fn ContractTemplate::expand(
  self : ContractTemplate,
  bindings : Array[(String, Value)],
  max_output~ : Int32 = 1048576,
  max_bindings~ : Int32 = 4096,
  max_value_length~ : Int32 = 1048576,
  max_items~ : Int32 = 4096,
) -> Result[String, ContractError] {
  match self.validate(bindings, max_bindings~, max_value_length~, max_items~) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  match
    self.template.expand(
      bindings,
      max_output~,
      max_bindings~,
      max_value_length~,
      max_items~,
    ) {
    Ok(value) => Ok(value)
    Err(error) => Err(InvalidBindings(error))
  }
}