///|
fn bit_workspace_error(message : String) -> BitWorkspaceResult {
  { workspace: None, errors: [message] }
}

///|
fn bit_changed_paths_error(message : String) -> BitChangedPathsResult {
  { paths: [], base_sha: "", head_sha: "", errors: [message] }
}

///|
fn normalize_repo_path(path : String) -> String {
  if path.length() > 1 && path.has_suffix("/") {
    String::unsafe_substring(path, start=0, end=path.length() - 1)
  } else {
    path
  }
}

///|
fn paths_overlap(left : String, right : String) -> Bool {
  let a = normalize_repo_path(left)
  let b = normalize_repo_path(right)
  if a == b {
    return true
  }
  let a_prefix = a + "/"
  let b_prefix = b + "/"
  a.has_prefix(b_prefix) || b.has_prefix(a_prefix)
}

///|
fn clear_workspace_root(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  path : String,
) -> Unit raise @bit.GitError {
  if rfs.is_file(path) {
    fs.remove_file(path)
    return
  }
  if !rfs.is_dir(path) {
    return
  }
  for entry in rfs.readdir(path) {
    clear_workspace_root(fs, rfs, @bit.join_path(path, entry))
  }
  fs.remove_dir(path)
}

///|
fn strip_ref_prefix(ref_name : String) -> String {
  if ref_name.has_prefix("refs/heads/") {
    return String::unsafe_substring(ref_name, start=11, end=ref_name.length())
  }
  if ref_name.has_prefix("refs/tags/") {
    return String::unsafe_substring(ref_name, start=10, end=ref_name.length())
  }
  ref_name
}

///|
fn resolve_event_ref_name(
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  ref_name : String,
) -> String {
  if ref_name.length() > 0 {
    return strip_ref_prefix(ref_name)
  }
  let head_path = @bit.join_path(git_dir, "HEAD")
  let head_bytes = rfs.read_file(head_path) catch { _ => return "HEAD" }
  let head_text = @utf8.decode_lossy(head_bytes[:]).trim().to_owned()
  if head_text.has_prefix("ref: refs/heads/") {
    return strip_ref_prefix(
      String::unsafe_substring(head_text, start=5, end=head_text.length()),
    )
  }
  "HEAD"
}

///|
fn git_refname_from_event_ref(ref_name : String) -> String {
  if ref_name == "HEAD" {
    "HEAD"
  } else if ref_name.has_prefix("refs/") {
    ref_name
  } else {
    "refs/heads/" + ref_name
  }
}

///|
fn is_zero_sha(sha : String) -> Bool {
  if sha.length() == 0 {
    return false
  }
  for c in sha {
    if c != '0' {
      return false
    }
  }
  true
}

///|
fn resolve_push_commit_id(
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  event : PushEvent,
) -> @bit.ObjectId raise @bit.GitError {
  if event.after_sha.length() > 0 {
    return @bit.ObjectId::from_hex(event.after_sha)
  }
  match @bitlib.resolve_head_commit(rfs, git_dir) {
    Some(commit_id) => commit_id
    None => raise @bit.GitError::InvalidObject("HEAD commit is missing")
  }
}

///|
fn resolve_push_base_commit_id(
  store : @bit.ObjectStore,
  event : PushEvent,
  head_commit_id : @bit.ObjectId,
) -> @bit.ObjectId? raise @bit.GitError {
  if event.before_sha.length() > 0 && !is_zero_sha(event.before_sha) {
    return Some(@bit.ObjectId::from_hex(event.before_sha))
  }
  let commit_obj = store.get(head_commit_id)
  guard commit_obj is Some(obj) else {
    raise @bit.GitError::InvalidObject("Missing commit object")
  }
  if obj.obj_type != @bit.ObjectType::Commit {
    raise @bit.GitError::InvalidObject("Object is not a commit")
  }
  let info = @bit.parse_commit(obj.data)
  if info.parents.length() == 0 {
    None
  } else {
    Some(info.parents[0])
  }
}

///|
fn commit_tree_id(
  store : @bit.ObjectStore,
  commit_id : @bit.ObjectId,
) -> @bit.ObjectId raise @bit.GitError {
  let commit_obj = store.get(commit_id)
  guard commit_obj is Some(obj) else {
    raise @bit.GitError::InvalidObject("Missing commit object")
  }
  if obj.obj_type != @bit.ObjectType::Commit {
    raise @bit.GitError::InvalidObject("Object is not a commit")
  }
  @bit.parse_commit(obj.data).tree
}

