///|
async fn digest_reader(reader : &@io.Reader) -> String {
  let hasher = @crypto.SHA256::new()
  while reader.read_some(max_len=65536) is Some(chunk) {
    hasher.update(chunk)
  }
  @crypto.bytes_to_hex_string(hasher.finalize())
}

///|
async fn digest_source(path : String) -> String {
  if path == "-" {
    digest_reader(@stdio.stdin)
  } else {
    let file = @fs.open(path, mode=ReadOnly)
    defer file.close()
    digest_reader(file)
  }
}

///|
fn split_lines(text : String) -> Array[String] {
  let lines : Array[String] = text.split("\n").map(v => v.to_owned()).collect()
  if text.has_suffix("\n") {
    ignore(lines.pop())
  }
  lines
}

///|
fn is_hex_digest(value : String) -> Bool {
  if value.length() != 64 {
    return false
  }
  for char in value {
    if !(char is ('0'..='9' | 'a'..='f' | 'A'..='F')) {
      return false
    }
  }
  true
}

///|
fn encode_filename(name : String) -> (String, Bool) {
  let output = StringBuilder()
  let mut escaped = false
  for char in name {
    match char {
      '\\' => {
        output.write_string("\\\\")
        escaped = true
      }
      '\n' => {
        output.write_string("\\n")
        escaped = true
      }
      _ => output.write_char(char)
    }
  }
  (output.to_string(), escaped)
}

///|
fn decode_filename(value : String) -> String? {
  let chars : Array[Char] = value.iter().collect()
  let output = StringBuilder()
  let mut index = 0
  while index < chars.length() {
    if chars[index] != '\\' {
      output.write_char(chars[index])
      index += 1
    } else if index + 1 >= chars.length() {
      return None
    } else {
      match chars[index + 1] {
        '\\' => output.write_char('\\')
        'n' => output.write_char('\n')
        _ => return None
      }
      index += 2
    }
  }
  Some(output.to_string())
}

///|
async fn check_manifest(path : String) -> (Int, Int) {
  let text = if path == "-" {
    @stdio.stdin.read_all().text()
  } else {
    @fs.read_file(path).text()
  }
  let mut checked = 0
  let mut failed = 0
  for line in split_lines(text) {
    if line == "" {
      continue
    }
    let escaped = line.has_prefix("\\")
    let record = if escaped { line[1:].to_owned() } else { line }
    if record.length() < 67 ||
      record[64] != ' ' ||
      (record[65] != ' ' && record[65] != '*') ||
      !is_hex_digest(record[0:64].to_owned()) {
      @stdio.stderr.write("sha256sum: improperly formatted checksum line\n")
      failed += 1
      continue
    }
    let expected = record[0:64].to_owned().to_lower()
    let encoded_name = record[66:].to_owned()
    let name = if escaped {
      match decode_filename(encoded_name) {
        Some(value) => value
        None => {
          @stdio.stderr.write("sha256sum: improperly escaped file name\n")
          failed += 1
          continue
        }
      }
    } else {
      encoded_name
    }
    let actual = digest_source(name) catch {
      err => {
        @stdio.stderr.write("sha256sum: '\{name}': \{err}\n")
        @stdio.stdout.write("\{name}: FAILED open or read\n")
        checked += 1
        failed += 1
        continue
      }
    }
    checked += 1
    if actual == expected {
      @stdio.stdout.write("\{name}: OK\n")
    } else {
      @stdio.stdout.write("\{name}: FAILED\n")
      failed += 1
    }
  }
  (checked, failed)
}

///|
async fn main {
  let args = @env.args()[1:]
  let parsed = @cli.parse(args, [
    @cli.flag("check", short='c'),
    @cli.flag("zero", short='z'),
    @cli.flag("binary", short='b'),
    @cli.flag("text", short='t'),
    @cli.flag("help"),
  ]) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("sha256sum: \{message}: '\{option}'\n")
      @sys.exit(2)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write("Usage: sha256sum [-c] [-z] [FILE...]\n")
    return
  }
  let files = parsed.operands
  let check = parsed.contains("check")
  let zero = parsed.contains("zero")
  let sources = if files.is_empty() { ["-"] } else { files }
  let mut failed = false
  if check {
    if zero {
      @stdio.stderr.write("sha256sum: -z is not supported with -c\n")
      @sys.exit(2)
      return
    }
    for manifest in sources {
      let (checked, errors) = check_manifest(manifest) catch {
        err => {
          @stdio.stderr.write("sha256sum: '\{manifest}': \{err}\n")
          failed = true
          continue
        }
      }
      if checked == 0 || errors > 0 {
        failed = true
      }
    }
  } else {
    let separator = if zero { "\u0000" } else { "\n" }
    for source in sources {
      let actual = digest_source(source) catch {
        err => {
          @stdio.stderr.write("sha256sum: '\{source}': \{err}\n")
          failed = true
          continue
        }
      }
      if zero {
        @stdio.stdout.write("\{actual}  \{source}\{separator}")
      } else {
        let (name, escaped) = encode_filename(source)
        let prefix = if escaped { "\\" } else { "" }
        @stdio.stdout.write("\{prefix}\{actual}  \{name}\{separator}")
      }
    }
  }
  if failed {
    @sys.exit(1)
  }
}