///| Unified hash state that dispatches to SHA-1 or SHA-256

///|
pub(all) enum HashState {
  HS1(Sha1State)
  HS256(Sha256State)
}

///|
pub fn HashState::new(algo : HashAlgorithm) -> HashState {
  match algo {
    Sha1 => HS1(Sha1State::new())
    Sha256 => HS256(Sha256State::new())
  }
}

///|
pub fn HashState::reset(self : HashState) -> Unit {
  match self {
    HS1(s) => s.reset()
    HS256(s) => s.reset()
  }
}

///|
pub fn HashState::update(self : HashState, data : Bytes) -> Unit {
  match self {
    HS1(s) => s.update(data)
    HS256(s) => s.update(data)
  }
}

///|
pub fn HashState::update_slice(
  self : HashState,
  data : Bytes,
  offset : Int,
  len : Int,
) -> Unit {
  match self {
    HS1(s) => s.update_slice(data, offset, len)
    HS256(s) => s.update_slice(data, offset, len)
  }
}

///|
pub fn HashState::update_byte(self : HashState, b : Byte) -> Unit {
  match self {
    HS1(s) => s.update_byte(b)
    HS256(s) => s.update_byte(b)
  }
}

///|
pub fn HashState::update_string(self : HashState, s : String) -> Unit {
  match self {
    HS1(st) => st.update_string(s)
    HS256(st) => st.update_string(s)
  }
}

///|
pub fn HashState::finish(self : HashState) -> ObjectId {
  match self {
    HS1(s) => s.finish()
    HS256(s) => s.finish()
  }
}

///|
/// Hash data prefix (first `len` bytes), dispatching by hash_size.
pub fn hash_prefix(data : Bytes, len : Int, hash_size? : Int = 20) -> ObjectId {
  if hash_size == 32 {
    sha256_prefix(data, len)
  } else {
    sha1_prefix(data, len)
  }
}

///|
/// Hash Array prefix (first `len` bytes), dispatching by hash_size.
pub fn hash_array_prefix(
  data : Array[Byte],
  len : Int,
  hash_size? : Int = 20,
) -> ObjectId {
  if hash_size == 32 {
    sha256_array_prefix(data, len)
  } else {
    sha1_array_prefix(data, len)
  }
}

///|
/// Hash object content using the specified algorithm.
pub fn hash_object_content_with_algo(
  algo : HashAlgorithm,
  obj_type : ObjectType,
  content : Bytes,
) -> ObjectId {
  let header = "\{obj_type.to_string()} \{content.length()}\u0000"
  match algo {
    Sha1 =>
      ObjectId::new(@hash.sha1_prefixed_raw(@utf8.encode(header), content))
    Sha256 => {
      let state = HashState::new(Sha256)
      state.update_string(header)
      state.update(content)
      state.finish()
    }
  }
}

///|
/// Create a git object (hash + compress) using the specified algorithm.
pub fn create_object_with_algo(
  algo : HashAlgorithm,
  obj_type : ObjectType,
  content : Bytes,
) -> (ObjectId, Bytes) {
  let raw = format_object(obj_type, content)
  let state = HashState::new(algo)
  state.update(raw)
  let id = state.finish()
  let compressed = @zlib.zlib_compress(raw)
  (id, compressed)
}

///|
pub fn create_tree_with_algo(
  algo : HashAlgorithm,
  entries : Array[TreeEntry],
) -> (ObjectId, Bytes) {
  let content : Array[Byte] = []
  for entry in entries {
    for c in entry.mode {
      content.push(c.to_int().to_byte())
    }
    content.push(b' ')
    for b in @utf8.encode(entry.name) {
      content.push(b)
    }
    content.push(b'\x00')
    for b in entry.id.bytes {
      content.push(b)
    }
  }
  create_object_with_algo(algo, ObjectType::Tree, array_to_bytes(content))
}

///|
pub fn create_commit_with_algo(
  algo : HashAlgorithm,
  commit : Commit,
) -> (ObjectId, Bytes) {
  create_object_with_algo(
    algo,
    ObjectType::Commit,
    serialize_commit_content(commit),
  )
}