///| Checkout implementation

///|
/// Restore files from index to working tree (like `git checkout -- ` or `git restore `).
pub fn restore_paths(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  paths : Array[String],
) -> Unit raise @bit.GitError {
  let git_dir = join_path(root, ".git")
  let actual_git_dir = if rfs.is_file(git_dir) {
    resolve_gitdir(rfs, git_dir)
  } else {
    git_dir
  }
  let object_git_dir = resolve_checkout_common_git_dir(rfs, actual_git_dir)
  let index_entries = read_index_entries(rfs, actual_git_dir)
  let index_map : Map[String, IndexEntry] = Map([])
  for e in index_entries {
    let safe_path = normalize_checkout_path(e.path)
    index_map[safe_path] = e
  }
  let autocrlf = read_autocrlf_setting(rfs, actual_git_dir)
  let core_eol = read_core_eol_setting(rfs, actual_git_dir)
  let db = ObjectDb::load(rfs, object_git_dir)
  for path in paths {
    let safe_path = normalize_checkout_path(path)
    match index_map.get(safe_path) {
      None =>
        raise @bit.GitError::InvalidObject(
          "error: pathspec '\{path}' did not match any file(s) known to git",
        )
      Some(entry) => {
        let obj = db.get(rfs, entry.id)
        match obj {
          None =>
            raise @bit.GitError::InvalidObject(
              "Object not found: \{entry.id.to_hex()}",
            )
          Some(o) => {
            let abs_path = join_path(root, safe_path)
            let dir = parent_dir(abs_path)
            if dir.length() > 0 && dir != root {
              fs.mkdir_p(dir)
            }
            let attrs = resolve_eol_attrs(rfs, root, safe_path)
            let data = if attrs.filter == Some("lfs") && is_lfs_pointer(o.data) {
              match parse_lfs_pointer(o.data) {
                Some(ptr) =>
                  match lfs_get_cached_content(rfs, actual_git_dir, ptr.oid) {
                    Some(cached) => cached
                    None => o.data
                  }
                None => o.data
              }
            } else {
              o.data
            }
            let output = smudge_for_checkout(data, attrs, autocrlf, core_eol~)
            fs.write_file(abs_path, output)
          }
        }
      }
    }
  }
}

///|
fn normalize_checkout_path(path : String) -> String raise @bit.GitError {
  if path.has_prefix("/") {
    raise @bit.GitError::InvalidObject("path must not be absolute")
  }
  normalize_repo_path(path)
}

///|
pub fn checkout(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  spec : String,
  detach? : Bool = false,
  update_worktree? : Bool = true,
  update_index? : Bool = true,
) -> @bit.ObjectId raise @bit.GitError {
  let git_dir = join_path(root, ".git")
  let actual_git_dir = if rfs.is_file(git_dir) {
    resolve_gitdir(rfs, git_dir)
  } else {
    git_dir
  }
  let object_git_dir = resolve_checkout_common_git_dir(rfs, actual_git_dir)
  let head_path = join_path(actual_git_dir, "HEAD")
  let mut target : @bit.ObjectId? = None
  let mut refname : String? = None
  if !detach {
    let name = if spec.has_prefix("refs/") {
      spec
    } else {
      "refs/heads/" + spec
    }
    match resolve_ref(rfs, actual_git_dir, name) {
      Some(id) => {
        target = Some(id)
        refname = Some(name)
      }
      None =>
        if !spec.has_prefix("refs/") {
          let remote_ref = "refs/remotes/origin/" + spec
          match resolve_ref(rfs, actual_git_dir, remote_ref) {
            Some(id) => {
              let local_ref = "refs/heads/" + spec
              let ref_path = join_path(actual_git_dir, local_ref)
              let dir = parent_dir(ref_path)
              if dir.length() > 0 {
                fs.mkdir_p(dir)
              }
              fs.write_string(ref_path, id.to_hex() + "\n")
              set_config_key(
                fs, rfs, actual_git_dir, "branch", spec, "remote", "origin",
              )
              set_config_key(
                fs,
                rfs,
                actual_git_dir,
                "branch",
                spec,
                "merge",
                "refs/heads/" + spec,
              )
              target = Some(id)
              refname = Some(local_ref)
            }
            None => ()
          }
        }
    }
  }
  match target {
    None => {
      let resolved = rev_parse(rfs, actual_git_dir, spec)
      match resolved {
        None => raise @bit.GitError::InvalidObject("Invalid ref: \{spec}")
        Some(id) => {
          let peeled_commit = rev_parse(rfs, actual_git_dir, spec + "^{commit}")
          target = Some(peeled_commit.unwrap_or(id))
        }
      }
    }
    Some(_) => ()
  }
  let commit_id = match target {
    None => raise @bit.GitError::InvalidObject("Invalid ref: \{spec}")
    Some(id) => id
  }
  if refname is Some(name) {
    fs.write_string(head_path, "ref: \{name}\n")
  } else {
    fs.write_string(head_path, commit_id.to_hex() + "\n")
  }
  if update_worktree || update_index {
    let db = ObjectDb::load_lazy(rfs, object_git_dir)
    db.set_skip_verify(true)
    let files = collect_tree_files_from_commit(db, rfs, commit_id)
    if update_worktree && update_index {
      // Combined path: read each blob only once
      let entries = write_worktree_and_build_index(
        db,
        fs,
        rfs,
        root,
        actual_git_dir,
        files,
        remove_missing=true,
      )
      write_index_entries(fs, actual_git_dir, entries)
    } else if update_worktree {
      write_worktree_from_files(
        db,
        fs,
        rfs,
        root,
        actual_git_dir,
        files,
        remove_missing=true,
      )
    } else {
      let entries = tree_files_to_index(db, rfs, files)
      write_index_entries(fs, actual_git_dir, entries)
    }
  }
  commit_id
}

///|
pub fn resolve_checkout_common_git_dir(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
) -> String {
  let commondir_path = join_path(git_dir, "commondir")
  if !fs.is_file(commondir_path) {
    return git_dir
  }
  let raw = @utf8.decode_lossy(
    (fs.read_file(commondir_path) catch { _ => Default::default() })[:],
  )
  let rel = @string_utils.trim_string(raw)
  if rel.length() == 0 {
    return git_dir
  }
  if rel.has_prefix("/") {
    normalize_path(rel)
  } else {
    normalize_path(join_path(git_dir, rel))
  }
}