///|
/// Indexing and storage behavior for one text field.
pub struct TextOptions {
  indexed : Bool
  stored : Bool
  fast : Bool
  multi_valued : Bool
  tokenizer_name : String
  analyzer_version : String
}

///|
pub fn TextOptions::new(indexed : Bool, stored : Bool) -> TextOptions {
  {
    indexed,
    stored,
    fast: false,
    multi_valued: false,
    tokenizer_name: "whitespace",
    analyzer_version: "",
  }
}

///|
pub fn valid_tokenizer_name(name : String) -> Bool {
  if name.length() == 0 {
    return false
  }
  for ch in name {
    if ch.is_whitespace() {
      return false
    }
  }
  true
}

///|
/// Selects the named Tokenizer pipeline used for this field at index and query
/// time.
pub fn TextOptions::with_tokenizer(
  self : TextOptions,
  tokenizer_name : String,
) -> TextOptions {
  guard valid_tokenizer_name(tokenizer_name) else {
    abort("invalid tokenizer name: \{tokenizer_name}")
  }
  {
    indexed: self.indexed,
    stored: self.stored,
    fast: self.fast,
    multi_valued: self.multi_valued,
    tokenizer_name,
    analyzer_version: self.analyzer_version,
  }
}

///|
pub fn TextOptions::with_fast(self : TextOptions, fast : Bool) -> TextOptions {
  {
    indexed: self.indexed,
    stored: self.stored,
    fast,
    multi_valued: self.multi_valued,
    tokenizer_name: self.tokenizer_name,
    analyzer_version: self.analyzer_version,
  }
}

///|
pub fn TextOptions::with_multi_valued(
  self : TextOptions,
  multi_valued : Bool,
) -> TextOptions {
  {
    indexed: self.indexed,
    stored: self.stored,
    fast: self.fast,
    multi_valued,
    tokenizer_name: self.tokenizer_name,
    analyzer_version: self.analyzer_version,
  }
}

///|
/// Pins the analyzer resource fingerprint expected when indexing or reopening
/// this field. Empty keeps backward-compatible name-only resolution.
pub fn TextOptions::with_analyzer_version(
  self : TextOptions,
  analyzer_version : String,
) -> TextOptions {
  guard !analyzer_version.contains("\n") && !analyzer_version.contains("\r") else {
    abort("analyzer version must be a single line")
  }
  {
    indexed: self.indexed,
    stored: self.stored,
    fast: self.fast,
    multi_valued: self.multi_valued,
    tokenizer_name: self.tokenizer_name,
    analyzer_version,
  }
}

///|
pub fn TextOptions::is_indexed(self : TextOptions) -> Bool {
  self.indexed
}

///|
pub fn TextOptions::is_stored(self : TextOptions) -> Bool {
  self.stored
}

///|
pub fn TextOptions::is_fast(self : TextOptions) -> Bool {
  self.fast
}

///|
pub fn TextOptions::is_multi_valued(self : TextOptions) -> Bool {
  self.multi_valued
}

///|
pub fn TextOptions::tokenizer_name(self : TextOptions) -> String {
  self.tokenizer_name
}

///|
pub fn TextOptions::analyzer_version(self : TextOptions) -> String {
  self.analyzer_version
}

///|
pub(all) enum FieldType {
  Text
  Keyword
  I64
  U64
  F64
  Bool
  Date
  Bytes
} derive(Eq, @debug.Debug)

///|
/// Index/storage/column flags for non-analyzed fields.
pub struct FieldOptions {
  indexed : Bool
  stored : Bool
  fast : Bool
  multi_valued : Bool
}

///|
pub fn FieldOptions::new(
  indexed : Bool,
  stored : Bool,
  fast : Bool,
) -> FieldOptions {
  { indexed, stored, fast, multi_valued: false }
}

///|
pub fn FieldOptions::with_multi_valued(
  self : FieldOptions,
  multi_valued : Bool,
) -> FieldOptions {
  { indexed: self.indexed, stored: self.stored, fast: self.fast, multi_valued }
}

///|
pub fn FieldOptions::is_indexed(self : FieldOptions) -> Bool {
  self.indexed
}

///|
pub fn FieldOptions::is_stored(self : FieldOptions) -> Bool {
  self.stored
}

///|
pub fn FieldOptions::is_fast(self : FieldOptions) -> Bool {
  self.fast
}

///|
pub fn FieldOptions::is_multi_valued(self : FieldOptions) -> Bool {
  self.multi_valued
}

///|
struct FieldEntry {
  name : String
  field_type : FieldType
  indexed : Bool
  stored : Bool
  fast : Bool
  multi_valued : Bool
  tokenizer_name : String
  analyzer_version : String
}

///|
/// Immutable registry of field names, identifiers, and text options.
pub struct Schema {
  fields : ReadOnlyArray[FieldEntry]
}

///|
pub fn Schema::field_count(self : Schema) -> Int {
  self.fields.length()
}

///|
pub fn Schema::field_name(self : Schema, field_id : FieldId) -> String? {
  if field_id.value < 0 || field_id.value >= self.fields.length() {
    None
  } else {
    Some(self.fields[field_id.value].name)
  }
}

///|
pub fn Schema::field(self : Schema, name : String) -> FieldId? {
  match self.fields.search_by(entry => entry.name == name) {
    Some(index) => Some(FieldId::new(index))
    None => None
  }
}

///|
pub fn Schema::is_indexed(self : Schema, field_id : FieldId) -> Bool {
  match self.entry(field_id) {
    Some(entry) => entry.indexed
    None => false
  }
}

