///|
priv struct RmOptions {
  recursive : Bool
  force : Bool
  empty_directories : Bool
  verbose : Bool
  interactive : Bool
}

///|
priv enum Removal {
  Visit(String, String)
  FinishDirectory(String, String)
}

///|
fn child_label(parent : String, name : String) -> String {
  parent +
  (if parent.has_suffix("/") ||
    (@async.platform is Windows && parent.has_suffix("\\")) {
    ""
  } else {
    "/"
  }) +
  name
}

///|
fn interaction_args(args : ArrayView[String]) -> Array[String] {
  let result : Array[String] = []
  let mut literal = false
  for arg in args {
    if literal {
      result.push(arg)
      continue
    }
    if arg == "--" {
      literal = true
      result.push(arg)
    } else if arg == "--interactive" {
      result.push("--interactive=always")
    } else if arg.has_prefix("-") && !arg.has_prefix("--") && arg != "-" {
      for option in arg[1:] {
        result.push(
          match option {
            'i' => "--interactive=always"
            'I' => "--interactive=once"
            _ => "-" + option.to_string()
          },
        )
      }
    } else {
      result.push(arg)
    }
  }
  result
}

///|
fn interactive_choice(value : String) -> String? {
  let values = [
    ("never", "never"),
    ("no", "never"),
    ("none", "never"),
    ("once", "once"),
    ("always", "always"),
    ("yes", "always"),
  ]
  let matches = values.filter(pair => pair.0.has_prefix(value))
  guard matches.get(0) is Some(first) else { return None }
  if matches.all(pair => pair.1 == first.1) {
    Some(first.1)
  } else {
    None
  }
}

///|
async fn confirm(message : String) -> Bool {
  @stdio.stderr.write("rm: \{message}? ")
  let mut first = true
  let mut accepted = false
  while @stdio.stdin.read_some(max_len=1) is Some(bytes) {
    let byte = bytes[0]
    if first {
      accepted = byte is (b'y' | b'Y')
      first = false
    }
    if byte == b'\n' {
      break
    }
  }
  accepted
}

///|
fn quote_path(path : String) -> String {
  if path.contains("'") &&
    !path.contains("\"") &&
    !path.contains("\\") &&
    !path.contains("$") &&
    !path.iter().any(c => c.to_int() < 32 || c.to_int() == 96) {
    return "\"" + path + "\""
  }
  let output = StringBuilder()
  output.write_char('\'')
  for char in path {
    match char {
      '\'' => output.write_string("'\\''")
      '\n' => output.write_string("'$'\\n''")
      '\r' => output.write_string("'$'\\r''")
      '\t' => output.write_string("'$'\\t''")
      _ => output.write_char(char)
    }
  }
  output.write_char('\'')
  output.to_string()
}

///|
fn remove_error(error : Error) -> String {
  match error {
    @os_error.OSError(_) as error if error.is_ENOENT() =>
      "No such file or directory"
    @os_error.OSError(_) as error if error.is_ENOTDIR() => "Not a directory"
    @os_error.OSError(_) as error if error.is_EACCES() => "Permission denied"
    _ => error.to_string()
  }
}

///|
async fn report_error(
  path : String,
  label : String,
  error : Error,
  directory : Bool,
) -> Unit {
  let denied = match error {
    @os_error.OSError(_) as error => error.is_EACCES()
    _ => false
  }
  let message = if directory && !denied {
    let nonempty = try
      !@fs.readdir(path, include_hidden=true, sort=false).is_empty()
    catch {
      _ => false
    }
    if nonempty {
      "Directory not empty"
    } else {
      remove_error(error)
    }
  } else {
    remove_error(error)
  }
  @stdio.stderr.write("rm: cannot remove \{quote_path(label)}: \{message}\n")
}

///|
async fn file_type(path : String, kind : @fs.FileKind) -> String {
  match kind {
    Regular =>
      if @fs.file_size(path) == 0L {
        "regular empty file"
      } else {
        "regular file"
      }
    Directory => "directory"
    SymLink => "symbolic link"
    Socket => "socket"
    Pipe => "fifo"
    BlockDevice => "block special file"
    CharDevice => "character special file"
    Unknown => "weird file"
  }
}

