///|
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
}

///|
priv struct CommOrder {
  mut previous : Bytes?
  mut descending : Bool
  mut warned : Bool
}

///|
async fn next_checked(
  scanner : @stream.LineScanner,
  state : CommOrder,
  name : String,
  order_mode : Int32,
  unpairable : Ref[Bool],
) -> @stream.Line? {
  let line = scanner.next()
  if line is Some(value) {
    state.descending = match state.previous {
      Some(old) => old[:].lexical_compare(value.data[:]) > 0
      None => false
    }
    state.previous = Some(value.data)
  }
  // At EOF GNU checks the final pair again after observing an unpaired row.
  if !state.warned &&
    state.descending &&
    (order_mode > 0 || (order_mode == 0 && unpairable.val)) {
    if order_mode > 0 {
      raise CommError("\{name} is not in sorted order")
    }
    @stdio.stderr.write("comm: \{name} is not in sorted order\n")
    state.warned = true
  }
  line
}

///|
async fn write_line(prefix : String, line : Bytes, delimiter : Byte) -> Unit {
  if prefix != "" {
    @stdio.stdout.write(prefix)
  }
  @stdio.stdout.write(line)
  @stdio.stdout.write(Bytes::from_array([delimiter]))
}

///|
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,
) -> Unit {
  let scanner1 = @stream.LineScanner::new(reader1, delimiter=record_delimiter)
  let scanner2 = @stream.LineScanner::new(reader2, delimiter=record_delimiter)
  let state1 : CommOrder = { previous: None, descending: false, warned: false, }
  let state2 : CommOrder = { previous: None, descending: false, warned: false, }
  let unpairable = Ref(false)
  let mut line1 = next_checked(
    scanner1, state1, "file 1", order_mode, unpairable,
  )
  let mut line2 = next_checked(
    scanner2, state2, "file 2", order_mode, unpairable,
  )
  let col2_prefix = if hide1 { "" } else { column_delimiter }
  let col3_prefix = (if hide1 { "" } else { column_delimiter }) +
    (if hide2 { "" } else { column_delimiter })
  let mut count1 : Int64 = 0
  let mut count2 : Int64 = 0
  let mut count3 : Int64 = 0
  while line1 is Some(left) && line2 is Some(right) {
    let order = left.data[:].lexical_compare(right.data[:])
    if order < 0 {
      unpairable.val = true
      count1 += 1
      if !hide1 {
        write_line("", left.data, record_delimiter)
      }
      line1 = next_checked(scanner1, state1, "file 1", order_mode, unpairable)
    } else if order > 0 {
      unpairable.val = true
      count2 += 1
      if !hide2 {
        write_line(col2_prefix, right.data, record_delimiter)
      }
      line2 = next_checked(scanner2, state2, "file 2", order_mode, unpairable)
    } else {
      count3 += 1
      if !hide3 {
        write_line(col3_prefix, left.data, record_delimiter)
      }
      line1 = next_checked(scanner1, state1, "file 1", order_mode, unpairable)
      line2 = next_checked(scanner2, state2, "file 2", order_mode, unpairable)
    }
  }
  while line1 is Some(left) {
    unpairable.val = true
    count1 += 1
    if !hide1 {
      write_line("", left.data, record_delimiter)
    }
    line1 = next_checked(scanner1, state1, "file 1", order_mode, unpairable)
  }
  while line2 is Some(right) {
    unpairable.val = true
    count2 += 1
    if !hide2 {
      write_line(col2_prefix, right.data, record_delimiter)
    }
    line2 = next_checked(scanner2, state2, "file 2", order_mode, unpairable)
  }
  if total {
    write_line(
      "\{count1}\{column_delimiter}\{count2}\{column_delimiter}\{count3}\{column_delimiter}",
      b"total",
      record_delimiter,
    )
  }
  if state1.warned || state2.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.2\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
    }
  }
}