///| Collect reachable objects for pack generation

///|
/// Collect objects for a single commit without walking parent history.
/// Used for subdir-push where parent history already exists on remote.
pub fn collect_commit_tree_only(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  commit_id : @bit.ObjectId,
) -> Array[@bit.PackObject] raise @bit.GitError {
  let seen : Map[String, Bool] = Map([])
  let out : Array[@bit.PackObject] = []
  let hex = commit_id.to_hex()
  seen[hex] = true
  let obj = db.get(fs, commit_id)
  match obj {
    None => raise @bit.GitError::InvalidObject("Missing commit object")
    Some(o) => {
      if o.obj_type != @bit.ObjectType::Commit {
        raise @bit.GitError::InvalidObject("Object is not a commit")
      }
      out.push(o)
      let info = @bit.parse_commit(o.data)
      collect_tree_objects(db, fs, info.tree, seen, out)
      // Do NOT walk parent commits - they already exist on remote
    }
  }
  out
}

///|
/// Collect diff objects between base and new commit.
/// Only collects objects that don't exist in the base tree.
/// Used for subdir-push where base objects already exist on remote.
pub fn collect_diff_objects(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  base_commit : @bit.ObjectId,
  new_commit : @bit.ObjectId,
) -> Array[@bit.PackObject] raise @bit.GitError {
  // First, collect all object IDs reachable from base (without fetching data)
  let base_ids : Map[String, Bool] = Map([])
  collect_object_ids_only(db, fs, base_commit, base_ids)
  // Now collect new commit's objects, skipping those in base_ids
  let out : Array[@bit.PackObject] = []
  collect_new_commit_objects(db, fs, new_commit, base_ids, out)
  out
}

///|
/// Collect object IDs reachable from a commit (without loading full data).
fn collect_object_ids_only(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  commit_id : @bit.ObjectId,
  seen : Map[String, Bool],
) -> Unit raise @bit.GitError {
  let hex = commit_id.to_hex()
  if seen.contains(hex) {
    return
  }
  seen[hex] = true
  let obj = db.get(fs, commit_id)
  match obj {
    None => () // Object might not exist locally - that's ok
    Some(o) =>
      if o.obj_type == @bit.ObjectType::Commit {
        let info = @bit.parse_commit(o.data)
        collect_tree_ids_only(db, fs, info.tree, seen)
        for p in info.parents {
          collect_object_ids_only(db, fs, p, seen)
        }
      }
  }
}

///|
fn collect_tree_ids_only(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  tree_id : @bit.ObjectId,
  seen : Map[String, Bool],
) -> Unit raise @bit.GitError {
  let hex = tree_id.to_hex()
  if seen.contains(hex) {
    return
  }
  seen[hex] = true
  let obj = db.get(fs, tree_id)
  match obj {
    None => () // Object might not exist locally
    Some(o) =>
      if o.obj_type == @bit.ObjectType::Tree {
        let entries = @bit.parse_tree(o.data)
        for entry in entries {
          if collect_is_gitlink_mode(entry.mode) {
            continue // Skip submodule entries
          }
          if collect_is_tree_mode(entry.mode) {
            collect_tree_ids_only(db, fs, entry.id, seen)
          } else {
            seen[entry.id.to_hex()] = true
          }
        }
      }
  }
}

///|
/// Collect objects from a commit, but only those NOT in the base_ids set.
fn collect_new_commit_objects(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  commit_id : @bit.ObjectId,
  base_ids : Map[String, Bool],
  out : Array[@bit.PackObject],
) -> Unit raise @bit.GitError {
  let hex = commit_id.to_hex()
  if base_ids.contains(hex) {
    return
  }
  let obj = db.get(fs, commit_id)
  match obj {
    None => raise @bit.GitError::InvalidObject("Missing commit object")
    Some(o) => {
      if o.obj_type != @bit.ObjectType::Commit {
        raise @bit.GitError::InvalidObject("Object is not a commit")
      }
      out.push(o)
      let info = @bit.parse_commit(o.data)
      collect_new_tree_objects(db, fs, info.tree, base_ids, out)
      // Do NOT walk parent commits - they're in base_ids
    }
  }
}

///|
fn collect_new_tree_objects(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  tree_id : @bit.ObjectId,
  base_ids : Map[String, Bool],
  out : Array[@bit.PackObject],
) -> Unit raise @bit.GitError {
  let hex = tree_id.to_hex()
  if base_ids.contains(hex) {
    return // Skip trees that exist in base
  }
  let obj = db.get(fs, tree_id)
  match obj {
    None => raise @bit.GitError::InvalidObject("Missing tree object: \{hex}")
    Some(o) => {
      if o.obj_type != @bit.ObjectType::Tree {
        raise @bit.GitError::InvalidObject("Object is not a tree")
      }
      out.push(o)
      let entries = @bit.parse_tree(o.data)
      for entry in entries {
        if collect_is_gitlink_mode(entry.mode) {
          continue // Skip submodule entries (commit refs to external repos)
        }
        let entry_hex = entry.id.to_hex()
        if base_ids.contains(entry_hex) {
          continue // Skip objects that exist in base
        }
        if collect_is_tree_mode(entry.mode) {
          collect_new_tree_objects(db, fs, entry.id, base_ids, out)
        } else {
          collect_new_blob_object(db, fs, entry.id, base_ids, out)
        }
      }
    }
  }
}

