///|
/// Trait for rendering @core.Schema as a Json value (JSON @core.Schema export).
///
/// Each method corresponds to one @core.SchemaType variant and returns a Json value
/// representing the JSON @core.Schema constraint.
pub(open) trait JsonSchemaRenderer {
  fn render_string(Self, schema : @core.Schema) -> Json
  fn render_number(Self, schema : @core.Schema) -> Json
  fn render_boolean(Self, schema : @core.Schema) -> Json
  fn render_null(Self, schema : @core.Schema) -> Json
  fn render_object(
    Self,
    spec : Map[String, @core.Schema],
    mode : @core.ObjectMode,
    schema : @core.Schema,
  ) -> Json
  fn render_array(Self, elem : @core.Schema, schema : @core.Schema) -> Json
  fn render_optional(Self, inner : @core.Schema, schema : @core.Schema) -> Json
  fn render_default(
    Self,
    inner : @core.Schema,
    default_val : Json,
    schema : @core.Schema,
  ) -> Json
  fn render_enum(Self, values : Array[String], schema : @core.Schema) -> Json
  fn render_union(Self, schemas : Array[@core.Schema], schema : @core.Schema) -> Json
  fn render_intersection(
    Self,
    schemas : Array[@core.Schema],
    schema : @core.Schema,
  ) -> Json
  fn render_transform(
    Self,
    inner : @core.Schema,
    closure : @core.TransformClosure,
    schema : @core.Schema,
  ) -> Json
  fn render_literal(Self, value : Json, schema : @core.Schema) -> Json
  fn render_any(Self, schema : @core.Schema) -> Json
  fn render_unknown(Self, schema : @core.Schema) -> Json
  fn render_tuple(Self, items : Array[@core.Schema], schema : @core.Schema) -> Json
  fn render_preprocess(Self, inner : @core.Schema, schema : @core.Schema) -> Json
  fn render_lazy(Self, resolved : @core.Schema, schema : @core.Schema) -> Json
}

///|
/// Dispatch to the appropriate JsonSchemaRenderer method based on @core.SchemaType.
pub fn[R : JsonSchemaRenderer] render_json_type(
  renderer : R,
  schema : @core.Schema,
) -> Json {
  match schema.schema_type {
    StringType => renderer.render_string(schema)
    NumberType => renderer.render_number(schema)
    BooleanType => renderer.render_boolean(schema)
    NullType => renderer.render_null(schema)
    AnyType => renderer.render_any(schema)
    UnknownType => renderer.render_unknown(schema)
    ObjectType(spec, mode) => renderer.render_object(spec, mode, schema)
    ArrayType(elem) => renderer.render_array(elem, schema)
    TupleType(items) => renderer.render_tuple(items, schema)
    OptionalType(inner) => renderer.render_optional(inner, schema)
    NullableType(inner) =>
      Json::object({
        "anyOf": Json::array([
          render_json_type(renderer, inner),
          Json::object({ "type": Json::string("null") }),
        ]),
      })
    PrefaultType(inner, value) => renderer.render_default(inner, value, schema)
    DefaultType(inner, default_val) =>
      renderer.render_default(inner, default_val, schema)
    EnumType(values) => renderer.render_enum(values, schema)
    UnionType(schemas) => renderer.render_union(schemas, schema)
    IntersectionType(schemas) => renderer.render_intersection(schemas, schema)
    PreprocessType(_, inner) => renderer.render_preprocess(inner, schema)
    TransformType(inner, closure) =>
      renderer.render_transform(inner, closure, schema)
    LiteralType(value) => renderer.render_literal(value, schema)
    LazyType(f) => renderer.render_lazy(f(), schema)
    DiscriminatedUnionType(_, options) =>
      renderer.render_union(options.values().collect(), schema)
    PipeType(input, _, _) =>
      render_json_type(renderer, without_inherited_name(schema, input))
  }
}

///|
/// Named-aware version: emits a `$ref` if schema has a name in the list.
pub fn[R : JsonSchemaRenderer] render_json_type_ref(
  renderer : R,
  schema : @core.Schema,
  named_names : Array[String],
) -> Json {
  if !schema.name.is_empty() {
    for n in named_names {
      if n == schema.name {
        return Json::object({
          "$ref": Json::string(
            "#/$defs/" + reference_fragment_token(schema.name),
          ),
        })
      }
    }
  }
  render_json_type(renderer, schema)
}

///|
// Escape a JSON Pointer token, then encode its UTF-8 bytes for a URI fragment.
fn reference_fragment_token(name : String) -> String {
  let result = StringBuilder()
  let hex = "0123456789ABCDEF".to_array()
  fn write_byte(value : Int32) {
    result.write_char('%')
    result.write_char(hex[value / 16])
    result.write_char(hex[value % 16])
  }
  for c in name.to_array() {
    if c == '~' {
      result.write_string("~0")
    } else if c == '/' {
      result.write_string("~1")
    } else if (c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      c == '-' ||
      c == '.' ||
      c == '_' {
      result.write_char(c)
    } else {
      let code = c.to_int()
      guard code < 0xD800 || code > 0xDFFF else {
        abort("Export names must contain well-formed Unicode")
      }
      if code < 0x80 {
        write_byte(code)
      } else if code < 0x800 {
        write_byte(0xC0 + code / 64)
        write_byte(0x80 + code % 64)
      } else if code < 0x10000 {
        write_byte(0xE0 + code / 4096)
        write_byte(0x80 + code / 64 % 64)
        write_byte(0x80 + code % 64)
      } else {
        write_byte(0xF0 + code / 262144)
        write_byte(0x80 + code / 4096 % 64)
        write_byte(0x80 + code / 64 % 64)
        write_byte(0x80 + code % 64)
      }
    }
  }
  result.to_string()
}