///|
/// Limits applied while checking an untrusted schema document.
pub(all) struct SchemaCheckOptions {
  max_depth : Int
  max_nodes : Int
  max_errors : Int
} derive(Eq, Debug)

///|
pub fn SchemaCheckOptions::default() -> SchemaCheckOptions {
  { max_depth: 128, max_nodes: 100000, max_errors: 100, }
}

///|
pub fn SchemaCheckOptions::new(
  max_depth? : Int = 128,
  max_nodes? : Int = 100000,
  max_errors? : Int = 100,
) -> SchemaCheckOptions {
  { max_depth, max_nodes, max_errors, }
}

///|
pub fn SchemaCheckOptions::max_depth(self : SchemaCheckOptions) -> Int {
  self.max_depth
}

///|
pub fn SchemaCheckOptions::max_nodes(self : SchemaCheckOptions) -> Int {
  self.max_nodes
}

///|
pub fn SchemaCheckOptions::max_errors(self : SchemaCheckOptions) -> Int {
  self.max_errors
}

///|
fn check_diagnostic(
  code : DiagnosticCode,
  message : String,
  path : JsonPointer,
) -> Diagnostic {
  Diagnostic::new(code, message, schema_path=path)
}

///|
fn add_check_error(
  errors : Array[Diagnostic],
  options : SchemaCheckOptions,
  diagnostic : Diagnostic,
) -> Unit {
  if errors.length() < options.max_errors {
    errors.push(diagnostic)
  }
}

///|
fn check_enum(
  values : Array[String],
  path : JsonPointer,
  errors : Array[Diagnostic],
  options : SchemaCheckOptions,
) -> Unit {
  if values.is_empty() {
    add_check_error(
      errors,
      options,
      check_diagnostic(
        EmptyEnum,
        "enum form must contain at least one value",
        path.property("enum"),
      ),
    )
    return
  }
  let seen : Map[String, Bool] = Map([])
  for index, value in values {
    if seen.contains(value) {
      add_check_error(
        errors,
        options,
        check_diagnostic(
          DuplicateEnumValue,
          "duplicate enum value '" + value + "'",
          path.property("enum").index(index),
        ),
      )
    } else {
      seen.set(value, true)
    }
  }
}

///|
fn check_property_overlap(
  required : Map[String, Schema],
  optional : Map[String, Schema],
  path : JsonPointer,
  errors : Array[Diagnostic],
  options : SchemaCheckOptions,
) -> Unit {
  for name, _ in required {
    if optional.contains(name) {
      add_check_error(
        errors,
        options,
        check_diagnostic(
          PropertyOverlap,
          "property '" + name + "' is both required and optional",
          path.property("optionalProperties").property(name),
        ),
      )
    }
  }
}

///|
fn check_discriminator_branch(
  tag : String,
  name : String,
  branch : Schema,
  path : JsonPointer,
  errors : Array[Diagnostic],
  options : SchemaCheckOptions,
) -> Unit {
  match branch.form() {
    PropertiesForm(required, optional, _) => {
      if branch.is_nullable() {
        add_check_error(
          errors,
          options,
          check_diagnostic(
            InvalidDiscriminatorMapping,
            "discriminator mapping branches cannot be nullable",
            path.property("nullable"),
          ),
        )
      }
      if required.contains(tag) || optional.contains(tag) {
        add_check_error(
          errors,
          options,
          check_diagnostic(
            DuplicateDiscriminatorProperty,
            "mapping branch '" +
            name +
            "' redeclares discriminator property '" +
            tag +
            "'",
            path,
          ),
        )
      }
    }
    _ =>
      add_check_error(
        errors,
        options,
        check_diagnostic(
          InvalidDiscriminatorMapping,
          "discriminator mapping branch must use properties form",
          path,
        ),
      )
  }
}

