///|
fn violation(
  shape : Shape,
  focus : Term,
  component : String,
  value : Term?,
  details? : Array[ValidationResult] = [],
  override_path? : Path? = None,
) -> ValidationResult {
  {
    focus_node: focus,
    source_shape: shape.id,
    component: sh + component + "ConstraintComponent",
    severity: shape.severity,
    path: (if override_path is Some(_) { override_path } else { shape.path }).map(
      copy_path,
    ),
    value,
    messages: shape.messages.copy(),
    details,
  }
}

///|
fn kind_matches(value : Term, kind : String) -> Bool {
  match value {
    Iri(_) => ["IRI", "BlankNodeOrIRI", "IRIOrLiteral"].any(k => kind == sh + k)
    Blank(_) =>
      ["BlankNode", "BlankNodeOrIRI", "BlankNodeOrLiteral"].any(k => {
        kind == sh + k
      })
    Literal(_, _, _) =>
      ["Literal", "BlankNodeOrLiteral", "IRIOrLiteral"].any(k => kind == sh + k)
  }
}

///|
fn siblings(plan : Plan, shape : Term, qualified : Term) -> Array[Term] {
  let result = []
  for parent in plan.parents.get_or_default(shape, []) {
    for rule in plan.shapes[parent].rules {
      match rule {
        Property(child) =>
          match plan.shapes[child].qualified_value {
            Some(q) if q != qualified => result.push(q)
            _ => ()
          }
        _ => ()
      }
    }
  }
  unique(result)
}

