///|
/// Primitive types defined by the Apache Avro data model.
pub(all) enum Primitive {
  Null
  Boolean
  Int
  Long
  Float
  Double
  Bytes
  String
} derive(Debug, Eq)

///|
/// A field in an Avro record declaration.
pub(all) struct Field {
  name : String
  schema : Schema
  default_value : Json?
  aliases : Array[String]
  doc : String?
} derive(Debug, Eq)

///|
pub fn Field::name(self : Field) -> String {
  self.name
}

///|
pub fn Field::schema(self : Field) -> Schema {
  self.schema
}

///|
pub fn Field::default_value(self : Field) -> Json? {
  self.default_value
}

///|
pub fn Field::aliases(self : Field) -> Array[String] {
  self.aliases
}

///|
/// A named definition retained by the root schema for reference resolution.
pub(all) struct NamedDefinition {
  name : String
  kind : SchemaKind
} derive(Debug, Eq)

///|
/// The complete Avro schema algebra. `Named` is a resolved full-name
/// reference; use `Schema::resolve_named` on the root schema to inspect its
/// declaration.
pub(all) enum SchemaKind {
  Primitive(Primitive)
  Record(
    name~ : String,
    namespace_~ : String?,
    aliases~ : Array[String],
    fields~ : Array[Field]
  )
  Enum(
    name~ : String,
    namespace_~ : String?,
    aliases~ : Array[String],
    symbols~ : Array[String],
    default_symbol~ : String?
  )
  Array(items~ : Schema)
  Map(values~ : Schema)
  Union(branches~ : Array[Schema])
  Fixed(
    name~ : String,
    namespace_~ : String?,
    aliases~ : Array[String],
    size~ : Int
  )
  Named(String)
} derive(Debug, Eq)

///|
/// Parsed Avro schema with its source retained for diagnostics and canonical
/// form generation.
pub struct Schema {
  kind : SchemaKind
  source : String
  definitions : Array[NamedDefinition]
} derive(Debug, Eq)

///|
pub fn Schema::new(kind : SchemaKind, source : String) -> Schema {
  { kind, source, definitions: [] }
}

///|
pub fn Schema::kind(self : Schema) -> SchemaKind {
  self.kind
}

///|
pub fn Schema::source(self : Schema) -> String {
  self.source
}

///|
/// Resolve a full or declared alias name from this parsed root schema.
pub fn Schema::resolve_named(self : Schema, name : String) -> SchemaKind? {
  for definition in self.definitions {
    if definition.name == name {
      return Some(definition.kind)
    }
    match definition.kind {
      Record(aliases~, ..) | Enum(aliases~, ..) | Fixed(aliases~, ..) =>
        if aliases.contains(name) {
          return Some(definition.kind)
        }
      _ => ()
    }
  }
  None
}

///|
/// Return the full names declared by this parsed root schema.
pub fn Schema::definition_names(self : Schema) -> Array[String] {
  self.definitions.map(definition => definition.name)
}

///|
pub fn Schema::kind_name(self : Schema) -> String {
  match self.kind {
    Primitive(Null) => "null"
    Primitive(Boolean) => "boolean"
    Primitive(Int) => "int"
    Primitive(Long) => "long"
    Primitive(Float) => "float"
    Primitive(Double) => "double"
    Primitive(Bytes) => "bytes"
    Primitive(String) => "string"
    Record(name=_, namespace_=_, aliases=_, fields=_) => "record"
    Enum(name=_, namespace_=_, aliases=_, symbols=_, default_symbol=_) => "enum"
    Array(items=_) => "array"
    Map(values=_) => "map"
    Union(branches=_) => "union"
    Fixed(name=_, namespace_=_, aliases=_, size=_) => "fixed"
    Named(name) => name
  }
}

///|
fn json_string(value : String) -> String {
  Json::string(value).stringify()
}

///|
fn canonical_fields(fields : Array[Field]) -> String {
  fields
  .map(field => {
    "{\"name\":\{json_string(field.name)},\"type\":\{canonical_kind(field.schema.kind)}}"
  })
  .join(",")
}

///|
fn canonical_kind(kind : SchemaKind) -> String {
  match kind {
    Primitive(Null) => "\"null\""
    Primitive(Boolean) => "\"boolean\""
    Primitive(Int) => "\"int\""
    Primitive(Long) => "\"long\""
    Primitive(Float) => "\"float\""
    Primitive(Double) => "\"double\""
    Primitive(Bytes) => "\"bytes\""
    Primitive(String) => "\"string\""
    Record(name~, namespace_=_, aliases=_, fields~) =>
      "{\"name\":\{json_string(name)},\"type\":\"record\",\"fields\":[\{canonical_fields(fields)}]}"
    Enum(name~, namespace_=_, aliases=_, symbols~, default_symbol=_) => {
      let rendered_symbols = symbols.map(json_string).join(",")
      "{\"name\":\{json_string(name)},\"type\":\"enum\",\"symbols\":[\{rendered_symbols}]}"
    }
    Array(items~) =>
      "{\"type\":\"array\",\"items\":\{canonical_kind(items.kind)}}"
    Map(values~) =>
      "{\"type\":\"map\",\"values\":\{canonical_kind(values.kind)}}"
    Union(branches~) =>
      "[\{branches.map(branch => canonical_kind(branch.kind)).join(",")}]"
    Fixed(name~, namespace_=_, aliases=_, size~) =>
      "{\"name\":\{json_string(name)},\"type\":\"fixed\",\"size\":\{size}}"
    Named(name) => json_string(name)
  }
}

///|
/// Return Avro parsing canonical form (PCF) for this schema.
pub fn Schema::canonical_form(self : Schema) -> String {
  canonical_kind(self.kind)
}

///|
/// Compute the Avro single-object-encoding CRC-64-AVRO fingerprint of the
/// parsing canonical form.
pub fn Schema::fingerprint64(self : Schema) -> UInt64 {
  let bytes = @utf8.encode(self.canonical_form())
  let empty = 0xc15d213aa4d7a795UL
  let mut fingerprint = empty
  for byte in bytes {
    let mut value = (fingerprint ^ byte.to_uint64()) & 0xffUL
    for _ in 0..<8 {
      value = if (value & 1UL) == 1UL {
        (value >> 1) ^ empty
      } else {
        value >> 1
      }
    }
    fingerprint = (fingerprint >> 8) ^ value
  }
  fingerprint
}