///|
/// Fluent builder for a JTD properties-form schema.
pub(all) struct ObjectSchemaBuilder {
  required : Map[String, Schema]
  optional : Map[String, Schema]
  additional : Bool
  nullable : Bool
  metadata : Map[String, Json]
} derive(Debug)

///|
pub fn ObjectSchemaBuilder::new() -> ObjectSchemaBuilder {
  {
    required: Map([]),
    optional: Map([]),
    additional: false,
    nullable: false,
    metadata: Map([]),
  }
}

///|
pub fn ObjectSchemaBuilder::required(
  self : ObjectSchemaBuilder,
  name : String,
  schema : Schema,
) -> ObjectSchemaBuilder {
  let required = self.required.copy()
  required.set(name, schema)
  { ..self, required, }
}

///|
pub fn ObjectSchemaBuilder::optional(
  self : ObjectSchemaBuilder,
  name : String,
  schema : Schema,
) -> ObjectSchemaBuilder {
  let optional = self.optional.copy()
  optional.set(name, schema)
  { ..self, optional, }
}

///|
pub fn ObjectSchemaBuilder::allow_additional(
  self : ObjectSchemaBuilder,
  allowed : Bool,
) -> ObjectSchemaBuilder {
  { ..self, additional: allowed, }
}

///|
pub fn ObjectSchemaBuilder::nullable(
  self : ObjectSchemaBuilder,
  nullable : Bool,
) -> ObjectSchemaBuilder {
  { ..self, nullable, }
}

///|
pub fn ObjectSchemaBuilder::metadata_entry(
  self : ObjectSchemaBuilder,
  name : String,
  value : Json,
) -> ObjectSchemaBuilder {
  let metadata = self.metadata.copy()
  metadata.set(name, value)
  { ..self, metadata, }
}

///|
pub fn ObjectSchemaBuilder::required_count(self : ObjectSchemaBuilder) -> Int {
  self.required.length()
}

///|
pub fn ObjectSchemaBuilder::optional_count(self : ObjectSchemaBuilder) -> Int {
  self.optional.length()
}

///|
pub fn ObjectSchemaBuilder::build(
  self : ObjectSchemaBuilder,
) -> Result[Schema, Diagnostic] {
  for name, _ in self.required {
    if self.optional.contains(name) {
      return Err(
        Diagnostic::new(
          PropertyOverlap,
          "property '" + name + "' cannot be both required and optional",
          schema_path=JsonPointer::root()
            .property("optionalProperties")
            .property(name),
        ),
      )
    }
  }
  Ok(
    Schema::properties(self.required, self.optional, self.additional)
    .with_nullable(self.nullable)
    .with_metadata(self.metadata),
  )
}

///|
/// Fluent builder for a JTD discriminator-form schema.
pub(all) struct DiscriminatorSchemaBuilder {
  tag : String
  mapping : Map[String, Schema]
  nullable : Bool
  metadata : Map[String, Json]
} derive(Debug)

///|
pub fn DiscriminatorSchemaBuilder::new(
  tag : String,
) -> DiscriminatorSchemaBuilder {
  { tag, mapping: Map([]), nullable: false, metadata: Map([]), }
}

///|
pub fn DiscriminatorSchemaBuilder::branch(
  self : DiscriminatorSchemaBuilder,
  tag_value : String,
  schema : Schema,
) -> DiscriminatorSchemaBuilder {
  let mapping = self.mapping.copy()
  mapping.set(tag_value, schema)
  { ..self, mapping, }
}

///|
pub fn DiscriminatorSchemaBuilder::nullable(
  self : DiscriminatorSchemaBuilder,
  nullable : Bool,
) -> DiscriminatorSchemaBuilder {
  { ..self, nullable, }
}

///|
pub fn DiscriminatorSchemaBuilder::metadata_entry(
  self : DiscriminatorSchemaBuilder,
  name : String,
  value : Json,
) -> DiscriminatorSchemaBuilder {
  let metadata = self.metadata.copy()
  metadata.set(name, value)
  { ..self, metadata, }
}

///|
pub fn DiscriminatorSchemaBuilder::branch_count(
  self : DiscriminatorSchemaBuilder,
) -> Int {
  self.mapping.length()
}

