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

///|
fn parse_count(text : String, option : String) -> Int64 raise CliError {
  let value = @string.parse_int64(text) catch {
    _ => raise CliError("cmp: invalid \{option}: '\{text}'")
  }
  if value < 0 {
    raise CliError("cmp: invalid \{option}: '\{text}'")
  }
  value
}

///|
fn parse_skips(text : String) -> (Int64, Int64) raise CliError {
  let parts : Array[String] = text.split(":").map(v => v.to_owned()).collect()
  match parts {
    [one] => {
      let value = parse_count(one, "skip")
      (value, value)
    }
    [left, right] => (parse_count(left, "skip"), parse_count(right, "skip"))
    _ => raise CliError("cmp: invalid skip: '\{text}'")
  }
}

///|
async fn drop_bytes(reader : &@io.Reader, count : Int64) -> Unit {
  let mut remaining = count
  while remaining > 0 {
    let request = if remaining > 0x3FFF_FFFF {
      0x3FFF_FFFF
    } else {
      remaining.to_int()
    }
    let dropped = reader.drop(request)
    if dropped < request {
      return
    }
    remaining -= dropped.to_int64()
  }
}

///|
async fn compare_readers(
  left : &@io.Reader,
  right : &@io.Reader,
  left_name : String,
  right_name : String,
  silent : Bool,
  verbose : Bool,
  limit : Int64?,
  skip_left : Int64,
  skip_right : Int64,
) -> Bool {
  drop_bytes(left, skip_left)
  drop_bytes(right, skip_right)
  let mut left_chunk : Bytes? = None
  let mut right_chunk : Bytes? = None
  let mut left_pos = 0
  let mut right_pos = 0
  let mut compared : Int64 = 0
  let mut line : Int64 = 1
  let mut different = false
  while limit is None || compared < limit.unwrap() {
    if left_chunk is None || left_pos >= left_chunk.unwrap().length() {
      left_chunk = left.read_some(max_len=65536)
      left_pos = 0
    }
    if right_chunk is None || right_pos >= right_chunk.unwrap().length() {
      right_chunk = right.read_some(max_len=65536)
      right_pos = 0
    }
    match (left_chunk, right_chunk) {
      (None, None) => break
      (None, Some(_)) | (Some(_), None) => {
        different = true
        if !silent {
          let shorter = if left_chunk is None { left_name } else { right_name }
          @stdio.stderr.write(
            "cmp: EOF on \{display_name(shorter)} after byte \{compared}, line \{line}\n",
          )
        }
        break
      }
      (Some(a), Some(b)) => {
        let left_available = a.length() - left_pos
        let right_available = b.length() - right_pos
        let mut take = if left_available < right_available {
          left_available
        } else {
          right_available
        }
        match limit {
          Some(maximum) if maximum - compared < take.to_int64() =>
            take = (maximum - compared).to_int()
          _ => ()
        }
        for offset in 0.. String {
  if path == "-" {
    "standard input"
  } else {
    path
  }
}

///|
fn pad_left(text : String, width : Int) -> String {
  if text.length() >= width {
    text
  } else {
    " ".repeat(width - text.length()) + text
  }
}

///|
async fn main {
  let args = @env.args()[1:]
  let parsed = @cli.parse(args, [
    @cli.flag("silent", short='s'),
    @cli.flag("quiet"),
    @cli.flag("verbose", short='l'),
    @cli.option("bytes", short='n'),
    @cli.option("ignore-initial", short='i'),
    @cli.flag("help"),
  ]) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("cmp: \{message}: '\{option}'\n")
      @sys.exit(2)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write(
      "Usage: cmp [-s|-l] [-n LIMIT] [-i SKIP[:SKIP2]] FILE1 FILE2\n",
    )
    return
  }
  let files = parsed.operands
  let silent = parsed.contains("silent") || parsed.contains("quiet")
  let verbose = parsed.contains("verbose")
  let limit = match parsed.last_value("bytes") {
    Some(value) =>
      Some(
        parse_count(value, "byte limit") catch {
          CliError(message) => {
            @stdio.stderr.write(message + "\n")
            @sys.exit(2)
            return
          }
        },
      )
    None => None
  }
  let (skip_left, skip_right) = match parsed.last_value("ignore-initial") {
    Some(value) =>
      parse_skips(value) catch {
        CliError(message) => {
          @stdio.stderr.write(message + "\n")
          @sys.exit(2)
          return
        }
      }
    None => (0L, 0L)
  }
  if files.length() != 2 || (files[0] == "-" && files[1] == "-") {
    @stdio.stderr.write("cmp: expected two distinct input sources\n")
    @sys.exit(2)
    return
  }
  let different = try {
    if files[0] == "-" {
      let right = @fs.open(files[1], mode=ReadOnly)
      defer right.close()
      compare_readers(
        @stdio.stdin,
        right,
        files[0],
        files[1],
        silent,
        verbose,
        limit,
        skip_left,
        skip_right,
      )
    } else if files[1] == "-" {
      let left = @fs.open(files[0], mode=ReadOnly)
      defer left.close()
      compare_readers(
        left,
        @stdio.stdin,
        files[0],
        files[1],
        silent,
        verbose,
        limit,
        skip_left,
        skip_right,
      )
    } else {
      let left = @fs.open(files[0], mode=ReadOnly)
      defer left.close()
      let right = @fs.open(files[1], mode=ReadOnly)
      defer right.close()
      compare_readers(
        left,
        right,
        files[0],
        files[1],
        silent,
        verbose,
        limit,
        skip_left,
        skip_right,
      )
    }
  } catch {
    err => {
      @stdio.stderr.write("cmp: \{err}\n")
      @sys.exit(2)
      return
    }
  }
  if different {
    @sys.exit(1)
  }
}