///| Minimal object store + checkout helpers

///|
pub struct ObjectStore {
  objects : Map[String, PackObject]
}

///|
pub fn ObjectStore::new() -> ObjectStore {
  { objects: {} }
}

///|
pub fn ObjectStore::from_pack(objects : Array[PackObject]) -> ObjectStore {
  let store = ObjectStore::new()
  store.add_objects(objects)
  store
}

///|
pub fn ObjectStore::add_objects(
  self : ObjectStore,
  objects : Array[PackObject],
) -> Unit {
  for obj in objects {
    // Use cached id if available (offset >= 0 means it was parsed from pack)
    let id = if obj.offset >= 0 {
      obj.id
    } else {
      hash_object_content(obj.obj_type, obj.data)
    }
    self.objects[id.to_hex()] = obj
  }
}

///|
pub fn ObjectStore::get(self : ObjectStore, id : ObjectId) -> PackObject? {
  self.objects.get(id.to_hex())
}

///|
pub fn ObjectStore::contains(self : ObjectStore, id : ObjectId) -> Bool {
  match self.objects.get(id.to_hex()) {
    Some(_) => true
    None => false
  }
}

///|
pub struct CommitInfo {
  tree : ObjectId
  parents : Array[ObjectId]
}

///|
pub fn parse_commit(content : Bytes) -> CommitInfo raise GitError {
  let text = @utf8.decode_lossy(content[:])
  let mut tree_id : ObjectId? = None
  let parents : Array[ObjectId] = []
  for line_view in text.split("\n") {
    let line = line_view.to_string()
    if line.length() == 0 {
      break
    }
    if line.has_prefix("tree ") {
      let hex = String::unsafe_substring(line, start=5, end=line.length())
      tree_id = Some(ObjectId::from_hex(hex))
    } else if line.has_prefix("parent ") {
      let hex = String::unsafe_substring(line, start=7, end=line.length())
      parents.push(ObjectId::from_hex(hex))
    }
  }
  match tree_id {
    Some(tree) => { tree, parents }
    None => raise GitError::InvalidObject("Missing tree in commit")
  }
}

///|
pub fn parse_tree(content : Bytes) -> Array[TreeEntry] raise GitError {
  let entries : Array[TreeEntry] = []
  let mut i = 0
  while i < content.length() {
    // Parse mode (ASCII only, so byte-by-byte is fine)
    let mode_buf = StringBuilder::new()
    while i < content.length() && content[i] != b' ' {
      mode_buf.write_char(content[i].to_int().unsafe_to_char())
      i += 1
    }
    if i >= content.length() {
      raise GitError::InvalidObject("Truncated tree entry mode")
    }
    i += 1 // skip space
    // Parse name - collect bytes and decode as UTF-8
    let name_start = i
    while i < content.length() && content[i] != b'\x00' {
      i += 1
    }
    if i >= content.length() {
      raise GitError::InvalidObject("Truncated tree entry name")
    }
    let name_len = i - name_start
    let name_bytes = FixedArray::makei(name_len, fn(j) {
      content[name_start + j]
    })
    let name = @utf8.decode_lossy(Bytes::from_array(name_bytes)[:])
    i += 1 // skip NUL
    if i + 20 > content.length() {
      raise GitError::InvalidObject("Truncated tree entry id")
    }
    let bytes : FixedArray[Byte] = FixedArray::make(20, b'\x00')
    for j = 0; j < 20; j = j + 1 {
      bytes[j] = content[i + j]
    }
    i += 20
    let id = ObjectId::new(bytes)
    entries.push(TreeEntry::new(mode_buf.to_string(), name, id))
  }
  entries
}

///|
pub fn checkout_commit(
  store : ObjectStore,
  commit_id : ObjectId,
) -> Map[String, Bytes] raise GitError {
  let commit_obj = store.get(commit_id)
  match commit_obj {
    None => raise GitError::InvalidObject("Missing commit object")
    Some(obj) => {
      if obj.obj_type != ObjectType::Commit {
        raise GitError::InvalidObject("Object is not a commit")
      }
      let info = parse_commit(obj.data)
      let files : Map[String, Bytes] = {}
      walk_tree(store, info.tree, "", files)
      files
    }
  }
}

///|
/// Materialize commit files into a filesystem under `root`.
pub fn checkout_commit_to_fs(
  store : ObjectStore,
  commit_id : ObjectId,
  fs : &FileSystem,
  root : String,
) -> Unit raise GitError {
  let files = checkout_commit(store, commit_id)
  for item in files.to_array() {
    let (path, data) = item
    let full_path = join_path(root, path)
    let dir = parent_dir(full_path)
    fs.mkdir_p(dir)
    fs.write_file(full_path, data)
  }
}

///|
fn walk_tree(
  store : ObjectStore,
  tree_id : ObjectId,
  prefix : String,
  files : Map[String, Bytes],
) -> Unit raise GitError {
  let tree_obj = store.get(tree_id)
  match tree_obj {
    None => raise GitError::InvalidObject("Missing tree object")
    Some(obj) => {
      if obj.obj_type != ObjectType::Tree {
        raise GitError::InvalidObject("Object is not a tree")
      }
      let entries = parse_tree(obj.data)
      for entry in entries {
        let path = if prefix.length() == 0 {
          entry.name
        } else {
          prefix + "/" + entry.name
        }
        if is_tree_mode(entry.mode) {
          walk_tree(store, entry.id, path, files)
        } else {
          let blob_obj = store.get(entry.id)
          match blob_obj {
            None => raise GitError::InvalidObject("Missing blob object")
            Some(b) => {
              if b.obj_type != ObjectType::Blob {
                raise GitError::InvalidObject("Object is not a blob")
              }
              files[path] = b.data
            }
          }
        }
      }
    }
  }
}

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

///|
fn join_path(root : String, path : String) -> String {
  if root.length() == 0 || root == "/" {
    if path.has_prefix("/") {
      path
    } else {
      "/" + path
    }
  } else if root.has_suffix("/") {
    root + path
  } else {
    root + "/" + path
  }
}

///|
fn parent_dir(path : String) -> String {
  match path.rev_find("/") {
    None => "/"
    Some(0) => "/"
    Some(i) => String::unsafe_substring(path, start=0, end=i)
  }
}