// Glaze Library
// For the license information refer to glaze.hpp

#pragma once

#include "glaze/beve/header.hpp"
#include "glaze/core/common.hpp"
#include "glaze/core/opts.hpp"
#include "glaze/core/wrappers.hpp"
#include "glaze/reflection/get_name.hpp"
#include "glaze/util/primes_64.hpp"

#if GLZ_REFLECTION26
#include <meta>
#endif

#if defined(_MSC_VER) && !defined(__clang__)
// Turn off MSVC warning for unreferenced formal parameter, which is referenced in a constexpr branch
#pragma warning(push)
#pragma warning(disable : 4100 4189)
#endif

namespace glz
{
   // Check if a size_t value exists in an array (used for hash collision detection)
   constexpr bool contains(const size_t* data, const size_t size, const size_t val) noexcept
   {
      for (size_t i = 0; i < size; ++i) {
         if (data[i] == val) {
            return true;
         }
      }
      return false;
   }

   // Convert up to 7 bytes to uint64_t (for short key hashing)
   inline constexpr uint64_t to_uint64_n_below_8(const char* bytes, const size_t N) noexcept
   {
      uint64_t res{};
      if consteval {
         // Compile-time: build value byte-by-byte in little-endian order
         for (size_t i = 0; i < N; ++i) {
            res |= (uint64_t(uint8_t(bytes[i])) << (i << 3));
         }
      }
      else {
         switch (N) {
         case 1: {
            std::memcpy(&res, bytes, 1);
            break;
         }
         case 2: {
            std::memcpy(&res, bytes, 2);
            break;
         }
         case 3: {
            std::memcpy(&res, bytes, 3);
            break;
         }
         case 4: {
            std::memcpy(&res, bytes, 4);
            break;
         }
         case 5: {
            std::memcpy(&res, bytes, 5);
            break;
         }
         case 6: {
            std::memcpy(&res, bytes, 6);
            break;
         }
         case 7: {
            std::memcpy(&res, bytes, 7);
            break;
         }
         default: {
            // zero size case
            break;
         }
         }
         // On big endian: byteswap to match little-endian layout
         // Example for "abc" (N=3):
         //   LE memcpy: res = 0x0000000000636261 (bytes in LSB positions)
         //   BE memcpy: res = 0x6162630000000000 (bytes in MSB positions)
         //   BE after byteswap: res = 0x0000000000636261 (matches LE)
         if constexpr (std::endian::native == std::endian::big) {
            res = std::byteswap(res);
         }
      }
      return res;
   }

   // Convert N bytes to uint64_t (template version for compile-time N)
   template <size_t N = 8>
   constexpr uint64_t to_uint64(const char* bytes) noexcept
   {
      static_assert(N <= sizeof(uint64_t));
      if consteval {
         uint64_t res{};
         for (size_t i = 0; i < N; ++i) {
            res |= (uint64_t(uint8_t(bytes[i])) << (i << 3));
         }
         return res;
      }
      else {
         if constexpr (N == 8) {
            uint64_t res;
            std::memcpy(&res, bytes, N);
            if constexpr (std::endian::native == std::endian::big) {
               res = std::byteswap(res);
            }
            return res;
         }
         else {
            uint64_t res{};
            std::memcpy(&res, bytes, N);
            if constexpr (std::endian::native == std::endian::big) {
               res = std::byteswap(res);
            }
            return res;
         }
      }
   }

   // Named helper instead of IIFE so MSVC's lambda name mangling doesn't alias it
   // against unrelated IIFEs at deeper template instantiation depths.
   template <class Tuple, template <class> class Predicate, size_t... Is>
   consteval auto filter_indices_expand(std::index_sequence<Is...>)
   {
      constexpr bool matches[] = {Predicate<glz::tuple_element_t<Is, Tuple>>::value...};
      constexpr size_t count = (matches[Is] + ...);

      std::array<size_t, count> indices{};
      size_t index = 0;
      ((void)((matches[Is] ? (indices[index++] = Is, true) : false)), ...);
      return indices;
   }

   // Get indices of elements satisfying a predicate
   template <class Tuple, template <class> class Predicate>
   consteval auto filter_indices()
   {
      constexpr auto N = tuple_size_v<Tuple>;
      if constexpr (N == 0) {
         return std::array<size_t, 0>{};
      }
      else {
         return filter_indices_expand<Tuple, Predicate>(std::make_index_sequence<N>{});
      }
   }

   template <class T>
   concept is_object_key_type = std::convertible_to<T, std::string_view>;

   template <class T>
   using object_key_type = std::bool_constant<is_object_key_type<T>>;

   template <class T>
   using not_object_key_type = std::bool_constant<not is_object_key_type<T>>;

   namespace detail
   {
      // The purpose of this is to allocate a new string_view to only the portion of memory
      // that we are concerned with. This lets the compiler reduce the binary on
      // reflected names;
      template <size_t I, class V>
      struct get_name_alloc
      {
         static constexpr auto alias = get_name<get<I>(meta_v<V>)>();
         static constexpr auto value = join_v<alias>;
      };
   }

   template <class T, size_t I>
   consteval sv get_key_element()
   {
      using V = std::decay_t<T>;
      if constexpr (I == 0) {
         if constexpr (is_object_key_type<decltype(get<0>(meta_v<V>))>) {
            return get<0>(meta_v<V>);
         }
         else {
            return detail::get_name_alloc<0, V>::value;
            // return get_name<get<0>(meta_v<V>)>();
         }
      }
      else if constexpr (is_object_key_type<decltype(get<I - 1>(meta_v<V>))>) {
         return get<I - 1>(meta_v<V>);
      }
      else {
         return detail::get_name_alloc<I, V>::value;
         // return get_name<get<I>(meta_v<V>)>();
      }
   };

   template <class T>
   struct reflect;

   // ============================================================================
   // keys_wrapper: A pseudo-type that provides reflect<> interface for arbitrary key arrays
   // Used to enable hash-based lookup for variant IDs and other key sources
   // ============================================================================

   template <const auto& Keys>
   struct keys_wrapper
   {
      // This type exists only to satisfy template parameters for hash_info and decode_hash
   };

   template <class T>
   struct is_keys_wrapper_t : std::false_type
   {};

   template <const auto& Keys>
   struct is_keys_wrapper_t<keys_wrapper<Keys>> : std::true_type
   {};

   template <class T>
   inline constexpr bool is_keys_wrapper_v = is_keys_wrapper_t<T>::value;

   // Specialize reflect for keys_wrapper to provide the keys interface
   template <const auto& Keys>
   struct reflect<keys_wrapper<Keys>>
   {
      static constexpr auto size = Keys.size();

      // Convert keys to array of string_view
      static constexpr auto keys = []() {
         std::array<sv, size> result{};
         for (size_t i = 0; i < size; ++i) {
            result[i] = sv(Keys[i]);
         }
         return result;
      }();

      // Provide values as indices for completeness (needed by some hash paths)
      static constexpr auto values = []() {
         std::array<size_t, size> result{};
         for (size_t i = 0; i < size; ++i) {
            result[i] = i;
         }
         return result;
      }();
   };

   // ============================================================================
   // End of keys_wrapper
   // ============================================================================

   // MSVC requires this template specialization for when the tuple size if zero,
   // otherwise MSVC tries to instantiate calls of get<0> in invalid branches
   template <class T>
      requires(!glaze_merge_t<T> && (glaze_object_t<T> || glaze_flags_t<T> || glaze_enum_t<T>) &&
               (tuple_size_v<meta_t<T>> == 0))
   struct reflect<T>
   {
      static constexpr auto size = 0;
      static constexpr auto values = tuple{};
      static constexpr std::array<sv, 0> keys{};

      template <size_t I>
      using type = std::nullptr_t;
   };

   template <class T>
      requires(!meta_keys<T> && !glaze_merge_t<T> && (glaze_object_t<T> || glaze_flags_t<T> || glaze_enum_t<T>) &&
               (tuple_size_v<meta_t<T>> != 0))
   struct reflect<T>
   {
      using V = std::remove_cvref_t<T>;
      static constexpr auto value_indices = filter_indices<meta_t<V>, not_object_key_type>();

      static constexpr auto values = [] {
         return [&]<size_t... I>(std::index_sequence<I...>) { //
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmissing-braces"
#endif
            return tuple{get<value_indices[I]>(meta_v<T>)...}; //
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
         }(std::make_index_sequence<value_indices.size()>{}); //
      }();

      static constexpr auto size = tuple_size_v<decltype(values)>;

      static constexpr auto keys = [] {
         std::array<sv, size> res{};
         [&]<size_t... I>(std::index_sequence<I...>) { //
            ((res[I] = get_key_element<T, value_indices[I]>()), ...);
         }(std::make_index_sequence<value_indices.size()>{});
         return res;
      }();

      template <size_t I>
      using elem = decltype(get<I>(values));

      template <size_t I>
      using type = member_t<V, decltype(get<I>(values))>;
   };

   namespace detail
   {
      // Chains an outer member pointer with inner sub-field access for merge-in-meta types.
      template <class ParentType, size_t OuterIdx, size_t InnerIdx>
      struct merge_accessor
      {
         static constexpr auto outer_ptr = get<OuterIdx>(meta_v<ParentType>);
         using SubType = std::remove_cvref_t<typename member_value<std::decay_t<decltype(outer_ptr)>>::type>;

         constexpr decltype(auto) operator()(auto&& parent) const
         {
            if constexpr (glaze_object_t<SubType>) {
               return get_member(parent.*outer_ptr, get<InnerIdx>(reflect<SubType>::values));
            }
            else {
               // Aggregate/reflectable sub-type without glz::meta — access fields via to_tie
               static_assert(reflectable<SubType>, "glz::merge sub-types must be glaze_object_t or reflectable");
               return get<InnerIdx>(to_tie(parent.*outer_ptr));
            }
         }
      };

      struct merge_index_pair
      {
         size_t outer;
         size_t inner;
      };
   }

   template <class T>
      requires(glaze_merge_t<T>)
   struct reflect<T>
   {
      using V = std::remove_cvref_t<T>;

      static constexpr auto num_merge_members = tuple_size_v<meta_t<V>>;

      // Get the sub-type pointed to by the I-th member pointer in the merge
      template <size_t I>
      using sub_type_at = std::remove_cvref_t<typename member_value<std::decay_t<decltype(get<I>(meta_v<V>))>>::type>;

      // Per-sub-type sizes — computed once and reused below.
      static constexpr auto sub_sizes = []<size_t... I>(std::index_sequence<I...>) constexpr {
         return std::array<size_t, num_merge_members>{reflect<sub_type_at<I>>::size...};
      }(std::make_index_sequence<num_merge_members>{});

      // Total number of flattened fields
      static constexpr size_t size = []() constexpr {
         size_t total = 0;
         for (auto s : sub_sizes) total += s;
         return total;
      }();

      // Concatenated keys from all sub-types
      static constexpr auto keys = []() constexpr {
         std::array<sv, size> result{};
         size_t offset = 0;
         auto copy_keys = [&]<size_t OuterI>() constexpr {
            using SubType = sub_type_at<OuterI>;
            constexpr auto sub_size = reflect<SubType>::size;
            for (size_t j = 0; j < sub_size; ++j) {
               result[offset + j] = reflect<SubType>::keys[j];
            }
            offset += sub_size;
         };
         [&]<size_t... I>(std::index_sequence<I...>) constexpr {
            (copy_keys.template operator()<I>(), ...);
         }(std::make_index_sequence<num_merge_members>{});
         return result;
      }();

      static constexpr bool unique_keys = []() constexpr {
         for (size_t i = 0; i < size; ++i) {
            for (size_t j = i + 1; j < size; ++j) {
               if (keys[i] == keys[j]) {
                  return false;
               }
            }
         }
         return true;
      }();
      static_assert(unique_keys, "glz::merge sub-types must not have duplicate keys");

      // Pre-computed (outer, inner) pair for each flat index. Lets `values`
      // build merge_accessor types via array lookups rather than per-index
      // consteval function template instantiations.
      static constexpr auto flat_layout = []() constexpr {
         std::array<detail::merge_index_pair, size> result{};
         size_t flat = 0;
         for (size_t o = 0; o < num_merge_members; ++o) {
            for (size_t i = 0; i < sub_sizes[o]; ++i) {
               result[flat++] = {o, i};
            }
         }
         return result;
      }();

      // Flat values tuple built directly — avoids tuplet::tuple_cat (which has GCC issues)
      static constexpr auto values = []<size_t... I>(std::index_sequence<I...>) {
         return tuple{detail::merge_accessor<V, flat_layout[I].outer, flat_layout[I].inner>{}...};
      }(std::make_index_sequence<size>{});

      template <size_t I>
      using elem = decltype(get<I>(values));

      template <size_t I>
      using type = member_t<V, decltype(get<I>(values))>;
   };

   template <class T, size_t N>
   inline constexpr auto c_style_to_sv(const std::array<T, N>& arr)
   {
      std::array<sv, N> ret{};
      for (size_t i = 0; i < N; ++i) {
         ret[i] = arr[i];
      }
      return ret;
   }

   template <class T>
      requires(meta_keys<T> && glaze_t<T>)
   struct reflect<T>
   {
      using V = std::remove_cvref_t<T>;
      static constexpr auto size = tuple_size_v<meta_keys_t<T>>;

      static constexpr auto values = meta_wrapper_v<T>;

      static constexpr auto keys = c_style_to_sv(meta_keys_v<T>);

      template <size_t I>
      using elem = decltype(get<I>(values));

      template <size_t I>
      using type = member_t<V, decltype(get<I>(values))>;
   };

   // Note: is_reflect_enum<T> types do NOT get a reflect<T> specialization here.
   // P2996 reflection must be done inline in the handlers (to<JSON, T>, from<JSON, T>)
   // because P2996 operations like std::meta::enumerators_of require a consteval context
   // that is not available during template class instantiation.

   // Note: is_reflect_enum handlers use the existing enum_nameof and enum_count
   // from get_name.hpp. The enum values are obtained using P2996 splicing inline.

   template <class T>
      requires(is_memory_object<T>)
   struct reflect<T> : reflect<memory_type<T>>
   {};

   template <class T>
      requires(glaze_array_t<T>)
   struct reflect<T>
   {
      using V = std::remove_cvref_t<T>;

      static constexpr auto values = meta_v<V>;

      static constexpr auto size = tuple_size_v<decltype(values)>;

      template <size_t I>
      using elem = decltype(get<I>(values));

      template <size_t I>
      using type = member_t<V, decltype(get<I>(values))>;
   };

   template <class T>
      requires reflectable<T>
   struct reflect<T>
   {
      using V = std::remove_cvref_t<T>;
      using tie_type = decltype(to_tie(std::declval<T&>()));

      static constexpr auto keys = member_names<V>;
      static constexpr auto size = keys.size();

      template <size_t I>
      using elem = decltype(get<I>(std::declval<tie_type>()));

      template <size_t I>
      using type = member_t<V, decltype(get<I>(std::declval<tie_type>()))>;
   };

