///|
priv enum ValueSource[V] {
  Original(V)
  KeyText(String)
}

///|
priv struct Frame[V] {
  source : ValueSource[V]
  expected : @smithy.ShapeId?
  owner : @smithy.ShapeId
  path : InstancePath
  depth : Int32
  nullable : Bool
  critical : (@smithy.ShapeId, String)?
}

///|
fn[V] value_kind(node : ValueNode[V]) -> String {
  match node {
    Null => "null"
    Boolean(_) => "boolean"
    Number(_) | InvalidNumber => "number"
    String(_) => "string"
    Array(_) => "array"
    Object(_) | LocatedObject(_) | NativeObject(_) => "object"
    Native(_) => "native scalar"
    Sexp(_) => "sexp"
    Annotated(_, _) => "annotated value"
  }
}

///|
fn[V] check_kind(
  shape : PreparedShape,
  node : ValueNode[V],
  config : Config,
  budget : Budget,
  path : InstancePath,
) -> Bool {
  let emit = fn(code, message) { budget.emit(code, path, shape.id, message) }
  match (shape.body, node) {
    (Simple(Document), _) => true
    (Simple(Blob), Native(Blob(_))) if config.profile == NativeProfile => true
    (Simple(Timestamp), Native(Timestamp(_))) if config.profile == NativeProfile =>
      true
    (Simple(BigDecimal | Float | Double as kind), Native(Decimal(n))) if config.profile ==
      NativeProfile => {
      if !numeric_bounds(kind, n) {
        emit(
          NumericBounds,
          "I expected a number within the target's numeric bounds.",
        )
      }
      true
    }
    (Simple(Float | Double as kind), Native(Float(n))) if config.profile ==
      NativeProfile => {
      match exact_float(n) {
        Some(value) =>
          if !numeric_bounds(kind, value) {
            emit(
              NumericBounds,
              "I expected a number within the target's numeric bounds.",
            )
          }
        None =>
          if (
              n != n &&
              (
                shape.constraints.min is Some(_) ||
                shape.constraints.max is Some(_)
              )
            ) ||
            (n > 0.0 && shape.constraints.max is Some(_)) ||
            (n < 0.0 && shape.constraints.min is Some(_)) {
            emit(
              Range,
              "I expected a numeric value satisfying the modeled range, but found a nonnumeric float.",
            )
          }
      }
      true
    }
    (Simple(Boolean), Boolean(_)) => true
    (Simple(String), String(_)) => true
    (Simple(Blob), String(text)) if config.profile == JsonProfile => {
      if strict_blob_length(text) is None {
        emit(
          InvalidEncoding,
          "I expected a canonical padded base64 blob, but found an invalid encoding.",
        )
      }
      true
    }
    (Simple(Timestamp), Number(_)) if config.profile == JsonProfile &&
      shape.constraints.timestamp == EpochSeconds => true
    (Simple(Timestamp), String(text)) if config.profile == JsonProfile &&
      shape.constraints.timestamp == DateTime => {
      if !date_time(text) {
        emit(
          InvalidTimestamp,
          "I expected a calendar-valid date-time timestamp with an offset.",
        )
      }
      true
    }
    (
      Simple(
        Byte
        | Short
        | Integer
        | Long
        | BigInteger
        | BigDecimal
        | Float
        | Double as kind
      ),
      Number(n),
    ) => {
      if !numeric_bounds(kind, n) {
        emit(
          NumericBounds,
          "I expected a number within the target's numeric bounds and integer requirements.",
        )
      }
      true
    }
    (Simple(Float | Double), String("NaN" | "Infinity" | "-Infinity" as text)) if config.profile ==
      JsonProfile &&
      config.nonnumeric_floats => {
      if (
          text == "NaN" &&
          (shape.constraints.min is Some(_) || shape.constraints.max is Some(_))
        ) ||
        (text == "Infinity" && shape.constraints.max is Some(_)) ||
        (text == "-Infinity" && shape.constraints.min is Some(_)) {
        emit(
          Range,
          "I expected a numeric value satisfying the modeled range, but found a nonnumeric float.",
        )
      }
      true
    }
    (List(_), Array(_)) => true
    (
      Structure(_)
      | Union(_)
      | Map(..),
      Object(_)
      | LocatedObject(_)
      | NativeObject(_),
    ) => true
    (Enum(_), String(_)) | (IntEnum(_), Number(_)) => true
    _ => false
  }
}

