///|
priv suberror CommError {
  CommError(String)
}

///|
fn help_message() -> String {
  let message =
    #|Usage: comm [-123] [-z] FILE1 FILE2
    #|  -h, --help      display this help and exit
    #|      --version   display version and exit
    #|
    #|Compare two sorted files line by line, producing three columns:
    #|lines only in FILE1, lines only in FILE2, and lines in both.
    #|
    #|Options:
    #|  -1  Suppress column 1 (lines unique to FILE1).
    #|  -2  Suppress column 2 (lines unique to FILE2).
    #|  -3  Suppress column 3 (lines in both files).
    #|      --output-delimiter=STR  Separate columns with STR (empty means NUL).
    #|      --total  Print counts for all three columns and a total label.
    #|      --check-order  Diagnose unsorted input.
    #|      --nocheck-order  Do not diagnose unsorted input.
    #|  -z, --zero-terminated  End records with NUL instead of newline.
    #|
    #|Use '-' as a file name to read stdin.
  message
}

///|
fn comm_record(prefix : Bytes, line : Bytes, delimiter : Byte) -> Bytes {
  let output = @buffer.Buffer(size_hint=prefix.length() + line.length() + 1)
  output.write_bytes(prefix)
  output.write_bytes(line)
  output.write_byte(delimiter)
  output.to_bytes()
}

///|
async fn merge_readers(
  reader1 : &@io.Reader,
  reader2 : &@io.Reader,
  hide1 : Bool,
  hide2 : Bool,
  hide3 : Bool,
  order_mode : Int32,
  record_delimiter : Byte,
  column_delimiter : String,
  total : Bool,
  writer~ : &@io.Writer = @stdio.stdout,
) -> Unit {
  let left = CommInput::new(reader1, record_delimiter)
  let right = CommInput::new(reader2, record_delimiter)
  let prefix2 = if hide1 { b"" } else { @utf8.encode(column_delimiter) }
  let prefix3 = @utf8.encode(
    (if hide1 { "" } else { column_delimiter }) +
    (if hide2 { "" } else { column_delimiter }),
  )
  let merger : CommMerge = {
    left,
    right,
    hide1,
    hide2,
    hide3,
    order_mode,
    record_delimiter,
    prefix2,
    prefix3,
    unpairable: false,
    count1: 0,
    count2: 0,
    count3: 0,
    output: @buffer.Buffer(size_hint=65536),
  }
  defer @stream.flush_buffer(writer, merger.output)
  for ;; {
    let action = merger.pump()
    @stream.flush_buffer(writer, merger.output)
    match action {
      Read(input) => {
        input.batch = input.records.next_batch()
        input.eof = input.batch is None
      }
      Warn(input, name) => {
        if order_mode > 0 {
          raise CommError("\{name} is not in sorted order")
        }
        @stdio.stderr.write("comm: \{name} is not in sorted order\n")
        input.warned = true
      }
      Flush => ()
      Done => break
    }
  }
  if total {
    writer.write(
      comm_record(
        @utf8.encode(
          "\{merger.count1}\{column_delimiter}\{merger.count2}\{column_delimiter}\{merger.count3}\{column_delimiter}",
        ),
        b"total",
        record_delimiter,
      ),
    )
  }
  if left.warned || right.warned {
    raise CommError("input is not in sorted order")
  }
}

///|
async fn main {
  let metadata_args = @env.args()[1:]
  if metadata_args.length() == 1 && metadata_args[0] == "--version" {
    @stdio.stdout.write("comm 0.2.5\n")
    return
  }
  let args = @env.args()[1:]
  let parsed = @cli.parse(args, [
    @cli.flag("suppress-1", short='1'),
    @cli.flag("suppress-2", short='2'),
    @cli.flag("suppress-3", short='3'),
    @cli.option("output-delimiter"),
    @cli.flag("total"),
    @cli.flag("help", short='h'),
    @cli.flag("check-order"),
    @cli.flag("nocheck-order"),
    @cli.flag("zero-terminated", short='z'),
  ]) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("comm: \{message}: '\{option}'\n")
      @sys.exit(2)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write(help_message() + "\n")
    return
  }
  let hide1 = parsed.contains("suppress-1")
  let hide2 = parsed.contains("suppress-2")
  let hide3 = parsed.contains("suppress-3")
  let order_mode = match
    parsed.last_occurrence(["check-order", "nocheck-order"]) {
    Some("nocheck-order") => -1
    Some(_) => 1
    None => 0
  }
  let delimiters = parsed.values("output-delimiter")
  let mut column_delimiter = "\t"
  for index, value in delimiters {
    if index > 0 && value != delimiters[0] {
      @stdio.stderr.write("comm: multiple output delimiters specified\n")
      @sys.exit(1)
      return
    }
    column_delimiter = if value == "" { "\u0000" } else { value }
  }
  let total = parsed.contains("total")
  let record_delimiter = if parsed.contains("zero-terminated") {
    b'\x00'
  } else {
    b'\n'
  }
  let files = parsed.operands
  if files.length() != 2 {
    @stdio.stderr.write(
      "comm: expected exactly two files\n\n" + help_message() + "\n",
    )
    @sys.exit(2)
    return
  }
  if files[0] == "-" && files[1] == "-" {
    @stdio.stderr.write("comm: both inputs cannot be standard input\n")
    @sys.exit(2)
    return
  }
  try {
    if files[0] == "-" {
      let file2 = @fs.open(files[1], mode=ReadOnly)
      defer file2.close()
      merge_readers(
        @stdio.stdin, file2, hide1, hide2, hide3, order_mode, record_delimiter, column_delimiter,
        total,
      )
    } else if files[1] == "-" {
      let file1 = @fs.open(files[0], mode=ReadOnly)
      defer file1.close()
      merge_readers(
        file1, @stdio.stdin, hide1, hide2, hide3, order_mode, record_delimiter, column_delimiter,
        total,
      )
    } else {
      let file1 = @fs.open(files[0], mode=ReadOnly)
      defer file1.close()
      let file2 = @fs.open(files[1], mode=ReadOnly)
      defer file2.close()
      merge_readers(
        file1, file2, hide1, hide2, hide3, order_mode, record_delimiter, column_delimiter,
        total,
      )
    }
  } catch {
    CommError(message) => {
      @stdio.stderr.write("comm: \{message}\n")
      @sys.exit(1)
      return
    }
    err => {
      @stdio.stderr.write("comm: \{err}\n")
      @sys.exit(1)
      return
    }
  }
}