///| Ref storage helpers (loose/packed refs)

///|
fn decode_bytes_lossy_refs(data : Bytes) -> String {
  @utf8.decode_lossy(data[:])
}

///|
fn trim_string_refs(s : String) -> String {
  let mut start = 0
  let mut end = s.length()
  while start < end {
    let c = s.unsafe_get(start)
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      start += 1
    } else {
      break
    }
  }
  while end > start {
    let c = s.unsafe_get(end - 1)
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      end -= 1
    } else {
      break
    }
  }
  if start == 0 && end == s.length() {
    s
  } else {
    String::unsafe_substring(s, start~, end~)
  }
}

///|
pub fn collect_loose_ref_ids(
  fs : &@bit.RepoFileSystem,
  dir : String,
  prefix : String,
  out : Map[String, @bit.ObjectId],
  filter_prefix : String?,
) -> Unit {
  if !fs.is_dir(dir) {
    return ()
  }
  // Early-exit: when a refname-prefix filter is set, skip whole
  // subtrees whose refname can never intersect with the filter
  // (e.g. don't walk `refs/tags/` when the caller asked for
  // `refs/heads/`). Before this guard the recursion stepped into
  // every `.git/refs/*` directory and filtered at the file level.
  if filter_prefix is Some(pfx) &&
    prefix.length() > 0 &&
    !refs_subtree_intersects_filter(prefix, pfx) {
    return ()
  }
  let entries = fs.readdir(dir) catch { _ => [] }
  for entry in entries {
    if entry == "." || entry == ".." {
      continue
    }
    let path = dir + "/" + entry
    let name = if prefix.length() == 0 { entry } else { prefix + "/" + entry }
    if fs.is_dir(path) {
      collect_loose_ref_ids(fs, path, name, out, filter_prefix)
    } else if fs.is_file(path) {
      if filter_prefix is Some(pfx) && !name.has_prefix(pfx) {
        continue
      }
      let content = decode_bytes_lossy_refs(
        fs.read_file(path) catch {
          _ => continue
        },
      )
      let hex = trim_string_refs(content)
      if hex.length() == 0 {
        continue
      }
      let id = @bit.ObjectId::from_hex(hex) catch { _ => continue }
      out[name] = id
    }
  }
}

///|
/// Whether refnames under directory `dir_refname` can intersect the
/// caller-supplied `filter_prefix`. Used to prune the loose-ref
/// recursion without reading every directory entry. Both inputs are
/// refname-style paths (no leading `/`).
///
/// A refname under `dir_refname/` matches `filter_prefix` iff one of
/// the two strings is a prefix of the other (after appending `/` to
/// the directory):
///   * filter narrower than dir: filter starts with `dir + "/"`
///   * dir inside filter range:  `dir + "/"` starts with filter
fn refs_subtree_intersects_filter(
  dir_refname : String,
  filter_prefix : String,
) -> Bool {
  let with_slash = dir_refname + "/"
  filter_prefix.has_prefix(with_slash) || with_slash.has_prefix(filter_prefix)
}

///|
pub fn collect_packed_ref_ids(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  out : Map[String, @bit.ObjectId],
  filter_prefix : String?,
) -> Unit {
  let packed_path = git_dir + "/packed-refs"
  if !fs.is_file(packed_path) {
    return ()
  }
  let packed = fs.read_file(packed_path) catch { _ => Default::default() }
  let text = @utf8.decode_lossy(packed[:])
  for line_view in text.split("\n") {
    let line = trim_string_refs(line_view.to_owned())
    if line.length() == 0 {
      continue
    }
    if line.has_prefix("#") || line.has_prefix("^") {
      continue
    }
    let space = line.find(" ")
    match space {
      None => continue
      Some(idx) => {
        if idx + 1 >= line.length() {
          continue
        }
        let id_hex = String::unsafe_substring(line, start=0, end=idx)
        let refname = String::unsafe_substring(
          line,
          start=idx + 1,
          end=line.length(),
        )
        if filter_prefix is Some(pfx) && !refname.has_prefix(pfx) {
          continue
        }
        if out.contains(refname) {
          continue
        }
        let id = @bit.ObjectId::from_hex(id_hex) catch { _ => continue }
        out[refname] = id
      }
    }
  }
}

///|
pub fn list_refs_with_ids(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  filter_prefix : String?,
) -> Map[String, @bit.ObjectId] {
  let out : Map[String, @bit.ObjectId] = Map([])
  // Collect from reftable if present
  if fs.is_dir(git_dir + "/reftable") {
    let reftable_refs = @reftable.collect_reftable_refs(fs, git_dir) catch {
      _ => []
    }
    for pair in reftable_refs {
      let (name, id) = pair
      let matches = match filter_prefix {
        Some(p) => name.has_prefix(p)
        None => true
      }
      if matches {
        out[name] = id
      }
    }
  }
  let refs_dir = git_dir + "/refs"
  collect_loose_ref_ids(fs, refs_dir, "refs", out, filter_prefix)
  collect_packed_ref_ids(fs, git_dir, out, filter_prefix)
  out
}

///|

///|
/// A single packed-refs entry with its optional peeled (`^`) line.
pub(all) struct PackedRefEntry {
  refname : String
  oid : String
  peeled : String?
} derive(Debug, Eq)