///|
fn evaluate_shape(
  plan : Plan,
  id : Term,
  focus : Term,
  reader : Reader,
) -> Array[ValidationResult] {
  let shape = plan.shapes[id]
  if shape.deactivated {
    return []
  }
  let values = match shape.path {
    None => [focus]
    Some(path) => evaluate_path(reader, focus, path, false)
  }
  let results = []
  for rule in shape.rules {
    match rule {
      Class(class) =>
        for value in values {
          if !is_instance(reader, value, class) {
            results.push(violation(shape, focus, "Class", Some(value)))
          }
        }
      Datatype(datatype) =>
        for value in values {
          let valid = match value {
            Literal(s, d, language) =>
              d == datatype && valid_lexical(s, d, language)
            _ => false
          }
          if !valid {
            results.push(violation(shape, focus, "Datatype", Some(value)))
          }
        }
      NodeKind(kind) =>
        for value in values {
          if !kind_matches(value, kind) {
            results.push(violation(shape, focus, "NodeKind", Some(value)))
          }
        }
      MinCount(n) =>
        if values.length() < n {
          results.push(violation(shape, focus, "MinCount", None))
        }
      MaxCount(n) =>
        if values.length() > n {
          results.push(violation(shape, focus, "MaxCount", None))
        }
      MinLength(n) | MaxLength(n) =>
        for value in values {
          let length = match value {
            Iri(s) | Literal(s, _, _) => Some(s.iter().count())
            Blank(_) => None
          }
          let is_min = rule is MinLength(_)
          let valid = match length {
            Some(length) => if is_min { length >= n } else { length <= n }
            None => false
          }
          if !valid {
            results.push(
              violation(
                shape,
                focus,
                if is_min {
                  "MinLength"
                } else {
                  "MaxLength"
                },
                Some(value),
              ),
            )
          }
        }
      LanguageIn(ranges) =>
        for value in values {
          let valid = match value {
            Literal(_, _, Some(tag)) =>
              ranges.any(range => {
                range == "*" ||
                tag.to_lower() == range ||
                tag.to_lower().has_prefix(range + "-")
              })
            _ => false
          }
          if !valid {
            results.push(violation(shape, focus, "LanguageIn", Some(value)))
          }
        }
      UniqueLang => {
        let counts : Map[String, Int] = Map([])
        let languages : Array[String] = []
        for value in values {
          match value {
            Literal(_, _, Some(tag)) => {
              let tag = tag.to_lower()
              if !counts.contains(tag) {
                languages.push(tag)
              }
              counts[tag] = counts.get_or_default(tag, 0) + 1
            }
            _ => ()
          }
        }
        for language in languages {
          if counts[language] > 1 {
            results.push(violation(shape, focus, "UniqueLang", None))
          }
        }
      }
      HasValue(required) =>
        if !values.contains(required) {
          results.push(violation(shape, focus, "HasValue", None))
        }
      In(allowed) =>
        for value in values {
          if !allowed.contains(value) {
            results.push(violation(shape, focus, "In", Some(value)))
          }
        }
      Property(child) =>
        for value in values {
          results.append(evaluate_shape(plan, child, value, reader))
        }
      Node(child) =>
        for value in values {
          let details = evaluate_shape(plan, child, value, reader)
          if !details.is_empty() {
            results.push(violation(shape, focus, "Node", Some(value), details~))
          }
        }
      Not(child) =>
        for value in values {
          if evaluate_shape(plan, child, value, reader).is_empty() {
            results.push(violation(shape, focus, "Not", Some(value)))
          }
        }
      And(children) | Or(children) | Xone(children) =>
        for value in values {
          let mut conforming = 0
          let details = []
          for child in children {
            let child_results = evaluate_shape(plan, child, value, reader)
            if child_results.is_empty() {
              conforming = conforming + 1
            } else {
              details.append(child_results)
            }
          }
          let (valid, name) = match rule {
            And(_) => (conforming == children.length(), "And")
            Or(_) => (conforming > 0, "Or")
            _ => (conforming == 1, "Xone")
          }
          if !valid {
            results.push(violation(shape, focus, name, Some(value), details~))
          }
        }
      Equals(p) => {
        let other = reader.objects(focus, p)
        for value in values {
          if !other.contains(value) {
            results.push(violation(shape, focus, "Equals", Some(value)))
          }
        }
        for value in other {
          if !values.contains(value) {
            results.push(violation(shape, focus, "Equals", Some(value)))
          }
        }
      }
      Disjoint(p) => {
        let other = reader.objects(focus, p)
        for value in values {
          if other.contains(value) {
            results.push(violation(shape, focus, "Disjoint", Some(value)))
          }
        }
      }
      Closed(allowed) =>
        for value in values {
          for t in reader.outgoing(value) {
            if !allowed.contains(t.predicate) {
              results.push(
                violation(
                  shape,
                  focus,
                  "Closed",
                  Some(t.object),
                  override_path=Some(Predicate(t.predicate)),
                ),
              )
            }
          }
        }
      Qualified(qualified, minimum, maximum, disjoint) => {
        let other = if disjoint { siblings(plan, id, qualified) } else { [] }
        let mut count = 0
        for value in values {
          let conforms = evaluate_shape(plan, qualified, value, reader).is_empty()
          // Do not short-circuit dependencies: exclusion relies on every sibling.
          let mut excluded = false
          for sibling in other {
            if evaluate_shape(plan, sibling, value, reader).is_empty() {
              excluded = true
            }
          }
          if conforms && !excluded {
            count = count + 1
          }
        }
        match minimum {
          Some(n) =>
            if count < n {
              results.push(violation(shape, focus, "QualifiedMinCount", None))
            }
          None => ()
        }
        match maximum {
          Some(n) =>
            if count > n {
              results.push(violation(shape, focus, "QualifiedMaxCount", None))
            }
          None => ()
        }
      }
    }
  }
  results
}

///|
/// Validation includes all severities in the W3C conforms flag.
pub fn Plan::validate(self : Plan, graph : Graph) -> Report {
  let results = []
  let mut checked = 0
  for id in self.shape_order {
    for focus in target_nodes(self.shapes[id], graph) {
      checked = checked + 1
      results.append(evaluate_shape(self, id, focus, { graph, reads: [], }))
    }
  }
  { conforms: results.is_empty(), results, checked, reused: 0, }
}

///|
/// Explicit focus validation does not depend on target declarations.
pub fn Plan::validate_node(
  self : Plan,
  graph : Graph,
  shape : Term,
  focus : Term,
) -> Result[Report, String] {
  if !self.shapes.contains(shape) {
    return Err("Unknown compiled shape")
  }
  let results = evaluate_shape(self, shape, focus, { graph, reads: [], })
  Ok({ conforms: results.is_empty(), results, checked: 1, reused: 0, })
}