   template <class T>
      requires readable_map_t<T>
   struct reflect<T>
   {
      static constexpr auto size = 0;
   };

   // The type of the field before get_member is applied
   template <class T, size_t I>
   using elem_t = reflect<T>::template elem<I>;

   // The type of the field after get_member is applied
   template <class T, size_t I>
   using refl_t = reflect<T>::template type<I>;

   // The decayed type after get_member is called
   template <class T, size_t I>
   using field_t = std::remove_cvref_t<refl_t<T, I>>;

   // Check if a custom_t getter (To) returns a nullable type (write side).
   // Complement of custom_type_is_nullable which checks the From/setter (read side).
   template <class V>
   consteval bool custom_getter_returns_nullable()
   {
      if constexpr (!is_specialization_v<V, custom_t>) {
         return false;
      }
      else {
         using To = typename V::to_t;
         using ParentT = std::remove_reference_t<decltype(std::declval<V&>().val)>;

         if constexpr (std::is_member_pointer_v<To>) {
            if constexpr (std::is_member_function_pointer_v<To>) {
               using Ret = std::decay_t<typename return_type<To>::type>;
               return null_t<Ret>;
            }
            else if constexpr (std::is_member_object_pointer_v<To>) {
               using Value = std::decay_t<decltype(std::declval<ParentT&>().*(std::declval<To>()))>;
               if constexpr (is_specialization_v<Value, std::function>) {
                  using Ret = std::decay_t<typename function_traits<Value>::result_type>;
                  return null_t<Ret>;
               }
               else {
                  return null_t<Value>;
               }
            }
            else {
               return false;
            }
         }
         else if constexpr (std::invocable<To, ParentT&>) {
            using Ret = std::decay_t<std::invoke_result_t<To, ParentT&>>;
            return null_t<Ret>;
         }
         else if constexpr (std::invocable<To, const ParentT&>) {
            using Ret = std::decay_t<std::invoke_result_t<To, const ParentT&>>;
            return null_t<Ret>;
         }
         else if constexpr (std::invocable<To, ParentT&, context&>) {
            using Ret = std::decay_t<std::invoke_result_t<To, ParentT&, context&>>;
            return null_t<Ret>;
         }
         else {
            return false;
         }
      }
   }

   // Check if a glaze_value_t wraps a nullable inner type (write side).
   template <class V>
   consteval bool glaze_value_is_nullable()
   {
      if constexpr (glaze_value_t<V>) {
         return null_t<remove_meta_wrapper_t<V>>;
      }
      else {
         return false;
      }
   }

   // Runtime check: is a glaze_value_t field currently null?
   template <class T, size_t I, class Value, class Tie>
   bool is_glaze_value_field_null(Value&& value, Tie&& t)
   {
      using val_t = field_t<T, I>;
      using Inner = remove_meta_wrapper_t<val_t>;
      decltype(auto) element = [&]() -> decltype(auto) {
         if constexpr (reflectable<T>) {
            return get<I>(t);
         }
         else {
            return get<I>(reflect<T>::values);
         }
      };
      auto&& inner_val = get_member(get_member(value, element()), meta_wrapper_v<val_t>);
      if constexpr (nullable_value_t<Inner>) {
         return !inner_val.has_value();
      }
      else {
         return !bool(inner_val);
      }
   }

   // A type whose default value can be detected at runtime for skip_default_members
   template <class T>
   concept has_skippable_default = str_t<T> || bool_t<T> || num_t<T> || (range<T> && !str_t<T> && has_empty<T>);

   // Check if a member value equals its default (for skip_default_members)
   template <class T>
   GLZ_ALWAYS_INLINE bool is_default_value(const T& value)
   {
      if constexpr (str_t<T> && has_empty<T>) {
         return value.empty();
      }
      else if constexpr (bool_t<T>) {
         return !value;
      }
      else if constexpr (num_t<T>) {
         return value == T{0};
      }
      else if constexpr (range<T> && has_empty<T>) {
         return value.empty();
      }
      else {
         return false;
      }
   }

   // Whether `meta<T>::skip` excludes the field at index I from the given operation.
   //
   // Consume this as `if constexpr (skipped_by_meta<...>) { ... } else { serialize/parse }`, never as
   // an early `return` inside an `if constexpr` block. A `return` stops the field from being touched
   // at runtime, but the code that follows it in the same block is still instantiated -- so the
   // skipped field's serializer is compiled anyway, and a field skipped precisely because its type
   // cannot be serialized in this context (a type with no `to`/`from`, or a non-owning view that
   // GLZ_ASSERT_OWNS_ITS_BYTES rejects for a streaming read) still breaks the build.
   template <class T, size_t I, operation Op>
   inline constexpr bool skipped_by_meta = [] {
      using V = std::remove_cvref_t<T>;
      if constexpr (meta_has_skip<V>) {
         return meta<V>::skip(reflect<V>::keys[I], meta_context{.op = Op});
      }
      else {
         return false;
      }
   }();

   // Whether `meta<T>::skip` excludes any field at all from the given operation.
   //
   // `meta::skip` is answered at compile time, so a `skip()` that never fires for an operation costs
   // that operation nothing. Asking this rather than whether `meta<T>::skip` merely exists is what
   // lets a read-side skip leave the writers alone.
   template <class T, operation Op>
   inline constexpr bool any_skipped_by_meta = [] {
      constexpr auto N = reflect<T>::size;
      if constexpr (meta_has_skip<T> && N > 0) {
         return []<size_t... I>(std::index_sequence<I...>) {
            return (skipped_by_meta<T, I, Op> || ...);
         }(std::make_index_sequence<N>{});
      }
      else {
         return false;
      }
   }();

   template <auto Opts, class T>
   inline constexpr bool maybe_skipped = [] {
      if constexpr (reflect<T>::size > 0) {
         constexpr auto N = reflect<T>::size;
         // skip_if decides per value at runtime, so its mere presence forces the dynamic path. skip
         // decides at compile time, so it only forces the dynamic path when it actually excludes a
         // field from serialization -- a skip() that only fires on parse leaves writing on the static
         // path, where separators and member counts are placed at compile time.
         if constexpr (meta_has_skip_if<T> || any_skipped_by_meta<T, operation::serialize>) {
            return true;
         }
         else {
            constexpr bool write_function_pointers = check_write_function_pointers(Opts);
            return [&]<size_t... I>(std::index_sequence<I...>) {
               return ((always_skipped<field_t<T, I>> ||
                        (!write_function_pointers && is_member_function_pointer<field_t<T, I>>) ||
                        (Opts.skip_null_members && (null_t<field_t<T, I>> ||
                                                    (is_specialization_v<field_t<T, I>, custom_t> &&
                                                     custom_getter_returns_nullable<field_t<T, I>>()) ||
                                                    glaze_value_is_nullable<field_t<T, I>>())) ||
                        (check_skip_default_members(Opts) && has_skippable_default<field_t<T, I>>)) ||
                       ...);
            }(std::make_index_sequence<N>{});
         }
      }
      else {
         return false;
      }
   }();
}

namespace glz
{
   template <size_t I, class T>
   constexpr auto key_name_v = [] {
      if constexpr (reflectable<T>) {
         return get<I>(member_names<T>);
      }
      else {
         return reflect<T>::keys[I];
      }
   }();

   // Check if a custom_t setter (From) accepts a nullable type (read side).
   // Complement of custom_getter_returns_nullable which checks the To/getter (write side).
   template <class V, class From>
   consteval bool custom_type_is_nullable()
   {
      if constexpr (std::is_member_pointer_v<From>) {
         if constexpr (std::is_member_function_pointer_v<From>) {
            using Ret = typename return_type<From>::type;
            if constexpr (std::is_void_v<Ret>) {
               using Tuple = typename inputs_as_tuple<From>::type;
               if constexpr (glz::tuple_size_v<Tuple> == 1) {
                  using Input = std::decay_t<glz::tuple_element_t<0, Tuple>>;
                  return bool(null_t<Input>);
               }
            }
         }
         else if constexpr (std::is_member_object_pointer_v<From>) {
            using Value = std::decay_t<decltype(std::declval<V>().val.*(std::declval<V>().from))>;
            if constexpr (is_specialization_v<Value, std::function>) {
               using Ret = typename function_traits<Value>::result_type;

               if constexpr (std::is_void_v<Ret>) {
                  using Tuple = typename function_traits<Value>::arguments;
                  if constexpr (glz::tuple_size_v<Tuple> == 1) {
                     using Input = std::decay_t<glz::tuple_element_t<0, Tuple>>;
                     return bool(null_t<Input>);
                  }
               }
            }
            else {
               return bool(null_t<Value>);
            }
         }
      }
      else {
         if constexpr (is_invocable_concrete<From>) {
            using Ret = invocable_result_t<From>;
            if constexpr (std::is_void_v<Ret>) {
               using Tuple = invocable_args_t<From>;
               constexpr auto N = glz::tuple_size_v<Tuple>;
               if constexpr (N == 2) {
                  using Input = std::decay_t<glz::tuple_element_t<1, Tuple>>;
                  return bool(null_t<Input>);
               }
            }
         }
      }

      return false;
   }

   // Runtime check: invoke a custom_t getter and return whether the result is null
   template <class V>
   bool custom_getter_is_null(V&& custom_val, auto&& ctx)
   {
      using CV = std::remove_cvref_t<V>;
      using To = typename CV::to_t;

      auto check_null = [](auto&& result) {
         using Ret = std::decay_t<decltype(result)>;
         if constexpr (nullable_value_t<Ret>) {
            return !result.has_value();
         }
         else {
            return !bool(result);
         }
      };

      if constexpr (std::is_member_pointer_v<To>) {
         if constexpr (std::is_member_function_pointer_v<To>) {
            return check_null((custom_val.val.*(custom_val.to))());
         }
         else if constexpr (std::is_member_object_pointer_v<To>) {
            auto& to_val = custom_val.val.*(custom_val.to);
            using Func = std::decay_t<decltype(to_val)>;
            if constexpr (is_specialization_v<Func, std::function>) {
               return check_null(to_val());
            }
            else {
               return check_null(to_val);
            }
         }
         else {
            return false;
         }
      }
      else if constexpr (std::invocable<To, decltype(custom_val.val)>) {
         return check_null(std::invoke(custom_val.to, custom_val.val));
      }
      else if constexpr (std::invocable<To, decltype(custom_val.val), std::remove_reference_t<decltype(ctx)>&>) {
         return check_null(std::invoke(custom_val.to, custom_val.val, ctx));
      }
      else {
         return false;
      }
   }

   // Check if a custom_t field at index I is null, given the parent value and tie.
   // Used by JSON/CBOR/BEVE write paths to skip null custom getter results.
   template <class T, size_t I, class Value, class Tie, class Ctx>
   bool is_custom_field_null(Value&& value, Tie&& t, Ctx&& ctx)
   {
      decltype(auto) custom_val = [&]() -> decltype(auto) {
         if constexpr (reflectable<T>) {
            return get_member(value, get<I>(t));
         }
         else {
            return get_member(value, get<I>(reflect<T>::values));
         }
      }();
      return custom_getter_is_null(custom_val, ctx);
   }

   template <class T, auto Opts>
   constexpr auto required_fields()
   {
      constexpr auto N = reflect<T>::size;

      bit_array<N> fields{};
      if constexpr (Opts.error_on_missing_keys) {
         for_each<N>([&]<auto I>() constexpr {
            using V = std::decay_t<refl_t<T, I>>;

            // Fields whose type is always_skipped (hidden, skip, includer, or a
            // type that opted out via meta::value = skip{}) are never written,
            // so they must not be marked required.
            if constexpr (always_skipped<V>) {
               fields[I] = false;
               return;
            }

            // Check if field is skipped during parse - if so, don't require it
            if constexpr (skipped_by_meta<T, I, operation::parse>) {
               fields[I] = false;
               return;
            }

            // Check if meta<T>::requires_key customization point exists
            if constexpr (meta_has_requires_key<T>) {
               constexpr auto key = reflect<T>::keys[I];
               constexpr bool is_nullable = [] {
                  if constexpr (is_specialization_v<V, custom_t>) {
                     using From = typename V::from_t;
                     return custom_type_is_nullable<V, From>();
                  }
                  else if constexpr (is_cast<V>) {
                     using CastType = typename V::cast_type;
                     return null_t<CastType>;
                  }
                  else if constexpr (glaze_value_t<V>) {
                     using Inner = remove_meta_wrapper_t<V>;
                     return null_t<Inner>;
                  }
                  else {
                     return null_t<V>;
                  }
               }();
               fields[I] = meta<T>::requires_key(key, is_nullable);
            }
            else if constexpr (is_specialization_v<V, custom_t>) {
               using From = typename V::from_t;

               // If we are reading a glz::custom_t, we must deduce the input argument and not require the key if it is
               // optional This allows error_on_missing_keys to work properly with glz::custom_t wrapping optional types
               constexpr bool nullable_in_custom = custom_type_is_nullable<V, From>();

               fields[I] = !Opts.skip_null_members || !(std::same_as<From, skip> || nullable_in_custom);
            }
            else if constexpr (is_cast<V>) {
               // Handle cast_t by checking if the cast type is nullable
               using CastType = typename V::cast_type;
               fields[I] = !Opts.skip_null_members || !null_t<CastType>;
            }
            else if constexpr (glaze_value_t<V>) {
               // Handle value types (structs with glaze::value) by checking the underlying type
               using Inner = remove_meta_wrapper_t<V>;
               fields[I] = !Opts.skip_null_members || !null_t<Inner>;
            }
            else {
               fields[I] = !Opts.skip_null_members || !null_t<V>;
            }
         });
      }
      return fields;
   }
}

namespace glz::detail
{
   // from
   // https://stackoverflow.com/questions/55941964/how-to-filter-duplicate-types-from-tuple-c
   template <class T, class... Ts>
   struct unique
   {
      using type = T;
   };

   template <template <class...> class T, class... Ts, class U, class... Us>
   struct unique<T<Ts...>, U, Us...>
      : std::conditional_t<(std::is_same_v<U, Ts> || ...), unique<T<Ts...>, Us...>, unique<T<Ts..., U>, Us...>>
   {};

   template <class... Ts>
   struct unique_variant : unique<std::variant<>, Ts...>
   {};

   template <class T>
   struct tuple_ptr_variant;

   template <class... Ts>
   struct tuple_ptr_variant<glz::tuple<Ts...>> : unique<std::variant<>, std::add_pointer_t<Ts>...>
   {};

   template <class... Ts>
   struct tuple_ptr_variant<std::tuple<Ts...>> : unique<std::variant<>, std::add_pointer_t<Ts>...>
   {};

   template <class... Ts>
   struct tuple_ptr_variant<std::pair<Ts...>> : unique<std::variant<>, std::add_pointer_t<Ts>...>
   {};

   template <class T, class = std::make_index_sequence<reflect<T>::size>>
   struct member_tuple_type;

