///|
/// Error returned when parsing a compact schema specification.
pub(all) struct SchemaSpecError {
  code : String
  message : String
  column : Int
} derive(Eq)

///|
fn schema_spec_error(
  code : String,
  message : String,
  column : Int,
) -> SchemaSpecError {
  { code, message, column }
}

///|
fn split_on_char(text : String, delimiter : Char) -> Array[String] {
  let parts : Array[String] = []
  let mut start = 0
  let mut i = 0
  while i < text.length() {
    match text.get_char(i) {
      Some(c) =>
        if c == delimiter {
          parts.push(text[start:i].to_owned())
          start = i + 1
        }
      None => ()
    }
    i = i + 1
  }
  parts.push(text[start:text.length()].to_owned())
  parts
}

///|
fn find_char(text : String, target : Char) -> Int? {
  let mut i = 0
  while i < text.length() {
    match text.get_char(i) {
      Some(c) => if c == target { return Some(i) }
      None => ()
    }
    i = i + 1
  }
  None
}

///|
fn parse_kind_label(label : String) -> FieldKind? {
  match label.trim().to_owned().to_lower() {
    "text" => Some(Text)
    "string" => Some(Text)
    "non_empty" => Some(NonEmpty)
    "nonempty" => Some(NonEmpty)
    "required" => Some(NonEmpty)
    "int" => Some(Integer)
    "integer" => Some(Integer)
    "decimal" => Some(Decimal)
    "number" => Some(Decimal)
    "bool" => Some(Boolean)
    "boolean" => Some(Boolean)
    _ => None
  }
}

///|
fn strip_required_suffix(kind_text : String) -> (String, Bool) {
  let trimmed = kind_text.trim().to_owned()
  if trimmed.is_empty() {
    ("", true)
  } else {
    let last_index = trimmed.length() - 1
    match trimmed.get_char(last_index) {
      Some('!') => (trimmed[0:last_index].to_owned().trim().to_owned(), true)
      Some('?') => (trimmed[0:last_index].to_owned().trim().to_owned(), false)
      _ => (trimmed, true)
    }
  }
}

///|
fn parse_column_rule(
  token : String,
  index : Int,
) -> Result[ColumnRule, SchemaSpecError] {
  let trimmed = token.trim().to_owned()
  if trimmed.is_empty() {
    return Err(schema_spec_error("empty_rule", "schema rule is empty", index))
  }
  match find_char(trimmed, ':') {
    Some(colon) => {
      let name = trimmed[0:colon].to_owned().trim().to_owned()
      let raw_kind = trimmed[colon + 1:trimmed.length()].to_owned()
      let (kind_text, required) = strip_required_suffix(raw_kind)
      if name.is_empty() {
        return Err(
          schema_spec_error("empty_name", "schema column name is empty", index),
        )
      }
      match parse_kind_label(kind_text) {
        Some(kind) => Ok({ name, kind, required })
        None =>
          Err(
            schema_spec_error(
              "unknown_kind", "unknown schema field kind", index,
            ),
          )
      }
    }
    None =>
      Err(
        schema_spec_error(
          "missing_kind", "schema rule must use name:kind", index,
        ),
      )
  }
}

///|
fn has_schema_rule_name(columns : Array[ColumnRule], name : String) -> Bool {
  for rule in columns {
    if rule.name == name {
      return true
    }
  }
  false
}

///|
/// Parse a compact schema specification.
///
/// Example: `name:non_empty!,age:int?,active:bool`.
pub fn parse_schema_spec(
  spec : String,
  allow_extra_columns : Bool,
) -> Result[Schema, SchemaSpecError] {
  let columns : Array[ColumnRule] = []
  let parts = split_on_char(spec, ',')
  let mut i = 0
  while i < parts.length() {
    match parse_column_rule(parts[i], i) {
      Ok(rule) => {
        if has_schema_rule_name(columns, rule.name) {
          return Err(
            schema_spec_error(
              "duplicate_name", "schema column name is duplicated", i,
            ),
          )
        }
        columns.push(rule)
      }
      Err(err) => return Err(err)
    }
    i = i + 1
  }
  Ok({ columns, allow_extra_columns })
}

///|
/// Serialize a schema back to the compact `name:kind!` form.
pub fn schema_to_spec(schema : Schema) -> String {
  let out = StringBuilder::StringBuilder()
  let mut i = 0
  while i < schema.columns.length() {
    if i > 0 {
      out.write_char(',')
    }
    let rule = schema.columns[i]
    out.write_string(rule.name)
    out.write_char(':')
    out.write_string(kind_name(rule.kind))
    if rule.required {
      out.write_char('!')
    } else {
      out.write_char('?')
    }
    i = i + 1
  }
  out.to_string()
}