///|
pub fn DiscriminatorSchemaBuilder::build(
  self : DiscriminatorSchemaBuilder,
) -> Result[Schema, Diagnostic] {
  if self.tag.is_empty() {
    return Err(
      Diagnostic::new(
        InvalidDiscriminatorTag,
        "discriminator property name must not be empty",
        schema_path=JsonPointer::root().property("discriminator"),
      ),
    )
  }
  for tag_value, branch in self.mapping {
    match branch.form() {
      PropertiesForm(required, optional, _) =>
        if required.contains(self.tag) || optional.contains(self.tag) {
          return Err(
            Diagnostic::new(
              DuplicateDiscriminatorProperty,
              "branch '" +
              tag_value +
              "' redeclares discriminator property '" +
              self.tag +
              "'",
              schema_path=JsonPointer::root()
                .property("mapping")
                .property(tag_value),
            ),
          )
        }
      _ =>
        return Err(
          Diagnostic::new(
            InvalidDiscriminatorMapping,
            "branch '" + tag_value + "' must be a properties schema",
            schema_path=JsonPointer::root()
              .property("mapping")
              .property(tag_value),
          ),
        )
    }
  }
  Ok(
    Schema::discriminator(self.tag, self.mapping)
    .with_nullable(self.nullable)
    .with_metadata(self.metadata),
  )
}

///|
/// Fluent builder for a complete schema document and its root definitions.
pub(all) struct DocumentBuilder {
  root : Schema
  definitions : Map[String, Schema]
} derive(Debug)

///|
pub fn DocumentBuilder::new(root : Schema) -> DocumentBuilder {
  { root, definitions: Map([]), }
}

///|
pub fn DocumentBuilder::definition(
  self : DocumentBuilder,
  name : String,
  schema : Schema,
) -> DocumentBuilder {
  let definitions = self.definitions.copy()
  definitions.set(name, schema)
  { ..self, definitions, }
}

///|
pub fn DocumentBuilder::definition_count(self : DocumentBuilder) -> Int {
  self.definitions.length()
}

///|
pub fn DocumentBuilder::build(
  self : DocumentBuilder,
) -> Result[SchemaDocument, Array[Diagnostic]] {
  let document = SchemaDocument::new(self.root, definitions=self.definitions)
  let errors = check_schema(document)
  if errors.is_empty() {
    Ok(document)
  } else {
    Err(errors)
  }
}

///|
/// Short constructor functions for schema-builder DSLs.
pub fn jtd_any() -> Schema {
  Schema::empty()
}

///|
pub fn jtd_boolean() -> Schema {
  Schema::type_(BooleanType)
}

///|
pub fn jtd_float32() -> Schema {
  Schema::type_(Float32Type)
}

///|
pub fn jtd_float64() -> Schema {
  Schema::type_(Float64Type)
}

///|
pub fn jtd_int8() -> Schema {
  Schema::type_(Int8Type)
}

///|
pub fn jtd_uint8() -> Schema {
  Schema::type_(Uint8Type)
}

///|
pub fn jtd_int16() -> Schema {
  Schema::type_(Int16Type)
}

///|
pub fn jtd_uint16() -> Schema {
  Schema::type_(Uint16Type)
}

///|
pub fn jtd_int32() -> Schema {
  Schema::type_(Int32Type)
}

///|
pub fn jtd_uint32() -> Schema {
  Schema::type_(Uint32Type)
}

///|
pub fn jtd_string() -> Schema {
  Schema::type_(StringType)
}

///|
pub fn jtd_timestamp() -> Schema {
  Schema::type_(TimestampType)
}

///|
pub fn jtd_reference(name : String) -> Schema {
  Schema::reference(name)
}

///|
pub fn jtd_enum(values : Array[String]) -> Schema {
  Schema::enum_(values)
}

///|
pub fn jtd_array(element : Schema) -> Schema {
  Schema::elements(element)
}

///|
pub fn jtd_map(value : Schema) -> Schema {
  Schema::values(value)
}

///|
pub fn jtd_object() -> ObjectSchemaBuilder {
  ObjectSchemaBuilder::new()
}

///|
pub fn jtd_union(tag : String) -> DiscriminatorSchemaBuilder {
  DiscriminatorSchemaBuilder::new(tag)
}

///|
pub fn jtd_document(root : Schema) -> DocumentBuilder {
  DocumentBuilder::new(root)
}