// Schema -> type conversion dispatch (upstream convert.rs). The arms of
// `convert_schema_object` are tried in upstream's order; each comment names
// the upstream arm.

///|
/// The conversion result: a type entry and the metadata that applies to it.
type Converted = (TypeEntry, @schema.Metadata?)

///|
fn TypeSpace::convert_schema(
  self : TypeSpace,
  type_name : Name,
  schema : @schema.Schema,
) -> Converted raise TypifyError {
  match schema {
    Object(obj) =>
      match self.cache_lookup(obj) {
        Some(entry) => (entry, obj.metadata)
        None => self.convert_schema_object(type_name, schema, obj)
      }
    Bool(true) => self.convert_permissive(None)
    Bool(false) => self.convert_never(type_name, schema)
  }
}

///|
/// Re-convert a modified schema, keeping the outer metadata as upstream's
/// `.map(|(te, m)| ...)` does.
fn TypeSpace::reconvert_keep_metadata(
  self : TypeSpace,
  type_name : Name,
  original_schema : @schema.Schema,
  new_schema : @schema.SchemaObject,
  metadata : @schema.Metadata?,
) -> Converted raise TypifyError {
  let (te, m) = self.convert_schema_object(
    type_name, original_schema, new_schema,
  )
  match m {
    Some(_) if m == metadata => (te, metadata)
    Some(_) => raise panic_with("unexpected metadata value")
    None => (te, None)
  }
}

