///|
/// A summary of loading a dictionary. Blank lines are ignored; `skipped_lines`
/// counts malformed nonblank lines skipped when `strict=false`.
pub(all) struct LoadReport {
  parsed_entries : Int32
  added_words : Int32
  skipped_lines : Int32
} derive(Eq, Debug)

///|
/// Load word/frequency pairs from text, merging them into this dictionary.
/// The default separator is Unicode whitespace; an explicit character supports
/// CSV-like dictionaries with spaces inside terms. Column indices are zero-based.
/// Counts must be nonnegative signed 64-bit integers. A leading BOM and CRLF
/// line endings are accepted. In strict mode, malformed lines raise an error
/// with a one-based line number and leave the dictionary unchanged.
pub fn SymSpell::load_dictionary(
  self : SymSpell,
  text : String,
  term_index~ : Int32 = 0,
  count_index~ : Int32 = 1,
  separator? : Char,
  strict~ : Bool = true,
) -> LoadReport raise SymSpellError {
  let parsed = parse_frequency_rows(
    text,
    term_index,
    count_index,
    separator,
    strict,
    bigram=false,
  )
  let mut added_words = 0
  for entry in parsed.entries {
    let (term, count) = entry
    if self.create_dictionary_entry(term, count) {
      added_words = added_words + 1
    }
  }
  {
    parsed_entries: parsed.entries.length(),
    added_words,
    skipped_lines: parsed.skipped_lines,
  }
}

///|
priv struct ParsedFrequencyRows {
  entries : Array[(String, Int64)]
  skipped_lines : Int32
}

///|
fn parse_frequency_rows(
  text : String,
  term_index : Int32,
  count_index : Int32,
  separator : Char?,
  strict : Bool,
  bigram~ : Bool,
) -> ParsedFrequencyRows raise SymSpellError {
  if term_index < 0 || count_index < 0 {
    raise InvalidColumns(term_index, count_index)
  }
  // Stage entries before mutation so a strict parse error is transactional.
  let entries : Array[(String, Int64)] = []
  let mut skipped_lines = 0
  let mut line_number = 0
  for raw_line in text.split("\n") {
    line_number = line_number + 1
    let line = if line_number == 1 {
      raw_line.trim_start(chars="\u{FEFF}").trim().to_owned()
    } else {
      raw_line.trim().to_owned()
    }
    if line.is_empty() {
      continue
    }
    let fields = match separator {
      Some(character) =>
        line.split(character.to_string()).map(StringView::to_owned).to_array()
      None => split_whitespace(line)
    }
    let parsed = if Int32::max(term_index, count_index) < fields.length() {
      let count = try @string.parse_int64(fields[count_index][:]) catch {
        _ => None
      } noraise {
        count => Some(count)
      }
      let term = if bigram && separator is None {
        if term_index < fields.length() - 1 {
          Some(fields[term_index] + " " + fields[term_index + 1])
        } else {
          None
        }
      } else {
        Some(fields[term_index])
      }
      let term = if bigram { term.bind(canonical_bigram) } else { term }
      match (term, count) {
        (Some(key), Some(value)) if value >= 0L => Some((key, value))
        _ => None
      }
    } else {
      None
    }
    match parsed {
      Some(entry) => entries.push(entry)
      None => {
        if strict {
          raise InvalidDictionaryLine(line_number, line)
        }
        skipped_lines = skipped_lines + 1
      }
    }
  }
  { entries, skipped_lines, }
}

///|
fn split_whitespace(text : String) -> Array[String] {
  let fields : Array[String] = []
  let builder = StringBuilder()
  for character in text {
    if character.is_whitespace() {
      if !builder.is_empty() {
        fields.push(builder.to_string())
        builder.reset()
      }
    } else {
      builder.write_char(character)
    }
  }
  if !builder.is_empty() {
    fields.push(builder.to_string())
  }
  fields
}