///| Git object types and common errors

///|
/// Object identifier (20 bytes for SHA-1, 32 bytes for SHA-256)
pub struct ObjectId {
  bytes : FixedArray[Byte]
}

///|
pub fn ObjectId::new(bytes : FixedArray[Byte]) -> ObjectId {
  { bytes, }
}

///|
pub fn ObjectId::zero(algo? : HashAlgorithm = Sha1) -> ObjectId {
  { bytes: FixedArray::make(algo.hash_size(), b'\x00') }
}

///|
/// Check if this ObjectId is the zero (null) object id.
pub fn ObjectId::is_zero(self : ObjectId) -> Bool {
  for b in self.bytes {
    if b != b'\x00' {
      return false
    }
  }
  true
}

///|
/// Return the byte length of this ObjectId (20 for SHA-1, 32 for SHA-256).
pub fn ObjectId::hash_size(self : ObjectId) -> Int {
  self.bytes.length()
}

///|
/// Convert ObjectId to Bytes - zero-copy when possible
pub fn ObjectId::to_bytes(self : ObjectId) -> Bytes {
  Bytes::from_array(self.bytes.iter().collect())
}

///|
/// Create ObjectId from Bytes (must be exactly 20 or 32 bytes)
pub fn ObjectId::from_bytes(data : Bytes) -> ObjectId raise GitError {
  if data.length() != 20 && data.length() != 32 {
    raise GitError::InvalidObject("Invalid ObjectId length: \{data.length()}")
  }
  let len = data.length()
  let bytes = FixedArray::make(len, b'\x00')
  for i in 0.. ObjectId {
  let bytes = FixedArray::make(hash_size, b'\x00')
  for i in 0.. ObjectId raise GitError {
  if hex.length() != 40 && hex.length() != 64 {
    raise GitError::InvalidObject("Invalid hex length: \{hex.length()}")
  }
  let byte_len = hex.length() / 2
  let bytes = FixedArray::make(byte_len, b'\x00')
  let chars = hex.to_array()
  for i = 0; i < byte_len; i = i + 1 {
    let hi = hex_char_to_int(chars[i * 2])
    let lo = hex_char_to_int(chars[i * 2 + 1])
    bytes[i] = ((hi << 4) | lo).to_byte()
  }
  { bytes, }
}

///|
fn hex_char_to_int(c : Char) -> Int raise GitError {
  if c >= '0' && c <= '9' {
    c.to_int() - '0'.to_int()
  } else if c >= 'a' && c <= 'f' {
    c.to_int() - 'a'.to_int() + 10
  } else if c >= 'A' && c <= 'F' {
    c.to_int() - 'A'.to_int() + 10
  } else {
    raise GitError::InvalidObject("Invalid hex char: \{c}")
  }
}

///|
pub fn ObjectId::to_hex(self : ObjectId) -> String {
  let result = StringBuilder::new()
  for b in self.bytes {
    let hi = (b.to_int() >> 4) & 0x0f
    let lo = b.to_int() & 0x0f
    result.write_char(int_to_hex_char(hi))
    result.write_char(int_to_hex_char(lo))
  }
  result.to_string()
}

///|
fn int_to_hex_char(i : Int) -> Char {
  if i < 10 {
    (i + '0'.to_int()).unsafe_to_char()
  } else {
    (i - 10 + 'a'.to_int()).unsafe_to_char()
  }
}

///|
pub impl Show for ObjectId with fn output(self, logger) {
  logger.write_string(self.to_hex())
}

///|
pub impl @debug.Debug for ObjectId with fn to_repr(self) {
  @debug.Repr::string(self.to_hex())
}

///|
pub impl Eq for ObjectId with fn equal(self, other) {
  if self.bytes.length() != other.bytes.length() {
    return false
  }
  for i = 0; i < self.bytes.length(); i = i + 1 {
    if self.bytes[i] != other.bytes[i] {
      return false
    }
  }
  true
}

///|
/// Check if a hex string represents the zero (null) object id.
pub fn ObjectId::is_zero_hex(hex : String) -> Bool {
  hex == "0000000000000000000000000000000000000000" ||
  hex == "0000000000000000000000000000000000000000000000000000000000000000"
}

///|
/// Hash implementation for using ObjectId as Map key
pub impl Hash for ObjectId with fn hash_combine(self, hasher) {
  // Use first 8 bytes as hash (sufficient for distribution)
  for i in 0..<8 {
    Hash::hash_combine(self.bytes[i], hasher)
  }
}

///|
/// Git object types
pub(all) enum ObjectType {
  Blob
  Tree
  Commit
  Tag
} derive(Eq, Debug)

///|
pub fn ObjectType::to_string(self : ObjectType) -> String {
  match self {
    Blob => "blob"
    Tree => "tree"
    Commit => "commit"
    Tag => "tag"
  }
}

///|
pub fn ObjectType::to_packfile_type(self : ObjectType) -> Int {
  match self {
    Commit => 1
    Tree => 2
    Blob => 3
    Tag => 4
  }
}

///|
pub impl Show for ObjectType with fn output(self, logger) {
  logger.write_string(self.to_string())
}

///|
/// Git operation errors
pub(all) suberror GitError {
  InvalidObject(String)
  HashMismatch(String, String)
  PackfileError(String)
  ProtocolError(String)
  IoError(String)
} derive(Debug, Eq)

///|
fn git_error_show_string(value : String) -> String {
  let buf = StringBuilder::new()
  buf.write_char('"')
  for c in value {
    if c == '"' {
      buf.write_string("\\\"")
    } else if c == '\\' {
      buf.write_string("\\\\")
    } else if c == '\n' {
      buf.write_string("\\n")
    } else if c == '\r' {
      buf.write_string("\\r")
    } else if c == '\t' {
      buf.write_string("\\t")
    } else {
      buf.write_char(c)
    }
  }
  buf.write_char('"')
  buf.to_string()
}

///|
pub impl Show for GitError with fn output(self, logger) {
  match self {
    InvalidObject(message) =>
      logger.write_string(
        "InvalidObject(" + git_error_show_string(message) + ")",
      )
    HashMismatch(expected, actual) =>
      logger.write_string(
        "HashMismatch(" +
        git_error_show_string(expected) +
        ", " +
        git_error_show_string(actual) +
        ")",
      )
    PackfileError(message) =>
      logger.write_string(
        "PackfileError(" + git_error_show_string(message) + ")",
      )
    ProtocolError(message) =>
      logger.write_string(
        "ProtocolError(" + git_error_show_string(message) + ")",
      )
    IoError(message) =>
      logger.write_string("IoError(" + git_error_show_string(message) + ")")
  }
}

///|
/// Tree entry in a Git tree object
pub struct TreeEntry {
  mode : String // "100644", "040000", etc.
  name : String
  id : ObjectId
}

///|
pub fn TreeEntry::new(mode : String, name : String, id : ObjectId) -> TreeEntry {
  { mode, name, id }
}

///|
/// Git commit object
pub struct Commit {
  tree : ObjectId
  parents : Array[ObjectId]
  author : String
  author_time : Int64
  author_tz : String
  committer : String
  commit_time : Int64
  committer_tz : String
  message : String
  encoding : String
  verbatim_message : Bool
}

///|
pub fn Commit::new(
  tree : ObjectId,
  parents : Array[ObjectId],
  author : String,
  author_time : Int64,
  author_tz : String,
  committer : String,
  commit_time : Int64,
  committer_tz : String,
  message : String,
  encoding? : String = "UTF-8",
  verbatim_message? : Bool = false,
) -> Commit {
  {
    tree,
    parents,
    author,
    author_time,
    author_tz,
    committer,
    commit_time,
    committer_tz,
    message,
    encoding,
    verbatim_message,
  }
}

///|
/// A Git object ready for packing
pub struct PackObject {
  obj_type : ObjectType
  data : Bytes // Uncompressed object content (without header)
  // Cached metadata for performance (computed during pack parsing)
  id : ObjectId // Object hash (cached)
  offset : Int // Offset in packfile (-1 if not from pack)
  crc32 : UInt // CRC32 of compressed data (0 if not computed)
}

///|
/// Delta encoding mode for packfile generation
pub(all) enum PackDeltaMode {
  NoDelta
  RefDelta
  OfsDelta
}

///|
/// Compression mode for packfile entries
pub(all) enum PackCompression {
  Default
  Stored
}

///|
pub fn PackObject::new(obj_type : ObjectType, data : Bytes) -> PackObject {
  let id = hash_object_content(obj_type, data)
  { obj_type, data, id, offset: -1, crc32: 0U }
}

///|
pub fn PackObject::with_metadata(
  obj_type : ObjectType,
  data : Bytes,
  id : ObjectId,
  offset : Int,
  crc32 : UInt,
) -> PackObject {
  { obj_type, data, id, offset, crc32 }
}