///| Pack index writer (v2)

///|
priv struct PackIndexEntry {
  id : ObjectId
  offset : Int
  crc : UInt
}

///|
/// Build a pack index (v2) from packfile bytes.
pub fn build_pack_index(pack : Bytes) -> Bytes raise GitError {
  let objects = parse_packfile(pack)
  build_pack_index_from_objects(pack, objects)
}

///|
/// Build a pack index (v2) from packfile bytes and parsed objects.
/// Uses cached metadata (id, offset, crc32) from PackObject when available.
pub fn build_pack_index_from_objects(
  pack : Bytes,
  objects : Array[PackObject],
) -> Bytes raise GitError {
  let entries : Array[PackIndexEntry] = []
  for obj in objects {
    // Use cached metadata if available (offset >= 0 means parsed from pack)
    if obj.offset >= 0 {
      entries.push({ id: obj.id, offset: obj.offset, crc: obj.crc32 })
    } else {
      // Fallback: compute values (shouldn't happen for parsed pack objects)
      let id = hash_object_content(obj.obj_type, obj.data)
      entries.push({ id, offset: 0, crc: 0U })
    }
  }
  entries.sort_by(fn(a, b) { compare_object_id(a.id, b.id) })
  let counts : Array[Int] = Array::make(256, 0)
  for entry in entries {
    let first = entry.id.bytes[0].to_int()
    counts[first] = counts[first] + 1
  }
  let out : Array[Byte] = []
  // Index header: magic + version
  out.push(b'\xff')
  out.push(b't')
  out.push(b'O')
  out.push(b'c')
  push_u32_be_from_int(out, 2)

  // Fanout table
  let mut sum = 0
  for i in 0..<256 {
    sum = sum + counts[i]
    push_u32_be_from_int(out, sum)
  }

  // Object name table
  for entry in entries {
    for b in entry.id.bytes {
      out.push(b)
    }
  }

  // CRC32 table
  for entry in entries {
    push_u32_be_from_uint(out, entry.crc)
  }

  // Offsets table (no large offsets due to Int range)
  for entry in entries {
    if entry.offset < 0 {
      raise GitError::PackfileError("Negative pack offset")
    }
    push_u32_be_from_int(out, entry.offset)
  }

  // Packfile checksum (trailer)
  if pack.length() < 20 {
    raise GitError::PackfileError("Packfile too short")
  }
  let trailer_offset = pack.length() - 20
  let pack_checksum = read_trailer_id(pack, trailer_offset)
  for b in pack_checksum.bytes {
    out.push(b)
  }

  // Index checksum
  let before_checksum = bytes_from_array(out)
  let index_checksum = sha1(before_checksum)
  for b in index_checksum.bytes {
    out.push(b)
  }
  bytes_from_array(out)
}

///|
/// Write a pack index to the filesystem.
pub fn write_pack_index(
  fs : &FileSystem,
  idx_path : String,
  pack : Bytes,
) -> Unit raise GitError {
  let idx = build_pack_index(pack)
  fs.write_file(idx_path, idx)
}

///|
/// Write a pack index using pre-parsed objects.
pub fn write_pack_index_from_objects(
  fs : &FileSystem,
  idx_path : String,
  pack : Bytes,
  objects : Array[PackObject],
) -> Unit raise GitError {
  let idx = build_pack_index_from_objects(pack, objects)
  fs.write_file(idx_path, idx)
}

///|
/// Write a pack index using pre-parsed objects with specific version.
pub fn write_pack_index_from_objects_versioned(
  fs : &FileSystem,
  idx_path : String,
  pack : Bytes,
  objects : Array[PackObject],
  version : Int,
) -> Unit raise GitError {
  let idx = if version == 1 {
    build_pack_index_v1_from_objects(pack, objects)
  } else {
    build_pack_index_from_objects(pack, objects)
  }
  fs.write_file(idx_path, idx)
}

///|
/// Build a pack index (v1) from packfile bytes and parsed objects.
fn build_pack_index_v1_from_objects(
  pack : Bytes,
  objects : Array[PackObject],
) -> Bytes raise GitError {
  let entries : Array[PackIndexEntry] = []
  for obj in objects {
    if obj.offset >= 0 {
      entries.push({ id: obj.id, offset: obj.offset, crc: obj.crc32 })
    } else {
      let id = hash_object_content(obj.obj_type, obj.data)
      entries.push({ id, offset: 0, crc: 0U })
    }
  }
  entries.sort_by(fn(a, b) { compare_object_id(a.id, b.id) })

  // Build fanout table (counts per first byte)
  let counts : Array[Int] = Array::make(256, 0)
  for entry in entries {
    let first = entry.id.bytes[0].to_int()
    counts[first] = counts[first] + 1
  }
  let out : Array[Byte] = []

  // v1 format: no header, starts directly with fanout
  // Fanout table (cumulative counts)
  let mut sum = 0
  for i in 0..<256 {
    sum = sum + counts[i]
    push_u32_be_from_int(out, sum)
  }

  // v1 entries: each entry is offset(4) + sha1(20) = 24 bytes
  for entry in entries {
    if entry.offset < 0 {
      raise GitError::PackfileError("Negative pack offset")
    }
    push_u32_be_from_int(out, entry.offset)
    for b in entry.id.bytes {
      out.push(b)
    }
  }

  // Packfile checksum (trailer)
  if pack.length() < 20 {
    raise GitError::PackfileError("Packfile too short")
  }
  let trailer_offset = pack.length() - 20
  let pack_checksum = read_trailer_id(pack, trailer_offset)
  for b in pack_checksum.bytes {
    out.push(b)
  }
  bytes_from_array(out)
}