   template <class T, size_t... I>
   struct member_tuple_type<T, std::index_sequence<I...>>
   {
      using type =
         std::conditional_t<sizeof...(I) == 0, tuple<>, tuple<std::remove_cvref_t<member_t<T, refl_t<T, I>>>...>>;
   };

   template <class T>
   using member_tuple_t = typename member_tuple_type<T>::type;

   template <class T, class = std::make_index_sequence<reflect<T>::size>>
   struct value_variant;

   template <class T, size_t... I>
   struct value_variant<T, std::index_sequence<I...>>
   {
      using type = typename unique_variant<std::remove_cvref_t<elem_t<T, I>>...>::type;
   };

   template <class T>
   using value_variant_t = typename value_variant<T>::type;

   template <class T>
   inline constexpr auto make_array()
   {
      return []<size_t... I>(std::index_sequence<I...>) {
         using value_t = value_variant_t<T>;
         return std::array<value_t, reflect<T>::size>{get<I>(reflect<T>::values)...};
      }(std::make_index_sequence<reflect<T>::size>{});
   }

   template <class Tuple, std::size_t... Is>
   inline constexpr auto tuple_runtime_getter(std::index_sequence<Is...>)
   {
      using value_t = typename tuple_ptr_variant<Tuple>::type;
      using tuple_ref = std::add_lvalue_reference_t<Tuple>;
      using getter_t = value_t (*)(tuple_ref);
      return std::array<getter_t, glz::tuple_size_v<Tuple>>{+[](tuple_ref t) -> value_t {
         if constexpr (is_std_tuple<Tuple>) {
            return &std::get<Is>(t);
         }
         else {
            return &glz::get<Is>(t);
         }
      }...};
   }

   template <class Tuple>
   inline auto get_runtime(Tuple&& t, const size_t index)
   {
      using T = std::decay_t<Tuple>;
      static constexpr auto indices = std::make_index_sequence<glz::tuple_size_v<T>>{};
      static constexpr auto runtime_getter = tuple_runtime_getter<T>(indices);
      return runtime_getter[index](t);
   }
}

namespace glz
{
   template <auto Enum>
      requires(std::is_enum_v<decltype(Enum)>)
   constexpr sv enum_name_v = []() -> std::string_view {
      using T = std::decay_t<decltype(Enum)>;

      if constexpr (glaze_t<T>) {
         using U = std::underlying_type_t<T>;
         return reflect<T>::keys[static_cast<U>(Enum)];
      }
      else {
         static_assert(false_v<decltype(Enum)>, "Enum requires glaze metadata for name");
      }
   }();
}

namespace glz
{
   // ============================================================================
   // Integer key hashing for enum values and integral variant IDs
   // ============================================================================

   enum struct int_hash_type {
      direct, // Sequential values starting at 0: value as index
      offset, // Sequential values with offset: value - min_value
      two_element, // N==2: compare against first value
      power_of_two, // Powers of 2 (flags): countr_zero(value)
      small_range, // Sparse lookup table for small ranges
      modular, // Perfect hash: (value * seed) % table_size
      modular_shifted, // Perfect hash with shift: ((value >> shift) * seed) % table_size
      linear_search, // Fallback: linear scan through values (N <= 16)
      binary_search // Fallback: binary search through sorted values (N > 16)
   };

   template <size_t N, size_t TableSize>
   struct int_keys_info_t
   {
      // Note: table must be first to work around Apple clang NTTP bug where
      // arrays at the end of structs get corrupted when passed as template parameters
      std::array<uint8_t, TableSize> table{}; // For small_range/modular: maps key → index
      int_hash_type type{};
      int64_t min_value{};
      int64_t max_value{};
      uint64_t seed{};
      size_t table_size{};
      uint8_t shift{}; // Right shift to apply before modular hash (handles common power-of-2 factors)
   };

   // Specialization for when no table is needed
   template <size_t N>
   struct int_keys_info_t<N, 0>
   {
      int_hash_type type{};
      int64_t min_value{};
      int64_t max_value{};
      uint64_t seed{};
      size_t table_size{};
      uint8_t shift{}; // Right shift to apply before modular hash (handles common power-of-2 factors)
   };

   template <class T>
      requires std::is_enum_v<T>
   constexpr auto make_int_keys_info()
   {
      using U = std::underlying_type_t<T>;
      constexpr auto N = reflect<T>::size;

      if constexpr (N == 0) {
         return int_keys_info_t<0, 0>{};
      }
      else if constexpr (N == 1) {
         // For single-element enums, check if value is 0 (direct) or needs offset
         constexpr auto value = static_cast<U>(glz::get<0>(reflect<T>::values));
         if constexpr (value == 0) {
            return int_keys_info_t<1, 0>{.type = int_hash_type::direct};
         }
         else {
            return int_keys_info_t<1, 0>{.type = int_hash_type::offset, .min_value = static_cast<int64_t>(value)};
         }
      }
      else if constexpr (N == 2) {
         // For two-element enums, compare against first value
         constexpr auto first_value = static_cast<int64_t>(static_cast<U>(glz::get<0>(reflect<T>::values)));
         constexpr auto second_value = static_cast<int64_t>(static_cast<U>(glz::get<1>(reflect<T>::values)));
         return int_keys_info_t<2, 0>{
            .type = int_hash_type::two_element, .min_value = first_value, .max_value = second_value};
      }
      else {
         // Extract values into array for analysis
         constexpr auto vals = []() constexpr {
            constexpr auto size = reflect<T>::size;
            std::array<U, size> result{};
            for_each<size>([&]<size_t I>() { result[I] = static_cast<U>(glz::get<I>(reflect<T>::values)); });
            return result;
         }();

         // Find min/max
         constexpr auto min_max = [&]() {
            U min_val = vals[0], max_val = vals[0];
            for (const auto v : vals) {
               if (v < min_val) min_val = v;
               if (v > max_val) max_val = v;
            }
            return std::pair{min_val, max_val};
         }();

         constexpr U min_val = min_max.first;
         constexpr U max_val = min_max.second;
         constexpr auto range = static_cast<size_t>(max_val - min_val);

         // Strategy 1: Dense sequential (most common case)
         constexpr bool is_sequential = [&]() {
            if (range + 1 != N) return false;
            std::array<bool, N> seen{};
            for (const auto v : vals) {
               const auto idx = static_cast<size_t>(v - min_val);
               if (idx >= N || seen[idx]) return false;
               seen[idx] = true;
            }
            return true;
         }();

         // Strategy 2: Powers of 2 (flag enums)
         constexpr auto power_of_two_info = [&]() {
            using UnsignedU = std::make_unsigned_t<U>;
            size_t max_bit = 0;

            for (const auto v : vals) {
               const auto uv = static_cast<UnsignedU>(v);
               if (uv == 0 || !std::has_single_bit(uv)) {
                  return std::pair{false, size_t{0}};
               }
               const auto bit = static_cast<size_t>(std::countr_zero(uv));
               if (bit > max_bit) max_bit = bit;
            }

            // Verify no collisions in bit positions
            std::array<bool, 64> used{};
            for (const auto v : vals) {
               const auto bit = std::countr_zero(static_cast<UnsignedU>(v));
               if (used[bit]) return std::pair{false, size_t{0}};
               used[bit] = true;
            }

            return std::pair{true, max_bit + 1};
         }();

         constexpr size_t sparse_threshold = 256;
         constexpr auto table_size = std::bit_ceil(N * 2);

         // Use proper if-else-if chain so only one return type is deduced
         if constexpr (is_sequential) {
            if constexpr (min_val == 0) {
               return int_keys_info_t<N, 0>{.type = int_hash_type::direct};
            }
            else {
               return int_keys_info_t<N, 0>{.type = int_hash_type::offset, .min_value = min_val};
            }
         }
         else if constexpr (power_of_two_info.first) {
            constexpr auto tbl_size = power_of_two_info.second;
            int_keys_info_t<N, tbl_size> info{.type = int_hash_type::power_of_two, .table_size = tbl_size};
            info.table.fill(static_cast<uint8_t>(N));

            using UnsignedU = std::make_unsigned_t<U>;
            for (size_t i = 0; i < N; ++i) {
               const auto bit = std::countr_zero(static_cast<UnsignedU>(vals[i]));
               info.table[bit] = static_cast<uint8_t>(i);
            }
            return info;
         }
         else if constexpr (range < sparse_threshold) {
            // Strategy 3: Small range → sparse lookup table
            int_keys_info_t<N, sparse_threshold> info{
               .type = int_hash_type::small_range, .min_value = min_val, .max_value = max_val, .table_size = range + 1};
            info.table.fill(static_cast<uint8_t>(N));

            for (size_t i = 0; i < N; ++i) {
               info.table[static_cast<size_t>(vals[i] - min_val)] = static_cast<uint8_t>(i);
            }
            return info;
         }
         else {
            // Strategy 4: Modular perfect hash (fallback)
            // First try standard modular hash (faster lookup)
            constexpr auto modular_info = [&]() {
               for (const auto prime : primes_64) {
                  std::array<bool, table_size> used{};
                  bool collision = false;

                  for (size_t i = 0; i < N; ++i) {
                     const auto h = (static_cast<uint64_t>(vals[i]) * prime) % table_size;
                     if (used[h]) {
                        collision = true;
                        break;
                     }
                     used[h] = true;
                  }

                  if (!collision) {
                     return std::pair{true, prime};
                  }
               }
               return std::pair{false, uint64_t{0}};
            }();

            if constexpr (modular_info.first) {
               // Standard modular hash works
               int_keys_info_t<N, table_size> info{
                  .type = int_hash_type::modular, .seed = modular_info.second, .table_size = table_size};
               info.table.fill(static_cast<uint8_t>(N));

               for (size_t i = 0; i < N; ++i) {
                  const auto h = (static_cast<uint64_t>(vals[i]) * info.seed) % table_size;
                  info.table[h] = static_cast<uint8_t>(i);
               }
               return info;
            }
            else {
               // Strategy 5: Modular hash with shift for sparse enums with common power-of-2 factors
               constexpr uint8_t common_shift = [&]() -> uint8_t {
                  uint8_t min_trailing = 64;
                  for (const auto v : vals) {
                     if (v != 0) {
                        const auto trailing = static_cast<uint8_t>(std::countr_zero(static_cast<uint64_t>(v)));
                        if (trailing < min_trailing) {
                           min_trailing = trailing;
                        }
                     }
                  }
                  return min_trailing == 64 ? 0 : min_trailing;
               }();

               constexpr auto shifted_info = [&]() {
                  for (const auto prime : primes_64) {
                     std::array<bool, table_size> used{};
                     bool collision = false;

                     for (size_t i = 0; i < N; ++i) {
                        const auto shifted = static_cast<uint64_t>(vals[i]) >> common_shift;
                        const auto h = (shifted * prime) % table_size;
                        if (used[h]) {
                           collision = true;
                           break;
                        }
                        used[h] = true;
                     }

                     if (!collision) {
                        return std::pair{true, prime};
                     }
                  }
                  return std::pair{false, uint64_t{0}};
               }();

               if constexpr (shifted_info.first) {
                  int_keys_info_t<N, table_size> info{.type = int_hash_type::modular_shifted,
                                                      .seed = shifted_info.second,
                                                      .table_size = table_size,
                                                      .shift = common_shift};
                  info.table.fill(static_cast<uint8_t>(N));

                  for (size_t i = 0; i < N; ++i) {
                     const auto shifted = static_cast<uint64_t>(vals[i]) >> common_shift;
                     const auto h = (shifted * info.seed) % table_size;
                     info.table[h] = static_cast<uint8_t>(i);
                  }
                  return info;
               }
               else {
                  // Fallback: linear search for small N, binary search for larger N
                  if constexpr (N <= 16) {
                     return int_keys_info_t<N, 0>{.type = int_hash_type::linear_search};
                  }
                  else {
                     return int_keys_info_t<N, 0>{.type = int_hash_type::binary_search};
                  }
               }
            }
         }
      }
   }

   template <class T>
      requires std::is_enum_v<T>
   constexpr auto enum_index_info = make_int_keys_info<T>();

   // Array of enum underlying values for runtime indexing
   template <class T>
      requires std::is_enum_v<T>
   constexpr auto enum_values_array = []<size_t... I>(std::index_sequence<I...>) {
      using U = std::underlying_type_t<T>;
      return std::array<U, sizeof...(I)>{static_cast<U>(glz::get<I>(reflect<T>::values))...};
   }(std::make_index_sequence<reflect<T>::size>{});

   template <class T, auto Info>
   struct enum_value_to_index
   {
      using U = std::underlying_type_t<T>;
      static constexpr auto N = reflect<T>::size;

      GLZ_ALWAYS_INLINE static constexpr size_t op(U value) noexcept
      {
         using enum int_hash_type;

         if constexpr (Info.type == direct) {
            // Bounds check done by caller
            return static_cast<size_t>(value);
         }
         else if constexpr (Info.type == offset) {
            return static_cast<size_t>(value - Info.min_value);
         }
         else if constexpr (Info.type == two_element) {
            // Compare against first value: if match return 0, else check second
            // Use uint64_t to handle both signed and unsigned underlying types correctly
            if (static_cast<uint64_t>(value) == static_cast<uint64_t>(Info.min_value)) {
               return 0;
            }
            else if (static_cast<uint64_t>(value) == static_cast<uint64_t>(Info.max_value)) {
               return 1;
            }
            return N; // Not found
         }
         else if constexpr (Info.type == power_of_two) {
            using UnsignedU = std::make_unsigned_t<U>;
            const auto uv = static_cast<UnsignedU>(value);
            // Must be a non-zero power of 2, otherwise countr_zero gives wrong result
            if (uv == 0 || !std::has_single_bit(uv)) [[unlikely]] {
               return N; // Invalid: not a power of 2
            }
            const auto bit = static_cast<size_t>(std::countr_zero(uv));
            if (bit >= Info.table_size) [[unlikely]] {
               return N; // Invalid: bit position out of range
            }
            return Info.table[bit];
         }
         else if constexpr (Info.type == small_range) {
            const auto idx = static_cast<int64_t>(value) - Info.min_value;
            if (idx < 0 || static_cast<size_t>(idx) >= Info.table_size) [[unlikely]] {
               return N; // Invalid: out of range
            }
            return Info.table[static_cast<size_t>(idx)]; // Returns N if slot is empty
         }
         else if constexpr (Info.type == modular) {
            const auto h = (static_cast<uint64_t>(value) * Info.seed) % Info.table_size;
            return Info.table[h]; // Returns N if slot is empty
         }
         else if constexpr (Info.type == modular_shifted) {
            const auto shifted = static_cast<uint64_t>(value) >> Info.shift;
            const auto h = (shifted * Info.seed) % Info.table_size;
            return Info.table[h]; // Returns N if slot is empty
         }
         else if constexpr (Info.type == linear_search) {
            // Linear scan through enum values
            constexpr auto& values = enum_values_array<T>;
            for (size_t i = 0; i < N; ++i) {
               if (values[i] == value) {
                  return i;
               }
            }
            return N; // Not found
         }
         else { // binary_search
            // Binary search through sorted enum values
            // Compute sorted indices and values together to avoid capture issues
            constexpr auto sorted_data = []() {
               struct result_t
               {
                  std::array<size_t, N> indices{};
                  std::array<U, N> values{};
               };
               result_t result{};

               // Initialize indices
               for (size_t i = 0; i < N; ++i) {
                  result.indices[i] = i;
               }

               // Sort indices by their corresponding values (bubble sort for constexpr)
               constexpr auto& src_values = enum_values_array<T>;
               for (size_t i = 0; i < N - 1; ++i) {
                  for (size_t j = i + 1; j < N; ++j) {
                     if (src_values[result.indices[j]] < src_values[result.indices[i]]) {
                        auto tmp = result.indices[i];
                        result.indices[i] = result.indices[j];
                        result.indices[j] = tmp;
                     }
                  }
               }

               // Build sorted values array
               for (size_t i = 0; i < N; ++i) {
                  result.values[i] = src_values[result.indices[i]];
               }

               return result;
            }();

            // Binary search
            size_t left = 0;
            size_t right = N;
            while (left < right) {
               const size_t mid = left + (right - left) / 2;
               if (sorted_data.values[mid] < value) {
                  left = mid + 1;
               }
               else {
                  right = mid;
               }
            }
            if (left < N && sorted_data.values[left] == value) {
               return sorted_data.indices[left];
            }
            return N; // Not found
         }
      }
   };

