///|
priv struct CpOptions {
  recursive : Bool
  no_clobber : Bool
  interactive : Bool
  no_target_directory : Bool
  verbose : Bool
  traversal : @fsops.LinkTraversal
  update : @fsops.UpdateMode
  backup : @fsops.BackupControl
  suffix : String
}

///|
fn normalized_args(args : ArrayView[String]) -> Array[String] {
  let result : Array[String] = []
  let mut options = true
  for arg in args {
    if options && arg == "--" {
      options = false
      result.push(arg)
    } else if options && arg == "--update" {
      result.push("--update-mode=older")
    } else if options && arg.has_prefix("--update=") {
      result.push("--update-mode=" + arg[9:].to_owned())
    } else if options && arg == "--backup" {
      result.push("--backup-control=existing")
    } else if options && arg.has_prefix("--backup=") {
      result.push("--backup-control=" + arg[9:].to_owned())
    } else if options && arg.has_prefix("--preserve=") {
      result.push("--preserve")
    } else {
      result.push(arg)
    }
  }
  result
}

///|
async fn confirm_overwrite(target : String) -> Bool {
  @stdio.stderr.write("cp: overwrite '\{target}'? ")
  match @stdio.stdin.read_until("\n") {
    Some(answer) => answer.has_prefix("y") || answer.has_prefix("Y")
    None => false
  }
}

///|
async fn replacement_decision(
  source : String,
  target : String,
  options : CpOptions,
) -> @fsops.OverwriteDecision {
  guard @fsops.kind_if_exists(target) is Some(target_kind) else {
    return ReplaceDestination
  }
  if options.no_clobber {
    return SkipDestination
  }
  if target_kind == Directory {
    return ReplaceDestination
  }
  let source_time = @fsops.read_file_timestamp(source, ModificationTime)
  let destination_time = @fsops.read_file_timestamp(
    target,
    ModificationTime,
    follow_symlink=false,
  )
  @fsops.overwrite_decision(options.update, source_time, Some(destination_time))
}

///|
async fn copy_tree_with_policy(
  source : String,
  target : String,
  options : CpOptions,
  command_line : Bool,
) -> Bool {
  let target_kind = @fsops.kind_if_exists(target)
  let source_kind = @fs.kind(source, follow_symlink=false)
  let source_kind = if source_kind == SymLink &&
    (
      options.traversal == FollowAllLinks ||
      (options.traversal == FollowCommandLineLinks && command_line)
    ) {
    @fs.kind(source, follow_symlink=true)
  } else {
    source_kind
  }
  match source_kind {
    Directory => {
      match target_kind {
        Some(Directory) => ()
        None => @fs.mkdir(target)
        Some(_) =>
          raise @fsops.FsOpError("destination is not a directory: '\{target}'")
      }
      let entries = @fs.readdir(
        source,
        include_hidden=true,
        include_special=false,
        sort=false,
      )
      entries.sort_by((left, right) => left.lexical_compare(right))
      let mut copied = false
      for entry in entries {
        if copy_tree_with_policy(
            @fsops.join(source, entry),
            @fsops.join(target, entry),
            options,
            false,
          ) {
          copied = true
        }
      }
      copied || target_kind is None
    }
    Regular => {
      match replacement_decision(source, target, options) {
        SkipDestination => return false
        RejectDestination =>
          raise @fsops.FsOpError("destination exists under update=none-fail")
        ReplaceDestination => ()
      }
      if target_kind == Some(Regular) && options.interactive {
        if !confirm_overwrite(target) {
          return false
        }
      }
      let backup = match target_kind {
        Some(Regular) =>
          @fsops.backup_destination(
            target,
            options.backup,
            suffix=options.suffix,
          )
        _ => None
      }
      let backup_to_restore = backup
      errdefer (match backup_to_restore {
        Some(path) => @fsops.restore_backup(target, path) catch { _ => () }
        None => ()
      })
      ignore(
        @fsops.copy_path_with_links(
          source,
          target,
          false,
          backup is None,
          options.traversal,
        ),
      )
      true
    }
    _ => raise @fsops.FsOpError("unsupported source type: '\{source}'")
  }
}

///|
async fn copy_one(
  source : String,
  target : String,
  options : CpOptions,
) -> Bool {
  @fsops.preflight_copy_path(
    source,
    target,
    options.recursive,
    options.traversal,
  )
  copy_tree_with_policy(source, target, options, true)
}

