// Copyright 2026 Noverberrain.
// SPDX-License-Identifier: Apache-2.0

///|
fn listpack_backlen_size(length : UInt64) -> Int32 {
  if length <= 127UL {
    1
  } else if length < 16383UL {
    2
  } else if length < 2097151UL {
    3
  } else if length < 268435455UL {
    4
  } else {
    5
  }
}

///|
// The last byte belongs to the listpack terminator, never an entry payload.
fn Reader::lp_read(
  self : Reader,
  count : UInt64,
  base : Int32,
) -> BytesView raise ParseError {
  let remaining = self.input.length() - 1 - self.pos
  if remaining < 0 || count > remaining.to_uint64() {
    raise InvalidListpack(
      offset=base,
      inner_offset=self.pos,
      reason="truncated entry",
    )
  }
  self.read_bytes(count)
}

///|
fn Reader::lp_unsigned(
  self : Reader,
  width : Int32,
  base : Int32,
) -> UInt64 raise ParseError {
  let bytes = self.lp_read(width.to_uint64(), base)
  for i = width - 1, value = 0UL; i >= 0; i = i - 1 {
    continue i - 1, (value << 8) | bytes[i].to_uint64()
  } nobreak {
    value
  }
}

///|
fn listpack_signed(raw : UInt64, bits : Int32) -> Int64 {
  if bits == 64 {
    raw.reinterpret_as_int64()
  } else if raw >= 1UL << (bits - 1) {
    raw.reinterpret_as_int64() - (1L << bits)
  } else {
    raw.reinterpret_as_int64()
  }
}

///|
fn Reader::lp_string(
  self : Reader,
  count : UInt64,
  base : Int32,
  max_string_bytes : Int32,
  budget : DecodeBudget,
) -> Bytes raise ParseError {
  check_resource_limit("string-bytes", base, count, max_string_bytes)
  let bytes = self.lp_read(count, base)
  budget.charge(count, base)
  bytes.to_owned()
}

///|
fn Reader::read_listpack_entry(
  self : Reader,
  base : Int32,
  max_string_bytes : Int32,
  budget : DecodeBudget,
) -> Bytes raise ParseError {
  let start = self.pos
  let initial_used = budget.used
  errdefer {
    self.pos = start
    budget.used = initial_used
  }
  let tag = self.lp_read(1UL, base)[0].to_int()
  let mut integer = false
  let value = if tag < 128 {
    integer = true
    b"\{tag}"
  } else if tag < 192 {
    self.lp_string((tag & 63).to_uint64(), base, max_string_bytes, budget)
  } else if tag < 224 {
    integer = true
    let raw = ((tag & 31).to_uint64() << 8) | self.lp_unsigned(1, base)
    b"\{listpack_signed(raw, 13)}"
  } else if tag < 240 {
    let count = ((tag & 15).to_uint64() << 8) | self.lp_unsigned(1, base)
    self.lp_string(count, base, max_string_bytes, budget)
  } else if tag == 240 {
    let count = self.lp_unsigned(4, base)
    self.lp_string(count, base, max_string_bytes, budget)
  } else if tag <= 244 {
    integer = true
    let width = match tag {
      241 => 2
      242 => 3
      243 => 4
      _ => 8
    }
    let raw = self.lp_unsigned(width, base)
    b"\{listpack_signed(raw, width * 8)}"
  } else {
    raise InvalidListpack(
      offset=base,
      inner_offset=start,
      reason="unknown entry encoding or premature terminator",
    )
  }
  if integer {
    check_resource_limit(
      "string-bytes",
      base,
      value.length().to_uint64(),
      max_string_bytes,
    )
    budget.charge(value.length().to_uint64(), base)
  }
  let length = (self.pos - start).to_uint64()
  let width = listpack_backlen_size(length)
  let back_start = self.pos
  let bytes = self.lp_read(width.to_uint64(), base)
  for i in 0..> ((width - 1 - i) * 7)) & 127UL).to_int()
    let expected = if i == 0 { part } else { part | 128 }
    if bytes[i].to_int() != expected {
      raise InvalidListpack(
        offset=base,
        inner_offset=back_start + i,
        reason="incorrect entry back length",
      )
    }
  }
  value
}

///|
fn decode_hash_listpack(
  input : Bytes,
  base : Int32,
  max_string_bytes : Int32,
  max_hash_entries : Int32,
  budget : DecodeBudget,
) -> Array[HashEntry] raise ParseError {
  let (reader, declared) = open_listpack(input, base)
  if declared != 65535 {
    if declared % 2 != 0 {
      raise InvalidHash(offset=base, reason="odd listpack element count")
    }
    check_resource_limit(
      "hash-entries",
      base,
      (declared / 2).to_uint64(),
      max_hash_entries,
    )
  }
  let entries = []
  let seen : Map[Bytes, Unit] = Map([])
  while reader.pos < input.length() - 1 {
    check_resource_limit(
      "hash-entries",
      base,
      entries.length().to_uint64() + 1UL,
      max_hash_entries,
    )
    let field = reader.read_listpack_entry(base, max_string_bytes, budget)
    if reader.pos == input.length() - 1 {
      raise InvalidHash(offset=base, reason="odd listpack element count")
    }
    if seen.contains(field) {
      raise InvalidHash(offset=base, reason="duplicate field")
    }
    seen[field] = ()
    let value = reader.read_listpack_entry(base, max_string_bytes, budget)
    entries.push({ field, value, })
  }
  if declared != 65535 &&
    declared.to_uint64() != entries.length().to_uint64() * 2UL {
    raise InvalidListpack(
      offset=base,
      inner_offset=4,
      reason="element count mismatch",
    )
  }
  if entries.is_empty() {
    raise InvalidHash(offset=base, reason="empty hash")
  }
  entries
}

///|
fn open_listpack(
  input : Bytes,
  base : Int32,
) -> (Reader, Int32) raise ParseError {
  if input.length() < 7 {
    raise InvalidListpack(
      offset=base,
      inner_offset=0,
      reason="short header or missing terminator",
    )
  }
  let reader = Reader::new(input, max_input_bytes=input.length())
  let total = reader.read_u32_le()
  if total != input.length().reinterpret_as_uint() {
    raise InvalidListpack(
      offset=base,
      inner_offset=0,
      reason="total length mismatch",
    )
  }
  let declared = reader.read_unsigned(2, little_endian=true).to_int()
  if input[input.length() - 1] != b'\xff' {
    raise InvalidListpack(
      offset=base,
      inner_offset=input.length() - 1,
      reason="missing terminator",
    )
  }
  (reader, declared)
}