   // ============================================================================
   // End of integer key hashing
   // ============================================================================

   // get a std::string_view from an enum value
   // Note: is_reflect_enum types are handled separately because P2996 requires inline consteval context
   template <class T>
      requires(glaze_t<T> && std::is_enum_v<std::decay_t<T>>)
   constexpr auto get_enum_name(T&& enum_value)
   {
      using V = std::decay_t<T>;
      using U = std::underlying_type_t<V>;
      constexpr auto N = reflect<V>::size;

      if constexpr (N == 0) {
         return std::string_view{};
      }
      else {
         constexpr auto& info = enum_index_info<V>;
         const auto index = enum_value_to_index<V, info>::op(static_cast<U>(enum_value));

         if (index >= N) [[unlikely]] {
            return std::string_view{};
         }

         // For direct/offset strategies, values are guaranteed sequential
         // For hash-based strategies, verify the value matches to avoid false positives
         if constexpr (info.type == int_hash_type::direct || info.type == int_hash_type::offset) {
            return reflect<V>::keys[index];
         }
         else {
            // Verify match for power_of_two/small_range/modular lookups
            constexpr auto& values = enum_values_array<V>;
            if (values[index] == static_cast<U>(enum_value)) {
               return reflect<V>::keys[index];
            }
            return std::string_view{};
         }
      }
   }

   template <glaze_flags_t T>
   consteval auto byte_length() noexcept
   {
      constexpr auto N = reflect<T>::size;

      if constexpr (N % 8 == 0) {
         return N / 8;
      }
      else {
         return (N / 8) + 1;
      }
   }
}

#include <initializer_list>

#include "glaze/core/common.hpp"
#include "glaze/reflection/get_name.hpp"
#include "glaze/reflection/to_tuple.hpp"

namespace glz
{
   // Use a dummy struct for make_reflectable so that we don't conflict with any user defined constructors
   struct dummy final
   {};

   // If you want to make an empty struct or a struct with constructors visible in reflected structs,
   // add the folllwing constructor to your type:
   // my_struct(glz::make_reflectable) {}
   using make_reflectable = std::initializer_list<dummy>;
}

namespace glz
{
   // TODO: This is returning the total keys and not the max keys for a particular variant object
   template <class T, size_t N>
   constexpr size_t get_max_keys = [] {
      size_t res{};
      for_each<N>([&]<auto I>() {
         using V = std::decay_t<std::variant_alternative_t<I, T>>;
         if constexpr (glaze_object_t<V> || reflectable<V>) {
            res += reflect<V>::size;
         }
         else if constexpr (is_memory_object<V>) {
            res += reflect<memory_type<V>>::size;
         }
      });
      return res;
   }();

   template <class T>
   constexpr auto get_combined_keys_from_variant()
   {
      constexpr auto N = std::variant_size_v<T>;

      std::array<sv, get_max_keys<T, N>> keys{};
      // This intermediate pointer is necessary for GCC 13 (otherwise segfaults with reflection logic)
      auto* data_ptr = &keys;
      size_t index = 0;
      for_each<N>([&]<auto I>() {
         using V = std::decay_t<std::variant_alternative_t<I, T>>;
         if constexpr (glaze_object_t<V> || reflectable<V> || is_memory_object<V>) {
            using X = std::conditional_t<is_memory_object<V>, memory_type<V>, V>;
            for (size_t i = 0; i < reflect<X>::size; ++i) {
               (*data_ptr)[index++] = reflect<X>::keys[i];
            }
         }
      });

      std::sort(keys.begin(), keys.end());
      const auto end = std::unique(keys.begin(), keys.end());
      const auto size = std::distance(keys.begin(), end);

      return pair{keys, size};
   }
}

namespace glz
{
   template <class T>
   consteval size_t key_index(const std::string_view key)
   {
      const auto n = reflect<T>::keys.size();
      for (size_t i = 0; i < n; ++i) {
         if (key == reflect<T>::keys[i]) {
            return i;
         }
      }
      return n;
   }
}

namespace glz
{
   GLZ_ALWAYS_INLINE constexpr uint64_t bitmix(uint64_t h, const uint64_t seed) noexcept
   {
      h *= seed;
      return h ^ std::rotr(h, 49);
   };

   // Use when hashing large chunks of characters that are likely very similar
   GLZ_ALWAYS_INLINE constexpr uint64_t rich_bitmix(uint64_t h, const uint64_t seed) noexcept
   {
      h ^= h >> 23;
      h *= 0x2127599bf4325c37ULL;
      h ^= seed;
      h *= 0x880355f21e6d1965ULL;
      h ^= h >> 47;
      return h;
   }

   template <size_t N>
   using bucket_value_t = std::conditional_t < N<256, uint8_t, uint16_t>;

   // The larger the underlying bucket the more we avoid collisions with invalid keys.
   // This improves performance of rejecting invalid keys because we don't have to do
   // string comparisons in these cases.
   // However, there are obvious memory costs with increasing the bucket size.

   enum struct hash_type { //
      invalid, // hashing failed
      unique_index, // A unique character index is used
      front_hash, // Hash on the front bytes of the key
      single_element, // Map is a single element
      mod4, // c % 4
      xor_mod4, // (c ^ c0) % 4
      minus_mod4, // (c - c0) % 4
      three_element_unique_index,
      unique_per_length, // Hash on a unique character index and the length of the key
      full_flat // Full key hash with a single table
   };

   // For N == 3 and N == 4 it is cheap to check mod4, xor_mod4, and minus_mod4 hashes.
   // Consecuative values like "x", "y", "z" for keys work with minus_mod4

   struct unique_per_length_t
   {
      bool valid{};
      std::array<uint8_t, 256> unique_index{};
   };

   inline constexpr unique_per_length_t unique_per_length_info(const auto& input_strings)
   {
      const auto N = input_strings.size();
      if (N == 0) {
         return {};
      }

      // Copy into a local array and insertion-sort by size. We use the caller's array
      // type directly (e.g. std::array<sv, N>) and a hand-rolled sort rather than
      // std::ranges::sort so the sort body is not template-instantiated per N — a
      // previous version went via std::vector specifically to avoid per-N ranges::sort
      // instantiations, at the cost of constexpr heap evaluation. For typical N
      // (struct key counts, usually ≤ max_pure_reflection_count) insertion sort is fine.
      auto strings = input_strings;
      for (size_t i = 1; i < N; ++i) {
         auto key = strings[i];
         size_t j = i;
         while (j > 0 && strings[j - 1].size() > key.size()) {
            strings[j] = strings[j - 1];
            --j;
         }
         strings[j] = key;
      }

      if (strings[0].empty() || strings[N - 1].size() >= 255) {
         return {};
      }

      unique_per_length_t info{.valid = true};
      info.unique_index.fill(255);

      for (size_t len = strings[0].size(); len <= strings[N - 1].size(); ++len) {
         // Locate the subrange of keys with this length. Input is sorted by size, so
         // entries of a given length form a contiguous run; a linear scan is enough
         // and sidesteps the std::lower_bound / std::upper_bound instantiations.
         size_t rb = N, re = N;
         for (size_t i = 0; i < N; ++i) {
            if (strings[i].size() == len) {
               if (rb == N) rb = i;
               re = i + 1;
            }
         }
         if (rb == re) continue;

         bool found = false;
         for (size_t pos = 0; pos < len; ++pos) {
            std::array<int, 256> char_count = {};
            for (size_t i = rb; i < re; ++i) {
               ++char_count[uint8_t(strings[i][pos])];
            }
            bool collision = false;
            for (const auto count : char_count) {
               if (count > 1) {
                  collision = true;
                  break;
               }
            }
            if (not collision) {
               info.unique_index[len] = uint8_t(pos);
               found = true;
               break;
            }
         }
         if (not found) {
            info.valid = false;
            return info;
         }
      }

      return info;
   }

   template <class T>
   inline constexpr auto per_length_info = unique_per_length_info(reflect<T>::keys);

   consteval size_t bucket_size(hash_type type, size_t N)
   {
      using enum hash_type;
      switch (type) {
      case invalid: {
         return 0;
      }
      case unique_index: {
         return 256;
      }
      case front_hash: {
         return (N == 1) ? 1 : std::bit_ceil(N * N) / 2;
      }
      case single_element: {
         return 0;
      }
      case mod4: {
         return 0;
      }
      case xor_mod4: {
         return 0;
      }
      case minus_mod4: {
         return 0;
      }
      case three_element_unique_index: {
         return 0;
      }
      case unique_per_length: {
         return (N == 1) ? 1 : std::bit_ceil(N * N) / 2;
      }
      case full_flat: {
         return (N == 1) ? 1 : std::bit_ceil(N * N) / 2;
      }
      default: {
         return 0;
      }
      }
   }

   struct keys_info_t
   {
      size_t N{};
      hash_type type{};
      size_t min_length = (std::numeric_limits<size_t>::max)();
      size_t max_length{};
      // uint8_t min_diff = (std::numeric_limits<uint8_t>::max)();
      uint64_t seed{};
      size_t unique_index = (std::numeric_limits<size_t>::max)();
      bool sized_hash = false;
      size_t front_hash_bytes{};
   };

   // For hash algorithm a value of the seed indicates an invalid hash

   // A value of N in the bucket indicates an invalid hash
   template <class T, size_t Slots>
   struct hash_info_t
   {
      hash_type type{};

      static constexpr auto N = reflect<T>::size;
      using V = bucket_value_t<N>;
      static constexpr auto invalid = static_cast<V>(N);

      std::array<V, Slots> table{}; // hashes to switch-case indices
      size_t min_length = (std::numeric_limits<size_t>::max)();
      size_t max_length{};
      uint64_t seed{};
      size_t unique_index = (std::numeric_limits<size_t>::max)();
      bool sized_hash = false;
      size_t front_hash_bytes{};
   };

   constexpr std::optional<size_t> find_unique_index(const auto& strings)
   {
      namespace ranges = std::ranges;

      const auto N = strings.size();

      if (N == 0) {
         return {};
      }

      size_t min_length = (std::numeric_limits<size_t>::max)();
      for (auto& s : strings) {
         const auto n = s.size();
         if (n < min_length) {
            min_length = n;
         }
      }

      if (min_length == 0) {
         return {};
      }

      std::vector<std::vector<uint8_t>> cols(min_length);

      for (const auto& s : strings) {
         // for each character in the string
         for (size_t c = 0; c < min_length; ++c) {
            cols[c].emplace_back(s[c]);
         }
      }

      // sort columns so that we can determine
      // if the column is unique
      size_t best_index{};
      size_t best_count{};
      for (size_t i = 0; i < min_length; ++i) {
         auto& col = cols[i];
         ranges::sort(col);
         if (auto it = ranges::adjacent_find(col); it == col.end()) {
            // no duplicates found
            best_count = col.size();
            best_index = i;
            break;
         }
      }

      if (best_count == 0) {
         return {};
      }

      return best_index;
   }

   constexpr std::optional<size_t> find_unique_sized_index(const auto& strings)
   {
      namespace ranges = std::ranges;

      const auto N = strings.size();

      if (N == 0) {
         return {};
      }

      size_t min_length = (std::numeric_limits<size_t>::max)();
      for (auto& s : strings) {
         if (s.contains('"')) {
            return {}; // Sized hashing requires looking for terminating quote
         }

         const auto n = s.size();
         if (n < min_length) {
            min_length = n;
         }
      }

      if (min_length == 0) {
         return {};
      }

      std::vector<std::vector<uint16_t>> cols(min_length);

      for (size_t i = 0; i < N; ++i) {
         const auto& s = strings[i];
         // for each character in the string
         for (size_t c = 0; c < min_length; ++c) {
            const auto k = uint16_t(uint16_t(s[c]) | (uint16_t(s.size()) << 8));
            cols[c].emplace_back(k);
         }
      }

      // sort columns so that we can determine
      // if the column is unique
      size_t best_index{};
      size_t best_count{};
      for (size_t i = 0; i < min_length; ++i) {
         auto& col = cols[i];
         ranges::sort(col);
         if (auto it = ranges::adjacent_find(col); it == col.end()) {
            // no duplicates found
            best_count = col.size();
            best_index = i;
            break;
         }
      }

      if (best_count == 0) {
         return {};
      }

      return best_index;
   }

   // For full hashing we perform a rich_bitmix on the tail end
   // This is because most cases that require full hashes are because
   // the tail end is the only unique part

   // Do not call this at runtime, it is assumes the key lies within min_length and max_length
   inline constexpr uint64_t full_hash_impl(const sv key, const uint64_t seed, const auto min_length,
                                            const auto max_length) noexcept
   {
      if (max_length < 8) {
         return bitmix(to_uint64_n_below_8(key.data(), key.size()), seed);
      }
      else if (min_length > 7) {
         const auto n = key.size();
         uint64_t h = seed;
         const auto* data = key.data();
         const auto* end7 = data + n - 7;
         for (auto d0 = data; d0 < end7; d0 += 8) {
            h = bitmix(to_uint64(d0), h);
         }
         // Handle potential tail. We know we have at least 8
         return rich_bitmix(to_uint64(data + n - 8), h);
      }
      else {
         const auto n = key.size();
         const auto* data = key.data();

         if (n < 8) {
            return bitmix(to_uint64_n_below_8(data, n), seed);
         }

         uint64_t h = seed;
         const auto* end7 = data + n - 7;
         for (auto d0 = data; d0 < end7; d0 += 8) {
            h = bitmix(to_uint64(d0), h);
         }
         // Handle potential tail. We know we have at least 8
         return rich_bitmix(to_uint64(data + n - 8), h);
      }
   }