///|
fn _scan_pack_entry_offsets(
  data : Bytes,
) -> (Array[Int], Array[Int]) raise GitError {
  if data.length() < 32 {
    raise GitError::PackfileError("Packfile too short")
  }
  if not(
      data[0] == b'P' && data[1] == b'A' && data[2] == b'C' && data[3] == b'K',
    ) {
    raise GitError::PackfileError("Invalid packfile magic")
  }
  let version = read_u32_be(data, 4)
  if version != 2 {
    raise GitError::PackfileError("Unsupported packfile version: \{version}")
  }
  let count = read_u32_be(data, 8)
  if count < 0 {
    raise GitError::PackfileError("Packfile object count out of range")
  }
  let mut offset = 12
  let starts : Array[Int] = []
  let ends : Array[Int] = []
  for _ in 0.. {
        let (content, after) = @zlib.zlib_decompress_at(data, offset) catch {
          e => raise GitError::PackfileError("Zlib error: \{e}")
        }
        if content.length() != size {
          raise GitError::PackfileError(
            "Object size mismatch: expected=\{size}, got=\{content.length()}",
          )
        }
        after
      }
      6 => {
        let (_, after_ref) = read_ofs_delta_offset(data, offset)
        let (delta, after) = @zlib.zlib_decompress_at(data, after_ref) catch {
          e => raise GitError::PackfileError("Zlib error: \{e}")
        }
        if delta.length() != size {
          raise GitError::PackfileError(
            "Delta size mismatch: expected=\{size}, got=\{delta.length()}",
          )
        }
        after
      }
      7 => {
        let (_, after_ref) = read_ref_delta_id(data, offset)
        let (delta, after) = @zlib.zlib_decompress_at(data, after_ref) catch {
          e => raise GitError::PackfileError("Zlib error: \{e}")
        }
        if delta.length() != size {
          raise GitError::PackfileError(
            "Delta size mismatch: expected=\{size}, got=\{delta.length()}",
          )
        }
        after
      }
      _ =>
        raise GitError::PackfileError(
          "Unknown packfile object type: \{type_id}",
        )
    }
    offset = after
    starts.push(start)
    ends.push(after)
  }
  let trailer_offset = data.length() - 20
  if offset != trailer_offset {
    raise GitError::PackfileError("Packfile length mismatch")
  }
  let pack_bytes = Bytes::from_array(
    FixedArray::makei(trailer_offset, fn(i) { data[i] }),
  )
  let computed = sha1(pack_bytes)
  let trailer = read_trailer_id(data, trailer_offset)
  if computed != trailer {
    raise GitError::HashMismatch(computed.to_hex(), trailer.to_hex())
  }
  (starts, ends)
}

///|
fn compare_object_id(a : ObjectId, b : ObjectId) -> Int {
  for i in 0..<20 {
    let av = a.bytes[i].to_int()
    let bv = b.bytes[i].to_int()
    if av != bv {
      return av - bv
    }
  }
  0
}

///|
pub fn crc32_range(data : Bytes, start : Int, end : Int) -> UInt raise GitError {
  if start < 0 || end < start || end > data.length() {
    raise GitError::PackfileError("Invalid CRC range")
  }
  let slice = Bytes::from_array(
    FixedArray::makei(end - start, fn(i) { data[start + i] }),
  )
  @zlib.crc32(slice)
}

///|
fn bytes_from_array(arr : Array[Byte]) -> Bytes {
  Bytes::from_array(FixedArray::makei(arr.length(), fn(i) { arr[i] }))
}

///|
fn push_u32_be_from_int(out : Array[Byte], value : Int) -> Unit {
  out.push(((value >> 24) & 0xff).to_byte())
  out.push(((value >> 16) & 0xff).to_byte())
  out.push(((value >> 8) & 0xff).to_byte())
  out.push((value & 0xff).to_byte())
}

///|
fn push_u32_be_from_uint(out : Array[Byte], value : UInt) -> Unit {
  let mask = Int::reinterpret_as_uint(0xff)
  let b0 = (value >> 24) & mask
  let b1 = (value >> 16) & mask
  let b2 = (value >> 8) & mask
  let b3 = value & mask
  out.push(UInt::reinterpret_as_int(b0).to_byte())
  out.push(UInt::reinterpret_as_int(b1).to_byte())
  out.push(UInt::reinterpret_as_int(b2).to_byte())
  out.push(UInt::reinterpret_as_int(b3).to_byte())
}