///| Asynchronous worktree materialization.

///|
/// Asynchronous counterpart to `checkout_commit_to_fs`.
pub async fn[FS : @types.AsyncFileSystem + @types.AsyncRepoFileSystem] checkout_commit_to_fs_async(
  store : ObjectStore,
  commit_id : @object.ObjectId,
  fs : FS,
  root : String,
) -> Unit raise @object.GitError {
  let commit_obj = store.get(commit_id)
  match commit_obj {
    None => raise @object.GitError::InvalidObject("Missing commit object")
    Some(obj) => {
      if obj.obj_type != @object.ObjectType::Commit {
        raise @object.GitError::InvalidObject("Object is not a commit")
      }
      let info = parse_commit(obj.data)
      let files : Map[String, Bool] = Map([])
      let opaque_dirs : Map[String, Bool] = Map([])
      collect_tree_paths(store, info.tree, "", files, opaque_dirs)
      remove_stale_worktree_paths_async(fs, root, files, opaque_dirs)
      walk_tree_to_fs_async(store, info.tree, "", fs, root)
    }
  }
}

///|
async fn[FS : @types.AsyncFileSystem + @types.AsyncRepoFileSystem] remove_stale_worktree_paths_async(
  fs : FS,
  root : String,
  keep_files : Map[String, Bool],
  opaque_dirs : Map[String, Bool],
) -> Unit raise @object.GitError {
  ignore(remove_stale_dir_async(fs, root, "", keep_files, opaque_dirs))
}

///|
async fn[FS : @types.AsyncFileSystem + @types.AsyncRepoFileSystem] remove_stale_dir_async(
  fs : FS,
  root : String,
  rel_dir : String,
  keep_files : Map[String, Bool],
  opaque_dirs : Map[String, Bool],
) -> Bool raise @object.GitError {
  let full_dir = if rel_dir.length() == 0 {
    root
  } else {
    join_path(root, rel_dir)
  }
  if !@types.AsyncRepoFileSystem::is_dir(fs, full_dir) {
    return true
  }
  let names = @types.AsyncRepoFileSystem::readdir(fs, full_dir)
  let mut emptied = true
  for name in names {
    if rel_dir.length() == 0 && name == ".git" {
      emptied = false
      continue
    }
    let rel_path = if rel_dir.length() == 0 {
      name
    } else {
      rel_dir + "/" + name
    }
    if opaque_dirs.contains(rel_path) {
      emptied = false
      continue
    }
    let full_path = join_path(root, rel_path)
    if @types.AsyncRepoFileSystem::is_dir(fs, full_path) {
      let child_emptied = remove_stale_dir_async(
        fs, root, rel_path, keep_files, opaque_dirs,
      )
      if child_emptied {
        @types.AsyncFileSystem::remove_dir(fs, full_path)
      } else {
        emptied = false
      }
    } else if keep_files.contains(rel_path) {
      emptied = false
    } else {
      @types.AsyncFileSystem::remove_file(fs, full_path)
    }
  }
  emptied
}

///|
async fn[FS : @types.AsyncFileSystem] walk_tree_to_fs_async(
  store : ObjectStore,
  tree_id : @object.ObjectId,
  prefix : String,
  fs : FS,
  root : String,
) -> Unit raise @object.GitError {
  let tree_obj = store.get(tree_id)
  match tree_obj {
    None => raise @object.GitError::InvalidObject("Missing tree object")
    Some(obj) => {
      if obj.obj_type != @object.ObjectType::Tree {
        raise @object.GitError::InvalidObject("Object is not a tree")
      }
      let entries = parse_tree(obj.data)
      for entry in entries {
        verify_tree_entry_name(entry.name)
        let path = if prefix.length() == 0 {
          entry.name
        } else {
          prefix + "/" + entry.name
        }
        if is_tree_mode(entry.mode) {
          walk_tree_to_fs_async(store, entry.id, path, fs, root)
        } else if is_gitlink_mode(entry.mode) {
          @types.AsyncFileSystem::mkdir_p(fs, join_path(root, path))
        } else {
          let blob_obj = store.get(entry.id)
          match blob_obj {
            None => raise @object.GitError::InvalidObject("Missing blob object")
            Some(blob) => {
              if blob.obj_type != @object.ObjectType::Blob {
                raise @object.GitError::InvalidObject("Object is not a blob")
              }
              let full_path = join_path(root, path)
              @types.AsyncFileSystem::mkdir_p(fs, parent_dir(full_path))
              @types.AsyncFileSystem::write_file(fs, full_path, blob.data)
            }
          }
        }
      }
    }
  }
}