///|
/// Parse packed-refs text into entries. Header (`#`) and blank lines are
/// dropped; a `^` line is attached to the entry that precedes it.
pub fn parse_packed_refs_text(text : String) -> Array[PackedRefEntry] {
  let entries : Array[PackedRefEntry] = []
  for line_view in text.split("\n") {
    let line = line_view.to_owned()
    if line.length() == 0 || line.has_prefix("#") {
      continue
    }
    if line.has_prefix("^") {
      if entries.length() > 0 {
        let last = entries[entries.length() - 1]
        entries[entries.length() - 1] = {
          refname: last.refname,
          oid: last.oid,
          peeled: Some(
            String::unsafe_substring(line, start=1, end=line.length()),
          ),
        }
      }
      continue
    }
    match line.find(" ") {
      Some(idx) =>
        if idx > 0 && idx + 1 < line.length() {
          entries.push({
            refname: String::unsafe_substring(
              line,
              start=idx + 1,
              end=line.length(),
            ),
            oid: String::unsafe_substring(line, start=0, end=idx),
            peeled: None,
          })
        }
      None => ()
    }
  }
  entries
}

///|
/// Serialize entries into canonical packed-refs text: exactly one entry per
/// refname (later duplicates win), sorted by refname, with git's standard
/// header. Every packed-refs write MUST go through this (or produce the same
/// invariants): real git trusts the `sorted` trait and binary-searches the
/// file, so an unsorted or duplicated file breaks ref resolution in real git.
pub fn serialize_packed_refs_entries(entries : Array[PackedRefEntry]) -> String {
  let dedup : Map[String, PackedRefEntry] = Map([])
  for e in entries {
    dedup[e.refname] = e
  }
  let unique : Array[PackedRefEntry] = dedup.iter().map(kv => kv.1).collect()
  // NOTE: String::compare is length-first in MoonBit; git requires plain
  // byte order (memcmp) for the `sorted` trait, which lexical_compare gives.
  unique.sort_by((a, b) => String::lexical_compare(a.refname, b.refname))
  let buf = StringBuilder::new()
  // Trailing space after "sorted" matches real git's header byte-for-byte.
  buf.write_string("# pack-refs with: peeled fully-peeled sorted \n")
  for e in unique {
    buf.write_string(e.oid + " " + e.refname + "\n")
    match e.peeled {
      Some(p) => buf.write_string("^" + p + "\n")
      None => ()
    }
  }
  buf.to_string()
}

///|
pub fn rewrite_packed_refs(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  transform : (String) -> String?,
) -> Unit raise @bit.GitError {
  let packed_path = git_dir + "/packed-refs"
  if !rfs.is_file(packed_path) {
    return ()
  }
  let data = rfs.read_file(packed_path) catch { _ => Default::default() }
  let text = @utf8.decode_lossy(data[:])
  let entries = parse_packed_refs_text(text)
  let kept : Array[PackedRefEntry] = []
  for e in entries {
    match transform(e.refname) {
      Some(new_ref) =>
        kept.push({ refname: new_ref, oid: e.oid, peeled: e.peeled })
      None => ()
    }
  }
  if kept.length() == 0 {
    fs.remove_file(packed_path) catch {
      _ => ()
    }
  } else {
    fs.write_string(packed_path, serialize_packed_refs_entries(kept))
  }
}

///|
pub fn remove_packed_ref(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  refname : String,
) -> Unit raise @bit.GitError {
  rewrite_packed_refs(fs, rfs, git_dir, fn(name) {
    if name == refname {
      None
    } else {
      Some(name)
    }
  })
}

///|
pub fn remove_packed_refs_with_prefix(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  prefix : String,
  exact : String,
) -> Unit raise @bit.GitError {
  rewrite_packed_refs(fs, rfs, git_dir, fn(name) {
    if name == exact || name.has_prefix(prefix) {
      None
    } else {
      Some(name)
    }
  })
}

///|
pub fn rename_packed_refs_prefix(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  old_prefix : String,
  new_prefix : String,
  old_exact : String,
  new_exact : String,
) -> Unit raise @bit.GitError {
  rewrite_packed_refs(fs, rfs, git_dir, fn(name) {
    if name == old_exact {
      Some(new_exact)
    } else if name.has_prefix(old_prefix) {
      let suffix = String::unsafe_substring(
        name,
        start=old_prefix.length(),
        end=name.length(),
      )
      Some(new_prefix + suffix)
    } else {
      Some(name)
    }
  })
}

///|
pub fn list_remote_tracking_refs(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  remote_name : String,
) -> Map[String, @bit.ObjectId] {
  let prefix = "refs/remotes/" + remote_name + "/"
  let refs = list_refs_with_ids(fs, git_dir, Some(prefix))
  let out : Map[String, @bit.ObjectId] = Map([])
  for refname, id in refs {
    if !refname.has_prefix(prefix) {
      continue
    }
    let name = String::unsafe_substring(
      refname,
      start=prefix.length(),
      end=refname.length(),
    )
    if name.length() == 0 || name == "HEAD" {
      continue
    }
    out[name] = id
  }
  out
}

///|
pub fn remove_remote_refs(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  name : String,
) -> Unit {
  let refs_dir = git_dir + "/refs/remotes/" + name
  if rfs.is_dir(refs_dir) || rfs.is_file(refs_dir) {
    remove_tree_internal(fs, rfs, refs_dir)
  }
  let logs_dir = git_dir + "/logs/refs/remotes/" + name
  if rfs.is_dir(logs_dir) || rfs.is_file(logs_dir) {
    remove_tree_internal(fs, rfs, logs_dir)
  }
}

///|
fn remove_tree_internal(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  path : String,
) -> Unit {
  if rfs.is_dir(path) {
    let entries = rfs.readdir(path) catch { _ => [] }
    for entry in entries {
      if entry == "." || entry == ".." {
        continue
      }
      remove_tree_internal(fs, rfs, path + "/" + entry)
    }
    fs.remove_dir(path) catch {
      _ => ()
    }
    return ()
  }
  if rfs.is_file(path) {
    fs.remove_file(path) catch {
      _ => ()
    }
  }
}