///|
pub fn Schema::is_stored(self : Schema, field_id : FieldId) -> Bool {
  match self.entry(field_id) {
    Some(entry) => entry.stored
    None => false
  }
}

///|
pub fn Schema::is_fast(self : Schema, field_id : FieldId) -> Bool {
  match self.entry(field_id) {
    Some(entry) => entry.fast
    None => false
  }
}

///|
pub fn Schema::is_multi_valued(self : Schema, field_id : FieldId) -> Bool {
  match self.entry(field_id) {
    Some(entry) => entry.multi_valued
    None => false
  }
}

///|
pub fn Schema::field_type(self : Schema, field_id : FieldId) -> FieldType? {
  match self.entry(field_id) {
    Some(entry) => Some(entry.field_type)
    None => None
  }
}

///|
/// Returns the Tokenizer registry name configured for one field.
pub fn Schema::tokenizer_name(self : Schema, field_id : FieldId) -> String? {
  match self.entry(field_id) {
    Some(entry) if entry.field_type == Text => Some(entry.tokenizer_name)
    None => None
    _ => None
  }
}

///|
pub fn Schema::analyzer_version(self : Schema, field_id : FieldId) -> String? {
  match self.entry(field_id) {
    Some(entry) if entry.field_type == Text => Some(entry.analyzer_version)
    _ => None
  }
}

///|
pub fn Schema::options(self : Schema, field_id : FieldId) -> TextOptions? {
  match self.entry(field_id) {
    Some(entry) if entry.field_type == Text =>
      Some({
        indexed: entry.indexed,
        stored: entry.stored,
        fast: entry.fast,
        multi_valued: entry.multi_valued,
        tokenizer_name: entry.tokenizer_name,
        analyzer_version: entry.analyzer_version,
      })
    _ => None
  }
}

///|
fn Schema::entry(self : Schema, field_id : FieldId) -> FieldEntry? {
  if field_id.value < 0 || field_id.value >= self.fields.length() {
    None
  } else {
    Some(self.fields[field_id.value])
  }
}

///|
/// Mutable builder that assigns sequential, stable FieldId values.
pub struct SchemaBuilder {
  fields : Array[FieldEntry]
}

///|
pub fn SchemaBuilder::new() -> SchemaBuilder {
  { fields: [] }
}

///|
pub fn SchemaBuilder::add_text_field(
  self : SchemaBuilder,
  name : String,
  options : TextOptions,
) -> FieldId {
  if self.fields.search_by(entry => entry.name == name) is Some(_) {
    abort("duplicate field name: \{name}")
  }
  let field_id = FieldId::new(self.fields.length())
  self.fields.push({
    name,
    field_type: Text,
    indexed: options.indexed,
    stored: options.stored,
    fast: options.fast,
    multi_valued: options.multi_valued,
    tokenizer_name: options.tokenizer_name,
    analyzer_version: options.analyzer_version,
  })
  field_id
}

///|
fn SchemaBuilder::add_typed_field(
  self : SchemaBuilder,
  name : String,
  field_type : FieldType,
  options : FieldOptions,
) -> FieldId {
  if self.fields.search_by(entry => entry.name == name) is Some(_) {
    abort("duplicate field name: \{name}")
  }
  let field_id = FieldId::new(self.fields.length())
  self.fields.push({
    name,
    field_type,
    indexed: options.indexed,
    stored: options.stored,
    fast: options.fast,
    multi_valued: options.multi_valued,
    tokenizer_name: "",
    analyzer_version: "",
  })
  field_id
}

///|
pub fn SchemaBuilder::add_keyword_field(
  self : SchemaBuilder,
  name : String,
  options : FieldOptions,
) -> FieldId {
  self.add_typed_field(name, Keyword, options)
}

///|
pub fn SchemaBuilder::add_i64_field(
  self : SchemaBuilder,
  name : String,
  options : FieldOptions,
) -> FieldId {
  self.add_typed_field(name, I64, options)
}

///|
pub fn SchemaBuilder::add_u64_field(
  self : SchemaBuilder,
  name : String,
  options : FieldOptions,
) -> FieldId {
  self.add_typed_field(name, U64, options)
}

///|
pub fn SchemaBuilder::add_f64_field(
  self : SchemaBuilder,
  name : String,
  options : FieldOptions,
) -> FieldId {
  self.add_typed_field(name, F64, options)
}

///|
pub fn SchemaBuilder::add_bool_field(
  self : SchemaBuilder,
  name : String,
  options : FieldOptions,
) -> FieldId {
  self.add_typed_field(name, Bool, options)
}

///|
pub fn SchemaBuilder::add_date_field(
  self : SchemaBuilder,
  name : String,
  options : FieldOptions,
) -> FieldId {
  self.add_typed_field(name, Date, options)
}

///|
pub fn SchemaBuilder::add_bytes_field(
  self : SchemaBuilder,
  name : String,
  options : FieldOptions,
) -> FieldId {
  self.add_typed_field(name, Bytes, options)
}

///|
pub fn SchemaBuilder::build(self : SchemaBuilder) -> Schema {
  let fields : Array[FieldEntry] = []
  for entry in self.fields {
    fields.push(entry)
  }
  { fields: ReadOnlyArray::from_array(fields) }
}

///|
pub fn schemas_equal(left : Schema?, right : Schema?) -> Bool {
  match (left, right) {
    (None, None) => true
    (Some(left), Some(right)) => {
      if left.fields.length() != right.fields.length() {
        return false
      }
      for index in 0.. false
  }
}