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

///|
priv enum GroupSeparator {
  NoSeparator
  Prepend
  Separate
  Append
  Both
}

///|
priv enum OutputMode {
  Summarize
  AllRepeated(GroupSeparator)
  EveryGroup(GroupSeparator)
}

///|
fn normalized_args(args : ArrayView[String]) -> Array[String] {
  let result : Array[String] = []
  let mut options = true
  let mut value_next = false
  for arg in args {
    if value_next {
      value_next = false
      result.push(arg)
      continue
    }
    if options && arg == "--" {
      options = false
      result.push(arg)
    } else if options && arg == "--all-repeated" {
      result.push("--all-repeated=none")
    } else if options && arg == "--group" {
      result.push("--group=separate")
    } else {
      result.push(arg)
    }
    if options {
      if arg is ("--skip-fields" | "--skip-chars" | "--check-chars") {
        value_next = true
      } else if arg.has_prefix("-") && !arg.has_prefix("--") {
        for offset in 1.. @cli.ParsedArgs raise @cli.CliError {
  @cli.parse(normalized_args(args), [
    @cli.flag("count", short='c'),
    @cli.flag("repeated", short='d'),
    // Long optional values and the value-free short flag share one public name.
    @cli.option("all-repeated"),
    @cli.flag("all-repeated", short='D'),
    @cli.option("group"),
    @cli.flag("unique", short='u'),
    @cli.flag("ignore-case", short='i'),
    @cli.option("skip-fields", short='f'),
    @cli.option("skip-chars", short='s'),
    @cli.option("check-chars", short='w'),
    @cli.flag("zero-terminated", short='z'),
    @cli.flag("help"),
  ])
}

///|
fn parse_group_separator(
  value : String,
  all_repeated : Bool,
) -> GroupSeparator raise CliError {
  let methods = if all_repeated {
    ["none", "prepend", "separate"]
  } else {
    ["prepend", "append", "separate", "both"]
  }
  let matches = methods.filter(choice => choice.has_prefix(value))
  if matches.length() != 1 {
    let option = if all_repeated { "--all-repeated" } else { "--group" }
    let problem = if matches.is_empty() { "invalid" } else { "ambiguous" }
    let valid = methods.map(choice => "  - '\{choice}'\n").join("")
    raise CliError(
      "uniq: \{problem} argument '\{value}' for '\{option}'\nValid arguments are:\n\{valid}Try 'uniq --help' for more information.\n",
    )
  }
  match matches[0] {
    "none" => NoSeparator
    "prepend" => Prepend
    "append" => Append
    "both" => Both
    _ => Separate
  }
}

///|
fn output_mode(parsed : @cli.ParsedArgs) -> OutputMode raise CliError {
  let mut repeated_separator = NoSeparator
  for value in parsed.occurrences.get("all-repeated").unwrap_or([]) {
    repeated_separator = match value {
      Some(value) => parse_group_separator(value, true)
      None => NoSeparator
    }
  }
  let mut group_separator = Separate
  for value in parsed.values("group") {
    group_separator = parse_group_separator(value, false)
  }
  let all_repeated = parsed.contains("all-repeated")
  if parsed.contains("group") &&
    (
      parsed.contains("count") ||
      parsed.contains("repeated") ||
      parsed.contains("unique") ||
      all_repeated
    ) {
    raise CliError(
      "uniq: --group is mutually exclusive with -c/-d/-D/-u\nTry 'uniq --help' for more information.\n",
    )
  }
  if parsed.contains("count") && all_repeated {
    raise CliError(
      "uniq: printing all duplicated lines and repeat counts is meaningless\nTry 'uniq --help' for more information.\n",
    )
  }
  if parsed.contains("group") {
    EveryGroup(group_separator)
  } else if all_repeated {
    AllRepeated(repeated_separator)
  } else {
    Summarize
  }
}

///|
fn pad_left(text : String, width : Int32) -> String {
  let sb = StringBuilder()
  for _ in 0..<(width - text.length()) {
    sb.write_char(' ')
  }
  sb.write_string(text)
  sb.to_string()
}

///|
fn parse_count(
  text : String,
  option : String,
  gnu_description? : String,
) -> Int32 raise CliError {
  let message = match gnu_description {
    Some(description) => "uniq: \{text}: invalid number of \{description}"
    None => "uniq: invalid number of \{option}: '\{text}'"
  }
  let count = @string.parse_int(text) catch { _ => raise CliError(message) }
  if count < 0 {
    raise CliError(message)
  }
  count
}

///|
fn is_blank(byte : Byte) -> Bool {
  byte is (b' ' | b'\t')
}

///|
fn comparison_key(
  line : Bytes,
  skip_fields : Int32,
  skip_chars : Int32,
  check_chars : Int32?,
  ignore_case : Bool,
) -> Bytes {
  let mut start = 0
  for _ in 0.. Int32::min(line.length(), start + width)
    None => line.length()
  }
  if !ignore_case {
    return line[start:end].to_owned()
  }
  let folded : Array[Byte] = []
  for byte in line[start:end] {
    let value = byte.to_int()
    folded.push(
      if value >= 0x41 && value <= 0x5A {
        (value + 0x20).to_byte()
      } else {
        byte
      },
    )
  }
  Bytes::from_array(folded)
}

///|
async fn write_group(
  writer : &@io.Writer,
  line : Bytes,
  count : Int32,
  show_count : Bool,
  only_repeated : Bool,
  only_unique : Bool,
  delimiter : Byte,
) -> Unit {
  let selected = if only_repeated && only_unique {
    false
  } else if only_repeated {
    count > 1
  } else if only_unique {
    count == 1
  } else {
    true
  }
  if selected {
    if show_count {
      writer.write(pad_left(count.to_string(), 7) + " ")
    }
    write_record(writer, line, delimiter)
  }
}

///|
async fn write_record(
  writer : &@io.Writer,
  line : Bytes,
  delimiter : Byte,
) -> Unit {
  writer.write(line)
  writer.write(if delimiter is b'\x00' { b"\x00" } else { b"\n" })
}

///|
async fn process_reader(
  reader : &@io.Reader,
  show_count : Bool,
  only_repeated : Bool,
  only_unique : Bool,
  ignore_case : Bool,
  skip_fields : Int32,
  skip_chars : Int32,
  check_chars : Int32?,
  delimiter : Byte,
  mode : OutputMode,
  writer~ : &@io.Writer = @stdio.stdout,
) -> Unit {
  let scanner = @stream.LineScanner::new(reader, delimiter~)
  let mut previous : Bytes? = None
  let mut previous_key : Bytes? = None
  let mut count = 0
  let mut repeated_group_printed = false
  while scanner.next() is Some(record) {
    let key = comparison_key(
      record.data,
      skip_fields,
      skip_chars,
      check_chars,
      ignore_case,
    )
    match mode {
      EveryGroup(separator) => {
        let first_group = previous_key is None
        let new_group = previous_key is None || previous_key != Some(key)
        if new_group &&
          (
            separator is (Prepend | Both) ||
            (!first_group && separator is (Separate | Append))
          ) {
          write_record(writer, b"", delimiter)
        }
        write_record(writer, record.data, delimiter)
        previous_key = Some(key)
      }
      AllRepeated(separator) =>
        match (previous, previous_key) {
          (Some(pending), Some(old_key)) if old_key == key => {
            if count == 1 {
              if separator is Prepend ||
                (separator is Separate && repeated_group_printed) {
                write_record(writer, b"", delimiter)
              }
              repeated_group_printed = true
              count = 2
            }
            write_record(writer, pending, delimiter)
            previous = Some(record.data)
          }
          _ => {
            if count > 1 && !only_unique && previous is Some(pending) {
              write_record(writer, pending, delimiter)
            }
            previous = Some(record.data)
            previous_key = Some(key)
            count = 1
          }
        }
      Summarize =>
        match (previous, previous_key) {
          (Some(_), Some(old_key)) if old_key == key => count += 1
          (Some(old), _) => {
            write_group(
              writer, old, count, show_count, only_repeated, only_unique, delimiter,
            )
            previous = Some(record.data)
            previous_key = Some(key)
            count = 1
          }
          _ => {
            previous = Some(record.data)
            previous_key = Some(key)
            count = 1
          }
        }
    }
  }
  match mode {
    Summarize =>
      if previous is Some(value) {
        write_group(
          writer, value, count, show_count, only_repeated, only_unique, delimiter,
        )
      }
    EveryGroup(Append | Both) =>
      if previous_key is Some(_) {
        write_record(writer, b"", delimiter)
      }
    AllRepeated(_) =>
      if count > 1 && !only_unique && previous is Some(pending) {
        write_record(writer, pending, delimiter)
      }
    _ => ()
  }
}

///|
async fn main {
  let metadata_args = @env.args()[1:]
  if metadata_args.length() == 1 && metadata_args[0] == "--version" {
    @stdio.stdout.write("uniq 0.2.2\n")
    return
  }
  let args = @env.args()[1:]
  let parsed = parse_args(args) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("uniq: \{message}: '\{option}'\n")
      @sys.exit(2)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write(
      "Usage: uniq [-cduDiz] [--all-repeated[=METHOD]] [--group[=METHOD]] [-f FIELDS] [-s CHARS] [-w CHARS] [FILE]\n      --help      display this help and exit\n      --version   display version and exit\n",
    )
    return
  }
  if parsed.operands.length() > 1 {
    @stdio.stderr.write("uniq: extra operand: '\{parsed.operands[1]}'\n")
    @sys.exit(2)
    return
  }
  let mode = output_mode(parsed) catch {
    CliError(message) => {
      @stdio.stderr.write(message)
      @sys.exit(1)
      return
    }
  }
  let (skip_fields, skip_chars, check_chars) = try {
    let gnu_counts = mode is AllRepeated(_)
    let fields = match parsed.last_value("skip-fields") {
      Some(text) =>
        parse_count(
          text,
          "fields",
          gnu_description?=if gnu_counts {
            Some("fields to skip")
          } else {
            None
          },
        )
      None => 0
    }
    let chars = match parsed.last_value("skip-chars") {
      Some(text) =>
        parse_count(
          text,
          "characters",
          gnu_description?=if gnu_counts { Some("bytes to skip") } else { None },
        )
      None => 0
    }
    let width = match parsed.last_value("check-chars") {
      Some(text) =>
        Some(
          parse_count(
            text,
            "characters",
            gnu_description?=if gnu_counts {
              Some("bytes to compare")
            } else {
              None
            },
          ),
        )
      None => None
    }
    (fields, chars, width)
  } catch {
    CliError(message) => {
      @stdio.stderr.write(message + "\n")
      @sys.exit(if mode is AllRepeated(_) { 1 } else { 2 })
      return
    }
  }
  let delimiter : Byte = if parsed.contains("zero-terminated") {
    b'\x00'
  } else {
    b'\n'
  }
  let path = if parsed.operands.is_empty() { "-" } else { parsed.operands[0] }
  try {
    if path == "-" {
      process_reader(
        @stdio.stdin,
        parsed.contains("count"),
        parsed.contains("repeated"),
        parsed.contains("unique"),
        parsed.contains("ignore-case"),
        skip_fields,
        skip_chars,
        check_chars,
        delimiter,
        mode,
      )
    } else {
      let file = @fs.open(path, mode=ReadOnly)
      defer file.close()
      process_reader(
        file,
        parsed.contains("count"),
        parsed.contains("repeated"),
        parsed.contains("unique"),
        parsed.contains("ignore-case"),
        skip_fields,
        skip_chars,
        check_chars,
        delimiter,
        mode,
      )
    }
  } catch {
    err => {
      @stdio.stderr.write("uniq: \{err}\n")
      @sys.exit(1)
      return
    }
  }
}