///|
fn check_schema_node(
  schema : Schema,
  definitions : Map[String, Schema],
  path : JsonPointer,
  depth : Int,
  visited : Ref[Int],
  errors : Array[Diagnostic],
  options : SchemaCheckOptions,
) -> Unit {
  if errors.length() >= options.max_errors {
    return
  }
  if depth > options.max_depth {
    add_check_error(
      errors,
      options,
      check_diagnostic(
        ResourceLimitExceeded,
        "schema depth exceeds configured limit " + options.max_depth.to_string(),
        path,
      ),
    )
    return
  }
  visited.val += 1
  if visited.val > options.max_nodes {
    add_check_error(
      errors,
      options,
      check_diagnostic(
        ResourceLimitExceeded,
        "schema node count exceeds configured limit " +
        options.max_nodes.to_string(),
        path,
      ),
    )
    return
  }
  match schema.form() {
    EmptyForm | TypeForm(_) => ()
    RefForm(name) =>
      if !definitions.contains(name) {
        add_check_error(
          errors,
          options,
          check_diagnostic(
            UnknownReference,
            "reference '" + name + "' is not present in root definitions",
            path.property("ref"),
          ),
        )
      }
    EnumForm(values) => check_enum(values, path, errors, options)
    ElementsForm(element) =>
      check_schema_node(
        element,
        definitions,
        path.property("elements"),
        depth + 1,
        visited,
        errors,
        options,
      )
    ValuesForm(value) =>
      check_schema_node(
        value,
        definitions,
        path.property("values"),
        depth + 1,
        visited,
        errors,
        options,
      )
    PropertiesForm(required, optional, _) => {
      check_property_overlap(required, optional, path, errors, options)
      for name, property in required {
        check_schema_node(
          property,
          definitions,
          path.property("properties").property(name),
          depth + 1,
          visited,
          errors,
          options,
        )
      }
      for name, property in optional {
        check_schema_node(
          property,
          definitions,
          path.property("optionalProperties").property(name),
          depth + 1,
          visited,
          errors,
          options,
        )
      }
    }
    DiscriminatorForm(tag, mapping) =>
      for name, branch in mapping {
        let branch_path = path.property("mapping").property(name)
        check_discriminator_branch(
          tag, name, branch, branch_path, errors, options,
        )
        check_schema_node(
          branch,
          definitions,
          branch_path,
          depth + 1,
          visited,
          errors,
          options,
        )
      }
  }
}

///|
/// Check cross-node constraints that cannot be decided while parsing one node.
pub fn check_schema_with(
  document : SchemaDocument,
  options : SchemaCheckOptions,
) -> Array[Diagnostic] {
  let errors : Array[Diagnostic] = []
  if options.max_depth < 0 || options.max_nodes < 1 || options.max_errors < 1 {
    errors.push(
      check_diagnostic(
        ResourceLimitExceeded,
        "schema check limits must be positive",
        JsonPointer::root(),
      ),
    )
    return errors
  }
  let visited = Ref(0)
  let definitions = document.definitions()
  for name, schema in definitions {
    check_schema_node(
      schema,
      definitions,
      JsonPointer::root().property("definitions").property(name),
      1,
      visited,
      errors,
      options,
    )
  }
  check_schema_node(
    document.root(),
    definitions,
    JsonPointer::root(),
    0,
    visited,
    errors,
    options,
  )
  errors
}

///|
pub fn check_schema(document : SchemaDocument) -> Array[Diagnostic] {
  check_schema_with(document, SchemaCheckOptions::default())
}

///|
/// Parse a schema and reject semantic violations in one operation.
pub fn parse_checked_schema(
  text : StringView,
) -> Result[SchemaDocument, Array[Diagnostic]] {
  let document = match parse_schema(text) {
    Ok(value) => value
    Err(SchemaDiagnostic(diagnostic)) => return Err([diagnostic])
  }
  let errors = check_schema(document)
  if errors.is_empty() {
    Ok(document)
  } else {
    Err(errors)
  }
}