///|
#derive(Eq)
priv enum LinkTraversal {
  NeverFollowLinks
  FollowCommandLineLinks
  FollowAllLinks
}

///|
async fn copy_regular_file(
  source : String,
  destination : String,
  replace : Bool,
) -> Bool {
  match @fsops.kind_if_exists(destination) {
    Some(Directory) =>
      raise @fsops.FsOpError("destination is a directory: '\{destination}'")
    Some(SymLink) =>
      raise @fsops.FsOpError(
        "refusing to overwrite symbolic link: '\{destination}'",
      )
    Some(Regular) => {
      if !replace {
        return false
      }
      raise @fsops.FsOpError(
        "overwriting an existing file requires public file identity and handle truncation APIs",
      )
    }
    Some(_) =>
      raise @fsops.FsOpError("unsupported destination type: '\{destination}'")
    None => ()
  }
  let input = @fs.open(source, mode=ReadOnly)
  defer input.close()
  let output = @fs.open(
    destination,
    mode=WriteOnly,
    create_mode=CreateNew,
    permission=0o666,
  )
  defer output.close()
  let buffer = FixedArray::make(65536, b'\x00')
  let bytes = buffer.unsafe_reinterpret_as_bytes()
  while input.read(buffer, max_len=buffer.length()) is count && count > 0 {
    output.write(bytes[:count])
  }
  true
}

///|
async fn copy_source_kind(
  source : String,
  traversal : LinkTraversal,
  command_line : Bool,
) -> @fs.FileKind {
  let kind = @fs.kind(source, follow_symlink=false)
  if kind != SymLink {
    return kind
  }
  if traversal == FollowAllLinks ||
    (traversal == FollowCommandLineLinks && command_line) {
    @fs.kind(source, follow_symlink=true)
  } else {
    raise @fsops.FsOpError(
      "symbolic-link sources are not supported: '\{source}'",
    )
  }
}

///|
async fn preflight_copy_entry(
  source : String,
  destination : String,
  traversal : LinkTraversal,
  command_line : Bool,
  ancestors : Array[String],
) -> Unit {
  let kind = copy_source_kind(source, traversal, command_line)
  match kind {
    Regular =>
      match @fsops.kind_if_exists(destination) {
        Some(Directory) =>
          raise @fsops.FsOpError("destination is a directory: '\{destination}'")
        Some(SymLink) =>
          raise @fsops.FsOpError(
            "refusing to overwrite symbolic link: '\{destination}'",
          )
        Some(Regular) =>
          raise @fsops.FsOpError(
            "overwriting an existing file requires public file identity and handle truncation APIs",
          )
        Some(_) =>
          raise @fsops.FsOpError(
            "unsupported destination type: '\{destination}'",
          )
        None => ()
      }
    Directory => {
      let real = @fs.realpath(source)
      if ancestors.contains(real) {
        raise @fsops.FsOpError("symbolic-link directory cycle at '\{source}'")
      }
      match @fsops.kind_if_exists(destination) {
        Some(Directory) | None => ()
        Some(_) =>
          raise @fsops.FsOpError(
            "destination is not a directory: '\{destination}'",
          )
      }
      let next_ancestors = ancestors.copy()
      next_ancestors.push(real)
      let entries = @fs.readdir(
        source,
        include_hidden=true,
        include_special=false,
        sort=false,
      )
      entries.sort_by((left, right) => left.lexical_compare(right))
      for entry in entries {
        preflight_copy_entry(
          @fsops.join(source, entry),
          @fsops.join(destination, entry),
          traversal,
          false,
          next_ancestors,
        )
      }
    }
    SymLink =>
      raise @fsops.FsOpError(
        "symbolic-link sources are not supported: '\{source}'",
      )
    _ => raise @fsops.FsOpError("unsupported source type: '\{source}'")
  }
}

///|
async fn copy_entry(
  source : String,
  destination : String,
  replace : Bool,
  traversal : LinkTraversal,
  command_line : Bool,
) -> Bool {
  match copy_source_kind(source, traversal, command_line) {
    Regular => copy_regular_file(source, destination, replace)
    Directory => {
      let created = match @fsops.kind_if_exists(destination) {
        Some(Directory) => false
        Some(_) =>
          raise @fsops.FsOpError(
            "destination is not a directory: '\{destination}'",
          )
        None => {
          @fs.mkdir(destination, permission=0o777)
          true
        }
      }
      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_entry(
            @fsops.join(source, entry),
            @fsops.join(destination, entry),
            replace,
            traversal,
            false,
          ) {
          copied = true
        }
      }
      copied || created
    }
    SymLink =>
      raise @fsops.FsOpError(
        "symbolic-link sources are not supported: '\{source}'",
      )
    _ => raise @fsops.FsOpError("unsupported source type: '\{source}'")
  }
}

///|
async fn copy_path_with_links(
  source : String,
  destination : String,
  recursive : Bool,
  replace : Bool,
  traversal : LinkTraversal,
  preserve_permissions~ : Bool = true,
) -> Bool {
  preflight_copy_path(
    source,
    destination,
    recursive,
    traversal,
    preserve_permissions~,
  )
  copy_entry(
    @platform.normalize_input_separators(source),
    @platform.normalize_input_separators(destination),
    replace,
    traversal,
    true,
  )
}

///|
async fn preflight_copy_path(
  source : String,
  destination : String,
  recursive : Bool,
  traversal : LinkTraversal,
  preserve_permissions~ : Bool = true,
) -> Unit {
  let source = @platform.normalize_input_separators(source)
  let destination = @platform.normalize_input_separators(destination)
  let source_kind = copy_source_kind(source, traversal, true)
  if source_kind == Directory && !recursive {
    raise @fsops.FsOpError("omitting directory: '\{source}'")
  }
  if source_kind == Directory {
    @fsops.ensure_not_nested_destination(source, destination)
  }
  if preserve_permissions && !@platform.permission_reads_supported() {
    raise @fsops.FsOpError(
      "preserving source permissions requires a public mode reader; use --no-preserve=mode for a new destination",
    )
  }
  preflight_copy_entry(source, destination, traversal, true, [])
}