/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include "iceberg/util/struct_like_set.h"

#include <bit>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <utility>

#include "iceberg/result.h"
#include "iceberg/type.h"
#include "iceberg/util/checked_cast.h"
#include "iceberg/util/macros.h"

namespace iceberg {

namespace {

/// \brief Helper for std::visit with multiple lambdas.
template <class... Ts>
struct Overloaded : Ts... {
  using Ts::operator()...;
};
template <class... Ts>
Overloaded(Ts...) -> Overloaded<Ts...>;

/// \brief A StructLike that owns its field values in a vector of Scalars.
class ArenaStructLike : public StructLike {
 public:
  explicit ArenaStructLike(std::pmr::vector<Scalar> fields)
      : fields_(std::move(fields)) {}

  Result<Scalar> GetField(size_t pos) const override {
    ICEBERG_PRECHECK(pos < fields_.size(), "field position {} out of range [0, {})", pos,
                     fields_.size());
    return fields_[pos];
  }

  size_t num_fields() const override { return fields_.size(); }

 private:
  std::pmr::vector<Scalar> fields_;
};

/// \brief An ArrayLike that owns its element values in a vector of Scalars.
class ArenaArrayLike : public ArrayLike {
 public:
  explicit ArenaArrayLike(std::pmr::vector<Scalar> elements)
      : elements_(std::move(elements)) {}

  Result<Scalar> GetElement(size_t pos) const override {
    ICEBERG_PRECHECK(pos < elements_.size(), "element position {} out of range [0, {})",
                     pos, elements_.size());
    return elements_[pos];
  }

  size_t size() const override { return elements_.size(); }

 private:
  std::pmr::vector<Scalar> elements_;
};

/// \brief A MapLike that owns its key/value data in vectors of Scalars.
class ArenaMapLike : public MapLike {
 public:
  ArenaMapLike(std::pmr::vector<Scalar> keys, std::pmr::vector<Scalar> values)
      : keys_(std::move(keys)), values_(std::move(values)) {}

  Result<Scalar> GetKey(size_t pos) const override {
    ICEBERG_PRECHECK(pos < keys_.size(), "key position {} out of range [0, {})", pos,
                     keys_.size());
    return keys_[pos];
  }

  Result<Scalar> GetValue(size_t pos) const override {
    ICEBERG_PRECHECK(pos < values_.size(), "value position {} out of range [0, {})", pos,
                     values_.size());
    return values_[pos];
  }

  size_t size() const override { return keys_.size(); }

