// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// The fingerprint of a struct: the number every language writes after the user
// type ID, so that a reader can tell a schema it agrees with from one it only
// thinks it agrees with.
//
// The fingerprint is a string — one line per field, `identifier,type,ref,null;`
// — and the number is the low half of its MurmurHash3 x64 128, seeded with 47.
// Both halves are the specification's; a peer computes the same number from the
// same schema, which is the only reason the check means anything.

///|
/// The fingerprint of a struct, as the text the hash is taken of.
///
/// Fields enter in identifier order, which is not the order they are written
/// in: the fingerprint is about the schema, and the schema does not depend on
/// how the values are laid out. A field of a user type contributes `UNKNOWN`
/// rather than its own type ID, because two languages name their own types
/// differently and a hash over the name would never agree.
fn fingerprint(fields : Array[FieldMeta]) -> String {
  let sorted = fields.copy()
  sorted.sort_by(fn(a, b) { compare_identifier(a, b) })
  let out = StringBuilder()
  for f in sorted {
    out.write_string(f.identifier())
    out.write_char(',')
    out.write_string((if f.ty.is_user() { 0 } else { f.ty.code() }).to_string())
    out.write_char(',')
    // A field of a user type never counts as shared here, whatever the schema
    // says about the slot it sits in: both reference implementations compute
    // this flag from the type and not from the place, so a schema that says
    // otherwise would fingerprint differently from a peer that can read it.
    out.write_char(if f.track_ref && !f.ty.is_user() { '1' } else { '0' })
    out.write_char(',')
    out.write_char(if f.nullable() { '1' } else { '0' })
    // A list says what it holds, because a list of strings and a list of
    // integers are different schemas with the same type ID. The element is
    // written as a type and two zeroes: an element is not a slot, so it is
    // never shared and never absent.
    match f.element {
      Some(element) =>
        if f.ty == List || f.ty == Set {
          out.write_char('[')
          out.write_string(
            (if element.is_user() { 0 } else { element.code() }).to_string(),
          )
          out.write_string(",0,0]")
        }
      None => ()
    }
    out.write_char(';')
  }
  out.to_string()
}

///|
/// The number written after a user type ID in same-schema mode.
pub fn StructMeta::hash(self : StructMeta) -> Int32 {
  let text = fingerprint(self.fields())
  murmur3_low32(@utf8.encode(text), 47)
}

///|
/// The low thirty-two bits of MurmurHash3 x64 128 over `bytes`, seeded with
/// `seed`.
fn murmur3_low32(bytes : Bytes, seed : UInt32) -> Int32 {
  let c1 = 0x87c37b91114253d5UL
  let c2 = 0x4cf5ad432745937fUL
  let mut h1 = seed.to_uint64()
  let mut h2 = h1
  let n = bytes.length()
  let view = bytes[:]
  let blocks = n / 16
  for i in 0.. 8 {
    h2 = h2 ^ (rotl64(k2 * c2, 33) * c1)
  }
  if tail > 0 {
    h1 = h1 ^ (rotl64(k1 * c1, 31) * c2)
  }
  let len = n.to_uint64()
  h1 = h1 ^ len
  h2 = h2 ^ len
  h1 = h1 + h2
  h2 = h2 + h1
  h1 = fmix64(h1)
  h2 = fmix64(h2)
  h1 = h1 + h2
  (h1 & 0xffffffffUL).to_uint().reinterpret_as_int()
}

///|
/// The final avalanche of MurmurHash3.
fn fmix64(input : UInt64) -> UInt64 {
  let mut k = input ^ (input >> 33)
  k = k * 0xff51afd7ed558ccdUL
  k = k ^ (k >> 33)
  k = k * 0xc4ceb9fe1a85ec53UL
  k ^ (k >> 33)
}

///|
/// Rotate sixty-four bits left.
fn rotl64(x : UInt64, n : Int32) -> UInt64 {
  (x << n) | (x >> (64 - n))
}

///|
/// Eight bytes, little endian.
fn read_u64_le(view : BytesView, at : Int32) -> UInt64 {
  let mut out = 0UL
  for i in 0..<8 {
    out = out |
      (view.at(at + i).to_int().reinterpret_as_uint().to_uint64() << (i * 8))
  }
  out
}