///|
fn collect_new_blob_object(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  blob_id : @bit.ObjectId,
  base_ids : Map[String, Bool],
  out : Array[@bit.PackObject],
) -> Unit raise @bit.GitError {
  let hex = blob_id.to_hex()
  if base_ids.contains(hex) {
    return // Skip blobs that exist in base
  }
  let obj = db.get(fs, blob_id)
  match obj {
    None => raise @bit.GitError::InvalidObject("Missing blob object: \{hex}")
    Some(o) => {
      if o.obj_type != @bit.ObjectType::Blob {
        raise @bit.GitError::InvalidObject("Object is not a blob")
      }
      out.push(o)
    }
  }
}

///|
pub fn collect_reachable_objects(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  commit_id : @bit.ObjectId,
) -> Array[@bit.PackObject] raise @bit.GitError {
  let seen : Map[String, Bool] = Map([])
  let out : Array[@bit.PackObject] = []
  collect_commit_objects(db, fs, commit_id, seen, out)
  out
}

///|
/// Collect reachable objects from multiple commits (deduplicated).
pub fn collect_reachable_objects_from_commits(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  commits : Array[@bit.ObjectId],
) -> Array[@bit.PackObject] raise @bit.GitError {
  let seen : Map[String, Bool] = Map([])
  let out : Array[@bit.PackObject] = []
  for commit_id in commits {
    collect_commit_objects(db, fs, commit_id, seen, out)
  }
  out
}

///|
/// Collect objects reachable from `commits`, stopping before any commit in
/// `excluded_commits`. This models upload-pack negotiation: a client's have
/// commits delimit the history it already owns, so their trees and ancestors
/// do not need a separate reachability walk.
pub fn collect_reachable_objects_excluding_commits(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  commits : Array[@bit.ObjectId],
  excluded_commits : Array[@bit.ObjectId],
) -> Array[@bit.PackObject] raise @bit.GitError {
  // Mark the complete object closure already owned by the receiver. Merely
  // stopping the commit walk at a have commit is insufficient: the wanted
  // commit's new tree can still reference unchanged blobs from that commit.
  let seen : Map[String, Bool] = Map([])
  for id in excluded_commits {
    collect_object_ids_only(db, fs, id, seen)
  }
  let out : Array[@bit.PackObject] = []
  for commit_id in commits {
    collect_commit_objects(db, fs, commit_id, seen, out)
  }
  out
}

///|
fn collect_commit_objects(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  commit_id : @bit.ObjectId,
  seen : Map[String, Bool],
  out : Array[@bit.PackObject],
) -> Unit raise @bit.GitError {
  let hex = commit_id.to_hex()
  if seen.contains(hex) {
    return
  }
  seen[hex] = true
  let obj = db.get(fs, commit_id)
  match obj {
    None => raise @bit.GitError::InvalidObject("Missing commit object")
    Some(o) => {
      if o.obj_type != @bit.ObjectType::Commit {
        raise @bit.GitError::InvalidObject("Object is not a commit")
      }
      out.push(o)
      let info = @bit.parse_commit(o.data)
      collect_tree_objects(db, fs, info.tree, seen, out)
      for p in info.parents {
        collect_commit_objects(db, fs, p, seen, out)
      }
    }
  }
}

///|
fn collect_tree_objects(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  tree_id : @bit.ObjectId,
  seen : Map[String, Bool],
  out : Array[@bit.PackObject],
) -> Unit raise @bit.GitError {
  let hex = tree_id.to_hex()
  if seen.contains(hex) {
    return
  }
  seen[hex] = true
  let obj = db.get(fs, tree_id)
  match obj {
    None => raise @bit.GitError::InvalidObject("Missing tree object")
    Some(o) => {
      if o.obj_type != @bit.ObjectType::Tree {
        raise @bit.GitError::InvalidObject("Object is not a tree")
      }
      out.push(o)
      let entries = @bit.parse_tree(o.data)
      for entry in entries {
        if collect_is_gitlink_mode(entry.mode) {
          continue // Skip submodule entries (commit refs to external repos)
        } else if collect_is_tree_mode(entry.mode) {
          collect_tree_objects(db, fs, entry.id, seen, out)
        } else {
          collect_blob_object(db, fs, entry.id, seen, out)
        }
      }
    }
  }
}

///|
fn collect_blob_object(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  blob_id : @bit.ObjectId,
  seen : Map[String, Bool],
  out : Array[@bit.PackObject],
) -> Unit raise @bit.GitError {
  let hex = blob_id.to_hex()
  if seen.contains(hex) {
    return
  }
  seen[hex] = true
  let obj = db.get(fs, blob_id)
  match obj {
    None => raise @bit.GitError::InvalidObject("Missing blob object: \{hex}")
    Some(o) => {
      if o.obj_type != @bit.ObjectType::Blob {
        raise @bit.GitError::InvalidObject("Object is not a blob")
      }
      out.push(o)
    }
  }
}

///|
fn collect_is_tree_mode(mode : String) -> Bool {
  mode == "40000" || mode == "040000"
}

///|
fn collect_is_gitlink_mode(mode : String) -> Bool {
  mode == "160000"
}