 private:
  std::pmr::vector<Scalar> keys_;
  std::pmr::vector<Scalar> values_;
};

constexpr uint32_t kCanonicalFloatNaNBits = 0x7fc00000U;
constexpr uint64_t kCanonicalDoubleNaNBits = 0x7ff8000000000000ULL;

uint32_t CanonicalFloatBits(float value) {
  if (std::isnan(value)) {
    return kCanonicalFloatNaNBits;
  }
  return std::bit_cast<uint32_t>(value);
}

uint64_t CanonicalDoubleBits(double value) {
  if (std::isnan(value)) {
    return kCanonicalDoubleNaNBits;
  }
  return std::bit_cast<uint64_t>(value);
}

Result<size_t> HashScalar(const Scalar& scalar);

/// \brief Hash a string_view using Java's String.hashCode() algorithm
size_t HashStringView(std::string_view sv) {
  size_t result = 177;
  for (unsigned char ch : sv) {
    result = 31 * result + ch;
  }
  return result;
}

/// \brief Hash a StructLike using Java's StructLikeHash algorithm
Result<size_t> HashStructLike(const StructLike& s) {
  size_t result = 97;
  size_t len = s.num_fields();
  result = 41 * result + len;
  for (size_t i = 0; i < len; ++i) {
    ICEBERG_ASSIGN_OR_RAISE(auto field_hash, s.GetField(i).and_then(HashScalar));
    result = 41 * result + field_hash;
  }
  return result;
}

/// \brief Hash an ArrayLike using Java's ListHash algorithm
Result<size_t> HashArrayLike(const ArrayLike& a) {
  size_t result = 17;
  size_t len = a.size();
  result = 37 * result + len;
  for (size_t i = 0; i < len; ++i) {
    ICEBERG_ASSIGN_OR_RAISE(auto elem, a.GetElement(i).and_then(HashScalar));
    result = 37 * result + elem;
  }
  return result;
}

Result<size_t> HashMapLike(const MapLike& m) {
  size_t result = 17;
  size_t len = m.size();
  result = 37 * result + len;
  for (size_t i = 0; i < len; ++i) {
    ICEBERG_ASSIGN_OR_RAISE(auto key_hash, m.GetKey(i).and_then(HashScalar));
    ICEBERG_ASSIGN_OR_RAISE(auto value_hash, m.GetValue(i).and_then(HashScalar));
    result = 37 * result + key_hash;
    result = 37 * result + value_hash;
  }
  return result;
}

size_t HashStructLikeUnchecked(const StructLike& s) noexcept {
  auto result = HashStructLike(s);
  ICEBERG_DCHECK(result.has_value(), "Validated StructLike hash must not fail");
  return result.value_or(0);
}

Result<size_t> HashScalar(const Scalar& scalar) {
  return std::visit(
      Overloaded{
          [](std::monostate) -> Result<size_t> { return 0; },
          [](bool v) -> Result<size_t> { return std::hash<bool>{}(v); },
          [](int32_t v) -> Result<size_t> { return std::hash<int32_t>{}(v); },
          [](int64_t v) -> Result<size_t> { return std::hash<int64_t>{}(v); },
          [](float v) -> Result<size_t> {
            return static_cast<size_t>(CanonicalFloatBits(v));
          },
          [](double v) -> Result<size_t> {
            uint64_t bits = CanonicalDoubleBits(v);
            return static_cast<size_t>(bits ^ (bits >> 32));
          },
          [](std::string_view v) -> Result<size_t> { return HashStringView(v); },
          [](const Decimal& v) -> Result<size_t> {
            return std::hash<uint64_t>{}(v.low()) ^ (std::hash<int64_t>{}(v.high()) << 1);
          },
          [](const std::shared_ptr<StructLike>& v) -> Result<size_t> {
            return v ? HashStructLike(*v) : Result<size_t>{0};
          },
          [](const std::shared_ptr<ArrayLike>& v) -> Result<size_t> {
            return v ? HashArrayLike(*v) : Result<size_t>{0};
          },
          [](const std::shared_ptr<MapLike>& v) -> Result<size_t> {
            return v ? HashMapLike(*v) : Result<size_t>{0};
          },
      },
      scalar);
}

Result<bool> ScalarEqual(const Scalar& lhs, const Scalar& rhs);

std::string_view ScalarTypeName(const Scalar& scalar) {
  return std::visit(
      Overloaded{
          [](std::monostate) -> std::string_view { return "null"; },
          [](bool) -> std::string_view { return "boolean"; },
          [](int32_t) -> std::string_view { return "int32"; },
          [](int64_t) -> std::string_view { return "int64"; },
          [](float) -> std::string_view { return "float"; },
          [](double) -> std::string_view { return "double"; },
          [](std::string_view) -> std::string_view { return "string"; },
          [](const Decimal&) -> std::string_view { return "decimal"; },
          [](const std::shared_ptr<StructLike>&) -> std::string_view { return "struct"; },
          [](const std::shared_ptr<ArrayLike>&) -> std::string_view { return "list"; },
          [](const std::shared_ptr<MapLike>&) -> std::string_view { return "map"; },
      },
      scalar);
}

Status ValidateScalarAgainstType(const Scalar& scalar, const Type& type);

Status ValidateStructLikeAgainstType(const StructLike& row, const StructType& type) {
  ICEBERG_PRECHECK(row.num_fields() == type.fields().size(),
                   "StructLike row has {} fields but expected {}", row.num_fields(),
                   type.fields().size());
  for (size_t i = 0; i < row.num_fields(); ++i) {
    ICEBERG_ASSIGN_OR_RAISE(auto scalar, row.GetField(i));
    ICEBERG_RETURN_UNEXPECTED(
        ValidateScalarAgainstType(scalar, *type.fields()[i].type()));
  }
  return {};
}

Status ValidateArrayLikeAgainstType(const ArrayLike& array, const ListType& type) {
  for (size_t i = 0; i < array.size(); ++i) {
    ICEBERG_ASSIGN_OR_RAISE(auto scalar, array.GetElement(i));
    ICEBERG_RETURN_UNEXPECTED(ValidateScalarAgainstType(scalar, *type.element().type()));
  }
  return {};
}

Status ValidateMapLikeAgainstType(const MapLike& map, const MapType& type) {
  for (size_t i = 0; i < map.size(); ++i) {
    ICEBERG_ASSIGN_OR_RAISE(auto key, map.GetKey(i));
    ICEBERG_ASSIGN_OR_RAISE(auto value, map.GetValue(i));
    ICEBERG_RETURN_UNEXPECTED(ValidateScalarAgainstType(key, *type.key().type()));
    ICEBERG_RETURN_UNEXPECTED(ValidateScalarAgainstType(value, *type.value().type()));
  }
  return {};
}

Status ValidateScalarAgainstType(const Scalar& scalar, const Type& type) {
  if (std::holds_alternative<std::monostate>(scalar)) {
    return {};
  }

  switch (type.type_id()) {
    case TypeId::kUnknown:
      return InvalidArgument("Expected unknown but got {}", ScalarTypeName(scalar));
    case TypeId::kBoolean:
      ICEBERG_PRECHECK(std::holds_alternative<bool>(scalar),
                       "Expected boolean but got {}", ScalarTypeName(scalar));
      return {};
    case TypeId::kInt:
    case TypeId::kDate:
      ICEBERG_PRECHECK(std::holds_alternative<int32_t>(scalar), "Expected {} but got {}",
                       type.ToString(), ScalarTypeName(scalar));
      return {};
    case TypeId::kLong:
    case TypeId::kTime:
    case TypeId::kTimestamp:
    case TypeId::kTimestampTz:
    case TypeId::kTimestampNs:
    case TypeId::kTimestampTzNs:
      ICEBERG_PRECHECK(std::holds_alternative<int64_t>(scalar), "Expected {} but got {}",
                       type.ToString(), ScalarTypeName(scalar));
      return {};
    case TypeId::kFloat:
      ICEBERG_PRECHECK(std::holds_alternative<float>(scalar), "Expected float but got {}",
                       ScalarTypeName(scalar));
      return {};
    case TypeId::kDouble:
      ICEBERG_PRECHECK(std::holds_alternative<double>(scalar),
                       "Expected double but got {}", ScalarTypeName(scalar));
      return {};
    case TypeId::kDecimal:
      ICEBERG_PRECHECK(std::holds_alternative<Decimal>(scalar),
                       "Expected decimal but got {}", ScalarTypeName(scalar));
      return {};
    case TypeId::kString:
    case TypeId::kBinary:
      ICEBERG_PRECHECK(std::holds_alternative<std::string_view>(scalar),
                       "Expected {} but got {}", type.ToString(), ScalarTypeName(scalar));
      return {};
    case TypeId::kFixed: {
      ICEBERG_PRECHECK(std::holds_alternative<std::string_view>(scalar),
                       "Expected fixed but got {}", ScalarTypeName(scalar));
      const auto& fixed = static_cast<const FixedType&>(type);
      auto value = std::get<std::string_view>(scalar);
      ICEBERG_PRECHECK(value.size() == fixed.length(),
                       "Expected fixed({}) but got byte length {}", fixed.length(),
                       value.size());
      return {};
    }
    case TypeId::kUuid: {
      ICEBERG_PRECHECK(std::holds_alternative<std::string_view>(scalar),
                       "Expected uuid but got {}", ScalarTypeName(scalar));
      auto value = std::get<std::string_view>(scalar);
      ICEBERG_PRECHECK(value.size() == 16, "Expected uuid byte length 16 but got {}",
                       value.size());
      return {};
    }
    case TypeId::kStruct: {
      ICEBERG_PRECHECK(std::holds_alternative<std::shared_ptr<StructLike>>(scalar),
                       "Expected struct but got {}", ScalarTypeName(scalar));
      const auto& row = std::get<std::shared_ptr<StructLike>>(scalar);
      ICEBERG_PRECHECK(row, "Expected struct but got null");
      return ValidateStructLikeAgainstType(
          *row, internal::checked_cast<const StructType&>(type));
    }
    case TypeId::kList: {
      ICEBERG_PRECHECK(std::holds_alternative<std::shared_ptr<ArrayLike>>(scalar),
                       "Expected list but got {}", ScalarTypeName(scalar));
      const auto& array = std::get<std::shared_ptr<ArrayLike>>(scalar);
      ICEBERG_PRECHECK(array, "Expected ArrayLike but got null");
      return ValidateArrayLikeAgainstType(*array,
                                          internal::checked_cast<const ListType&>(type));
    }
    case TypeId::kMap: {
      ICEBERG_PRECHECK(std::holds_alternative<std::shared_ptr<MapLike>>(scalar),
                       "Expected map but got {}", ScalarTypeName(scalar));
      const auto& map = std::get<std::shared_ptr<MapLike>>(scalar);
      ICEBERG_PRECHECK(map, "Expected MapLike but got null");
      return ValidateMapLikeAgainstType(*map,
                                        internal::checked_cast<const MapType&>(type));
    }
  }

  std::unreachable();
}

Status ValidateRowAgainstTypes(const StructLike& row,
                               std::span<const std::shared_ptr<Type>> field_types) {
  if (row.num_fields() != field_types.size()) {
    return InvalidArgument("StructLike row has {} fields but expected {}",
                           row.num_fields(), field_types.size());
  }
  for (size_t i = 0; i < field_types.size(); ++i) {
    ICEBERG_ASSIGN_OR_RAISE(auto scalar, row.GetField(i));
    ICEBERG_RETURN_UNEXPECTED(ValidateScalarAgainstType(scalar, *field_types[i]));
  }
  return {};
}

Result<bool> StructLikeEqual(const StructLike& lhs, const StructLike& rhs) {
  if (lhs.num_fields() != rhs.num_fields()) {
    return false;
  }
  for (size_t i = 0; i < lhs.num_fields(); ++i) {
    ICEBERG_ASSIGN_OR_RAISE(auto fa, lhs.GetField(i));
    ICEBERG_ASSIGN_OR_RAISE(auto fb, rhs.GetField(i));
    ICEBERG_ASSIGN_OR_RAISE(auto equal, ScalarEqual(fa, fb));
    if (!equal) {
      return false;
    }
  }
  return true;
}

Result<bool> ArrayLikeEqual(const ArrayLike& lhs, const ArrayLike& rhs) {
  if (lhs.size() != rhs.size()) {
    return false;
  }
  for (size_t i = 0; i < lhs.size(); ++i) {
    ICEBERG_ASSIGN_OR_RAISE(auto ea, lhs.GetElement(i));
    ICEBERG_ASSIGN_OR_RAISE(auto eb, rhs.GetElement(i));
    ICEBERG_ASSIGN_OR_RAISE(auto equal, ScalarEqual(ea, eb));
    if (!equal) {
      return false;
    }
  }
  return true;
}

Result<bool> MapLikeEqual(const MapLike& lhs, const MapLike& rhs) {
  if (lhs.size() != rhs.size()) {
    return false;
  }
  for (size_t i = 0; i < lhs.size(); ++i) {
    ICEBERG_ASSIGN_OR_RAISE(auto ka, lhs.GetKey(i));
    ICEBERG_ASSIGN_OR_RAISE(auto kb, rhs.GetKey(i));
    ICEBERG_ASSIGN_OR_RAISE(auto va, lhs.GetValue(i));
    ICEBERG_ASSIGN_OR_RAISE(auto vb, rhs.GetValue(i));
    ICEBERG_ASSIGN_OR_RAISE(auto keys_equal, ScalarEqual(ka, kb));
    if (!keys_equal) {
      return false;
    }
    ICEBERG_ASSIGN_OR_RAISE(auto values_equal, ScalarEqual(va, vb));
    if (!values_equal) {
      return false;
    }
  }
  return true;
}

bool StructLikeEqualUnchecked(const StructLike& lhs, const StructLike& rhs) noexcept {
  auto result = StructLikeEqual(lhs, rhs);
  ICEBERG_DCHECK(result.has_value(), "Validated StructLike equality must not fail");
  return result.value_or(false);
}

Result<bool> ScalarEqual(const Scalar& lhs, const Scalar& rhs) {
  if (lhs.index() != rhs.index()) {
    return false;
  }

  return std::visit(
      Overloaded{
          [](std::monostate, const Scalar&) -> Result<bool> { return true; },
          [](bool v, const Scalar& other) -> Result<bool> {
            return v == std::get<bool>(other);
          },
          [](int32_t v, const Scalar& other) -> Result<bool> {
            return v == std::get<int32_t>(other);
          },
          [](int64_t v, const Scalar& other) -> Result<bool> {
            return v == std::get<int64_t>(other);
          },
          [](float v, const Scalar& other) -> Result<bool> {
            return CanonicalFloatBits(v) == CanonicalFloatBits(std::get<float>(other));
          },
          [](double v, const Scalar& other) -> Result<bool> {
            return CanonicalDoubleBits(v) == CanonicalDoubleBits(std::get<double>(other));
          },
          [](std::string_view v, const Scalar& other) -> Result<bool> {
            return v == std::get<std::string_view>(other);
          },
          [](const Decimal& v, const Scalar& other) -> Result<bool> {
            return v == std::get<Decimal>(other);
          },
          [](const std::shared_ptr<StructLike>& l, const Scalar& other) -> Result<bool> {
            const auto& r = std::get<std::shared_ptr<StructLike>>(other);
            if (!l && !r) return true;
            if (!l || !r) return false;
            return StructLikeEqual(*l, *r);
          },
          [](const std::shared_ptr<ArrayLike>& l, const Scalar& other) -> Result<bool> {
            const auto& r = std::get<std::shared_ptr<ArrayLike>>(other);
            if (!l && !r) return true;
            if (!l || !r) return false;
            return ArrayLikeEqual(*l, *r);
          },
          [](const std::shared_ptr<MapLike>& l, const Scalar& other) -> Result<bool> {
            const auto& r = std::get<std::shared_ptr<MapLike>>(other);
            if (!l && !r) return true;
            if (!l || !r) return false;
            return MapLikeEqual(*l, *r);
          },
      },
      lhs, rhs);
}

}  // namespace

template <bool kValidate>
StructLikeSet<kValidate>::StructLikeSet(const StructType& type, size_t arena_initial_size)
    : arena_(arena_initial_size) {
  field_types_.reserve(type.fields().size());
  for (const auto& field : type.fields()) {
    field_types_.push_back(field.type());
  }
}

template <bool kValidate>
StructLikeSet<kValidate>::~StructLikeSet() = default;

template <bool kValidate>
std::string_view StructLikeSet<kValidate>::CopyToArena(std::string_view src) const {
  if (src.empty()) {
    return {};
  }
  auto buf = static_cast<char*>(arena_.allocate(src.size(), 1));
  std::memcpy(buf, src.data(), src.size());
  return {buf, src.size()};
}

template <bool kValidate>
Result<Scalar> StructLikeSet<kValidate>::DeepCopyScalar(const Scalar& scalar) const {
  return std::visit(
      Overloaded{
          [](std::monostate v) -> Result<Scalar> { return Scalar{v}; },
          [](bool v) -> Result<Scalar> { return Scalar{v}; },
          [](int32_t v) -> Result<Scalar> { return Scalar{v}; },
          [](int64_t v) -> Result<Scalar> { return Scalar{v}; },
          [](float v) -> Result<Scalar> { return Scalar{v}; },
          [](double v) -> Result<Scalar> { return Scalar{v}; },
          [this](std::string_view sv) -> Result<Scalar> {
            return Scalar{CopyToArena(sv)};
          },
          [](const Decimal& v) -> Result<Scalar> { return Scalar{v}; },
          [this](const std::shared_ptr<StructLike>& s) -> Result<Scalar> {
            ICEBERG_PRECHECK(s, "StructLike scalar must not be null");
            std::pmr::vector<Scalar> fields(&arena_);
            fields.resize(s->num_fields());
            for (size_t i = 0; i < s->num_fields(); ++i) {
              ICEBERG_ASSIGN_OR_RAISE(auto field, s->GetField(i));
              ICEBERG_ASSIGN_OR_RAISE(fields[i], DeepCopyScalar(field));
            }
            return Scalar{std::make_shared<ArenaStructLike>(std::move(fields))};
          },
          [this](const std::shared_ptr<ArrayLike>& a) -> Result<Scalar> {
            ICEBERG_PRECHECK(a, "ArrayLike scalar must not be null");
            std::pmr::vector<Scalar> elements(&arena_);
            elements.resize(a->size());
            for (size_t i = 0; i < a->size(); ++i) {
              ICEBERG_ASSIGN_OR_RAISE(auto element, a->GetElement(i));
              ICEBERG_ASSIGN_OR_RAISE(elements[i], DeepCopyScalar(element));
            }
            return Scalar{std::make_shared<ArenaArrayLike>(std::move(elements))};
          },
          [this](const std::shared_ptr<MapLike>& m) -> Result<Scalar> {
            ICEBERG_PRECHECK(m, "MapLike scalar must not be null");
            std::pmr::vector<Scalar> keys(&arena_);
            std::pmr::vector<Scalar> values(&arena_);
            keys.resize(m->size());
            values.resize(m->size());
            for (size_t i = 0; i < m->size(); ++i) {
              ICEBERG_ASSIGN_OR_RAISE(auto key, m->GetKey(i));
              ICEBERG_ASSIGN_OR_RAISE(auto value, m->GetValue(i));
              ICEBERG_ASSIGN_OR_RAISE(keys[i], DeepCopyScalar(key));
              ICEBERG_ASSIGN_OR_RAISE(values[i], DeepCopyScalar(value));
            }
            return Scalar{
                std::make_shared<ArenaMapLike>(std::move(keys), std::move(values))};
          },
      },
      scalar);
}

template <bool kValidate>
Result<std::unique_ptr<StructLike>> StructLikeSet<kValidate>::MakeArenaRow(
    const StructLike& row) const {
  std::pmr::vector<Scalar> fields(&arena_);
  fields.resize(field_types_.size());
  for (size_t i = 0; i < field_types_.size(); ++i) {
    ICEBERG_ASSIGN_OR_RAISE(auto scalar, row.GetField(i));
    ICEBERG_ASSIGN_OR_RAISE(fields[i], DeepCopyScalar(scalar));
  }
  return std::make_unique<ArenaStructLike>(std::move(fields));
}

template <bool kValidate>
Status StructLikeSet<kValidate>::Insert(const StructLike& row) {
  if constexpr (kValidate) {
    ICEBERG_RETURN_UNEXPECTED(ValidateRowAgainstTypes(row, field_types_));
  }
  if (set_.contains(row)) {
    return {};
  }
  ICEBERG_ASSIGN_OR_RAISE(auto arena_row, MakeArenaRow(row));
  set_.insert(std::move(arena_row));
  return {};
}

template <bool kValidate>
Result<bool> StructLikeSet<kValidate>::Contains(const StructLike& row) const {
  if constexpr (kValidate) {
    ICEBERG_RETURN_UNEXPECTED(ValidateRowAgainstTypes(row, field_types_));
  }
  return set_.find(row) != set_.end();
}

template <bool kValidate>
bool StructLikeSet<kValidate>::IsEmpty() const {
  return set_.empty();
}

template <bool kValidate>
size_t StructLikeSet<kValidate>::Size() const {
  return set_.size();
}

// --- KeyHash ---

template <bool kValidate>
size_t StructLikeSet<kValidate>::KeyHash::operator()(
    const std::unique_ptr<StructLike>& p) const noexcept {
  return HashStructLikeUnchecked(*p);
}

template <bool kValidate>
size_t StructLikeSet<kValidate>::KeyHash::operator()(const StructLike& s) const noexcept {
  return HashStructLikeUnchecked(s);
}

// --- KeyEqual ---

template <bool kValidate>
bool StructLikeSet<kValidate>::KeyEqual::operator()(
    const std::unique_ptr<StructLike>& lhs,
    const std::unique_ptr<StructLike>& rhs) const noexcept {
  return StructLikeEqualUnchecked(*lhs, *rhs);
}

template <bool kValidate>
bool StructLikeSet<kValidate>::KeyEqual::operator()(
    const StructLike& lhs, const std::unique_ptr<StructLike>& rhs) const noexcept {
  return StructLikeEqualUnchecked(lhs, *rhs);
}

template <bool kValidate>
bool StructLikeSet<kValidate>::KeyEqual::operator()(
    const std::unique_ptr<StructLike>& lhs, const StructLike& rhs) const noexcept {
  return StructLikeEqualUnchecked(*lhs, rhs);
}

template class ICEBERG_TEMPLATE_EXPORT StructLikeSet<true>;
template class ICEBERG_TEMPLATE_EXPORT StructLikeSet<false>;

}  // namespace iceberg