///|
async fn main {
  let parsed = @cli.parse(normalized_args(@env.args()[1:]), [
    @cli.flag("recursive", short='R'),
    @cli.flag("recursive", short='r'),
    @cli.flag("force", short='f'),
    @cli.flag("no-clobber", short='n'),
    @cli.flag("interactive", short='i'),
    @cli.flag("update", short='u'),
    @cli.option("update-mode"),
    @cli.flag("backup", short='b'),
    @cli.option("backup-control"),
    @cli.option("suffix", short='S'),
    @cli.flag("dereference-command-line", short='H'),
    @cli.flag("dereference", short='L'),
    @cli.flag("no-dereference", short='P'),
    @cli.flag("no-target-directory", short='T'),
    @cli.flag("preserve", short='p'),
    @cli.flag("archive", short='a'),
    @cli.flag("verbose", short='v'),
    @cli.flag("help"),
  ]) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("cp: \{message}: '\{option}'\n")
      @sys.exit(2)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write(
      "Usage: cp [-R] [-H|-L|-P] [-f|-i|-n] [-u|--update=MODE] [-b|--backup[=CONTROL]] [-S SUFFIX] [-T] [-v] SOURCE... DEST\n",
    )
    return
  }
  if parsed.contains("preserve") || parsed.contains("archive") {
    @stdio.stderr.write(
      "cp: metadata and symbolic-link preservation are unavailable in the portable filesystem profile\n",
    )
    @sys.exit(1)
    return
  }
  let operands = parsed.operands
  if operands.length() < 2 {
    @stdio.stderr.write("cp: missing source or destination operand\n")
    @sys.exit(1)
    return
  }
  let update_text = match parsed.last_occurrence(["update", "update-mode"]) {
    Some("update") => "older"
    Some("update-mode") => parsed.last_value("update-mode").unwrap()
    _ => "all"
  }
  let update = @fsops.parse_update_mode(update_text) catch {
    @fsops.FsOpError(message) => {
      @stdio.stderr.write("cp: \{message}\n")
      @sys.exit(2)
      return
    }
  }
  let backup_text = match parsed.last_occurrence(["backup", "backup-control"]) {
    Some("backup") => "existing"
    Some("backup-control") => parsed.last_value("backup-control").unwrap()
    _ => "none"
  }
  let backup = @fsops.parse_backup_control(backup_text) catch {
    @fsops.FsOpError(message) => {
      @stdio.stderr.write("cp: \{message}\n")
      @sys.exit(2)
      return
    }
  }
  let recursive = parsed.contains("recursive")
  let traversal = match
    parsed.last_occurrence([
      "dereference-command-line", "dereference", "no-dereference",
    ]) {
    Some("dereference-command-line") =>
      @fsops.LinkTraversal::FollowCommandLineLinks
    Some("dereference") => @fsops.LinkTraversal::FollowAllLinks
    Some("no-dereference") => @fsops.LinkTraversal::NeverFollowLinks
    _ => if recursive { NeverFollowLinks } else { FollowCommandLineLinks }
  }
  let options : CpOptions = {
    recursive,
    no_clobber: parsed.last_occurrence(["force", "interactive", "no-clobber"]) ==
    Some("no-clobber"),
    interactive: parsed.last_occurrence(["force", "interactive", "no-clobber"]) ==
    Some("interactive"),
    no_target_directory: parsed.contains("no-target-directory"),
    verbose: parsed.contains("verbose"),
    traversal,
    update,
    backup,
    suffix: parsed.last_value("suffix").unwrap_or("~"),
  }
  let destination = operands[operands.length() - 1]
  let sources = operands[0:operands.length() - 1]
  if options.no_target_directory && sources.length() > 1 {
    @stdio.stderr.write("cp: -T cannot be used with multiple sources\n")
    @sys.exit(1)
    return
  }
  let destination_kind = @fsops.kind_if_exists(destination) catch {
    err => {
      @stdio.stderr.write("cp: cannot inspect '\{destination}': \{err}\n")
      @sys.exit(1)
      return
    }
  }
  if sources.length() > 1 && destination_kind != Some(Directory) {
    @stdio.stderr.write(
      "cp: destination is not a directory: '\{destination}'\n",
    )
    @sys.exit(1)
    return
  }
  let mut failed = false
  for source in sources {
    let target = if sources.length() > 1 ||
      (!options.no_target_directory && destination_kind == Some(Directory)) {
      @fsops.join(destination, @fsops.basename(source))
    } else {
      destination
    }
    let copied = copy_one(source, target, options) catch {
      @fsops.FsOpError(message) => {
        @stdio.stderr.write(
          "cp: cannot copy '\{source}' to '\{target}': \{message}\n",
        )
        failed = true
        false
      }
      err => {
        @stdio.stderr.write(
          "cp: cannot copy '\{source}' to '\{target}': \{err}\n",
        )
        failed = true
        false
      }
    }
    if copied && options.verbose {
      @stdio.stdout.write("'\{source}' -> '\{target}'\n")
    }
  }
  if failed {
    @sys.exit(1)
  }
}