   // runtime full hash algorithm
   template <uint64_t min_length, uint64_t max_length, uint64_t seed>
   inline constexpr uint64_t full_hash(const auto* it, const size_t n) noexcept
   {
      if constexpr (max_length < 8) {
         if (n > 7) {
            return seed;
         }
         return bitmix(to_uint64_n_below_8(it, n), seed);
      }
      else if constexpr (min_length > 7) {
         if (n < 8) {
            return seed;
         }
         uint64_t h = seed;
         const auto* end7 = it + n - 7;
         for (auto d0 = it; d0 < end7; d0 += 8) {
            h = bitmix(to_uint64(d0), h);
         }
         // Handle potential tail. We know we have at least 8
         return rich_bitmix(to_uint64(it + n - 8), h);
      }
      else {
         if (n < 8) {
            return bitmix(to_uint64_n_below_8(it, n), seed);
         }

         uint64_t h = seed;
         const auto* end7 = it + n - 7;
         for (auto d0 = it; d0 < end7; d0 += 8) {
            h = bitmix(to_uint64(d0), h);
         }
         // Handle potential tail. We know we have at least 8
         return rich_bitmix(to_uint64(it + n - 8), h);
      }
   }

   template <std::integral ChunkType, size_t N>
   constexpr bool front_bytes_hash_info(const std::array<sv, N>& keys, keys_info_t& info) noexcept
   {
      if (info.min_length < sizeof(ChunkType)) {
         return false;
      }

      // check for uniqueness
      std::array<ChunkType, N> k;
      for (size_t i = 0; i < N; ++i) {
         if constexpr (std::same_as<ChunkType, uint16_t>) {
            k[i] = uint16_t(keys[i][0]) | (uint16_t(keys[i][1]) << 8);
         }
         else if constexpr (std::same_as<ChunkType, uint32_t>) {
            k[i] = uint32_t(keys[i][0]) //
                   | (uint32_t(keys[i][1]) << 8) //
                   | (uint32_t(keys[i][2]) << 16) //
                   | (uint32_t(keys[i][3]) << 24);
         }
         else if constexpr (std::same_as<ChunkType, uint64_t>) {
            k[i] = uint64_t(keys[i][0]) //
                   | (uint64_t(keys[i][1]) << 8) //
                   | (uint64_t(keys[i][2]) << 16) //
                   | (uint64_t(keys[i][3]) << 24) //
                   | (uint64_t(keys[i][4]) << 32) //
                   | (uint64_t(keys[i][5]) << 40) //
                   | (uint64_t(keys[i][6]) << 48) //
                   | (uint64_t(keys[i][7]) << 56);
         }
         else {
            static_assert(false_v<ChunkType>);
         }
      }

      std::ranges::sort(k);

      for (size_t i = 0; i < N - 1; ++i) {
         const auto diff = k[i + 1] - k[i];
         if (diff == 0) {
            return false;
         }
      }

      using enum hash_type;
      constexpr uint64_t invalid_seed = 0;
      auto& seed = info.seed;
      auto hash_alg = [&] {
         std::array<size_t, N> bucket_index{};
         constexpr auto bsize = bucket_size(front_hash, N);

         for (size_t i = 0; i < primes_64.size(); ++i) {
            seed = primes_64[i];
            size_t index = 0;
            for (const auto& key : keys) {
               const auto hash = [&]() -> size_t {
                  if constexpr (std::same_as<ChunkType, uint16_t>) {
                     return bitmix(uint16_t(key[0]) | (uint16_t(key[1]) << 8), seed);
                  }
                  else if constexpr (std::same_as<ChunkType, uint32_t>) {
                     return bitmix(uint32_t(key[0]) //
                                      | (uint32_t(key[1]) << 8) //
                                      | (uint32_t(key[2]) << 16) //
                                      | (uint32_t(key[3]) << 24),
                                   seed);
                  }
                  else if constexpr (std::same_as<ChunkType, uint64_t>) {
                     return rich_bitmix(uint64_t(key[0]) //
                                           | (uint64_t(key[1]) << 8) //
                                           | (uint64_t(key[2]) << 16) //
                                           | (uint64_t(key[3]) << 24) //
                                           | (uint64_t(key[4]) << 32) //
                                           | (uint64_t(key[5]) << 40) //
                                           | (uint64_t(key[6]) << 48) //
                                           | (uint64_t(key[7]) << 56),
                                        seed);
                  }
                  else {
                     static_assert(false_v<ChunkType>);
                  }
               }();
               if (hash == seed) {
                  break;
               }
               const auto bucket = hash % bsize;
               if (contains(bucket_index.data(), index, bucket)) {
                  break;
               }
               bucket_index[index] = bucket;
               ++index;
            }

            if (index == N) {
               // make sure the seed does not collide with any hashes
               const auto bucket = seed % bsize;
               if (not contains(bucket_index.data(), N, bucket)) {
                  return; // found working seed
               }
            }
         }

         seed = invalid_seed;
      };

      hash_alg();
      if (seed != invalid_seed) {
         info.type = front_hash;
         info.front_hash_bytes = sizeof(ChunkType);
         return true;
      }

      return false;
   }

   // The sequence of hashing algorithms written here determines the selection preference
   template <size_t N>
   constexpr auto make_keys_info(const std::array<sv, N>& keys)
   {
      namespace ranges = std::ranges;

      keys_info_t info{N};

      if constexpr (N == 0) {
         return info;
      }

      for (size_t i = 0; i < N; ++i) {
         const auto n = keys[i].size();
         if (n < info.min_length) {
            info.min_length = n;
         }
         if (n > info.max_length) {
            info.max_length = n;
         }
      }

      using enum hash_type;

      if constexpr (N == 1) {
         info.type = single_element;
         return info;
      }

      // N == 2 is optimized within other hashing methods

      if constexpr (N == 3 || N == 4) {
         if (info.min_length > 0) {
            bool valid = true;
            for (size_t i = 0; i < N; ++i) {
               if (keys[i][0] % 4 != uint8_t(i)) {
                  valid = false;
               }
            }
            if (valid) {
               info.type = mod4;
               return info;
            }

            const auto c0 = keys[0][0];

            valid = true;
            for (size_t i = 0; i < N; ++i) {
               if ((keys[i][0] ^ c0) % 4 != uint8_t(i)) {
                  valid = false;
               }
            }
            if (valid) {
               info.type = xor_mod4;
               return info;
            }

            valid = true;
            for (size_t i = 0; i < N; ++i) {
               if ((keys[i][0] - c0) % 4 != uint8_t(i)) {
                  valid = false;
               }
            }
            if (valid) {
               info.type = minus_mod4;
               return info;
            }
         }
      }

      auto& seed = info.seed;
      constexpr uint64_t invalid_seed = 0;

      if (const auto uindex = find_unique_index(keys)) {
         info.unique_index = uindex.value();

         if constexpr (N == 3) {
            // An xor of the first unique character with itself will result in 0 (our desired index)
            // We use a hash algorithm that will produce zero if zero is given, so we can avoid a branch
            // We need a seed produces hashes of [1, 2] for the 2nd and 3rd keys

            const auto u = info.unique_index;
            const auto first = uint8_t(keys[0][u]);
            const auto mix1 = uint8_t(keys[1][u]) ^ first;
            const auto mix2 = uint8_t(keys[2][u]) ^ first;

            for (size_t i = 0; i < primes_64.size(); ++i) {
               seed = primes_64[i];
               uint8_t h1 = (mix1 * seed) % 4;
               uint8_t h2 = (mix2 * seed) % 4;

               if (h1 == 1 && h2 == 2) {
                  info.type = three_element_unique_index;
                  return info;
               }
            }
            // Otherwise we failed to find a seed and we'll use a normal unique_index hash
         }

         info.type = unique_index;
         return info;
      }

      if (front_bytes_hash_info<uint16_t>(keys, info)) {
         return info;
      }
      else if (front_bytes_hash_info<uint32_t>(keys, info)) {
         return info;
      }
      else if (front_bytes_hash_info<uint64_t>(keys, info)) {
         return info;
      }

      if (const auto uindex = find_unique_sized_index(keys)) {
         info.unique_index = uindex.value();
         info.sized_hash = true;

         auto sized_unique_hash = [&] {
            std::array<size_t, N> bucket_index{};
            constexpr auto bsize = bucket_size(unique_index, N);

            for (size_t i = 0; i < primes_64.size(); ++i) {
               seed = primes_64[i];
               size_t index = 0;
               for (const auto& key : keys) {
                  const auto hash = bitmix(uint16_t(key[info.unique_index]) | (uint16_t(key.size()) << 8), seed);
                  if (hash == seed) {
                     break;
                  }
                  const auto bucket = hash % bsize;
                  if (contains(bucket_index.data(), index, bucket)) {
                     break;
                  }
                  bucket_index[index] = bucket;
                  ++index;
               }

               if (index == N) {
                  // make sure the seed does not collide with any hashes
                  const auto bucket = seed % bsize;
                  if (not contains(bucket_index.data(), N, bucket)) {
                     return; // found working seed
                  }
               }
            }

            seed = invalid_seed;
         };

         sized_unique_hash();
         if (seed != invalid_seed) {
            info.type = unique_index;
            return info;
         }
      }

      // TODO: MSVC fixed the related compiler bug, but GitHub Actions has not caught up yet
#if !defined(_MSC_VER)
      // TODO: Use meta-programming to cache this value
      const auto per_length_data = unique_per_length_info(keys);
      if (per_length_data.valid) {
         auto sized_unique_hash = [&] {
            std::array<size_t, N> bucket_index{};
            constexpr auto bsize = bucket_size(unique_per_length, N);

            for (size_t i = 0; i < primes_64.size(); ++i) {
               seed = primes_64[i];
               size_t index = 0;
               for (const auto& key : keys) {
                  const auto n = uint8_t(key.size());
                  const auto hash = bitmix(uint16_t(key[per_length_data.unique_index[n]]) | (uint16_t(n) << 8), seed);
                  if (hash == seed) {
                     break;
                  }
                  const auto bucket = hash % bsize;
                  if (contains(bucket_index.data(), index, bucket)) {
                     break;
                  }
                  bucket_index[index] = bucket;
                  ++index;
               }

               if (index == N) {
                  // make sure the seed does not collide with any hashes
                  const auto bucket = seed % bsize;
                  if (not contains(bucket_index.data(), N, bucket)) {
                     return; // found working seed
                  }
               }
            }

            seed = invalid_seed;
         };

         sized_unique_hash();
         if (seed != invalid_seed) {
            info.type = unique_per_length;
            return info;
         }
      }
#endif

      // full_flat
      {
         auto full_flat_hash = [&] {
            std::array<size_t, N> bucket_index{};
            constexpr auto bsize = bucket_size(full_flat, N);

            for (size_t i = 0; i < primes_64.size(); ++i) {
               seed = primes_64[i];
               size_t index = 0;
               for (const auto& key : keys) {
                  const auto hash = full_hash_impl(key, seed, info.min_length, info.max_length);
                  if (hash == seed) {
                     break;
                  }
                  const auto bucket = hash % bsize;
                  if (contains(bucket_index.data(), index, bucket)) {
                     break;
                  }
                  bucket_index[index] = bucket;
                  ++index;
               }

               if (index == N) {
                  // make sure the seed does not collide with any hashes
                  const auto bucket = seed % bsize;
                  if (not contains(bucket_index.data(), N, bucket)) {
                     return; // found working seed
                  }
               }
            }

            seed = invalid_seed;
         };

         full_flat_hash();
         if (seed != invalid_seed) {
            info.type = full_flat;
            return info;
         }
      }

      return info;
   }

   template <class T>
   constexpr auto keys_info = make_keys_info(reflect<T>::keys);

   template <class T>
   constexpr auto hash_info = [] {
      if constexpr ((glaze_object_t<T> || reflectable<T> || glaze_flags_t<T> ||
                     ((std::is_enum_v<std::remove_cvref_t<T>> && meta_keys<T>) || glaze_enum_t<T>) ||
                     is_keys_wrapper_v<T>) &&
                    (reflect<T>::size > 0)) {
         constexpr auto& k_info = keys_info<T>;
         constexpr auto& type = k_info.type;
         constexpr auto& N = reflect<T>::size;
         constexpr auto& keys = reflect<T>::keys;

         using enum hash_type;
         if constexpr (type == single_element) {
            hash_info_t<T, bucket_size(single_element, N)> info{.type = single_element};
            info.min_length = k_info.min_length;
            info.max_length = k_info.max_length;
            return info;
         }
         else if constexpr (type == mod4) {
            hash_info_t<T, bucket_size(mod4, N)> info{.type = mod4};
            info.min_length = k_info.min_length;
            info.max_length = k_info.max_length;
            return info;
         }
         else if constexpr (type == xor_mod4) {
            hash_info_t<T, bucket_size(xor_mod4, N)> info{.type = xor_mod4};
            info.min_length = k_info.min_length;
            info.max_length = k_info.max_length;
            return info;
         }
         else if constexpr (type == minus_mod4) {
            hash_info_t<T, bucket_size(minus_mod4, N)> info{.type = minus_mod4};
            info.min_length = k_info.min_length;
            info.max_length = k_info.max_length;
            return info;
         }
         else if constexpr (type == three_element_unique_index) {
            hash_info_t<T, bucket_size(three_element_unique_index, N)> info{.type = three_element_unique_index};
            info.min_length = k_info.min_length;
            info.max_length = k_info.max_length;
            info.seed = k_info.seed;
            info.unique_index = k_info.unique_index;
            return info;
         }
         else if constexpr (type == front_hash) {
            constexpr auto bsize = bucket_size(front_hash, N);
            hash_info_t<T, bsize> info{.type = front_hash, .seed = k_info.seed};
            info.min_length = k_info.min_length;
            info.max_length = k_info.max_length;
            info.front_hash_bytes = k_info.front_hash_bytes;
            info.table.fill(uint8_t(N));

            for (uint8_t i = 0; i < N; ++i) {
               auto& key = keys[i];
               const auto h = [&]() -> size_t {
                  if (info.front_hash_bytes == sizeof(uint16_t)) {
                     return bitmix(uint16_t(key[0]) | (uint16_t(key[1]) << 8), info.seed) % bsize;
                  }
                  else if (info.front_hash_bytes == sizeof(uint32_t)) {
                     return bitmix(uint32_t(key[0]) //
                                      | (uint32_t(key[1]) << 8) //
                                      | (uint32_t(key[2]) << 16) //
                                      | (uint32_t(key[3]) << 24),
                                   info.seed) %
                            bsize;
                  }
                  else if (info.front_hash_bytes == sizeof(uint64_t)) {
                     return rich_bitmix(uint64_t(key[0]) //
                                           | (uint64_t(key[1]) << 8) //
                                           | (uint64_t(key[2]) << 16) //
                                           | (uint64_t(key[3]) << 24) //
                                           | (uint64_t(key[4]) << 32) //
                                           | (uint64_t(key[5]) << 40) //
                                           | (uint64_t(key[6]) << 48) //
                                           | (uint64_t(key[7]) << 56),
                                        info.seed) %
                            bsize;
                  }
                  else {
                     return 0; // MSVC has a compiler bug that prevents us from returning N, but this is unreachable
                  }
               }();
               info.table[h] = i;
            }

            return info;
         }
         else if constexpr (type == unique_index && N < 256) {
            hash_info_t<T, bucket_size(unique_index, N)> info{.type = unique_index, .seed = k_info.seed};
            info.min_length = k_info.min_length;
            info.max_length = k_info.max_length;
            info.table.fill(bucket_value_t<N>(N));
            info.unique_index = k_info.unique_index;

            if constexpr (k_info.sized_hash) {
               info.sized_hash = true;
               constexpr auto bsize = bucket_size(unique_index, N);
               for (uint8_t i = 0; i < N; ++i) {
                  const auto x = uint16_t(keys[i][k_info.unique_index]) | (uint16_t(keys[i].size()) << 8);
                  const auto h = bitmix(x, info.seed) % bsize;
                  info.table[h] = i;
               }
            }
            else {
               for (uint8_t i = 0; i < N; ++i) {
                  const auto h = uint8_t(keys[i][k_info.unique_index]);
                  info.table[h] = i;
               }
            }
            return info;
         }
         // TODO: MSVC fixed the related compiler bug, but GitHub Actions has not caught up yet
#if !defined(_MSC_VER)
         else if constexpr (type == unique_per_length) {
            hash_info_t<T, bucket_size(unique_per_length, N)> info{.type = unique_per_length, .seed = k_info.seed};
            info.min_length = k_info.min_length;
            info.max_length = k_info.max_length;
            info.table.fill(uint8_t(N));
            info.sized_hash = true;
            constexpr auto bsize = bucket_size(unique_per_length, N);
            constexpr auto& data = per_length_info<T>;
            for (uint8_t i = 0; i < N; ++i) {
               const auto n = keys[i].size();
               const auto x = uint16_t(keys[i][data.unique_index[n]]) | (uint16_t(n) << 8);
               const auto h = bitmix(x, info.seed) % bsize;
               info.table[h] = i;
            }
            return info;
         }
#endif
         else if constexpr (type == full_flat) {
            hash_info_t<T, bucket_size(full_flat, N)> info{.type = full_flat, .seed = k_info.seed};
            info.min_length = k_info.min_length;
            info.max_length = k_info.max_length;
            info.table.fill(uint8_t(N));
            constexpr auto bsize = bucket_size(full_flat, N);
            for (uint8_t i = 0; i < N; ++i) {
               const auto h = full_hash_impl(keys[i], info.seed, info.min_length, info.max_length) % bsize;
               info.table[h] = i;
            }
            return info;
         }
         else {
            // invalid
            return hash_info_t<T, 0>{};
         }
      }
      else {
         return hash_info_t<T, 0>{};
      }
   }();

