///|
fn builtin_mode(name : String) -> Bool {
  [
    "one-byte-octal", "one-byte-char", "canonical", "two-bytes-decimal", "two-bytes-octal",
    "two-bytes-hex", "one-byte-hex",
  ].contains(name)
}

///|
fn finish_program(
  entries : Array[FormatEntry],
) -> FormatProgram raise HexdumpError {
  let mut block_size = 0
  let mut final_unit : FormatUnit? = None
  for entry in entries {
    match entry {
      Builtin(mode) => {
        if block_size < 16 {
          block_size = 16
        }
        let format = compile_format(
          @utf8.encode(
            if mode == "canonical" {
              "\"%08.8_Ax\\n\""
            } else {
              "\"%07.7_Ax\\n\""
            },
          )[:],
          false,
        )
        final_unit = format.units.get(0)
      }
      Custom(format) => {
        if block_size < format.count {
          block_size = format.count
        }
        for unit in format.units {
          if unit.final_address {
            final_unit = Some(unit)
          }
        }
      }
    }
  }
  for entry in entries {
    if entry is Custom(format) &&
      format.count < block_size &&
      format.units.last() is Some(unit) &&
      !unit.explicit_reps &&
      unit.count > 0 {
      unit.reps += (block_size - format.count) / unit.count
    }
  }
  { entries, block_size, final_unit, }
}

///|
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 {
      result.push(arg)
      value_next = false
      continue
    }
    if arg == "--" {
      options = false
      result.push(arg)
      continue
    }
    if !options {
      result.push(arg)
      continue
    }
    if arg == "--color" {
      result.push("--color=auto")
      continue
    }
    let mut rewritten = false
    if arg.has_prefix("-") && !arg.has_prefix("--") {
      for index in 1.. 1 {
            result.push(arg[:index].to_owned())
          }
          let value = if index + 1 == arg.length() {
            "auto"
          } else {
            arg[index + 1:].to_owned()
          }
          result.push("--color=" + value)
          rewritten = true
          break
        }
      }
    }
    if !rewritten {
      result.push(arg)
    }
    if ["--format", "--format-file", "--length", "--skip"].contains(arg) {
      value_next = true
    }
  }
  result
}

///|
async fn program(parsed : @cli.ParsedArgs) -> FormatProgram {
  let entries : Array[FormatEntry] = []
  let mut colors = false
  for value in parsed.values("color") {
    match value {
      "always" => colors = true
      "auto" | "never" => colors = false
      _ => raise HexdumpError("unsupported color mode: '\{value}'")
    }
  }
  let mut format_index = 0
  let mut file_index = 0
  for name in parsed.order {
    if builtin_mode(name) {
      entries.push(Builtin(name))
    } else if name == "format" {
      let source = parsed.values("format")[format_index]
      entries.push(Custom(compile_format(@utf8.encode(source)[:], colors)))
      format_index += 1
    } else if name == "format-file" {
      let file = parsed.values("format-file")[file_index]
      let bytes = @fs.read_file(file).binary()
      let mut start = 0
      for index in 0..<=bytes.length() {
        if index == bytes.length() || bytes[index] == b'\n' {
          let mut first = start
          while first < index && format_space(bytes[first]) {
            first += 1
          }
          if first < index && bytes[first] != b'#' {
            entries.push(Custom(compile_format(bytes[first:index], colors)))
          }
          start = index + 1
        }
      }
      file_index += 1
    }
  }
  if entries.is_empty() {
    entries.push(Builtin("default"))
  }
  finish_program(entries)
}