///|
async fn item_prompt(
  path : String,
  label : String,
  kind : @fs.FileKind,
  descend : Bool,
) -> Bool {
  let write_protected = !(kind is SymLink) && !@fs.can_write(path)
  if descend {
    confirm(
      "descend into " +
      (if write_protected { "write-protected " } else { "" }) +
      "directory " +
      quote_path(label),
    )
  } else {
    confirm(
      "remove " +
      (if write_protected { "write-protected " } else { "" }) +
      file_type(path, kind) +
      " " +
      quote_path(label),
    )
  }
}

///|
async fn remove_operand(operand : String, options : RmOptions) -> Bool {
  // A trailing separator makes the host resolve a directory link even for a
  // no-follow kind query. Inspect the final entry before traversal or prompts.
  let mut end = operand.length()
  while end > 0 &&
        (
          operand[end - 1] == '/' ||
          (@async.platform is Windows && operand[end - 1] == '\\')
        ) {
    end -= 1
  }
  if end > 0 && end < operand.length() {
    let entry = operand[:end].to_owned()
    let kind = @fsops.kind_if_exists(entry) catch {
      error => {
        report_error(entry, operand, error, false)
        return false
      }
    }
    if kind is Some(SymLink) {
      @stdio.stderr.write(
        "rm: refusing to traverse symbolic link with trailing separator '\{operand}'\n",
      )
      return false
    }
  }
  let pending : Array[Removal] = [Visit(operand, operand)]
  let mut failed = false
  while pending.pop() is Some(work) {
    let (path, label) = match work {
      Visit(path, label) | FinishDirectory(path, label) => (path, label)
    }
    if work is FinishDirectory(_, _) {
      if options.interactive && !item_prompt(path, label, Directory, false) {
        continue
      }
      @fs.rmdir(path) catch {
        error => {
          report_error(path, label, error, true)
          failed = true
          continue
        }
      }
      if options.verbose {
        @stdio.stdout.write("removed directory \{quote_path(label)}\n")
      }
      continue
    }
    let kind = @fsops.kind_if_exists(path) catch {
      error => {
        report_error(path, label, error, false)
        failed = true
        continue
      }
    }
    match kind {
      None =>
        if !options.force {
          @stdio.stderr.write(
            "rm: cannot remove \{quote_path(label)}: No such file or directory\n",
          )
          failed = true
        }
      Some(Directory) => {
        if !options.recursive && !options.empty_directories {
          @stdio.stderr.write(
            "rm: cannot remove \{quote_path(label)}: Is a directory\n",
          )
          failed = true
          continue
        }
        let entries = @fs.readdir(
          path,
          include_hidden=true,
          include_special=false,
          sort=false,
        ) catch {
          error => {
            report_error(path, label, error, true)
            failed = true
            continue
          }
        }
        if options.recursive && !entries.is_empty() {
          if options.interactive && !item_prompt(path, label, Directory, true) {
            continue
          }
          pending.push(FinishDirectory(path, label))
          for index = entries.length() - 1; index >= 0; index = index - 1 {
            pending.push(
              Visit(
                @fsops.join(path, entries[index]),
                child_label(label, entries[index]),
              ),
            )
          }
        } else {
          if options.interactive && !entries.is_empty() {
            @stdio.stderr.write(
              "rm: cannot remove \{quote_path(label)}: Is a directory\n",
            )
            failed = true
            continue
          }
          if options.interactive && !item_prompt(path, label, Directory, false) {
            continue
          }
          @fs.rmdir(path) catch {
            error => {
              report_error(path, label, error, true)
              failed = true
              continue
            }
          }
          if options.verbose {
            @stdio.stdout.write("removed directory \{quote_path(label)}\n")
          }
        }
      }
      Some(kind) => {
        if options.interactive && !item_prompt(path, label, kind, false) {
          continue
        }
        @fs.remove(path) catch {
          error => {
            report_error(path, label, error, false)
            failed = true
            continue
          }
        }
        if options.verbose {
          @stdio.stdout.write("removed \{quote_path(label)}\n")
        }
      }
    }
  }
  !failed
}

