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

///|
fn help_message() -> String {
  let message =
    #|Usage: comm [-123] [-z] FILE1 FILE2
    #|
    #|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).
    #|      --check-order  Diagnose unsorted input.
    #|      --nocheck-order  Do not diagnose unsorted input.
    #|  -z, --zero-terminated  End records with NUL instead of newline.
    #|  -h, --help  Show this help message.
    #|
    #|Use '-' as a file name to read stdin.
  message
}

///|
async fn next_checked(
  scanner : @stream.LineScanner,
  previous : Ref[Bytes?],
  unordered : Ref[Bool],
  name : String,
  order_mode : Int,
  unpairable : Ref[Bool],
) -> @stream.Line? {
  let line = scanner.next()
  match line {
    Some(value) => {
      match previous.val {
        Some(old) if old[:].lexical_compare(value.data[:]) > 0 => {
          unordered.val = true
          if order_mode > 0 || (order_mode == 0 && unpairable.val) {
            raise CommError("\{name} is not in sorted order")
          }
        }
        _ => ()
      }
      previous.val = Some(value.data)
    }
    None => ()
  }
  line
}

///|
fn mark_unpairable(
  order_mode : Int,
  unpairable : Ref[Bool],
  unordered1 : Ref[Bool],
  unordered2 : Ref[Bool],
) -> Unit raise CommError {
  unpairable.val = true
  if order_mode == 0 && (unordered1.val || unordered2.val) {
    raise CommError("input is not in sorted order")
  }
}

///|
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 : Int,
  record_delimiter : Byte,
) -> Unit {
  let scanner1 = @stream.LineScanner::new(reader1, delimiter=record_delimiter)
  let scanner2 = @stream.LineScanner::new(reader2, delimiter=record_delimiter)
  let previous1 : Ref[Bytes?] = Ref(None)
  let previous2 : Ref[Bytes?] = Ref(None)
  let unordered1 = Ref(false)
  let unordered2 = Ref(false)
  let unpairable = Ref(false)
  let mut line1 = next_checked(
    scanner1, previous1, unordered1, "file 1", order_mode, unpairable,
  )
  let mut line2 = next_checked(
    scanner2, previous2, unordered2, "file 2", order_mode, unpairable,
  )
  let col2_prefix = if hide1 { "" } else { "\t" }
  let col3_prefix = (if hide1 { "" } else { "\t" }) +
    (if hide2 { "" } else { "\t" })
  while line1 is Some(left) && line2 is Some(right) {
    let order = left.data[:].lexical_compare(right.data[:])
    if order < 0 {
      mark_unpairable(order_mode, unpairable, unordered1, unordered2)
      if !hide1 {
        write_line("", left.data, record_delimiter)
      }
      line1 = next_checked(
        scanner1, previous1, unordered1, "file 1", order_mode, unpairable,
      )
    } else if order > 0 {
      mark_unpairable(order_mode, unpairable, unordered1, unordered2)
      if !hide2 {
        write_line(col2_prefix, right.data, record_delimiter)
      }
      line2 = next_checked(
        scanner2, previous2, unordered2, "file 2", order_mode, unpairable,
      )
    } else {
      if !hide3 {
        write_line(col3_prefix, left.data, record_delimiter)
      }
      line1 = next_checked(
        scanner1, previous1, unordered1, "file 1", order_mode, unpairable,
      )
      line2 = next_checked(
        scanner2, previous2, unordered2, "file 2", order_mode, unpairable,
      )
    }
  }
  while line1 is Some(left) {
    mark_unpairable(order_mode, unpairable, unordered1, unordered2)
    if !hide1 {
      write_line("", left.data, record_delimiter)
    }
    line1 = next_checked(
      scanner1, previous1, unordered1, "file 1", order_mode, unpairable,
    )
  }
  while line2 is Some(right) {
    mark_unpairable(order_mode, unpairable, unordered1, unordered2)
    if !hide2 {
      write_line(col2_prefix, right.data, record_delimiter)
    }
    line2 = next_checked(
      scanner2, previous2, unordered2, "file 2", order_mode, unpairable,
    )
  }
}

///|
async fn main {
  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.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 = if parsed.contains("nocheck-order") {
    -1
  } else if parsed.contains("check-order") {
    1
  } else {
    0
  }
  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,
      )
    } 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,
      )
    } 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,
      )
    }
  } catch {
    CommError(message) => {
      @stdio.stderr.write("comm: \{message}\n")
      @sys.exit(1)
      return
    }
    err => {
      @stdio.stderr.write("comm: \{err}\n")
      @sys.exit(1)
      return
    }
  }
}