///|
fn json_escape(s : String) -> String {
  let buf = StringBuilder(size_hint=s.length() + 2)
  for c in s.iter() {
    match c {
      '\\' => buf.write_string("\\\\")
      '"' => buf.write_string("\\\"")
      '\n' => buf.write_string("\\n")
      '\t' => buf.write_string("\\t")
      _ => buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
fn json_string(s : String) -> String {
  "\"" + json_escape(s) + "\""
}

///|
fn record_to_json(rec : @lib.SeqRecord) -> String {
  let buf = StringBuilder()
  buf.write_string("{\"id\": ")
  buf.write_string(json_string(rec.id))
  buf.write_string(", \"name\": ")
  buf.write_string(json_string(rec.name))
  buf.write_string(", \"description\": ")
  buf.write_string(json_string(rec.description))
  buf.write_string(", \"seq\": ")
  buf.write_string(json_string(rec.seq.to_string()))
  buf.write_string(", \"length\": ")
  buf.write_string(rec.length().to_string())
  buf.write_char('}')
  buf.to_string()
}

///|
fn records_to_json(records : Array[@lib.SeqRecord]) -> String {
  let buf = StringBuilder()
  buf.write_char('[')
  for i, rec in records {
    if i > 0 {
      buf.write_string(", ")
    }
    buf.write_string(record_to_json(rec))
  }
  buf.write_char(']')
  buf.to_string()
}

///|
fn alignment_to_json(align : @lib.MultipleSeqAlignment) -> String {
  let buf = StringBuilder()
  buf.write_string("{\"num_records\": ")
  buf.write_string(align.num_records().to_string())
  buf.write_string(", \"alignment_length\": ")
  buf.write_string(align.get_alignment_length().to_string())
  buf.write_string(", \"records\": ")
  buf.write_string(records_to_json(align.iter()))
  buf.write_char('}')
  buf.to_string()
}

///|
fn emit(name : String, payload : String) -> Unit {
  println("=== CASE: " + name + " ===")
  println(payload)
  println("=== END ===")
}

///|
fn run_alignio_test() -> Unit raise @lib.AlignIOError {
  let clustal_sample = "CLUSTAL X (1.81) multiple sequence alignment\n\nAF191659.1      TATACATTAAAGAAGGGGGATGCGGATAAATGGAAAGGCGAAAG\nAF191658.1      TATACATTAAAGAAGGGGGATGCGGATAAATGGAAAGGCGAAAG\nAF191661.1      TATACATTAAAGAAGGGGGATGCGGATAAATGGAAAGGCGAAAG\nAF191660.1      TATACATAAAAGAAGGGGGATGCGGATAAATGGAAAGGCGAAAG\n\nAF191659.1      AGA\nAF191658.1      AGA\nAF191661.1      AGA\nAF191660.1      AGA\n"

  let phylip_sample = "3 20\nseq1        ATGCCGATCGATCGATCGAT\nseq2        ATGCCGATCGATCGATCGAT\nseq3        ATGCCGATCGATCGATCGAT\n"

  let phylip_seq_sample = "3 20\nseq1        ATGCCGATCGATCGATCGAT\nseq2        ATGCCGATCGATCGATCGAT\nseq3        ATGCCGATCGATCGATCGAT\n"

  let fasta_align_sample = ">seq1 Description 1\nATGCCGATCGATCGATCGATCGATCGATCGATCGATC\n>seq2 Description 2\nATGCCGATCGATCGATCGATCGATCGATCGATCGATC\n>seq3 Description 3\nATGCCGATCGATCGATCGATCGATCGATCGATCGATC\n"

  let clustal_align = @lib.alignio_read(clustal_sample, "clustal")
  emit("parse_clustal", alignment_to_json(clustal_align))

  let phylip_align = @lib.alignio_read(phylip_sample, "phylip")
  emit("parse_phylip", alignment_to_json(phylip_align))

  let phylip_seq_align = @lib.alignio_read(
    phylip_seq_sample, "phylip-sequential",
  )
  emit("parse_phylip_sequential", alignment_to_json(phylip_seq_align))

  let fasta_align = @lib.alignio_read(fasta_align_sample, "fasta")
  emit("parse_fasta_align", alignment_to_json(fasta_align))

  let write_clustal_recs = [
    @lib.SeqRecord::new(
      @lib.Seq::new("TATACATTAAAGAAGGGGGATGCGGATAAATGGAAAGGCGAAAGAGA"),
      id="AF191659.1",
      name="AF191659.1",
      description="AF191659.1",
    ),
    @lib.SeqRecord::new(
      @lib.Seq::new("TATACATTAAAGAAGGGGGATGCGGATAAATGGAAAGGCGAAAGAGA"),
      id="AF191658.1",
      name="AF191658.1",
      description="AF191658.1",
    ),
    @lib.SeqRecord::new(
      @lib.Seq::new("TATACATAAAAGAAGGGGGATGCGGATAAATGGAAAGGCGAAAGAGA"),
      id="AF191660.1",
      name="AF191660.1",
      description="AF191660.1",
    ),
  ]
  let write_clustal_align = @lib.MultipleSeqAlignment::new(write_clustal_recs)
  let clustal_out = @lib.alignio_write([write_clustal_align], "clustal")
  emit("write_clustal", json_string(clustal_out))

  let write_phylip_recs = [
    @lib.SeqRecord::new(
      @lib.Seq::new("ATGCCGATCGATCGATCGAT"),
      id="seq1",
      name="seq1",
      description="seq1",
    ),
    @lib.SeqRecord::new(
      @lib.Seq::new("ATGCCGATCGATCGATCGAT"),
      id="seq2",
      name="seq2",
      description="seq2",
    ),
    @lib.SeqRecord::new(
      @lib.Seq::new("ATGCCGATCGATCGATCGAT"),
      id="seq3",
      name="seq3",
      description="seq3",
    ),
  ]
  let write_phylip_align = @lib.MultipleSeqAlignment::new(write_phylip_recs)
  let phylip_out = @lib.alignio_write([write_phylip_align], "phylip")
  emit("write_phylip", json_string(phylip_out))

  let write_fasta_recs = [
    @lib.SeqRecord::new(
      @lib.Seq::new("ATGCCGATCGATCGATCGATCGATCGATCGATCGATC"),
      id="seq1",
      name="seq1",
      description="seq1 desc",
    ),
    @lib.SeqRecord::new(
      @lib.Seq::new("ATGCCGATCGATCGATCGATCGATCGATCGATCGATC"),
      id="seq2",
      name="seq2",
      description="seq2 desc",
    ),
  ]
  let write_fasta_align = @lib.MultipleSeqAlignment::new(write_fasta_recs)
  let fasta_out = @lib.alignio_write([write_fasta_align], "fasta")
  emit("write_fasta_align", json_string(fasta_out))

  emit(
    "alignment_length",
    "{\"num_records\": " +
    clustal_align.num_records().to_string() +
    ", \"alignment_length\": " +
    clustal_align.get_alignment_length().to_string() +
    "}",
  )

  let col0 = match clustal_align.column(0) {
    Some(c) => c
    None => ""
  }
  let col5 = match clustal_align.column(5) {
    Some(c) => c
    None => ""
  }
  emit(
    "alignment_column",
    "{\"column_0\": " +
    json_string(col0) +
    ", \"column_5\": " +
    json_string(col5) +
    "}",
  )

  let err_payload : String = try {
    ignore(@lib.alignio_read(clustal_sample + "\n" + clustal_sample, "clustal"))
    "{\"error\": null}"
  } catch {
    _ =>
      "{\"error\": \"ValueError\", \"message\": \"More than one alignment found in handle\"}"
  }
  emit("read_multiple_error", err_payload)
}

///|
fn alignio_test() -> Unit {
  let _ = try {
    run_alignio_test()
    ()
  } catch {
    _ => println("Test error")
  }
}