///|
pub enum Delimiter {
  Comma
  Tab
} derive(Debug, Eq, ToJson)

///|
pub fn Delimiter::comma() -> Delimiter {
  Comma
}

///|
pub fn Delimiter::tab() -> Delimiter {
  Tab
}

///|
pub struct ImportOptions {
  has_header : Bool
  delimiter : Delimiter
  reject_unknown_columns : Bool
} derive(Debug, Eq, ToJson)

///|
pub fn ImportOptions::csv() -> ImportOptions {
  { has_header: true, delimiter: Comma, reject_unknown_columns: true }
}

///|
pub fn ImportOptions::tsv() -> ImportOptions {
  { has_header: true, delimiter: Tab, reject_unknown_columns: true }
}

///|
pub fn ImportOptions::new(
  has_header : Bool,
  delimiter : Delimiter,
  reject_unknown_columns : Bool,
) -> ImportOptions {
  { has_header, delimiter, reject_unknown_columns }
}

///|
pub struct ImportDiagnostic {
  line : Int
  column : Int
  message : String
  hint : String
} derive(Debug, Eq, ToJson)

///|
pub struct ImportResult {
  entries : Array[IcdEntry]
  diagnostics : Array[ImportDiagnostic]
} derive(Debug, Eq, ToJson)

///|
pub fn import_entries(text : String, options : ImportOptions) -> ImportResult {
  let diagnostics = []
  let rows = scan_delimited(text, options.delimiter, diagnostics)
  let header_offset = if options.has_header { 1 } else { 0 }
  if options.has_header && rows.length() == 0 {
    diagnostics.push(
      import_diagnostic(
        1, 1, "missing header", "provide the six required column names",
      ),
    )
    return { entries: [], diagnostics }
  }
  if options.has_header {
    validate_header(rows[0], options, diagnostics)
  }
  let entries = []
  for row_index in header_offset.. entries.push(entry)
      None => ()
    }
  }
  if diagnostics.is_empty() {
    try Catalog::from_entries(entries) catch {
      CatalogError::DuplicateCode(code) =>
        diagnostics.push(
          import_diagnostic(
            0,
            0,
            "duplicate code: " + code,
            "remove the duplicate row",
          ),
        )
      CatalogError::MissingReference(code, reference) =>
        diagnostics.push(
          import_diagnostic(
            0,
            0,
            "missing reference from " + code + ": " + reference,
            "include the referenced code or remove the relation",
          ),
        )
      CatalogError::InvalidEntry(code, message) =>
        diagnostics.push(
          import_diagnostic(
            0,
            0,
            code + ": " + message,
            "fix the row and import again",
          ),
        )
    } noraise {
      _ => ()
    }
  }
  let normalized_entries = if diagnostics.is_empty() {
    Catalog::from_entries(entries).all() catch {
      _ => []
    }
  } else {
    []
  }
  if diagnostics.is_empty() {
    { entries: normalized_entries, diagnostics }
  } else {
    { entries: [], diagnostics }
  }
}

///|
fn import_diagnostic(
  line : Int,
  column : Int,
  message : String,
  hint : String,
) -> ImportDiagnostic {
  { line, column, message, hint }
}

///|
fn validate_header(
  row : Array[String],
  options : ImportOptions,
  diagnostics : Array[ImportDiagnostic],
) -> Unit {
  let expected = ["code", "title", "chapter_id", "parent", "excludes", "note"]
  if row.length() != expected.length() {
    diagnostics.push(
      import_diagnostic(
        1,
        row.length() + 1,
        "invalid header width",
        "use the six required column names",
      ),
    )
    return
  }
  for i in 0.. IcdEntry? {
  let code = row[0].trim().to_owned()
  if code.is_empty() {
    diagnostics.push(
      import_diagnostic(line, 1, "code is empty", "provide an ICD-10 code"),
    )
    return None
  }
  let parent = if row[3].trim().is_empty() {
    None
  } else {
    Some(row[3].trim().to_owned())
  }
  let excludes = if row[4].trim().is_empty() {
    []
  } else {
    split_exclusions(row[4])
  }
  Some(
    IcdEntry::new(
      code,
      row[1],
      row[2].trim().to_owned(),
      parent,
      excludes,
      row[5],
    ),
  )
}

///|
fn split_exclusions(value : String) -> Array[String] {
  let result = []
  let mut current = ""
  for c in value {
    if c == ';' {
      result.push(current.trim().to_owned())
      current = ""
    } else {
      current = current + c.to_string()
    }
  }
  if !current.trim().is_empty() {
    result.push(current.trim().to_owned())
  }
  result
}

///|
fn delimiter_char(delimiter : Delimiter) -> Char {
  match delimiter {
    Comma => ','
    Tab => '\t'
  }
}

///|
fn scan_delimited(
  text : String,
  delimiter : Delimiter,
  diagnostics : Array[ImportDiagnostic],
) -> Array[Array[String]] {
  let rows = []
  let row = []
  let mut field = ""
  let mut quoted = false
  let mut line = 1
  let separator = delimiter_char(delimiter)
  let mut i = 0
  while i < text.length() {
    let current = text.get_char(i).unwrap()
    if quoted {
      if current == '"' {
        if i + 1 < text.length() && text.get_char(i + 1).unwrap() == '"' {
          field = field + "\""
          i += 1
        } else {
          quoted = false
        }
      } else {
        field = field + current.to_string()
        if current == '\n' {
          line += 1
        }
      }
    } else if current == '"' && field.is_empty() {
      quoted = true
    } else if current == separator {
      row.push(field)
      field = ""
    } else if current == '\n' {
      row.push(field)
      field = ""
      rows.push(row.copy())
      row.clear()
      line += 1
    } else if current != '\r' {
      field = field + current.to_string()
    }
    i += 1
  }
  if quoted {
    diagnostics.push(
      import_diagnostic(
        line,
        field.length() + 1,
        "unterminated quoted field",
        "close the quoted value with a double quote",
      ),
    )
  }
  if !field.is_empty() || !row.is_empty() {
    row.push(field)
    rows.push(row)
  }
  rows
}