   template <size_t min_length>
   GLZ_ALWAYS_INLINE constexpr const void* quote_memchr(auto&& it, auto end) noexcept
   {
      if consteval {
         const auto count = size_t(end - it);
         for (std::size_t i = 0; i < count; ++i) {
            if (it[i] == '"') {
               return it + i;
            }
         }
         return nullptr;
      }
      else {
         if constexpr (min_length >= 4) {
            // Skipping makes the bifurcation worth it
            const auto* start = it + min_length;
            if (start >= end) [[unlikely]] {
               return nullptr;
            }
            else [[likely]] {
               return std::memchr(start, '"', size_t(end - start));
            }
         }
         else {
            return std::memchr(it, '"', size_t(end - it));
         }
      }
   }

   template <uint32_t Format, class T, auto HashInfo, hash_type Type>
   struct decode_hash;

   template <class T, auto HashInfo>
   struct decode_hash<JSON, T, HashInfo, hash_type::single_element>
   {
      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& /*it*/, auto&& /*end*/) noexcept { return 0; }
   };

   template <class T, auto HashInfo>
   struct decode_hash<JSON, T, HashInfo, hash_type::mod4>
   {
      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto&& /*end*/) noexcept { return uint8_t(*it) % 4; }
   };

   template <class T, auto HashInfo>
   struct decode_hash<JSON, T, HashInfo, hash_type::xor_mod4>
   {
      static constexpr auto first_key_char = reflect<T>::keys[0][0];

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto&& /*end*/) noexcept
      {
         return (uint8_t(*it) ^ first_key_char) % 4;
      }
   };

   template <class T, auto HashInfo>
   struct decode_hash<JSON, T, HashInfo, hash_type::minus_mod4>
   {
      static constexpr auto first_key_char = reflect<T>::keys[0][0];

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto&& /*end*/) noexcept
      {
         return (uint8_t(*it) - first_key_char) % 4;
      }
   };

   template <class T, auto HashInfo>
   struct decode_hash<JSON, T, HashInfo, hash_type::unique_index>
   {
      static constexpr auto N = reflect<T>::size;
      static constexpr auto bsize = bucket_size(hash_type::unique_index, N);
      static constexpr auto uindex = HashInfo.unique_index;

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto end) noexcept
      {
         if constexpr (HashInfo.sized_hash) {
            const auto* c = quote_memchr<HashInfo.min_length>(it, end);
            if (c) [[likely]] {
               const auto n = size_t(static_cast<std::decay_t<decltype(it)>>(c) - it);
               if (n == 0 || n > HashInfo.max_length || HashInfo.unique_index >= size_t(end - it)) [[unlikely]] {
                  return N; // error
               }

               const auto h = bitmix(uint16_t(it[HashInfo.unique_index]) | (uint16_t(n) << 8), HashInfo.seed);
               return HashInfo.table[h % bsize];
            }
            else [[unlikely]] {
               return N;
            }
         }
         else {
            if constexpr (N == 2) {
               if constexpr (uindex > 0) {
                  if (size_t(end - it) <= uindex) [[unlikely]] {
                     return N; // error
                  }
               }
               // Avoids using a hash table
               constexpr auto first_key_char = reflect<T>::keys[0][uindex];
               return size_t(bool(it[uindex] ^ first_key_char));
            }
            else {
               if constexpr (uindex > 0) {
                  if (size_t(end - it) <= uindex) [[unlikely]] {
                     return N; // error
                  }
               }
               return HashInfo.table[uint8_t(it[uindex])];
            }
         }
      }
   };

   template <class T, auto HashInfo>
   struct decode_hash<JSON, T, HashInfo, hash_type::three_element_unique_index>
   {
      static constexpr auto N = reflect<T>::size;
      static constexpr auto uindex = HashInfo.unique_index;

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto end) noexcept
      {
         if constexpr (uindex > 0) {
            if (size_t(end - it) <= uindex) [[unlikely]] {
               return N; // error
            }
         }
         // Avoids using a hash table
         constexpr auto first_key_char = reflect<T>::keys[0][uindex];
         return (uint8_t(it[uindex] ^ first_key_char) * HashInfo.seed) % 4;
      }
   };

   template <class T, auto HashInfo>
   struct decode_hash<JSON, T, HashInfo, hash_type::front_hash>
   {
      static constexpr auto N = reflect<T>::size;
      static constexpr auto bsize = bucket_size(hash_type::front_hash, N);

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto end) noexcept
      {
         if constexpr (HashInfo.front_hash_bytes == 2) {
            if (size_t(end - it) <= 2) [[unlikely]] {
               return N; // error
            }
            uint16_t h;
            if consteval {
               h = 0;
               for (size_t i = 0; i < 2; ++i) {
                  h |= static_cast<uint16_t>(static_cast<uint8_t>(it[i])) << (8 * i);
               }
            }
            else {
               std::memcpy(&h, it, 2);
               if constexpr (std::endian::native == std::endian::big) {
                  h = std::byteswap(h);
               }
            }
            return HashInfo.table[bitmix(h, HashInfo.seed) % bsize];
         }
         else if constexpr (HashInfo.front_hash_bytes == 4) {
            if (size_t(end - it) <= 4) [[unlikely]] {
               return N;
            }
            uint32_t h;
            if consteval {
               h = 0;
               for (size_t i = 0; i < 4; ++i) {
                  h |= static_cast<uint32_t>(static_cast<uint8_t>(it[i])) << (8 * i);
               }
            }
            else {
               std::memcpy(&h, it, 4);
               if constexpr (std::endian::native == std::endian::big) {
                  h = std::byteswap(h);
               }
            }
            return HashInfo.table[bitmix(h, HashInfo.seed) % bsize];
         }
         else if constexpr (HashInfo.front_hash_bytes == 8) {
            if (size_t(end - it) <= 8) [[unlikely]] {
               return N;
            }
            uint64_t h;
            if consteval {
               h = 0;
               for (size_t i = 0; i < 8; ++i) {
                  h |= static_cast<uint64_t>(static_cast<uint8_t>(it[i])) << (8 * i);
               }
            }
            else {
               std::memcpy(&h, it, 8);
               if constexpr (std::endian::native == std::endian::big) {
                  h = std::byteswap(h);
               }
            }
            return HashInfo.table[rich_bitmix(h, HashInfo.seed) % bsize];
         }
         else {
            static_assert(false_v<T>, "invalid hash algorithm");
         }
      }
   };

   template <class T, auto HashInfo>
   struct decode_hash<JSON, T, HashInfo, hash_type::unique_per_length>
   {
      static constexpr auto N = reflect<T>::size;
      static constexpr auto bsize = bucket_size(hash_type::unique_per_length, N);

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto end) noexcept
      {
         const auto* c = quote_memchr<HashInfo.min_length>(it, end);
         if (c) [[likely]] {
            const auto n = uint8_t(static_cast<std::decay_t<decltype(it)>>(c) - it);
            const auto pos = per_length_info<T>.unique_index[n];
            if (size_t(end - it) <= pos) [[unlikely]] {
               return N; // error
            }
            const auto h = bitmix(uint16_t(it[pos]) | (uint16_t(n) << 8), HashInfo.seed);
            return HashInfo.table[h % bsize];
         }
         else [[unlikely]] {
            return N;
         }
      }
   };

   template <class T, auto HashInfo>
   struct decode_hash<JSON, T, HashInfo, hash_type::full_flat>
   {
      static constexpr auto N = reflect<T>::size;
      static constexpr auto bsize = bucket_size(hash_type::full_flat, N);
      static constexpr auto min_length = HashInfo.min_length;
      static constexpr auto max_length = HashInfo.max_length;
      static constexpr auto length_range = max_length - min_length;

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto end) noexcept
      {
         // Bounds checks ensure we can safely read the string content and determine its length.
         // Note: This is used for both object keys and enum values, so we cannot assume
         // extra characters exist after the closing quote (e.g., standalone enum: "value")

         if constexpr (length_range == 0) {
            if (size_t(end - it) <= min_length) [[unlikely]] {
               return N;
            }
            const auto h = full_hash<HashInfo.min_length, HashInfo.max_length, HashInfo.seed>(it, min_length);
            return HashInfo.table[h % bsize];
         }
         else {
            if constexpr (length_range == 1) {
               auto quote = it + min_length;
               // Ensure we can read *quote to determine if string is min_length or max_length.
               // The check (quote + 1) > end ensures quote < end, making *quote dereferenceable.
               if ((quote + 1) > end) [[unlikely]] {
                  return N;
               }

               const auto n = min_length + uint8_t(*quote != '"');
               const auto h = full_hash<HashInfo.min_length, HashInfo.max_length, HashInfo.seed>(it, n);
               return HashInfo.table[h % bsize];
            }
            else {
               const auto* c = quote_memchr<HashInfo.min_length>(it, end);
               if (c) [[likely]] {
                  const auto n = uint8_t(static_cast<std::decay_t<decltype(it)>>(c) - it);
                  const auto h = full_hash<HashInfo.min_length, HashInfo.max_length, HashInfo.seed>(it, n);
                  return HashInfo.table[h % bsize];
               }
               else [[unlikely]] {
                  return N;
               }
            }
         }
      }
   };

   template <uint32_t Format, class T, auto HashInfo, hash_type Type>
   struct decode_hash_with_size_impl;

   // Single entry point for the in-place key-hash readers (BSON, MessagePack, CBOR, CSV, TOML, plus
   // the compile-time-key callers). Every reader below dereferences key bytes only at offsets that
   // are guaranteed below min_length (front_hash_bytes <= min_length, unique_index < min_length, and
   // the mod4 family reads offset 0 where min_length >= 1) or reads at most n bytes. A key whose
   // length is outside [min_length, max_length] therefore cannot match any reflected key and must
   // not be hashed, so rejecting it here bounds every reader's key access in one place. This is why
   // the individual readers carry no per-read bounds checks; the sole exception is unique_per_length,
   // whose length-indexed table yields 255 for absent lengths and so keeps its own end check.
   template <uint32_t Format, class T, auto HashInfo, hash_type Type>
   struct decode_hash_with_size
   {
      static constexpr auto N = reflect<T>::size;

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto&& end, const size_t n) noexcept
      {
         if (n < HashInfo.min_length || n > HashInfo.max_length) [[unlikely]] {
            return N;
         }
         return decode_hash_with_size_impl<Format, T, HashInfo, Type>::op(it, end, n);
      }
   };

   template <uint32_t Format, class T, auto HashInfo>
   struct decode_hash_with_size_impl<Format, T, HashInfo, hash_type::single_element>
   {
      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&&, auto&&, const size_t) noexcept { return 0; }
   };

   template <uint32_t Format, class T, auto HashInfo>
   struct decode_hash_with_size_impl<Format, T, HashInfo, hash_type::mod4>
   {
      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto&&, const size_t) noexcept
      {
         return uint8_t(*it) % 4;
      }
   };

   template <uint32_t Format, class T, auto HashInfo>
   struct decode_hash_with_size_impl<Format, T, HashInfo, hash_type::xor_mod4>
   {
      static constexpr auto first_key_char = reflect<T>::keys[0][0];

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto&&, const size_t) noexcept
      {
         return (uint8_t(*it) ^ first_key_char) % 4;
      }
   };

   template <uint32_t Format, class T, auto HashInfo>
   struct decode_hash_with_size_impl<Format, T, HashInfo, hash_type::minus_mod4>
   {
      static constexpr auto first_key_char = reflect<T>::keys[0][0];

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto&&, const size_t) noexcept
      {
         return (uint8_t(*it) - first_key_char) % 4;
      }
   };

   template <uint32_t Format, class T, auto HashInfo>
   struct decode_hash_with_size_impl<Format, T, HashInfo, hash_type::unique_index>
   {
      static constexpr auto N = reflect<T>::size;
      static constexpr auto bsize = bucket_size(hash_type::unique_index, N);
      static constexpr auto uindex = HashInfo.unique_index;

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto&&, const size_t n) noexcept
      {
         // unique_index < min_length <= n, so it[unique_index] is within the key (the wrapper has
         // already rejected lengths outside [min_length, max_length]).
         if constexpr (HashInfo.sized_hash) {
            const auto h = bitmix(uint16_t(it[HashInfo.unique_index]) | (uint16_t(n) << 8), HashInfo.seed);
            return HashInfo.table[h % bsize];
         }
         else {
            if constexpr (N == 2) {
               // Avoids using a hash table
               constexpr auto first_key_char = reflect<T>::keys[0][uindex];
               return size_t(bool(it[uindex] ^ first_key_char));
            }
            else {
               return HashInfo.table[uint8_t(it[uindex])];
            }
         }
      }
   };

   template <uint32_t Format, class T, auto HashInfo>
   struct decode_hash_with_size_impl<Format, T, HashInfo, hash_type::three_element_unique_index>
   {
      static constexpr auto uindex = HashInfo.unique_index;

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto&&, const size_t) noexcept
      {
         // uindex < min_length <= n (the wrapper bounded n), so it[uindex] is within the key.
         // Avoids using a hash table
         constexpr auto first_key_char = reflect<T>::keys[0][uindex];
         return (uint8_t(it[uindex] ^ first_key_char) * HashInfo.seed) % 4;
      }
   };

   template <uint32_t Format, class T, auto HashInfo>
   struct decode_hash_with_size_impl<Format, T, HashInfo, hash_type::front_hash>
   {
      static constexpr auto N = reflect<T>::size;
      static constexpr auto bsize = bucket_size(hash_type::front_hash, N);

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto&&, const size_t) noexcept
      {
         // front_hash_bytes <= min_length <= n, so reading the prefix stays within the key (the
         // wrapper rejects keys shorter than min_length before dispatching here).
         if constexpr (HashInfo.front_hash_bytes == 2) {
            uint16_t h;
            if consteval {
               h = 0;
               for (size_t i = 0; i < 2; ++i) {
                  h |= static_cast<uint16_t>(static_cast<uint8_t>(it[i])) << (8 * i);
               }
            }
            else {
               std::memcpy(&h, it, 2);
               if constexpr (std::endian::native == std::endian::big) {
                  h = std::byteswap(h);
               }
            }
            return HashInfo.table[bitmix(h, HashInfo.seed) % bsize];
         }
         else if constexpr (HashInfo.front_hash_bytes == 4) {
            uint32_t h;
            if consteval {
               h = 0;
               for (size_t i = 0; i < 4; ++i) {
                  h |= static_cast<uint32_t>(static_cast<uint8_t>(it[i])) << (8 * i);
               }
            }
            else {
               std::memcpy(&h, it, 4);
               if constexpr (std::endian::native == std::endian::big) {
                  h = std::byteswap(h);
               }
            }
            return HashInfo.table[bitmix(h, HashInfo.seed) % bsize];
         }
         else if constexpr (HashInfo.front_hash_bytes == 8) {
            uint64_t h;
            if consteval {
               h = 0;
               for (size_t i = 0; i < 8; ++i) {
                  h |= static_cast<uint64_t>(static_cast<uint8_t>(it[i])) << (8 * i);
               }
            }
            else {
               std::memcpy(&h, it, 8);
               if constexpr (std::endian::native == std::endian::big) {
                  h = std::byteswap(h);
               }
            }
            return HashInfo.table[rich_bitmix(h, HashInfo.seed) % bsize];
         }
         else {
            static_assert(false_v<T>, "invalid hash algorithm");
         }
      }
   };

   template <uint32_t Format, class T, auto HashInfo>
   struct decode_hash_with_size_impl<Format, T, HashInfo, hash_type::unique_per_length>
   {
      static constexpr auto N = reflect<T>::size;
      static constexpr auto bsize = bucket_size(hash_type::unique_per_length, N);

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto&& end, const size_t n) noexcept
      {
         // Unlike the other readers, the read offset here is indexed by key length and absent
         // lengths map to 255 (see unique_per_length_info), so a foreign key whose length falls in
         // a gap of [min_length, max_length] would read it[255]. The wrapper's length pre-screen
         // does not catch that, so this reader keeps its own end check.
         const auto pos = per_length_info<T>.unique_index[uint8_t(n)];
         if (size_t(end - it) <= pos) [[unlikely]] {
            return N; // error
         }
         const auto h = bitmix(uint16_t(it[pos]) | (uint16_t(n) << 8), HashInfo.seed);
         return HashInfo.table[h % bsize];
      }
   };

   template <uint32_t Format, class T, auto HashInfo>
   struct decode_hash_with_size_impl<Format, T, HashInfo, hash_type::full_flat>
   {
      static constexpr auto N = reflect<T>::size;
      static constexpr auto bsize = bucket_size(hash_type::full_flat, N);

      GLZ_ALWAYS_INLINE static constexpr size_t op(auto&& it, auto&&, const size_t n) noexcept
      {
         const auto h = full_hash<HashInfo.min_length, HashInfo.max_length, HashInfo.seed>(it, n);
         return HashInfo.table[h % bsize];
      }
   };

   // ============================================================================
   // variant_deduction: Maps field keys to bit arrays for variant type deduction
   // Uses compile-time decode_hash for O(1) lookup
   // ============================================================================

   // Number of unique keys from all variant types
   template <is_variant T>
   constexpr size_t variant_deduction_key_count = get_combined_keys_from_variant<T>().second;

   // Array of unique keys (sorted) from all variant types
   template <is_variant T>
   constexpr auto variant_deduction_keys = []() {
      constexpr auto pair = get_combined_keys_from_variant<T>();
      std::array<sv, variant_deduction_key_count<T>> result{};
      for (size_t i = 0; i < variant_deduction_key_count<T>; ++i) {
         result[i] = pair.first[i];
      }
      return result;
   }();

   // Variant deduction bits - for each unique key, tracks which variant types contain it
   template <is_variant T>
   constexpr auto variant_deduction_bits = []() {
      static constexpr size_t K = variant_deduction_key_count<T>;
      using bits_type = bit_array<std::variant_size_v<T>>;
      std::array<bits_type, K> bits{};

      if constexpr (K > 0) {
         using keys_t = keys_wrapper<variant_deduction_keys<T>>;

         // Populate bit arrays - for each key, set bits for variant types that have it
         for_each<std::variant_size_v<T>>([&]<auto I>() {
            using V = decay_keep_volatile_t<std::variant_alternative_t<I, T>>;
            if constexpr (glaze_object_t<V> || reflectable<V> || is_memory_object<V>) {
               using X = std::conditional_t<is_memory_object<V>, memory_type<V>, V>;
               constexpr auto Size = reflect<X>::size;
               if constexpr (Size > 0) {
                  constexpr auto& HashInfo = hash_info<keys_t>;
                  for (size_t J = 0; J < Size; ++J) {
                     sv key = reflect<X>::keys[J];
                     const auto index = decode_hash_with_size<JSON, keys_t, HashInfo, HashInfo.type>::op(
                        key.data(), key.data() + key.size(), key.size());
                     if (index < K) {
                        bits[index][I] = true;
                     }
                  }
               }
            }
         });
      }

      return bits;
   }();

   // ============================================================================
   // variant_id_to_index: Maps variant type IDs to indices using perfect hashing
   // ============================================================================

   // Helper to create int_keys_info from ids_v<T> for integral variant IDs
   template <is_variant T>
      requires(std::integral<std::decay_t<decltype(ids_v<T>[0])>>)
   constexpr auto make_variant_int_keys_info()
   {
      using U = std::decay_t<decltype(ids_v<T>[0])>;
      constexpr auto N = ids_v<T>.size();

      if constexpr (N == 0) {
         return int_keys_info_t<0, 0>{};
      }
      else if constexpr (N == 1) {
         // For single-element IDs, check if value is 0 (direct) or needs offset
         constexpr auto value = static_cast<U>(ids_v<T>[0]);
         if constexpr (value == 0) {
            return int_keys_info_t<1, 0>{.type = int_hash_type::direct};
         }
         else {
            return int_keys_info_t<1, 0>{.type = int_hash_type::offset, .min_value = static_cast<int64_t>(value)};
         }
      }
      else if constexpr (N == 2) {
         // For two-element IDs, compare against first value
         constexpr auto first_value = static_cast<int64_t>(ids_v<T>[0]);
         constexpr auto second_value = static_cast<int64_t>(ids_v<T>[1]);
         return int_keys_info_t<2, 0>{
            .type = int_hash_type::two_element, .min_value = first_value, .max_value = second_value};
      }
      else {
         // Extract values from ids_v<T>
         constexpr auto vals = []() constexpr {
            constexpr auto size = ids_v<T>.size();
            std::array<U, size> result{};
            for (size_t i = 0; i < size; ++i) {
               result[i] = ids_v<T>[i];
            }
            return result;
         }();

         // Find min/max
         constexpr auto min_max = [&]() {
            U min_val = vals[0], max_val = vals[0];
            for (const auto v : vals) {
               if (v < min_val) min_val = v;
               if (v > max_val) max_val = v;
            }
            return std::pair{min_val, max_val};
         }();

         constexpr U min_val = min_max.first;
         constexpr U max_val = min_max.second;
         constexpr auto range = static_cast<size_t>(max_val - min_val);

         // Check for sequential IDs
         constexpr bool is_sequential = [&]() {
            if (range + 1 != N) return false;
            std::array<bool, N> seen{};
            for (const auto v : vals) {
               const auto idx = static_cast<size_t>(v - min_val);
               if (idx >= N || seen[idx]) return false;
               seen[idx] = true;
            }
            return true;
         }();

         // Check for powers of 2
         constexpr auto power_of_two_info = [&]() {
            using UnsignedU = std::make_unsigned_t<U>;
            size_t max_bit = 0;

            for (const auto v : vals) {
               const auto uv = static_cast<UnsignedU>(v);
               if (uv == 0 || !std::has_single_bit(uv)) {
                  return std::pair{false, size_t{0}};
               }
               const auto bit = static_cast<size_t>(std::countr_zero(uv));
               if (bit > max_bit) max_bit = bit;
            }

            std::array<bool, 64> used{};
            for (const auto v : vals) {
               const auto bit = std::countr_zero(static_cast<UnsignedU>(v));
               if (used[bit]) return std::pair{false, size_t{0}};
               used[bit] = true;
            }

            return std::pair{true, max_bit + 1};
         }();

         constexpr size_t sparse_threshold = 256;
         constexpr auto table_size = std::bit_ceil(N * 2);

         if constexpr (is_sequential) {
            if constexpr (min_val == 0) {
               return int_keys_info_t<N, 0>{.type = int_hash_type::direct};
            }
            else {
               return int_keys_info_t<N, 0>{.type = int_hash_type::offset, .min_value = min_val};
            }
         }
         else if constexpr (power_of_two_info.first) {
            constexpr auto tbl_size = power_of_two_info.second;
            int_keys_info_t<N, tbl_size> info{.type = int_hash_type::power_of_two, .table_size = tbl_size};
            info.table.fill(static_cast<uint8_t>(N));

            using UnsignedU = std::make_unsigned_t<U>;
            for (size_t i = 0; i < N; ++i) {
               const auto bit = std::countr_zero(static_cast<UnsignedU>(vals[i]));
               info.table[bit] = static_cast<uint8_t>(i);
            }
            return info;
         }
         else if constexpr (range < sparse_threshold) {
            int_keys_info_t<N, sparse_threshold> info{
               .type = int_hash_type::small_range, .min_value = min_val, .max_value = max_val, .table_size = range + 1};
            info.table.fill(static_cast<uint8_t>(N));

            for (size_t i = 0; i < N; ++i) {
               info.table[static_cast<size_t>(vals[i] - min_val)] = static_cast<uint8_t>(i);
            }
            return info;
         }
         else {
            // Strategy 4: Modular perfect hash (fallback)
            // First try standard modular hash (faster lookup)
            constexpr auto modular_info = [&]() {
               for (const auto prime : primes_64) {
                  std::array<bool, table_size> used{};
                  bool collision = false;

                  for (size_t i = 0; i < N; ++i) {
                     const auto h = (static_cast<uint64_t>(vals[i]) * prime) % table_size;
                     if (used[h]) {
                        collision = true;
                        break;
                     }
                     used[h] = true;
                  }

                  if (!collision) {
                     return std::pair{true, prime};
                  }
               }
               return std::pair{false, uint64_t{0}};
            }();

            if constexpr (modular_info.first) {
               // Standard modular hash works
               int_keys_info_t<N, table_size> info{
                  .type = int_hash_type::modular, .seed = modular_info.second, .table_size = table_size};
               info.table.fill(static_cast<uint8_t>(N));

               for (size_t i = 0; i < N; ++i) {
                  const auto h = (static_cast<uint64_t>(vals[i]) * info.seed) % table_size;
                  info.table[h] = static_cast<uint8_t>(i);
               }
               return info;
            }
            else {
               // Strategy 5: Modular hash with shift for sparse values with common power-of-2 factors
               constexpr uint8_t common_shift = [&]() -> uint8_t {
                  uint8_t min_trailing = 64;
                  for (const auto v : vals) {
                     if (v != 0) {
                        const auto trailing = static_cast<uint8_t>(std::countr_zero(static_cast<uint64_t>(v)));
                        if (trailing < min_trailing) {
                           min_trailing = trailing;
                        }
                     }
                  }
                  return min_trailing == 64 ? 0 : min_trailing;
               }();

               constexpr auto shifted_info = [&]() {
                  for (const auto prime : primes_64) {
                     std::array<bool, table_size> used{};
                     bool collision = false;

                     for (size_t i = 0; i < N; ++i) {
                        const auto shifted = static_cast<uint64_t>(vals[i]) >> common_shift;
                        const auto h = (shifted * prime) % table_size;
                        if (used[h]) {
                           collision = true;
                           break;
                        }
                        used[h] = true;
                     }

                     if (!collision) {
                        return std::pair{true, prime};
                     }
                  }
                  return std::pair{false, uint64_t{0}};
               }();

               if constexpr (shifted_info.first) {
                  int_keys_info_t<N, table_size> info{.type = int_hash_type::modular_shifted,
                                                      .seed = shifted_info.second,
                                                      .table_size = table_size,
                                                      .shift = common_shift};
                  info.table.fill(static_cast<uint8_t>(N));

                  for (size_t i = 0; i < N; ++i) {
                     const auto shifted = static_cast<uint64_t>(vals[i]) >> common_shift;
                     const auto h = (shifted * info.seed) % table_size;
                     info.table[h] = static_cast<uint8_t>(i);
                  }
                  return info;
               }
               else {
                  // Fallback: linear search for small N, binary search for larger N
                  if constexpr (N <= 16) {
                     return int_keys_info_t<N, 0>{.type = int_hash_type::linear_search};
                  }
                  else {
                     return int_keys_info_t<N, 0>{.type = int_hash_type::binary_search};
                  }
               }
            }
         }
      }
   }

   // Primary template for variant_id_to_index
   template <is_variant T, bool IsIntegral = std::integral<std::decay_t<decltype(ids_v<T>[0])>>>
   struct variant_id_to_index;

   // Specialization for string IDs
   template <is_variant T>
   struct variant_id_to_index<T, false>
   {
      using keys_t = keys_wrapper<ids_v<T>>;
      static constexpr auto& HashInfo = hash_info<keys_t>;
      static constexpr auto N = ids_v<T>.size();

      GLZ_ALWAYS_INLINE static constexpr size_t op(const char* data, const char* end, size_t n) noexcept
      {
         const auto index = decode_hash_with_size<JSON, keys_t, HashInfo, HashInfo.type>::op(data, end, n);
         // Verify the key matches to avoid false positives from hash collisions
         if (index < N) {
            const sv key{data, n};
            if (ids_v<T>[index] == key) {
               return index;
            }
         }
         return N; // Not found
      }
   };

   // Specialization for integral IDs
   template <is_variant T>
   struct variant_id_to_index<T, true>
   {
      using U = std::decay_t<decltype(ids_v<T>[0])>;
      static constexpr auto Info = make_variant_int_keys_info<T>();
      static constexpr auto N = ids_v<T>.size();

      GLZ_ALWAYS_INLINE static constexpr size_t op(U id) noexcept
      {
         using enum int_hash_type;

         if constexpr (Info.type == direct) {
            return static_cast<size_t>(id);
         }
         else if constexpr (Info.type == offset) {
            return static_cast<size_t>(id - Info.min_value);
         }
         else if constexpr (Info.type == two_element) {
            // Compare against first value: if match return 0, else check second
            // Use uint64_t to handle both signed and unsigned underlying types correctly
            if (static_cast<uint64_t>(id) == static_cast<uint64_t>(Info.min_value)) {
               return 0;
            }
            else if (static_cast<uint64_t>(id) == static_cast<uint64_t>(Info.max_value)) {
               return 1;
            }
            return N; // Not found
         }
         else if constexpr (Info.type == power_of_two) {
            using UnsignedU = std::make_unsigned_t<U>;
            const auto uv = static_cast<UnsignedU>(id);
            if (uv == 0 || !std::has_single_bit(uv)) [[unlikely]] {
               return N;
            }
            const auto bit = static_cast<size_t>(std::countr_zero(uv));
            if (bit >= Info.table_size) [[unlikely]] {
               return N;
            }
            return Info.table[bit];
         }
         else if constexpr (Info.type == small_range) {
            const auto idx = static_cast<int64_t>(id) - Info.min_value;
            if (idx < 0 || static_cast<size_t>(idx) >= Info.table_size) [[unlikely]] {
               return N;
            }
            return Info.table[static_cast<size_t>(idx)];
         }
         else if constexpr (Info.type == modular) {
            const auto h = (static_cast<uint64_t>(id) * Info.seed) % Info.table_size;
            return Info.table[h];
         }
         else if constexpr (Info.type == modular_shifted) {
            const auto shifted = static_cast<uint64_t>(id) >> Info.shift;
            const auto h = (shifted * Info.seed) % Info.table_size;
            return Info.table[h];
         }
         else if constexpr (Info.type == linear_search) {
            for (size_t i = 0; i < N; ++i) {
               if (ids_v<T>[i] == id) {
                  return i;
               }
            }
            return N;
         }
         else { // binary_search
            constexpr auto sorted_data = []() {
               struct result_t
               {
                  std::array<size_t, N> indices{};
                  std::array<U, N> values{};
               };
               result_t result{};

               for (size_t i = 0; i < N; ++i) {
                  result.indices[i] = i;
               }
               for (size_t i = 0; i < N - 1; ++i) {
                  for (size_t j = i + 1; j < N; ++j) {
                     if (ids_v<T>[result.indices[j]] < ids_v<T>[result.indices[i]]) {
                        auto tmp = result.indices[i];
                        result.indices[i] = result.indices[j];
                        result.indices[j] = tmp;
                     }
                  }
               }
               for (size_t i = 0; i < N; ++i) {
                  result.values[i] = ids_v<T>[result.indices[i]];
               }
               return result;
            }();

            size_t left = 0;
            size_t right = N;
            while (left < right) {
               const size_t mid = left + (right - left) / 2;
               if (sorted_data.values[mid] < id) {
                  left = mid + 1;
               }
               else {
                  right = mid;
               }
            }
            if (left < N && sorted_data.values[left] == id) {
               return sorted_data.indices[left];
            }
            return N;
         }
      }
   };

   // ============================================================================
   // End of variant_id_to_index
   // ============================================================================
}

