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

///|
fn base64_command() -> @argparse.Command {
  Command(
    "base64",
    about="Base64 encode or decode a file or stdin.",
    flags=[FlagArg("decode", short='d', about="Decode instead of encode.")],
    options=[
      OptionArg(
        "wrap",
        short='w',
        about="Wrap encoded lines after N characters (0 disables; default 76).",
        default_values=["76"],
      ),
    ],
    positionals=[
      PositionArg(
        "input",
        about="[file] ('-' or no file reads stdin)",
        num_args=ValueRange(lower=0, upper=1),
      ),
    ],
    disable_help_subcommand=true,
  )
}

///|
fn b64_char(v : Int) -> Char {
  if v < 26 {
    (0x41 + v).unsafe_to_char()
  } else if v < 52 {
    (0x61 + v - 26).unsafe_to_char()
  } else if v < 62 {
    (0x30 + v - 52).unsafe_to_char()
  } else if v == 62 {
    '+'
  } else {
    '/'
  }
}

///|
fn sextet(c : Char) -> Int raise CliError {
  match c {
    'A'..='Z' => c.to_int() - 0x41
    'a'..='z' => c.to_int() - 0x61 + 26
    '0'..='9' => c.to_int() - 0x30 + 52
    '+' => 62
    '/' => 63
    _ => raise CliError("base64: invalid input character: '\{c}'")
  }
}

///|
fn encode(data : Bytes, wrap : Int) -> String {
  let sb = StringBuilder()
  let mut column = 0
  fn emit(c : Char) {
    sb.write_char(c)
    column += 1
    if wrap > 0 && column == wrap {
      sb.write_char('\n')
      column = 0
    }
  }

  let mut i = 0
  while i < data.length() {
    let b0 = data[i].to_int()
    let b1 = if i + 1 < data.length() { data[i + 1].to_int() } else { 0 }
    let b2 = if i + 2 < data.length() { data[i + 2].to_int() } else { 0 }
    let n = (b0 << 16) | (b1 << 8) | b2
    emit(b64_char((n >> 18) & 0x3F))
    emit(b64_char((n >> 12) & 0x3F))
    if i + 1 < data.length() {
      emit(b64_char((n >> 6) & 0x3F))
    } else {
      emit('=')
    }
    if i + 2 < data.length() {
      emit(b64_char(n & 0x3F))
    } else {
      emit('=')
    }
    i += 3
  }
  let text = sb.to_string()
  if text is "" {
    ""
  } else if text.has_suffix("\n") {
    text
  } else {
    text + "\n"
  }
}

///|
fn decode(text : String) -> Bytes raise CliError {
  let cleaned : Array[Char] = []
  for c in text {
    // Skip all ASCII whitespace, including vertical tab and form feed.
    if c.to_int() is (0x20 | 0x09 | 0x0A | 0x0D | 0x0B | 0x0C) {
      continue
    }
    cleaned.push(c)
  }
  if cleaned.length() % 4 != 0 {
    raise CliError("base64: invalid input length")
  }
  let out : Array[Byte] = []
  let mut i = 0
  while i < cleaned.length() {
    let last_quad = i + 4 == cleaned.length()
    let c0 = cleaned[i]
    let c1 = cleaned[i + 1]
    let c2 = cleaned[i + 2]
    let c3 = cleaned[i + 3]
    if c0 == '=' || c1 == '=' {
      raise CliError("base64: invalid padding")
    }
    if (c2 == '=' || c3 == '=') && !last_quad {
      raise CliError("base64: invalid padding")
    }
    if c2 == '=' && c3 != '=' {
      raise CliError("base64: invalid padding")
    }
    let v0 = sextet(c0)
    let v1 = sextet(c1)
    let v2 = if c2 == '=' { 0 } else { sextet(c2) }
    let v3 = if c3 == '=' { 0 } else { sextet(c3) }
    // Reject non-canonical encodings whose discarded padding bits are set,
    // e.g. AB== (low bits of v1) or AAB= (low bits of v2).
    if c2 == '=' && (v1 & 0xF) != 0 {
      raise CliError("base64: invalid input")
    }
    if c3 == '=' && (v2 & 0x3) != 0 {
      raise CliError("base64: invalid input")
    }
    let n = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3
    out.push(((n >> 16) & 0xFF).to_byte())
    if c2 != '=' {
      out.push(((n >> 8) & 0xFF).to_byte())
    }
    if c3 != '=' {
      out.push((n & 0xFF).to_byte())
    }
    i += 4
  }
  Bytes::from_array(out)
}

///|
fn option_value(matches : @argparse.Matches, name : String) -> String? {
  match matches.values.get(name) {
    Some(vals) =>
      if vals.is_empty() {
        None
      } else {
        Some(vals[vals.length() - 1])
      }
    None => None
  }
}

///|
async fn read_source_bytes(path : String) -> Bytes {
  if path == "-" {
    @stdio.stdin.read_all().binary()
  } else {
    @fs.read_file_to_bytes(path)
  }
}

///|
async fn read_source_text(path : String) -> String {
  if path == "-" {
    @stdio.stdin.read_all().text()
  } else {
    @fs.read_file_to_string(path)
  }
}

///|
async fn main {
  let args = @env.args()[1:]
  let command = base64_command()
  let matches = command.parse(argv=args, env=Map([])) catch {
    err => {
      @stdio.stderr.write("\{err}\n")
      @sys.exit(2)
      return
    }
  }
  let decode_mode = matches.flags.get_or_default("decode", false)
  let wrap_text = option_value(matches, "wrap").unwrap_or("76")
  let wrap = @string.parse_int(wrap_text) catch {
    _ => {
      @stdio.stderr.write("base64: invalid wrap size: '\{wrap_text}'\n")
      @sys.exit(2)
      return
    }
  }
  if wrap < 0 {
    @stdio.stderr.write("base64: invalid wrap size: '\{wrap_text}'\n")
    @sys.exit(2)
    return
  }
  let inputs = matches.values.get("input").unwrap_or([])
  let path = if inputs.is_empty() { "-" } else { inputs[0] }
  try {
    if decode_mode {
      @stdio.stdout.write(decode(read_source_text(path)))
    } else {
      @stdio.stdout.write(encode(read_source_bytes(path), wrap))
    }
  } catch {
    CliError(msg) => {
      @stdio.stderr.write("\{msg}\n")
      @sys.exit(1)
      return
    }
    err => {
      @stdio.stderr.write("base64: \{err}\n")
      @sys.exit(1)
      return
    }
  }
}