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

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

///|
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, tokenizer_name }
}

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

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

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

///|
struct FieldEntry {
  name : String
  options : TextOptions
}

///|
/// 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.options(field_id) {
    Some(options) => options.is_indexed()
    None => false
  }
}

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

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

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

///|
/// 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, options })
  field_id
}

///|
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
  }
}