///|
fn[V : ValueView] check_constraints(
  shape : PreparedShape,
  node : ValueNode[V],
  budget : Budget,
  path : InstancePath,
  config : Config,
) -> Unit {
  let c = shape.constraints
  let emit = fn(code, message) { budget.emit(code, path, shape.id, message) }
  if exact_numeric(node) is Some(n) {
    if !budget.charge(path, shape.id) {
      return
    }
    if (c.min is Some(min) && n.numeric_compare(min) < 0) ||
      (c.max is Some(max) && n.numeric_compare(max) > 0) {
      emit(Range, "I expected the number to satisfy the modeled range.")
    }
  }
  let count = match node {
    String(text) =>
      if shape.body is Simple(Blob) {
        strict_blob_length(text)
      } else {
        Some(text.iter().count())
      }
    Array(values) => Some(values.length())
    Object(fields) => Some(fields.length())
    LocatedObject(fields) => Some(fields.length())
    NativeObject(fields) => Some(fields.length())
    Native(Blob(bytes)) => Some(bytes.length())
    _ => None
  }
  if count is Some(count) &&
    (c.min_length is Some(_) || c.max_length is Some(_)) {
    if !budget.charge(path, shape.id) {
      return
    }
    let n = number(count.to_string())
    if (c.min_length is Some(min) && n.numeric_compare(min) < 0) ||
      (c.max_length is Some(max) && n.numeric_compare(max) > 0) {
      emit(
        Length,
        "I expected the value's length to satisfy the modeled bounds.",
      )
    }
  }
  if c.pattern is Some(pattern) && node is String(text) {
    if !budget.complete {
      return
    }
    // The matcher counts its own work and stops when it needs more than is left.
    let work = budget.limits.work - budget.used
    let (matched, units) = match pattern.matches(text, work~) {
      Some(result) => result
      None => (true, work + 1)
    }
    if !budget.charge(path, shape.id, units~) {
      return
    }
    if !matched {
      emit(Pattern, "I expected the string to match the modeled pattern.")
    }
  }
  if !shape.enum_values.is_empty() {
    let mut found = false
    for candidate in shape.enum_values {
      if !budget.charge(path, shape.id) {
        return
      }
      match (node, candidate) {
        (String(a), String(b)) if a == b => found = true
        (Number(a), Number(b)) if a == b => found = true
        _ => ()
      }
      if found {
        break
      }
    }
    if !found {
      emit(EnumValue, "I expected a declared enum value.")
    }
  }
  if c.unique && node is Array(values) {
    for i in 0.. ValidationResult {
  self.validate_with_budget(value, base_path, Budget::new(self.config.limits))
}

///|
/// Validate with a request-scoped meter. The validation charges the meter's remaining work instead of
/// a fresh allowance, and an incomplete result exhausts the meter. Depth and finding limits still come
/// from the prepared configuration.
pub fn[V : ValueView] PreparedValidator::validate_metered(
  self : PreparedValidator,
  value : V,
  meter~ : WorkMeter,
  base_path~ : InstancePath = InstancePath::root(),
) -> ValidationResult {
  let limits = { ..self.config.limits, work: meter.limit, }
  let budget = Budget::new(limits, used=meter.used)
  if meter.exhausted {
    budget.stop(base_path, self.target, "work")
  }
  let result = self.validate_with_budget(value, base_path, budget)
  meter.used = budget.used
  if !result.complete {
    meter.exhausted = true
  }
  result
}

///|
fn[V : ValueView] PreparedValidator::validate_with_budget(
  self : PreparedValidator,
  value : V,
  base_path : InstancePath,
  budget : Budget,
) -> ValidationResult {
  let stack : Array[Frame[V]] = [
    {
      source: Original(value),
      expected: Some(self.target),
      owner: self.target,
      path: base_path,
      depth: 0,
      nullable: false,
      critical: None,
    },
  ]
  while stack.pop() is Some(frame) {
    let frame = match frame.source {
      Original(value) =>
        match value.source() {
          Some(source) => { ..frame, path: base_path.append(source), }
          None => frame
        }
      KeyText(_) => frame
    }
    if !budget.charge(frame.path, frame.owner) {
      break
    }
    if frame.depth > budget.limits.depth {
      budget.stop(frame.path, frame.owner, "depth")
      break
    }
    let mut node = match frame.source {
      Original(value) => {
        if !budget.room(
            frame.path,
            frame.owner,
            value.expansion_size(),
            pending=stack.length(),
          ) {
          break
        }
        value.inspect()
      }
      KeyText(text) => String(text)
    }
    let shape = frame.expected.bind(id => self.shapes.get(id))
    let mut unsupported = false
    match node {
      InvalidNumber => {
        budget.emit(
          InvalidNumber,
          frame.path,
          frame.owner,
          "I expected an exact finite numeric token, but found an invalid host number.",
        )
        continue
      }
      Annotated(annotations, value) => {
        if !budget.charge(frame.path, frame.owner, units=annotations.length()) {
          break
        }
        budget.emit(
          UnsupportedValue,
          frame.path,
          frame.owner,
          "I cannot validate an annotated value.",
        )
        stack.push({ ..frame, source: Original(value), })
        continue
      }
      Sexp(values) => {
        budget.emit(
          UnsupportedValue,
          frame.path,
          frame.owner,
          "I cannot validate an s-expression value.",
        )
        node = Array(values)
        unsupported = true
      }
      Native(scalar) => {
        let invalid = if self.config.profile == JsonProfile {
          true
        } else {
          match scalar {
            Symbol(_) | Clob(_) | InvalidTimestamp => true
            Timestamp(t) =>
              !t.full() ||
              shape.map(s => s.body is Simple(Document)).unwrap_or(true)
            Blob(_) =>
              shape.map(s => s.body is Simple(Document)).unwrap_or(true)
            Float(n) =>
              exact_float(n) is None &&
              (
                !self.config.nonnumeric_floats ||
                !shape
                .map(s => s.body is Simple(Float | Double))
                .unwrap_or(false)
              )
            _ => false
          }
        }
        if invalid {
          budget.emit(
            UnsupportedValue,
            frame.path,
            frame.owner,
            "I cannot validate this native value kind in the selected value profile or placement.",
          )
          continue
        }
        if scalar is TypedNull(_) {
          node = Null
        }
      }
      NativeObject(_) if self.config.profile == JsonProfile => {
        budget.emit(
          UnsupportedValue,
          frame.path,
          frame.owner,
          "I cannot validate a native object in the JSON value profile.",
        )
        unsupported = true
      }
      _ => ()
    }
    if node is Null && frame.nullable {
      continue
    }
    if !unsupported && shape is Some(shape) {
      if check_kind(shape, node, self.config, budget, frame.path) {
        check_constraints(shape, node, budget, frame.path, self.config)
      } else {
        budget.emit(
          TypeMismatch,
          frame.path,
          shape.id,
          "I expected a " +
          shape.body.keyword() +
          " value, but found " +
          value_kind(node) +
          ".",
        )
      }
    }
    if frame.critical is Some((enclosing, separator)) &&
      node is String(text) &&
      !self.critical_path_known(enclosing, text, separator, budget, frame.path) {
      budget.emit(
        UnknownCriticalPath,
        frame.path,
        frame.owner,
        "I expected critical path \{text} to name declared structure members.",
      )
    }
    if !budget.complete {
      break
    }
    match node {
      Array(values) => {
        if !budget.room(
            frame.path,
            frame.owner,
            values.length(),
            pending=stack.length(),
          ) {
          break
        }
        let child = shape.bind(s => {
          match s.body {
            List(id) => Some(id)
            _ => None
          }
        })
        let nullable = shape.map(s => s.sparse).unwrap_or(false)
        let critical = shape.bind(s => {
          s.critical_separator.map(separator => (s.id.root(), separator))
        })
        for i = values.length() - 1; i >= 0; i = i - 1 {
          stack.push({
            source: Original(values[i]),
            expected: child,
            owner: child.unwrap_or(frame.owner),
            path: frame.path.child(Index(i)),
            depth: frame.depth + 1,
            nullable,
            critical,
          })
        }
      }
      Object(_) | LocatedObject(_) | NativeObject(_) => {
        let count = match node {
          Object(fields) => fields.length()
          LocatedObject(fields) => fields.length()
          NativeObject(fields) => fields.length()
          _ => 0
        }
        if !budget.room(frame.path, frame.owner, count, pending=stack.length()) {
          break
        }
        if shape.map(s => s.body is Map(..)).unwrap_or(false) &&
          !budget.room(
            frame.path,
            frame.owner,
            count * 2,
            pending=stack.length(),
          ) {
          break
        }
        let unresolved : Array[(V, InstancePath)] = []
        let fields = match node {
          Object(fields) =>
            fields.map(entry => {
              LocatedField::new(
                entry.0,
                entry.1,
                InstancePath::root().child(Field(entry.0, 0)),
              )
            })
          LocatedObject(fields) => fields
          NativeObject(fields) => {
            let resolved : Array[LocatedField[V]] = []
            for i in 0..
                  resolved.push(
                    LocatedField::new(name, value, InstancePath::root()),
                  )
                None => {
                  let path = frame.path.child(FieldPosition(i))
                  budget.emit(
                    UnsupportedValue,
                    frame.path.child(FieldNamePosition(i)),
                    frame.owner,
                    "I expected a resolved object field name, but found an unresolved field symbol.",
                  )
                  unresolved.push((value, path))
                }
              }
            }
            resolved
          }
          _ => []
        }
        let occurrences : Map[String, Int32] = Map([])
        let present : Set[String] = Set([])
        let located : Array[(LocatedField[V], InstancePath, Int32)] = []
        for field in fields {
          let occurrence = occurrences.get(field.name).unwrap_or(0)
          occurrences[field.name] = occurrence + 1
          present.add(field.name)
          let path = if node is (Object(_) | NativeObject(_)) {
            frame.path.child(Field(field.name, occurrence))
          } else {
            frame.path.append(field.source)
          }
          if occurrence > 0 {
            budget.emit(
              DuplicateMember,
              path,
              frame.owner,
              "I expected unique object member names, but found a duplicate member.",
            )
          }
          located.push((field, path, occurrence))
        }
        if shape is Some(shape) {
          if shape.body is Union(_) && count != 1 {
            budget.emit(
              UnionMember,
              frame.path,
              shape.id,
              "I expected exactly one union member.",
            )
          }
          for member_ in shape.members {
            if !budget.charge(frame.path, shape.id) {
              break
            }
            if member_.required && !present.contains(member_.name) {
              budget.emit(
                RequiredMember,
                frame.path.child(Field(member_.name, 0)),
                member_.id,
                "I expected the required structure member, but found it absent.",
              )
            }
          }
        }
        for (value, path) in unresolved {
          stack.push({
            source: Original(value),
            expected: None,
            owner: frame.owner,
            path,
            depth: frame.depth + 1,
            nullable: false,
            critical: None,
          })
        }
        for i = located.length() - 1; i >= 0; i = i - 1 {
          let (field, path, occurrence) = located[i]
          let mut expected = None
          let mut nullable = false
          if shape is Some(shape) {
            match shape.body {
              Structure(_) | Union(_) =>
                match shape.by_name.get(field.name) {
                  Some(member_) => expected = Some(member_.id)
                  None =>
                    if shape.unknown_members == Reject || shape.body is Union(_) {
                      budget.emit(
                        UnknownMember,
                        path,
                        shape.id,
                        "I expected a declared member, but found an unknown member.",
                      )
                    }
                }
              Map(key~, value~) => {
                expected = Some(value)
                nullable = shape.sparse
                stack.push({
                  source: KeyText(field.name),
                  expected: Some(key),
                  owner: key,
                  path: frame.path.child(MapKey(field.name, occurrence)),
                  depth: frame.depth + 1,
                  nullable: false,
                  critical: None,
                })
              }
              _ => ()
            }
          }
          stack.push({
            source: Original(field.value),
            expected,
            owner: expected.unwrap_or(frame.owner),
            path,
            depth: frame.depth + 1,
            nullable,
            critical: None,
          })
        }
      }
      _ => ()
    }
  }
  { findings: budget.findings, complete: budget.complete, }
}

///|
pub fn PreparedValidator::validate_json(
  self : PreparedValidator,
  value : Json,
  base_path~ : InstancePath = InstancePath::root(),
) -> ValidationResult {
  self.validate(value, base_path~)
}