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

///|
priv enum SetToken {
  One(Byte)
  Dash
  Many(Array[Byte])
}

///|
fn tr_command() -> @argparse.Command {
  Command(
    "tr",
    about="Translate, squeeze, or delete bytes read from stdin.",
    flags=[
      FlagArg("delete", short='d', about="Delete bytes in SET1."),
      FlagArg(
        "squeeze-repeats",
        short='s',
        about="Squeeze repeated output bytes that occur in the last set.",
      ),
      FlagArg("complement", short='c', about="Use the complement of SET1."),
    ],
    positionals=[
      PositionArg(
        "sets",
        about="SET1 [SET2] (supports escapes, ranges a-z, and [:class:])",
        num_args=ValueRange(lower=1, upper=2),
        allow_hyphen_values=true,
      ),
    ],
    disable_help_subcommand=true,
  )
}

///|
fn char_to_byte(c : Char, spec : String) -> Byte raise CliError {
  let code = c.to_int()
  if code > 0xFF {
    raise CliError(
      "tr: only Latin-1 characters are supported in sets: '\{spec}'",
    )
  }
  code.to_byte()
}

///|
fn class_bytes(name : String) -> Array[Byte] raise CliError {
  let out : Array[Byte] = []
  fn push_range(lo : Int, hi : Int) {
    for code in lo..<=hi {
      out.push(code.to_byte())
    }
  }

  match name {
    "lower" => push_range(0x61, 0x7A)
    "upper" => push_range(0x41, 0x5A)
    "digit" => push_range(0x30, 0x39)
    "alpha" => {
      push_range(0x41, 0x5A)
      push_range(0x61, 0x7A)
    }
    "alnum" => {
      push_range(0x30, 0x39)
      push_range(0x41, 0x5A)
      push_range(0x61, 0x7A)
    }
    "space" =>
      for b in [b'\t', b'\n', 0x0B, 0x0C, b'\r', b' '] {
        out.push(b)
      }
    "xdigit" => {
      push_range(0x30, 0x39)
      push_range(0x41, 0x46)
      push_range(0x61, 0x66)
    }
    _ => raise CliError("tr: unsupported character class: '[:\{name}:]'")
  }
  out
}

///|
fn expand_set(spec : String) -> Array[Byte] raise CliError {
  let chars : Array[Char] = spec.iter().collect()
  let tokens : Array[SetToken] = []
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if c == '\\' && i + 1 < chars.length() {
      let escaped = match chars[i + 1] {
        'n' => b'\n'
        't' => b'\t'
        'r' => b'\r'
        '0' => b'\x00'
        'a' => b'\x07'
        'b' => b'\x08'
        'f' => b'\x0C'
        'v' => b'\x0B'
        other => char_to_byte(other, spec)
      }
      tokens.push(One(escaped))
      i += 2
    } else if c == '[' && i + 1 < chars.length() && chars[i + 1] == ':' {
      let mut j = i + 2
      let name = StringBuilder()
      while j + 1 < chars.length() && !(chars[j] == ':' && chars[j + 1] == ']') {
        name.write_char(chars[j])
        j += 1
      }
      if j + 1 >= chars.length() {
        raise CliError("tr: unterminated character class in: '\{spec}'")
      }
      tokens.push(Many(class_bytes(name.to_string())))
      i = j + 2
    } else if c == '-' {
      tokens.push(Dash)
      i += 1
    } else {
      tokens.push(One(char_to_byte(c, spec)))
      i += 1
    }
  }
  let bytes : Array[Byte] = []
  let mut k = 0
  while k < tokens.length() {
    match tokens[k] {
      Dash =>
        if k > 0 &&
          k + 1 < tokens.length() &&
          tokens[k - 1] is One(lo) &&
          tokens[k + 1] is One(hi) {
          if lo.to_int() > hi.to_int() {
            raise CliError("tr: range endpoints out of order in: '\{spec}'")
          }
          // The lower endpoint was already pushed when One(lo) was visited.
          for code in (lo.to_int() + 1)..<=hi.to_int() {
            bytes.push(code.to_byte())
          }
          k += 2
        } else {
          bytes.push(b'-')
          k += 1
        }
      One(b) => {
        bytes.push(b)
        k += 1
      }
      Many(class) => {
        bytes.append(class)
        k += 1
      }
    }
  }
  bytes
}