namespace glz
{
   [[nodiscard]] inline std::string format_error(const error_code& ec)
   {
      return std::string{meta<error_code>::keys[uint32_t(ec)]};
   }

   [[nodiscard]] inline std::string format_error(const error_ctx& pe)
   {
      std::string error_str{meta<error_code>::keys[uint32_t(pe.ec)]};
      if (pe.custom_error_message.size()) {
         error_str.append(" ");
         error_str.append(pe.custom_error_message.begin(), pe.custom_error_message.end());
      }
      return error_str;
   }

   [[nodiscard]] inline std::string format_error(const error_ctx& pe, const auto& buffer)
   {
      const auto error_type_str = meta<error_code>::keys[uint32_t(pe.ec)];

      const auto info = detail::get_source_info(buffer, pe.count);
      auto error_str = detail::generate_error_string(error_type_str, info);
      if (pe.custom_error_message.size()) {
         error_str.append(" ");
         error_str.append(pe.custom_error_message.begin(), pe.custom_error_message.end());
      }
      return error_str;
   }

   template <class T>
   [[nodiscard]] std::string format_error(const expected<T, error_ctx>& pe, const auto& buffer)
   {
      if (not pe) {
         return format_error(pe.error(), buffer);
      }
      else {
         return "";
      }
   }