///|
async fn main {
  let parsed = @cli.parse(interaction_args(@env.args()[1:]), [
    @cli.flag("recursive", short='r'),
    @cli.flag("recursive", short='R'),
    @cli.flag("force", short='f'),
    @cli.flag("dir", short='d'),
    @cli.flag("verbose", short='v'),
    @cli.option("interactive"),
    @cli.flag("preserve-root"),
    @cli.flag("no-preserve-root"),
    @cli.flag("help"),
    @cli.flag("version"),
  ]) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("rm: \{message}: '\{option}'\n")
      @sys.exit(2)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write(
      "Usage: rm [OPTION]... FILE...\n" +
      "  -f, --force             ignore missing files; never prompt\n" +
      "  -i                      prompt for each removal using stdin\n" +
      "  -I                      prompt once for recursion or more than three operands\n" +
      "      --interactive[=WHEN] never, once or always (default with option)\n" +
      "  -r, -R, --recursive     remove directory trees without following symlinks\n" +
      "                          reject symbolic links with trailing separators\n" +
      "  -d, --dir               remove empty directories\n" +
      "  -v, --verbose           print removed paths\n" +
      "      --preserve-root     protect filesystem roots (default)\n" +
      "      --no-preserve-root  disable root protection\n" +
      "      --help              display this help and exit\n" +
      "      --version           display version and exit\n",
    )
    return
  }
  if parsed.contains("version") {
    @stdio.stdout.write("rm 0.2.4\n")
    return
  }
  let mut preserve_root = true
  let mut force = false
  let mut always = false
  let mut once = false
  let values = parsed.values("interactive")
  let mut value_index = 0
  for option in parsed.order {
    match option {
      "preserve-root" => preserve_root = true
      "no-preserve-root" => preserve_root = false
      "force" => {
        force = true
        always = false
        once = false
      }
      "interactive" => {
        let value = values[value_index]
        value_index += 1
        match interactive_choice(value) {
          Some("never") => {
            always = false
            once = false
          }
          Some("once") => {
            always = false
            once = true
            force = false
          }
          Some(_) => {
            always = true
            once = false
            force = false
          }
          None => {
            let reason = if value.is_empty() { "ambiguous" } else { "invalid" }
            @stdio.stderr.write(
              "rm: \{reason} argument '\{value}' for '--interactive'\n",
            )
            @stdio.stderr.write(
              "Valid arguments are:\n  - 'never', 'no', 'none'\n  - 'once'\n  - 'always', 'yes'\n",
            )
            @stdio.stderr.write("Try 'rm --help' for more information.\n")
            @sys.exit(1)
            return
          }
        }
      }
      _ => ()
    }
  }
  let options : RmOptions = {
    recursive: parsed.contains("recursive"),
    force,
    empty_directories: parsed.contains("dir"),
    verbose: parsed.contains("verbose"),
    interactive: always,
  }
  let paths = parsed.operands
  if paths.is_empty() {
    if force {
      return
    }
    @stdio.stderr.write(
      "rm: missing operand\nTry 'rm --help' for more information.\n",
    )
    @sys.exit(1)
    return
  }
  if once && (options.recursive || paths.length() > 3) {
    let noun = if paths.length() == 1 { "argument" } else { "arguments" }
    let recursive = if options.recursive { " recursively" } else { "" }
    if !confirm("remove \{paths.length()} \{noun}\{recursive}") {
      return
    }
  }
  let mut failed = false
  for path in paths {
    if @fsops.protects_removal_operand(path, preserve_root~) {
      @stdio.stderr.write("rm: refusing to remove protected path '\{path}'\n")
      failed = true
      continue
    }
    if !remove_operand(path, options) {
      failed = true
    }
  }
  if failed {
    @sys.exit(1)
  }
}