///|
fn membership(set : Array[Byte]) -> FixedArray[Bool] {
  let table = FixedArray::make(256, false)
  for b in set {
    table[b.to_int()] = true
  }
  table
}

///|
fn complement_set(set : Array[Byte]) -> Array[Byte] {
  let table = membership(set)
  let out : Array[Byte] = []
  for code in 0..<=255 {
    if !table[code] {
      out.push(code.to_byte())
    }
  }
  out
}

///|
fn squeeze(data : Array[Byte], table : FixedArray[Bool]) -> Array[Byte] {
  let out : Array[Byte] = []
  let mut previous = -1
  for b in data {
    let code = b.to_int()
    if code == previous && table[code] {
      continue
    }
    out.push(b)
    previous = code
  }
  out
}

///|
async fn main {
  let args = @env.args()[1:]
  let command = tr_command()
  let matches = command.parse(argv=args, env=Map([])) catch {
    err => {
      @stdio.stderr.write("\{err}\n")
      @sys.exit(2)
      return
    }
  }
  let delete = matches.flags.get_or_default("delete", false)
  let squeeze_repeats = matches.flags.get_or_default("squeeze-repeats", false)
  let complement = matches.flags.get_or_default("complement", false)
  let sets = matches.values.get("sets").unwrap_or([])
  if sets.is_empty() {
    @stdio.stderr.write("tr: missing operand\n")
    @sys.exit(2)
    return
  }
  try {
    let set1_bytes = {
      let expanded = expand_set(sets[0])
      if complement {
        complement_set(expanded)
      } else {
        expanded
      }
    }
    let set2_bytes = if sets.length() > 1 { expand_set(sets[1]) } else { [] }
    if !delete && sets.length() == 1 && !squeeze_repeats {
      raise CliError("tr: two sets must be given when translating")
    }
    if delete && sets.length() > 1 && !squeeze_repeats {
      raise CliError(
        "tr: extra operand '\{sets[1]}' (a second set is only used with -s)",
      )
    }
    if delete && squeeze_repeats && sets.length() == 1 {
      raise CliError(
        "tr: two sets must be given when both deleting and squeezing repeats",
      )
    }
    let input : Array[Byte] = []
    for b in @stdio.stdin.read_all().binary() {
      input.push(b)
    }
    let output = if delete {
      let del = membership(set1_bytes)
      let kept : Array[Byte] = []
      for b in input {
        if !del[b.to_int()] {
          kept.push(b)
        }
      }
      if squeeze_repeats && sets.length() > 1 {
        squeeze(kept, membership(set2_bytes))
      } else {
        kept
      }
    } else if sets.length() > 1 {
      if set2_bytes.is_empty() {
        raise CliError("tr: SET2 must be non-empty when translating")
      }
      let map = FixedArray::make(256, b'\x00')
      for code in 0..<=255 {
        map[code] = code.to_byte()
      }
      for index, from in set1_bytes {
        let to = if index < set2_bytes.length() {
          set2_bytes[index]
        } else {
          set2_bytes[set2_bytes.length() - 1]
        }
        map[from.to_int()] = to
      }
      let translated : Array[Byte] = input.map(b => map[b.to_int()])
      if squeeze_repeats {
        squeeze(translated, membership(set2_bytes))
      } else {
        translated
      }
    } else {
      // -s with a single set.
      squeeze(input, membership(set1_bytes))
    }
    @stdio.stdout.write(Bytes::from_array(output))
  } catch {
    CliError(msg) => {
      @stdio.stderr.write("\{msg}\n")
      @sys.exit(2)
      return
    }
    err => {
      @stdio.stderr.write("tr: \{err}\n")
      @sys.exit(1)
      return
    }
  }
}