   template <class T>
   [[nodiscard]] std::string format_error(const expected<T, error_ctx>& pe)
   {
      if (not pe) {
         return format_error(pe.error());
      }
      else {
         return "";
      }
   }

   template <class T>
   inline constexpr size_t maximum_key_size = [] {
      constexpr auto N = reflect<T>::size;
      size_t maximum{};
      for (size_t i = 0; i < N; ++i) {
         if (reflect<T>::keys[i].size() > maximum) {
            maximum = reflect<T>::keys[i].size();
         }
      }
      return maximum + 2; // add quotes for JSON
   }();

   inline constexpr uint64_t round_up_to_nearest_16(const uint64_t value) noexcept { return (value + 15) & ~15ull; }
}

namespace glz
{
   // The Callable comes second as ranges::for_each puts the callable at the end

   template <class Callable, reflectable T>
   void for_each_field(T&& value, Callable&& callable)
   {
      constexpr auto N = reflect<T>::size;
      if constexpr (N > 0) {
         [&]<size_t... I>(std::index_sequence<I...>) constexpr {
            (callable(get_member(value, get<I>(to_tie(value)))), ...);
         }(std::make_index_sequence<N>{});
      }
   }

   template <class Callable, glaze_object_t T>
   void for_each_field(T&& value, Callable&& callable)
   {
      constexpr auto N = reflect<T>::size;
      if constexpr (N > 0) {
         [&]<size_t... I>(std::index_sequence<I...>) constexpr {
            (callable(get_member(value, get<I>(reflect<T>::values))), ...);
         }(std::make_index_sequence<N>{});
      }
   }

   // Check if a type has a member with a specific name
   template <class T>
   consteval bool has_member_with_name(const sv& name) noexcept
   {
      if constexpr (reflectable<T> || glaze_object_t<T>) {
         constexpr auto N = reflect<T>::size;
         for (size_t i = 0; i < N; ++i) {
            if (reflect<T>::keys[i] == name) {
               return true;
            }
         }
      }
      return false;
   }

   // Concept to check if glz::reflect<T> can be instantiated and used
   // This concept is automatically satisfied by any type that has a valid reflect<T> specialization
   template <class T>
   concept has_reflect = requires {
      sizeof(reflect<T>); // Ensure reflect<T> is complete
      { reflect<T>::size } -> std::convertible_to<std::size_t>;
   };

   // ---------------------------------------------------------------------------------------------
   // Variant tagging representation
   //
   // How a discriminated variant is laid out on the wire is a property of the *variant type*, not of
   // whichever alternative happens to be active. Choosing it per alternative -- emitting a merged tag
   // for object alternatives and silently nothing for the rest -- produces a type whose wire shape
   // has no single schema, and lets two tag-less alternatives that share a shape be confused for one
   // another on read. So the representation is decided once, here, and every alternative obeys it.
   //
   //   internal (glz::meta `tag` only)
   //      {"type": "circle", "radius": 5}
   //      The discriminator is one more member of the alternative's own object. Nicest shape, but it
   //      only exists for alternatives that *are* objects.
   //
   //   adjacent (glz::meta `tag` and `content`)
   //      {"type": "vec", "value": [1, 2, 3]}
   //      The discriminator sits beside the value under a second fixed key. Works for every
   //      alternative type -- objects, arrays, maps, scalars, null -- at the cost of one nesting
   //      level. Under `structs_as_arrays` this projects to the two element array [id, value].
   //
   // Declaring `tag` alone for a variant that has an alternative internal tagging cannot represent is
   // a static_assert rather than a silent degradation: that combination is exactly the code that is
   // quietly producing unreadable output today.
   // ---------------------------------------------------------------------------------------------

   enum struct variant_tagging_kind : uint8_t { none, internal, adjacent };

   // The object type an alternative presents to the serializers: memory objects (smart pointers,
   // std::optional of a struct, ...) tag through the type they point to.
   template <class V>
   using variant_alternative_object_t = std::conditional_t<is_memory_object<V>, memory_type<V>, V>;

   // A unit alternative carries no data at all: std::monostate, std::nullptr_t, std::nullopt_t.
   // Internal tagging renders it as a discriminator-only object -- exactly the object an empty struct
   // alternative already produces -- so that every alternative of an internally tagged variant is an
   // object carrying the tag. Writing it as a bare `null` instead, which is what Glaze did before,
   // left one alternative of the union with no discriminator on it for no reason anyone chose: the
   // `null` writer simply predates the tagging machinery. Untagged variants still write it as `null`,
   // and both readers still accept `null` for it so existing data keeps parsing.
   template <class V>
   inline constexpr bool variant_unit_alternative = always_null_t<std::decay_t<V>>;

   // An alternative can host a merged discriminator only if it serializes as an object whose members
   // Glaze itself emits, so one more member can be spliced in, or if it carries no data and can be
   // rendered as the discriminator alone. Maps and pairs are objects on the wire but no writer merges
   // a tag into them, and scalars and arrays have nowhere to put one.
   //
   // A custom-serialized alternative is taken at its word: the JSON writer merges the tag into
   // whatever body the user's serializer produces. BEVE cannot do the same because its objects are
   // length-prefixed and the member count of a custom body is not knowable in advance -- the BEVE
   // writer static_asserts on that combination rather than silently dropping the discriminator.
   template <class V>
   inline constexpr bool internally_taggable_alternative =
      glaze_object_t<variant_alternative_object_t<V>> || reflectable<variant_alternative_object_t<V>> ||
      custom_write<V> || variant_unit_alternative<V>;

   namespace detail
   {
      // An empty reflected object, used to consume the body of a discriminator-only object on behalf
      // of a unit alternative. Routing through it reuses the object readers' key handling, unknown-key
      // policy, and terminator consumption rather than duplicating them per format.
      struct variant_unit_body
      {};
   }

   // True when the alternative itself declares a member named like the discriminator. Such an
   // alternative carries the discriminator in its own field: the writer must not emit a second one
   // (doing so produced a duplicate key for glaze_object_t alternatives) and the reader stores the id
   // into that member. The custom cases are excluded because their serializer, not reflection,
   // decides which members exist.
   template <class V>
   consteval bool alternative_declares_key(const sv& name) noexcept
   {
      if constexpr (custom_write<V>) {
         return false;
      }
      else {
         return has_member_with_name<variant_alternative_object_t<V>>(name);
      }
   }

   namespace detail
   {
      // Instantiated only to fail, so the compiler's instantiation backtrace names the alternative
      // that internal tagging cannot represent.
      template <class Variant, class Alternative>
      struct internal_tagging_requires_object_alternatives : std::false_type
      {};

      // Same trick for the two representation limits that are specific to BEVE rather than to the
      // variant's declaration, and so are diagnosed where they bite instead of here.
      template <class Variant, class Alternative>
      struct beve_internal_tagging_needs_reflected_alternative : std::false_type
      {};

      template <class Variant>
      struct beve_positional_tagging_needs_content : std::false_type
      {};
   }

   template <is_variant T>
   struct variant_tagging
   {
      static constexpr size_t size = std::variant_size_v<T>;
      static constexpr bool has_tag = not tag_v<T>.empty();
      static constexpr bool has_content = not content_v<T>.empty();

      static_assert(has_tag || not has_content,
                    "glz::meta `content` selects adjacent tagging for a variant and means nothing on "
                    "its own. Declare `tag` beside it, or remove `content`.");

      static_assert(not(has_tag && has_content) || tag_v<T> != content_v<T>,
                    "glz::meta `tag` and `content` must differ: adjacent tagging writes them as two "
                    "distinct keys of the same object.");

      static constexpr auto kind = has_tag ? (has_content ? variant_tagging_kind::adjacent //
                                                          : variant_tagging_kind::internal)
                                           : variant_tagging_kind::none;

      // Index of the first alternative internal tagging cannot represent, or `size` if there is none.
      static constexpr size_t unrepresentable = []<size_t... I>(std::index_sequence<I...>) {
         size_t r = size;
         (((not internally_taggable_alternative<std::variant_alternative_t<I, T>> && r == size) ? (void)(r = I)
                                                                                                : (void)0),
          ...);
         return r;
      }(std::make_index_sequence<size>{});

      static constexpr bool representable = (kind != variant_tagging_kind::internal) || unrepresentable == size;

      // Clamped so the type below is always nameable; it is only reported when the assert fails.
      static constexpr size_t report = unrepresentable < size ? unrepresentable : 0;

      static_assert(
         representable ||
            detail::internal_tagging_requires_object_alternatives<T, std::variant_alternative_t<report, T>>::value,
         "Internal tagging (glz::meta `tag` without `content`) merges the discriminator into the "
         "alternative's own object, so every alternative must be an object -- or a unit type such as "
         "std::monostate, which is rendered as the discriminator alone. This variant has an "
         "alternative that is neither: a scalar, an array, a map, or a pair. It would be written with "
         "no discriminator at all, giving the variant a wire shape no schema can describe and leaving "
         "same-shaped alternatives impossible to tell apart on read. Declare `content` beside `tag` to "
         "select adjacent tagging, which carries the discriminator for every alternative type. The "
         "offending alternative is the second template argument of "
         "internal_tagging_requires_object_alternatives in the instantiation backtrace.");
   };

   template <is_variant T>
   inline constexpr variant_tagging_kind variant_tagging_v = variant_tagging<T>::kind;

   template <is_variant T>
   inline constexpr bool internally_tagged_v = variant_tagging_v<T> == variant_tagging_kind::internal;

   template <is_variant T>
   inline constexpr bool adjacently_tagged_v = variant_tagging_v<T> == variant_tagging_kind::adjacent;
}

#if defined(_MSC_VER) && !defined(__clang__)
// restore disabled warnings
#pragma warning(pop)
#endif