///|
fn TypeSpace::convert_schema_object(
  self : TypeSpace,
  type_name : Name,
  original_schema : @schema.Schema,
  schema : @schema.SchemaObject,
) -> Converted raise TypifyError {
  if self.convert_rust_extension(schema) is Some(entry) {
    return (entry, schema.metadata)
  }
  let metadata = schema.metadata
  let any_validation = F_META | F_FORMAT | F_NUM | F_STR | F_ARR | F_OBJ
  let single = single_type(schema)
  // Option: exactly two instance types, one of them null.
  if schema.instance_type is Some(Vec(multiple)) &&
    multiple.length() == 2 &&
    multiple.contains(Null) {
    let only_null = schema.enum_values is Some(values) &&
      values.iter().all(v => v.is_null())
    if only_null {
      return self.convert_null(metadata)
    }
    match multiple.search_by(t => t != Null) {
      Some(i) => {
        let other_type = multiple[i]
        let enum_values = schema.enum_values.map(values => {
          values.filter(v => !v.is_null())
        })
        let ss : @schema.Schema = Object({
          ..schema,
          instance_type: Some(Single(other_type)),
          enum_values,
        })
        let inner_type_name = match type_name {
          Required(name) => Name::Suggested("\{name}Inner")
          _ => type_name
        }
        return self.convert_option(inner_type_name, metadata, ss)
      }
      None => {
        let new_schema = { ..schema, instance_type: Some(Single(Null)), }
        return self.reconvert_keep_metadata(
          type_name, original_schema, new_schema, metadata,
        )
      }
    }
  }
  // Strings
  if single is Some(String) && shape(schema, some=F_TYPE, any=any_validation) {
    return self.convert_string(
      type_name,
      original_schema,
      metadata,
      schema.format,
      schema.string,
    )
  }
  // Strings with the type omitted, but validation present
  if shape(schema, some=F_STR, any=F_META | F_FORMAT) {
    return self.convert_string(
      type_name,
      original_schema,
      metadata,
      schema.format,
      schema.string,
    )
  }
  // Enumerated string type
  if single is Some(String) &&
    shape(schema, some=F_TYPE | F_ENUM, any=any_validation) {
    return self.convert_enum_string(
      type_name,
      original_schema,
      metadata,
      schema.enum_values.unwrap(),
      schema.string,
    )
  }
  // Integers
  if single is Some(Integer) && shape(schema, some=F_TYPE, any=any_validation) {
    return self.convert_integer(metadata, schema.number, schema.format)
  }
  // Numbers
  if single is Some(Number) && shape(schema, some=F_TYPE, any=any_validation) {
    return self.convert_number(metadata, schema.format)
  }
  // Boolean
  if single is Some(Boolean) &&
    shape(
      schema,
      some=F_TYPE,
      any=F_META | F_ENUM | F_NUM | F_STR | F_ARR | F_OBJ,
    ) {
    return self.convert_bool(metadata)
  }
  // Object
  if single is Some(Object) &&
    shape(schema, some=F_TYPE, any=F_META | F_NUM | F_STR | F_ARR | F_OBJ) {
    return self.convert_object(
      type_name,
      original_schema,
      metadata,
      schema.object,
    )
  }
  // Object with the type omitted, but validation present
  if shape(schema, some=F_OBJ, any=F_META) {
    return self.convert_object(
      type_name,
      original_schema,
      metadata,
      schema.object,
    )
  }
  // Array
  if single is Some(Array) &&
    shape(schema, some=F_TYPE | F_ARR, any=F_META | F_NUM | F_STR | F_OBJ) {
    return self.convert_array(type_name, metadata, schema.array.unwrap())
  }
  // Array with the type omitted, but validation present
  if shape(schema, some=F_ARR, any=F_META) {
    return self.convert_array(type_name, metadata, schema.array.unwrap())
  }
  // Arrays of anything
  if single is Some(Array) &&
    shape(schema, some=F_TYPE, any=F_META | F_NUM | F_STR | F_OBJ) {
    return self.convert_array_of_any(metadata)
  }
  // The permissive schema
  if shape(schema, any=F_META) {
    return self.convert_permissive(metadata)
  }
  // Null
  if single is Some(Null) && shape(schema, some=F_TYPE, any=any_validation) {
    return self.convert_null(metadata)
  }
  // Reference
  if shape(schema, some=F_REF, any=F_META) {
    return self.convert_reference(metadata, schema.reference.unwrap())
  }
  // References that also include the type.
  if shape(schema, some=F_REF, any=F_META | F_TYPE) {
    let reference = schema.reference.unwrap()
    let ref_schema = self.definition(reference)
    guard ref_schema is Object(r) && r.instance_type == schema.instance_type else {
      raise panic_with("assertion failed: $ref type mismatch for \{reference}")
    }
    return self.convert_reference(metadata, reference)
  }
  // References with other constraints: merge with the referenced chain.
  if schema.reference is Some(reference) {
    let mut def = self.definition(reference)
    let mut new_schema : @schema.Schema = Object({ ..schema, reference: None, })
    while def is Object({ reference: r, .. }) {
      let schema_only_ref : @schema.Schema = Object({
        ..@schema.SchemaObject::default(),
        reference: r,
      })
      let schema_without_ref : @schema.Schema = Object({
        ..def.into_object(),
        reference: None,
      })
      new_schema = merge_all(
        [schema_without_ref, schema_only_ref, new_schema],
        self.definitions,
      )
      match r {
        Some(r) => def = self.definition(r)
        None => break
      }
    }
    let (type_entry, _) = self.convert_schema(type_name, new_schema) catch {
      e => raise panic_with("called `Result::unwrap()` on an `Err` value: \{e}")
    }
    return (type_entry, metadata)
  }
  // Enum of a single, known, non-String type (strings above).
  if single is Some(_) && schema.enum_values is Some(enum_values) {
    return self.convert_typed_enum(
      type_name, original_schema, schema, enum_values,
    )
  }
  // Enum of unknown type
  if shape(schema, some=F_ENUM, any=F_META) {
    return self.convert_unknown_enum(
      type_name,
      original_schema,
      metadata,
      schema.enum_values.unwrap(),
    )
  }
  // Subschemas
  if shape(schema, some=F_SUB, any=F_META | F_TYPE) {
    let subschemas = schema.subschemas.unwrap()
    let m = sub_present(subschemas)
    if m == S_ALL {
      return self.convert_all_of(
        type_name,
        original_schema,
        metadata,
        subschemas.all_of.unwrap(),
      )
    } else if m == S_ANY {
      return self.convert_any_of(
        type_name,
        original_schema,
        metadata,
        subschemas.any_of.unwrap(),
      )
    } else if m == S_ONE {
      return self.convert_one_of(
        type_name,
        original_schema,
        metadata,
        subschemas.one_of.unwrap(),
      )
    } else if m == S_NOT {
      return self.convert_not(
        type_name,
        original_schema,
        metadata,
        subschemas.not.unwrap(),
      )
    }
    // Multiple subschemas: merge, then convert.
    let schema_object = { ..schema, subschemas: None, }
    return match
      try_merge_with_subschemas(
        schema_object,
        Some(subschemas),
        self.definitions,
      ) {
      Some(s) => {
        let (type_entry, _) = self.convert_schema_object(
          type_name, original_schema, s,
        )
        (type_entry, None)
      }
      None => self.convert_never(type_name, original_schema)
    }
  }
  // Subschemas with other stuff.
  if schema.subschemas is Some(subschemas) {
    let without_subschemas = { ..schema, subschemas: None, metadata: None, }
    return match
      try_merge_with_subschemas(
        without_subschemas,
        Some(subschemas),
        self.definitions,
      ) {
      Some(merged) => {
        let merged = { ..merged, metadata, }
        let (type_entry, _) = self.convert_schema_object(
          type_name, original_schema, merged,
        )
        (type_entry, None)
      }
      None => self.convert_never(type_name, original_schema)
    }
  }
  // Constant values are ignored.
  if schema.const_value is Some(_) {
    return self.reconvert_keep_metadata(
      type_name,
      original_schema,
      { ..schema, const_value: None, },
      metadata,
    )
  }
  if schema.instance_type is Some(Vec(instance_types)) {
    // All seven instance types: strip them and try again.
    let all : Array[@schema.InstanceType] = [
      Null,
      Boolean,
      Object,
      Array,
      Number,
      String,
      Integer,
    ]
    if all.iter().all(t => instance_types.contains(t)) {
      let (type_entry, _) = self.convert_schema_object(
        type_name,
        original_schema,
        { ..schema, instance_type: None, },
      )
      return (type_entry, metadata)
    }
    // A singleton type array is a singleton type.
    if instance_types is [it] {
      let (type_entry, _) = self.convert_schema_object(
        type_name,
        original_schema,
        { ..schema, instance_type: Some(Single(it)), },
      )
      return (type_entry, metadata)
    }
    // Several types: an untagged enum labeled by type.
    if shape(schema, some=F_TYPE, any=any_validation) {
      let unique_types : Array[@schema.InstanceType] = []
      for t in instance_types {
        if !unique_types.contains(t) {
          unique_types.push(t)
        }
      }
      unique_types.sort_by((a, b) => {
        reordered_rank(a).compare(reordered_rank(b))
      })
      let subschemas = unique_types.map(it => {
        let base = {
          ..@schema.SchemaObject::default(),
          instance_type: Some(Single(it)),
        }
        let (label, inner) : (String, @schema.SchemaObject) = match it {
          Null => ("null", base)
          Boolean => ("boolean", base)
          Object => ("object", { ..base, object: schema.object, })
          Array => ("array", { ..base, array: schema.array, })
          Number =>
            (
              "number",
              { ..base, format: schema.format, number: schema.number, },
            )
          String =>
            (
              "string",
              { ..base, format: schema.format, string: schema.string, },
            )
          Integer =>
            (
              "integer",
              { ..base, format: schema.format, number: schema.number, },
            )
        }
        @schema.Schema::Object({
          ..@schema.SchemaObject::default(),
          metadata: Some({ ..@schema.Metadata::default(), title: Some(label), }),
          subschemas: Some({
            ..@schema.SubschemaValidation::default(),
            all_of: Some([Object(inner)]),
          }),
        })
      })
      let type_entry = self.untagged_enum(
        type_name, original_schema, metadata, subschemas,
      )
      return (type_entry, metadata)
    }
  }
  raise panic_with(
    "not yet implemented: invalid (or unexpected) schema:\n\{@schema.Schema::Object(schema).to_value().to_string_pretty()}",
  )
}

///|
/// Look up the definition for a `$ref` (upstream `.get(..).unwrap()`).
fn TypeSpace::definition(
  self : TypeSpace,
  reference : String,
) -> @schema.Schema raise TypifyError {
  match self.definitions.get(ref_key(reference)) {
    Some(s) => s
    None =>
      raise panic_with(
        "called `Option::unwrap()` on a `None` value ($ref \{reference})",
      )
  }
}