///|
fn sort_paths(paths : Array[String]) -> Array[String] {
  let result = paths.copy()
  result.sort_by(String::lexical_compare)
  result
}

///|
pub fn compute_bit_changed_paths(
  rfs : &@bit.RepoFileSystem,
  repo_root : String,
  event : PushEvent,
) -> BitChangedPathsResult {
  let detected = @bitlib.detect_git_dir(rfs, repo_root)
  guard detected is Some((git_dir, _is_bare)) else {
    return bit_changed_paths_error("bit repository not found: " + repo_root)
  }

  let head_commit_id = resolve_push_commit_id(rfs, git_dir, event) catch {
    err => return bit_changed_paths_error(err.to_string())
  }
  let store = @bitlib.load_object_store_from_fs(rfs, git_dir) catch {
    err => return bit_changed_paths_error(err.to_string())
  }
  let base_commit_id = resolve_push_base_commit_id(store, event, head_commit_id) catch {
    err => return bit_changed_paths_error(err.to_string())
  }
  let new_tree = commit_tree_id(store, head_commit_id) catch {
    err => return bit_changed_paths_error(err.to_string())
  }
  let old_tree = match base_commit_id {
    Some(id) => {
      let old_tree_result = try? commit_tree_id(store, id)
      match old_tree_result {
        Ok(tree_id) => Some(tree_id)
        Err(err) => return bit_changed_paths_error(err.to_string())
      }
    }
    None => None
  }
  let diff_files = @bitdiff.diff_trees(rfs, git_dir, old_tree, new_tree) catch {
    err => return bit_changed_paths_error(err.to_string())
  }
  let paths : Array[String] = []
  for diff in diff_files {
    paths.push(diff.path)
  }
  {
    paths: sort_paths(paths),
    base_sha: base_commit_id.map(id => id.to_hex()).unwrap_or(""),
    head_sha: head_commit_id.to_hex(),
    errors: [],
  }
}

///|
pub fn compute_bit_changed_paths_native(
  repo_root : String,
  event : PushEvent,
) -> BitChangedPathsResult {
  let fs = @bitosfs.OsFs::new()
  compute_bit_changed_paths(fs, repo_root, event)
}

///|
pub fn materialize_bit_push_workspace(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  repo_root : String,
  event : PushEvent,
  workspace_root : String,
  remote_url? : String = "",
) -> BitWorkspaceResult {
  let detected = @bitlib.detect_git_dir(rfs, repo_root)
  guard detected is Some((git_dir, _is_bare)) else {
    return bit_workspace_error("bit repository not found: " + repo_root)
  }
  if paths_overlap(repo_root, workspace_root) {
    return bit_workspace_error("workspace root must not overlap repo root")
  }

  let commit_id = resolve_push_commit_id(rfs, git_dir, event) catch {
    err => return bit_workspace_error(err.to_string())
  }
  let store = @bitlib.load_object_store_from_fs(rfs, git_dir) catch {
    err => return bit_workspace_error(err.to_string())
  }
  let refname = resolve_event_ref_name(rfs, git_dir, event.ref_name)
  let git_refname = git_refname_from_event_ref(refname)
  let resolved_remote_url = if remote_url.length() > 0 {
    remote_url
  } else {
    repo_root
  }

  clear_workspace_root(fs, rfs, workspace_root) catch {
    err => return bit_workspace_error(err.to_string())
  }
  @bit.materialize_clone_to_fs(
    store, commit_id, git_refname, resolved_remote_url, fs, workspace_root,
  ) catch {
    err => return bit_workspace_error(err.to_string())
  }

  {
    workspace: Some({
      repo_root,
      git_dir,
      workspace_root,
      commit_sha: commit_id.to_hex(),
      refname,
      remote_url: resolved_remote_url,
    }),
    errors: [],
  }
}

///|
pub fn materialize_bit_push_workspace_native(
  repo_root : String,
  event : PushEvent,
  workspace_root : String,
  remote_url? : String = "",
) -> BitWorkspaceResult {
  let fs = @bitosfs.OsFs::new()
  materialize_bit_push_workspace(
    fs,
    fs,
    repo_root,
    event,
    workspace_root,
    remote_url~,
  )
}