#pragma once

#include "CesiumGltf/ClassProperty.h"
#include "CesiumGltf/PropertyAttributeProperty.h"
#include "CesiumGltf/PropertyTableProperty.h"
#include "CesiumGltf/PropertyTextureProperty.h"
#include "CesiumGltf/PropertyTypeTraits.h"

#include <cstring>
#include <optional>

namespace CesiumGltf {

typedef int32_t PropertyViewStatusType;

/**
 * @brief Indicates the status of a property view.
 *
 * The {@link PropertyView} constructor always completes successfully.
 * However, there may be fundamental errors with the property definition. In
 * such cases, this enumeration provides the reason.
 *
 * This is defined with a class of static consts as opposed to an enum, so that
 * derived property view classes can extend the statuses with their own specific
 * errors.
 */
class PropertyViewStatus {
public:
  /**
   * @brief This property view is valid and ready to use.
   */
  static const PropertyViewStatusType Valid = 0;

  /**
   * @brief This property view does not contain any data, but specifies a
   * default value. This happens when a class property is defined with a default
   * value and omitted from an instance of the class's collective properties. In
   * this case, it is not possible to retrieve the raw data from a property, but
   * its default value will be accessible.
   */
  static const PropertyViewStatusType EmptyPropertyWithDefault = 1;

  /**
   * @brief This property view is trying to view a property that does not
   * exist.
   */
  static const PropertyViewStatusType ErrorNonexistentProperty = 2;

  /**
   * @brief This property view's type does not match what is
   * specified in {@link ClassProperty::type}.
   */
  static const PropertyViewStatusType ErrorTypeMismatch = 3;

  /**
   * @brief This property view's component type does not match what
   * is specified in {@link ClassProperty::componentType}.
   */
  static const PropertyViewStatusType ErrorComponentTypeMismatch = 4;

  /**
   * @brief This property view differs from what is specified in
   * {@link ClassProperty::array}.
   */
  static const PropertyViewStatusType ErrorArrayTypeMismatch = 5;

  /**
   * @brief This property says it is normalized, but it does not have an integer
   * component type.
   */
  static const PropertyViewStatusType ErrorInvalidNormalization = 6;

  /**
   * @brief This property view's normalization differs from what
   * is specified in {@link ClassProperty::normalized}
   */
  static const PropertyViewStatusType ErrorNormalizationMismatch = 7;

  /**
   * @brief The property provided an invalid offset value.
   */
  static const PropertyViewStatusType ErrorInvalidOffset = 8;

  /**
   * @brief The property provided an invalid scale value.
   */
  static const PropertyViewStatusType ErrorInvalidScale = 9;

  /**
   * @brief The property provided an invalid maximum value.
   */
  static const PropertyViewStatusType ErrorInvalidMax = 10;

  /**
   * @brief The property provided an invalid minimum value.
   */
  static const PropertyViewStatusType ErrorInvalidMin = 11;

  /**
   * @brief The property provided an invalid "no data" value.
   */
  static const PropertyViewStatusType ErrorInvalidNoDataValue = 12;

  /**
   * @brief The property provided an invalid default value.
   */
  static const PropertyViewStatusType ErrorInvalidDefaultValue = 13;
};

template <typename T>
PropertyViewStatusType
validatePropertyType(const ClassProperty& classProperty) {
  if (TypeToPropertyType<T>::value !=
      convertStringToPropertyType(classProperty.type)) {
    return PropertyViewStatus::ErrorTypeMismatch;
  }

  PropertyComponentType expectedComponentType =
      TypeToPropertyType<T>::component;

  if (!classProperty.componentType &&
      expectedComponentType != PropertyComponentType::None) {
    return PropertyViewStatus::ErrorComponentTypeMismatch;
  }

  if (classProperty.componentType &&
      expectedComponentType !=
          convertStringToPropertyComponentType(*classProperty.componentType)) {
    return PropertyViewStatus::ErrorComponentTypeMismatch;
  }

  if (classProperty.array) {
    return PropertyViewStatus::ErrorArrayTypeMismatch;
  }

  return PropertyViewStatus::Valid;
}

template <typename T>
PropertyViewStatusType
validateArrayPropertyType(const ClassProperty& classProperty) {
  using ElementType = typename MetadataArrayType<T>::type;
  if (TypeToPropertyType<ElementType>::value !=
      convertStringToPropertyType(classProperty.type)) {
    return PropertyViewStatus::ErrorTypeMismatch;
  }

  PropertyComponentType expectedComponentType =
      TypeToPropertyType<ElementType>::component;

  if (!classProperty.componentType &&
      expectedComponentType != PropertyComponentType::None) {
    return PropertyViewStatus::ErrorComponentTypeMismatch;
  }

  if (classProperty.componentType &&
      expectedComponentType !=
          convertStringToPropertyComponentType(*classProperty.componentType)) {
    return PropertyViewStatus::ErrorComponentTypeMismatch;
  }

  if (!classProperty.array) {
    return PropertyViewStatus::ErrorArrayTypeMismatch;
  }

  return PropertyViewStatus::Valid;
}

template <typename T>
static std::optional<T> getScalar(const CesiumUtility::JsonValue& jsonValue) {
  return jsonValue.getSafeNumber<T>();
}

template <typename VecType>
static std::optional<VecType>
getVecN(const CesiumUtility::JsonValue& jsonValue) {
  if (!jsonValue.isArray()) {
    return std::nullopt;
  }

  const CesiumUtility::JsonValue::Array& array = jsonValue.getArray();
  constexpr glm::length_t N = VecType::length();
  if (array.size() != N) {
    return std::nullopt;
  }

  using T = typename VecType::value_type;

  VecType result;
  for (glm::length_t i = 0; i < N; i++) {
    std::optional<T> value = getScalar<T>(array[i]);
    if (!value) {
      return std::nullopt;
    }

    result[i] = *value;
  }

  return result;
}

template <typename MatType>
static std::optional<MatType>
getMatN(const CesiumUtility::JsonValue& jsonValue) {
  if (!jsonValue.isArray()) {
    return std::nullopt;
  }

  const CesiumUtility::JsonValue::Array& array = jsonValue.getArray();
  constexpr glm::length_t N = MatType::length();
  if (array.size() != N * N) {
    return std::nullopt;
  }

  using T = typename MatType::value_type;

  MatType result;
  for (glm::length_t i = 0; i < N; i++) {
    // Try to parse each value in the column.
    for (glm::length_t j = 0; j < N; j++) {
      std::optional<T> value = getScalar<T>(array[i * N + j]);
      if (!value) {
        return std::nullopt;
      }

      result[i][j] = *value;
    }
  }

  return result;
}

template <typename ElementType>
int64_t getCount(std::optional<std::vector<std::byte>>& buffer) {
  if (!buffer) {
    return 0;
  }

  return static_cast<int64_t>(buffer->size() / sizeof(ElementType));
}

/**
 * @brief Represents a metadata property in EXT_structural_metadata.
 */
template <typename ElementType, bool Normalized = false> class PropertyView;

/**
 * @brief Represents a non-normalized metadata property in
 * EXT_structural_metadata.
 *
 * Whether they belong to property tables, property textures, or property
 * attributes, properties have their own sub-properties affecting the actual
 * property values. Although they are typically defined via class property, they
 * may be overridden by individual instances of the property themselves. The
 * constructor is responsible for resolving those differences.
 *
 * @tparam ElementType The C++ type of the values in this property
 */
template <typename ElementType> class PropertyView<ElementType, false> {
public:
  /**
   * @brief Constructs an empty property instance.
   */
  PropertyView()
      : _status(PropertyViewStatus::ErrorNonexistentProperty),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _offset(std::nullopt),
        _scale(std::nullopt),
        _max(std::nullopt),
        _min(std::nullopt),
        _required(false),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {}

  /**
   * @brief Constructs a property instance from a class definition only.
   */
  PropertyView(const ClassProperty& classProperty)
      : _status(validatePropertyType<ElementType>(classProperty)),
        _name(classProperty.name),
        _semantic(classProperty.semantic),
        _description(classProperty.description),
        _offset(std::nullopt),
        _scale(std::nullopt),
        _max(std::nullopt),
        _min(std::nullopt),
        _required(classProperty.required),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    if (classProperty.normalized) {
      _status = PropertyViewStatus::ErrorNormalizationMismatch;
      return;
    }

    getNumericPropertyValues(classProperty);
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    if (classProperty.noData) {
      if (!_required) {
        // "noData" can only be defined if the property is not required.
        _noData = getValue(*classProperty.noData);
      }

      if (!_noData) {
        // The value was specified but something went wrong.
        _status = PropertyViewStatus::ErrorInvalidNoDataValue;
        return;
      }
    }

    if (classProperty.defaultProperty) {
      if (!_required) {
        // "default" can only be defined if the property is not required.
        _defaultValue = getValue(*classProperty.defaultProperty);
      }

      if (!_defaultValue) {
        // The value was specified but something went wrong.
        _status = PropertyViewStatus::ErrorInvalidDefaultValue;
        return;
      }
    }
  }

protected:
  /**
   * @brief Constructs an invalid instance for an erroneous property.
   *
   * @param status The value of {@link PropertyViewStatus} indicating the error
   * with the property.
   */
  PropertyView(PropertyViewStatusType status)
      : _status(status),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _offset(std::nullopt),
        _scale(std::nullopt),
        _max(std::nullopt),
        _min(std::nullopt),
        _required(false),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {}

  /**
   * @brief Constructs a property instance from a property table property and
   * its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyTableProperty& property)
      : PropertyView(classProperty) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    // If the property has its own values, override the class-provided values.
    getNumericPropertyValues(property);
  }

  /**
   * @brief Constructs a property instance from a property texture property and
   * its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyTextureProperty& property)
      : PropertyView(classProperty) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    // If the property has its own values, override the class-provided values.
    getNumericPropertyValues(property);
  }

  /**
   * @brief Constructs a property instance from a property attribute property
   * and its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyAttributeProperty& property)
      : PropertyView(classProperty) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    // If the property has its own values, override the class-provided values.
    getNumericPropertyValues(property);
  }

public:
  /**
   * @brief Gets the status of this property view, indicating whether an error
   * occurred.
   *
   * @return The status of this property view.
   */
  PropertyViewStatusType status() const noexcept { return _status; }

  /**
   * @brief Gets the name of the property being viewed. Returns std::nullopt if
   * no name was specified.
   */
  const std::optional<std::string>& name() const noexcept { return _name; }

  /**
   * @brief Gets the semantic of the property being viewed. The semantic is an
   * identifier that describes how this property should be interpreted, and
   * cannot be used by other properties in the class. Returns std::nullopt if no
   * semantic was specified.
   */
  const std::optional<std::string>& semantic() const noexcept {
    return _semantic;
  }

  /**
   * @brief Gets the description of the property being viewed. Returns
   * std::nullopt if no description was specified.
   */
  const std::optional<std::string>& description() const noexcept {
    return _description;
  }

  /**
   * @brief Get the element count of the fixed-length arrays in this property.
   * Only applicable when the property is an array type.
   *
   * @return The count of this property.
   */
  int64_t arrayCount() const noexcept { return 0; }

  /**
   * @brief Whether this property has a normalized integer type.
   */
  bool normalized() const noexcept { return false; }

  /**
   * @brief Gets the offset to apply to property values. Only applicable to
   * SCALAR, VECN, and MATN types when the component type is FLOAT32 or
   * FLOAT64, or when the property is normalized.
   *
   * @returns The property's offset, or std::nullopt if it was not specified.
   */
  std::optional<ElementType> offset() const noexcept { return _offset; }

  /**
   * @brief Gets the scale to apply to property values. Only applicable to
   * SCALAR, VECN, and MATN types when the component type is FLOAT32 or
   * FLOAT64, or when the property is normalized.
   *
   * @returns The property's scale, or std::nullopt if it was not specified.
   */
  std::optional<ElementType> scale() const noexcept { return _scale; }

  /**
   * @brief Gets the maximum allowed value for the property. Only applicable to
   * SCALAR, VECN, and MATN types. This is the maximum of all property
   * values, after the transforms based on the normalized, offset, and
   * scale properties have been applied.
   *
   * @returns The property's maximum value, or std::nullopt if it was not
   * specified.
   */
  std::optional<ElementType> max() const noexcept { return _max; }

  /**
   * @brief Gets the minimum allowed value for the property. Only applicable to
   * SCALAR, VECN, and MATN types. This is the minimum of all property
   * values, after the transforms based on the normalized, offset, and
   * scale properties have been applied.
   *
   * @returns The property's minimum value, or std::nullopt if it was not
   * specified.
   */
  std::optional<ElementType> min() const noexcept { return _min; }

  /**
   * @brief Whether the property must be present in every entity conforming to
   * the class. If not required, instances of the property may include "no data"
   * values, or the entire property may be omitted.
   */
  bool required() const noexcept { return _required; }

  /**
   * @brief Gets the "no data" value, i.e., the value representing missing data
   * in the property wherever it appears. Also known as a sentinel value. This
   * is given as the plain property value, without the transforms from the
   * normalized, offset, and scale properties.
   *
   * @returns The property's "no data" value, or std::nullopt if it was not
   * specified.
   */
  std::optional<ElementType> noData() const noexcept { return _noData; }

  /**
   * @brief Gets the default value to use when encountering a "no data" value or
   * an omitted property. The value is given in its final form, taking the
   * effect of normalized, offset, and scale properties into account.
   *
   * @returns The property's default value, or std::nullopt if it was not
   * specified.
   */
  std::optional<ElementType> defaultValue() const noexcept {
    return _defaultValue;
  }

protected:
  PropertyViewStatusType _status;

private:
  std::optional<std::string> _name;
  std::optional<std::string> _semantic;
  std::optional<std::string> _description;

  std::optional<ElementType> _offset;
  std::optional<ElementType> _scale;
  std::optional<ElementType> _max;
  std::optional<ElementType> _min;

  bool _required;
  std::optional<ElementType> _noData;
  std::optional<ElementType> _defaultValue;

  /**
   * @brief Attempts to parse an ElementType from the given json value.
   *
   * If ElementType is a type with multiple components, e.g. a VECN or MATN
   * type, this will return std::nullopt if one or more components could not be
   * parsed.
   *
   * @return The value as an instance of ElementType, or std::nullopt if it
   * could not be parsed.
   */
  static std::optional<ElementType>
  getValue(const CesiumUtility::JsonValue& jsonValue) {
    if constexpr (IsMetadataScalar<ElementType>::value) {
      return getScalar<ElementType>(jsonValue);
    }

    if constexpr (IsMetadataVecN<ElementType>::value) {
      return getVecN<ElementType>(jsonValue);
    }

    if constexpr (IsMetadataMatN<ElementType>::value) {
      return getMatN<ElementType>(jsonValue);
    }
  }

  using PropertyDefinitionType = std::variant<
      ClassProperty,
      PropertyTableProperty,
      PropertyTextureProperty,
      PropertyAttributeProperty>;

  /**
   * @brief Attempts to parse offset, scale, min, and max properties from the
   * given property type.
   */
  void getNumericPropertyValues(const PropertyDefinitionType& inProperty) {
    std::visit(
        [this](auto property) {
          if (property.offset) {
            // Only floating point types can specify an offset.
            switch (TypeToPropertyType<ElementType>::component) {
            case PropertyComponentType::Float32:
            case PropertyComponentType::Float64:
              this->_offset = getValue(*property.offset);
              if (this->_offset) {
                break;
              }
              // If it does not break here, something went wrong.
              [[fallthrough]];
            default:
              this->_status = PropertyViewStatus::ErrorInvalidOffset;
              return;
            }
          }

          if (property.scale) {
            // Only floating point types can specify a scale.
            switch (TypeToPropertyType<ElementType>::component) {
            case PropertyComponentType::Float32:
            case PropertyComponentType::Float64:
              this->_scale = getValue(*property.scale);
              if (this->_scale) {
                break;
              }
              // If it does not break here, something went wrong.
              [[fallthrough]];
            default:
              this->_status = PropertyViewStatus::ErrorInvalidScale;
              return;
            }
          }

          if (property.max) {
            this->_max = getValue(*property.max);
            if (!this->_max) {
              // The value was specified but something went wrong.
              this->_status = PropertyViewStatus::ErrorInvalidMax;
              return;
            }
          }

          if (property.min) {
            this->_min = getValue(*property.min);
            if (!this->_min) {
              // The value was specified but something went wrong.
              this->_status = PropertyViewStatus::ErrorInvalidMin;
              return;
            }
          }
        },
        inProperty);
  }
};

/**
 * @brief Represents a normalized metadata property in
 * EXT_structural_metadata.
 *
 * Whether they belong to property tables, property textures, or property
 * attributes, properties have their own sub-properties affecting the actual
 * property values. Although they are typically defined via class property, they
 * may be overridden by individual instances of the property themselves. The
 * constructor is responsible for resolving those differences.
 *
 * @tparam ElementType The C++ type of the values in this property. Must have an
 * integer component type.
 */
template <typename ElementType> class PropertyView<ElementType, true> {
private:
  using NormalizedType = typename TypeToNormalizedType<ElementType>::type;

public:
  /**
   * @brief Constructs an empty property instance.
   */
  PropertyView()
      : _status(PropertyViewStatus::ErrorNonexistentProperty),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _offset(std::nullopt),
        _scale(std::nullopt),
        _max(std::nullopt),
        _min(std::nullopt),
        _required(false),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {}

  /**
   * @brief Constructs a property instance from a class definition only.
   */
  PropertyView(const ClassProperty& classProperty)
      : _status(validatePropertyType<ElementType>(classProperty)),
        _name(classProperty.name),
        _semantic(classProperty.semantic),
        _description(classProperty.description),
        _offset(std::nullopt),
        _scale(std::nullopt),
        _max(std::nullopt),
        _min(std::nullopt),
        _required(classProperty.required),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    if (!classProperty.normalized) {
      _status = PropertyViewStatus::ErrorNormalizationMismatch;
    }

    getNumericPropertyValues(classProperty);
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    if (classProperty.noData) {
      if (!_required) {
        // "noData" should not be defined if the property is required.
        _noData = getValue<ElementType>(*classProperty.noData);
      }
      if (!_noData) {
        // The value was specified but something went wrong.
        _status = PropertyViewStatus::ErrorInvalidNoDataValue;
        return;
      }
    }

    if (classProperty.defaultProperty) {
      // default value should not be defined if the property is required.
      if (!_required) {
        _defaultValue =
            getValue<NormalizedType>(*classProperty.defaultProperty);
      }
      if (!_defaultValue) {
        // The value was specified but something went wrong.
        _status = PropertyViewStatus::ErrorInvalidDefaultValue;
        return;
      }
    }
  }

protected:
  /**
   * @brief Constructs an invalid instance for an erroneous property.
   *
   * @param status The value of {@link PropertyViewStatus} indicating the error with the property.
   */
  PropertyView(PropertyViewStatusType status)
      : _status(status),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _offset(std::nullopt),
        _scale(std::nullopt),
        _max(std::nullopt),
        _min(std::nullopt),
        _required(false),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {}

  /**
   * @brief Constructs a property instance from a property table property and
   * its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyTableProperty& property)
      : PropertyView(classProperty) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    // If the property has its own values, override the class-provided values.
    getNumericPropertyValues(property);
  }

  /**
   * @brief Constructs a property instance from a property texture property and
   * its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyTextureProperty& property)
      : PropertyView(classProperty) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    // If the property has its own values, override the class-provided values.
    getNumericPropertyValues(property);
  }

  /**
   * @brief Constructs a property instance from a property attribute property
   * and its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyAttributeProperty& property)
      : PropertyView(classProperty) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    // If the property has its own values, override the class-provided values.
    getNumericPropertyValues(property);
  }

public:
  /**
   * @copydoc PropertyView::status
   */
  PropertyViewStatusType status() const noexcept { return _status; }

  /**
   * @copydoc PropertyView::name
   */
  const std::optional<std::string>& name() const noexcept { return _name; }

  /**
   * @copydoc PropertyView::semantic
   */
  const std::optional<std::string>& semantic() const noexcept {
    return _semantic;
  }

  /**
   * @copydoc PropertyView::description
   */
  const std::optional<std::string>& description() const noexcept {
    return _description;
  }

  /**
   * @copydoc PropertyView::arrayCount
   */
  int64_t arrayCount() const noexcept { return 0; }

  /**
   * @copydoc PropertyView::normalized
   */
  bool normalized() const noexcept { return true; }

  /**
   * @copydoc PropertyView::offset
   */
  std::optional<NormalizedType> offset() const noexcept { return _offset; }

  /**
   * @copydoc PropertyView::scale
   */
  std::optional<NormalizedType> scale() const noexcept { return _scale; }

  /**
   * @copydoc PropertyView::max
   */
  std::optional<NormalizedType> max() const noexcept { return _max; }

  /**
   * @copydoc PropertyView::min
   */
  std::optional<NormalizedType> min() const noexcept { return _min; }

  /**
   * @copydoc PropertyView::required
   */
  bool required() const noexcept { return _required; }

  /**
   * @copydoc PropertyView::noData
   */
  std::optional<ElementType> noData() const noexcept { return _noData; }

  /**
   * @copydoc PropertyView::defaultValue
   */
  std::optional<NormalizedType> defaultValue() const noexcept {
    return _defaultValue;
  }

protected:
  PropertyViewStatusType _status;

private:
  std::optional<std::string> _name;
  std::optional<std::string> _semantic;
  std::optional<std::string> _description;

  std::optional<NormalizedType> _offset;
  std::optional<NormalizedType> _scale;
  std::optional<NormalizedType> _max;
  std::optional<NormalizedType> _min;

  bool _required;
  std::optional<ElementType> _noData;
  std::optional<NormalizedType> _defaultValue;

  /**
   * @brief Attempts to parse from the given json value.
   *
   * If T is a type with multiple components, e.g. a VECN or MATN type, this
   * will return std::nullopt if one or more components could not be parsed.
   *
   * @return The value as an instance of T, or std::nullopt if it could not be
   * parsed.
   */
  template <typename T>
  static std::optional<T> getValue(const CesiumUtility::JsonValue& jsonValue) {
    if constexpr (IsMetadataScalar<T>::value) {
      return getScalar<T>(jsonValue);
    }

    if constexpr (IsMetadataVecN<T>::value) {
      return getVecN<T>(jsonValue);
    }

    if constexpr (IsMetadataMatN<T>::value) {
      return getMatN<T>(jsonValue);
    }
  }

  using PropertyDefinitionType = std::variant<
      ClassProperty,
      PropertyTableProperty,
      PropertyTextureProperty,
      PropertyAttributeProperty>;

  /**
   * @brief Attempts to parse offset, scale, min, and max properties from the
   * given property type.
   */
  void getNumericPropertyValues(const PropertyDefinitionType& inProperty) {
    std::visit(
        [this](auto property) {
          if (property.offset) {
            _offset = getValue<NormalizedType>(*property.offset);
            if (!_offset) {
              // The value was specified but something went wrong.
              _status = PropertyViewStatus::ErrorInvalidOffset;
              return;
            }
          }

          if (property.scale) {
            _scale = getValue<NormalizedType>(*property.scale);
            if (!_scale) {
              // The value was specified but something went wrong.
              _status = PropertyViewStatus::ErrorInvalidScale;
              return;
            }
          }

          if (property.max) {
            _max = getValue<NormalizedType>(*property.max);
            if (!_scale) {
              // The value was specified but something went wrong.
              _status = PropertyViewStatus::ErrorInvalidMax;
              return;
            }
          }

          if (property.min) {
            _min = getValue<NormalizedType>(*property.min);
            if (!_scale) {
              // The value was specified but something went wrong.
              _status = PropertyViewStatus::ErrorInvalidMin;
              return;
            }
          }
        },
        inProperty);
  }
};

/**
 * @brief Represents a boolean metadata property in
 * EXT_structural_metadata.
 */
template <> class PropertyView<bool> {
public:
  /**
   * @brief Constructs an empty property instance.
   */
  PropertyView()
      : _status(PropertyViewStatus::ErrorNonexistentProperty),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _required(false),
        _defaultValue(std::nullopt) {}

  /**
   * @brief Constructs a property instance from a class definition only.
   */
  PropertyView(const ClassProperty& classProperty)
      : _status(validatePropertyType<bool>(classProperty)),
        _name(classProperty.name),
        _semantic(classProperty.semantic),
        _description(classProperty.description),
        _required(classProperty.required),
        _defaultValue(std::nullopt) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    if (classProperty.defaultProperty) {
      if (!_required) {
        _defaultValue = getBooleanValue(*classProperty.defaultProperty);
      }

      if (!_defaultValue) {
        // The value was specified but something went wrong.
        _status = PropertyViewStatus::ErrorInvalidDefaultValue;
        return;
      }
    }
  }

protected:
  /**
   * @brief Constructs an invalid instance for an erroneous property.
   *
   * @param status The value of {@link PropertyViewStatus} indicating the error with the property.
   */
  PropertyView(PropertyViewStatusType status)
      : _status(status),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _required(false),
        _defaultValue(std::nullopt) {}

  /**
   * @brief Constructs a property instance from a property table property and
   * its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyTableProperty& /*property*/)
      : PropertyView(classProperty) {}

public:
  /**
   * @copydoc PropertyView::status
   */
  PropertyViewStatusType status() const noexcept { return _status; }

  /**
   * @copydoc PropertyView::name
   */
  const std::optional<std::string>& name() const noexcept { return _name; }

  /**
   * @copydoc PropertyView::semantic
   */
  const std::optional<std::string>& semantic() const noexcept {
    return _semantic;
  }

  /**
   * @copydoc PropertyView::description
   */
  const std::optional<std::string>& description() const noexcept {
    return _description;
  }

  /**
   * @copydoc PropertyView::arrayCount
   */
  int64_t arrayCount() const noexcept { return 0; }

  /**
   * @copydoc PropertyView::normalized
   */
  bool normalized() const noexcept { return false; }

  /**
   * @copydoc PropertyView::offset
   */
  std::optional<bool> offset() const noexcept { return std::nullopt; }

  /**
   * @copydoc PropertyView::scale
   */
  std::optional<bool> scale() const noexcept { return std::nullopt; }

  /**
   * @copydoc PropertyView::max
   */
  std::optional<bool> max() const noexcept { return std::nullopt; }

  /**
   * @copydoc PropertyView::min
   */
  std::optional<bool> min() const noexcept { return std::nullopt; }

  /**
   * @copydoc PropertyView::required
   */
  bool required() const noexcept { return _required; }

  /**
   * @copydoc PropertyView::noData
   */
  std::optional<bool> noData() const noexcept { return std::nullopt; }

  /**
   * @copydoc PropertyView::defaultValue
   */
  std::optional<bool> defaultValue() const noexcept { return _defaultValue; }

protected:
  PropertyViewStatusType _status;

private:
  std::optional<std::string> _name;
  std::optional<std::string> _semantic;
  std::optional<std::string> _description;

  bool _required;
  std::optional<bool> _defaultValue;

  static std::optional<bool>
  getBooleanValue(const CesiumUtility::JsonValue& value) {
    if (!value.isBool()) {
      return std::nullopt;
    }

    return value.getBool();
  }
};

/**
 * @brief Represents a string metadata property in
 * EXT_structural_metadata.
 */
template <> class PropertyView<std::string_view> {
public:
  /**
   * @brief Constructs an empty property instance.
   */
  PropertyView()
      : _status(PropertyViewStatus::ErrorNonexistentProperty),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _required(false),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {}

  /**
   * @brief Constructs a property instance from a class definition only.
   */
  PropertyView(const ClassProperty& classProperty)
      : _status(validatePropertyType<std::string_view>(classProperty)),
        _name(classProperty.name),
        _semantic(classProperty.semantic),
        _description(classProperty.description),
        _required(classProperty.required),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    if (classProperty.noData) {
      if (!_required) {
        _noData = getStringValue(*classProperty.noData);
      }

      if (!_noData) {
        // The value was specified but something went wrong.
        _status = PropertyViewStatus::ErrorInvalidNoDataValue;
        return;
      }
    }

    if (classProperty.defaultProperty) {
      if (!_required) {
        _defaultValue = getStringValue(*classProperty.defaultProperty);
      }

      if (!_defaultValue) {
        // The value was specified but something went wrong.
        _status = PropertyViewStatus::ErrorInvalidDefaultValue;
        return;
      }
    }
  }

protected:
  /**
   * @brief Constructs an invalid instance for an erroneous property.
   *
   * @param status The value of {@link PropertyViewStatus} indicating the error with the property.
   */
  PropertyView(PropertyViewStatusType status)
      : _status(status),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _required(false),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {}

  /**
   * @brief Constructs a property instance from a property table property and
   * its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyTableProperty& /*property*/)
      : PropertyView(classProperty) {}

public:
  /**
   * @copydoc PropertyView::status
   */
  PropertyViewStatusType status() const noexcept { return _status; }

  /**
   * @copydoc PropertyView::name
   */
  const std::optional<std::string>& name() const noexcept { return _name; }

  /**
   * @copydoc PropertyView::semantic
   */
  const std::optional<std::string>& semantic() const noexcept {
    return _semantic;
  }

  /**
   * @copydoc PropertyView::description
   */
  const std::optional<std::string>& description() const noexcept {
    return _description;
  }

  /**
   * @copydoc PropertyView::arrayCount
   */
  int64_t arrayCount() const noexcept { return 0; }

  /**
   * @copydoc PropertyView::normalized
   */
  bool normalized() const noexcept { return false; }

  /**
   * @copydoc PropertyView::offset
   */
  std::optional<std::string_view> offset() const noexcept {
    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::scale
   */
  std::optional<std::string_view> scale() const noexcept {
    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::max
   */
  std::optional<std::string_view> max() const noexcept { return std::nullopt; }

  /**
   * @copydoc PropertyView::min
   */
  std::optional<std::string_view> min() const noexcept { return std::nullopt; }

  /**
   * @copydoc PropertyView::required
   */
  bool required() const noexcept { return _required; }

  /**
   * @copydoc PropertyView::noData
   */
  std::optional<std::string_view> noData() const noexcept {
    if (_noData)
      return std::string_view(*_noData);

    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::defaultValue
   */
  std::optional<std::string_view> defaultValue() const noexcept {
    if (_defaultValue)
      return std::string_view(*_defaultValue);

    return std::nullopt;
  }

protected:
  PropertyViewStatusType _status;

private:
  std::optional<std::string> _name;
  std::optional<std::string> _semantic;
  std::optional<std::string> _description;

  bool _required;
  std::optional<std::string> _noData;
  std::optional<std::string> _defaultValue;

  static std::optional<std::string>
  getStringValue(const CesiumUtility::JsonValue& value) {
    if (!value.isString()) {
      return std::nullopt;
    }

    return std::string(value.getString().c_str());
  }
};

/**
 * @brief Represents a non-normalized array metadata property in
 * EXT_structural_metadata.
 *
 * Whether they belong to property tables, property textures, or property
 * attributes, properties have their own sub-properties affecting the actual
 * property values. Although they are typically defined via class property, they
 * may be overridden by individual instances of the property themselves. The
 * constructor is responsible for resolving those differences.
 *
 * @tparam ElementType The C++ type of the elements in the array values for this
 * property.
 */
template <typename ElementType>
class PropertyView<PropertyArrayView<ElementType>, false> {
public:
  /**
   * @brief Constructs an empty property instance.
   */
  PropertyView()
      : _status(PropertyViewStatus::ErrorNonexistentProperty),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _count(0),
        _offset(std::nullopt),
        _scale(std::nullopt),
        _max(std::nullopt),
        _min(std::nullopt),
        _required(false),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {}

  /**
   * @brief Constructs a property instance from a class definition only.
   */
  PropertyView(const ClassProperty& classProperty)
      : _status(validateArrayPropertyType<PropertyArrayView<ElementType>>(
            classProperty)),
        _name(classProperty.name),
        _semantic(classProperty.semantic),
        _description(classProperty.description),
        _count(_count = classProperty.count ? *classProperty.count : 0),
        _offset(std::nullopt),
        _scale(std::nullopt),
        _max(std::nullopt),
        _min(std::nullopt),
        _required(classProperty.required),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    if (classProperty.normalized) {
      _status = PropertyViewStatus::ErrorNormalizationMismatch;
      return;
    }

    getNumericPropertyValues(classProperty);
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    if (classProperty.noData) {
      if (!_required) {
        _noData = getArrayValue(*classProperty.noData);
      }

      if (!_noData) {
        _status = PropertyViewStatus::ErrorInvalidNoDataValue;
        return;
      }
    }

    if (classProperty.defaultProperty) {
      if (!_required) {
        _defaultValue = getArrayValue(*classProperty.defaultProperty);
      }

      if (!_defaultValue) {
        // The value was specified but something went wrong.
        _status = PropertyViewStatus::ErrorInvalidDefaultValue;
        return;
      }
    }
  }

protected:
  /**
   * @brief Constructs an invalid instance for an erroneous property.
   *
   * @param status The value of {@link PropertyViewStatus} indicating the error with the property.
   */
  PropertyView(PropertyViewStatusType status)
      : _status(status),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _count(0),
        _offset(std::nullopt),
        _scale(std::nullopt),
        _max(std::nullopt),
        _min(std::nullopt),
        _required(false),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {}

  /**
   * @brief Constructs a property instance from a property table property and
   * its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyTableProperty& property)
      : PropertyView(classProperty) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    // If the property has its own values, override the class-provided values.
    getNumericPropertyValues(property);
  }

  /**
   * @brief Constructs a property instance from a property texture property and
   * its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyTextureProperty& property)
      : PropertyView(classProperty) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    // If the property has its own values, override the class-provided values.
    getNumericPropertyValues(property);
  }

public:
  /**
   * @copydoc PropertyView::status
   */
  PropertyViewStatusType status() const noexcept { return _status; }

  /**
   * @copydoc PropertyView::name
   */
  const std::optional<std::string>& name() const noexcept { return _name; }

  /**
   * @copydoc PropertyView::semantic
   */
  const std::optional<std::string>& semantic() const noexcept {
    return _semantic;
  }

  /**
   * @copydoc PropertyView::description
   */
  const std::optional<std::string>& description() const noexcept {
    return _description;
  }

  /**
   * @copydoc PropertyView::arrayCount
   */
  int64_t arrayCount() const noexcept { return _count; }

  /**
   * @copydoc PropertyView::normalized
   */
  bool normalized() const noexcept { return false; }

  /**
   * @copydoc PropertyView::offset
   */
  std::optional<PropertyArrayView<ElementType>> offset() const noexcept {
    if (!_offset) {
      return std::nullopt;
    }

    return PropertyArrayView<ElementType>(
        std::span<const std::byte>(_offset->data(), _offset->size()));
  }

  /**
   * @copydoc PropertyView::scale
   */
  std::optional<PropertyArrayView<ElementType>> scale() const noexcept {
    if (!_scale) {
      return std::nullopt;
    }

    return PropertyArrayView<ElementType>(
        std::span<const std::byte>(_scale->data(), _scale->size()));
  }

  /**
   * @copydoc PropertyView::max
   */
  std::optional<PropertyArrayView<ElementType>> max() const noexcept {
    if (!_max) {
      return std::nullopt;
    }

    return PropertyArrayView<ElementType>(
        std::span<const std::byte>(_max->data(), _max->size()));
  }

  /**
   * @copydoc PropertyView::min
   */
  std::optional<PropertyArrayView<ElementType>> min() const noexcept {
    if (!_min) {
      return std::nullopt;
    }

    return PropertyArrayView<ElementType>(
        std::span<const std::byte>(_min->data(), _min->size()));
  }

  /**
   * @copydoc PropertyView::required
   */
  bool required() const noexcept { return _required; }

  /**
   * @copydoc PropertyView::noData
   */
  std::optional<PropertyArrayView<ElementType>> noData() const noexcept {
    if (!_noData) {
      return std::nullopt;
    }

    return PropertyArrayView<ElementType>(
        std::span<const std::byte>(_noData->data(), _noData->size()));
  }

  /**
   * @copydoc PropertyView::defaultValue
   */
  std::optional<PropertyArrayView<ElementType>> defaultValue() const noexcept {
    if (!_defaultValue) {
      return std::nullopt;
    }

    return PropertyArrayView<ElementType>(std::span<const std::byte>(
        _defaultValue->data(),
        _defaultValue->size()));
  }

protected:
  PropertyViewStatusType _status;

private:
  std::optional<std::string> _name;
  std::optional<std::string> _semantic;
  std::optional<std::string> _description;

  int64_t _count;

  std::optional<std::vector<std::byte>> _offset;
  std::optional<std::vector<std::byte>> _scale;
  std::optional<std::vector<std::byte>> _max;
  std::optional<std::vector<std::byte>> _min;

  bool _required;
  std::optional<std::vector<std::byte>> _noData;
  std::optional<std::vector<std::byte>> _defaultValue;

  using PropertyDefinitionType = std::
      variant<ClassProperty, PropertyTableProperty, PropertyTextureProperty>;
  void getNumericPropertyValues(const PropertyDefinitionType& inProperty) {
    std::visit(
        [this](auto property) {
          if (property.offset) {
            // Only floating point types can specify an offset.
            switch (TypeToPropertyType<ElementType>::component) {
            case PropertyComponentType::Float32:
            case PropertyComponentType::Float64:
              if (this->_count > 0) {
                this->_offset = getArrayValue(*property.offset);
              }
              if (this->_offset &&
                  getCount<ElementType>(this->_offset) == this->_count) {
                break;
              }
              // If it does not break here, something went wrong.
              [[fallthrough]];
            default:
              this->_status = PropertyViewStatus::ErrorInvalidOffset;
              return;
            }
          }

          if (property.scale) {
            // Only floating point types can specify a scale.
            switch (TypeToPropertyType<ElementType>::component) {
            case PropertyComponentType::Float32:
            case PropertyComponentType::Float64:
              if (_count > 0) {
                this->_scale = getArrayValue(*property.scale);
              }
              if (this->_scale &&
                  getCount<ElementType>(this->_scale) == this->_count) {
                break;
              }
              // If it does not break here, something went wrong.
              [[fallthrough]];
            default:
              this->_status = PropertyViewStatus::ErrorInvalidScale;
              return;
            }
          }

          if (property.max) {
            if (this->_count > 0) {
              this->_max = getArrayValue(*property.max);
            }
            if (!this->_max ||
                getCount<ElementType>(this->_max) != this->_count) {
              // The value was specified but something went wrong.
              this->_status = PropertyViewStatus::ErrorInvalidMax;
              return;
            }
          }

          if (property.min) {
            if (this->_count > 0) {
              this->_min = getArrayValue(*property.min);
            }
            if (!this->_min ||
                getCount<ElementType>(this->_min) != this->_count) {
              // The value was specified but something went wrong.
              this->_status = PropertyViewStatus::ErrorInvalidMin;
              return;
            }
          }
        },
        inProperty);
  }

  static std::optional<std::vector<std::byte>>
  getArrayValue(const CesiumUtility::JsonValue& jsonValue) {
    if (!jsonValue.isArray()) {
      return std::nullopt;
    }

    const CesiumUtility::JsonValue::Array& array = jsonValue.getArray();
    std::vector<ElementType> values;
    values.reserve(array.size());

    if constexpr (IsMetadataScalar<ElementType>::value) {
      for (size_t i = 0; i < array.size(); i++) {
        std::optional<ElementType> element = getScalar<ElementType>(array[i]);
        if (!element) {
          return std::nullopt;
        }

        values.push_back(*element);
      }
    }
    if constexpr (IsMetadataVecN<ElementType>::value) {
      for (size_t i = 0; i < array.size(); i++) {
        std::optional<ElementType> element = getVecN<ElementType>(array[i]);
        if (!element) {
          return std::nullopt;
        }

        values.push_back(*element);
      }
    }

    if constexpr (IsMetadataMatN<ElementType>::value) {
      for (size_t i = 0; i < array.size(); i++) {
        std::optional<ElementType> element = getMatN<ElementType>(array[i]);
        if (!element) {
          return std::nullopt;
        }

        values.push_back(*element);
      }
    }

    std::vector<std::byte> result(values.size() * sizeof(ElementType));
    std::memcpy(result.data(), values.data(), result.size());

    return result;
  }
};

/**
 * @brief Represents a normalized array metadata property in
 * EXT_structural_metadata.
 *
 * Whether they belong to property tables, property textures, or property
 * attributes, properties have their own sub-properties affecting the actual
 * property values. Although they are typically defined via class property, they
 * may be overridden by individual instances of the property themselves. The
 * constructor is responsible for resolving those differences.
 *
 * @tparam ElementType The C++ type of the elements in the array values for this
 * property. Must have an integer component type.
 */
template <typename ElementType>
class PropertyView<PropertyArrayView<ElementType>, true> {
private:
  using NormalizedType = typename TypeToNormalizedType<ElementType>::type;

public:
  /**
   * @brief Constructs an empty property instance.
   */
  PropertyView()
      : _status(PropertyViewStatus::ErrorNonexistentProperty),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _count(0),
        _offset(std::nullopt),
        _scale(std::nullopt),
        _max(std::nullopt),
        _min(std::nullopt),
        _required(false),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {}

  /**
   * @brief Constructs a property instance from a class definition only.
   */
  PropertyView(const ClassProperty& classProperty)
      : _status(validateArrayPropertyType<PropertyArrayView<ElementType>>(
            classProperty)),
        _name(classProperty.name),
        _semantic(classProperty.semantic),
        _description(classProperty.description),
        _count(_count = classProperty.count ? *classProperty.count : 0),
        _offset(std::nullopt),
        _scale(std::nullopt),
        _max(std::nullopt),
        _min(std::nullopt),
        _required(classProperty.required),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    if (!classProperty.normalized) {
      _status = PropertyViewStatus::ErrorNormalizationMismatch;
      return;
    }

    getNumericPropertyValues(classProperty);
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    if (classProperty.noData) {
      if (!_required) {
        _noData = getArrayValue<ElementType>(*classProperty.noData);
      }

      if (!_noData) {
        _status = PropertyViewStatus::ErrorInvalidNoDataValue;
        return;
      }
    }

    if (classProperty.defaultProperty) {
      if (!_required) {
        _defaultValue =
            getArrayValue<NormalizedType>(*classProperty.defaultProperty);
      }

      if (!_defaultValue) {
        _status = PropertyViewStatus::ErrorInvalidDefaultValue;
        return;
      }
    }
  }

protected:
  /**
   * @brief Constructs an invalid instance for an erroneous property.
   *
   * @param status The value of {@link PropertyViewStatus} indicating the error with the property.
   */
  PropertyView(PropertyViewStatusType status)
      : _status(status),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _count(0),
        _offset(std::nullopt),
        _scale(std::nullopt),
        _max(std::nullopt),
        _min(std::nullopt),
        _required(false),
        _noData(std::nullopt),
        _defaultValue(std::nullopt) {}

  /**
   * @brief Constructs a property instance from a property table property and
   * its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyTableProperty& property)
      : PropertyView(classProperty) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    // If the property has its own values, override the class-provided values.
    getNumericPropertyValues(property);
  }

  /**
   * @brief Constructs a property instance from a property texture property and
   * its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyTextureProperty& property)
      : PropertyView(classProperty) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    // If the property has its own values, override the class-provided values.
    getNumericPropertyValues(property);
  }

public:
  /**
   * @copydoc PropertyView::status
   */
  PropertyViewStatusType status() const noexcept { return _status; }

  /**
   * @copydoc PropertyView::name
   */
  const std::optional<std::string>& name() const noexcept { return _name; }

  /**
   * @copydoc PropertyView::semantic
   */
  const std::optional<std::string>& semantic() const noexcept {
    return _semantic;
  }

  /**
   * @copydoc PropertyView::description
   */
  const std::optional<std::string>& description() const noexcept {
    return _description;
  }

  /**
   * @copydoc PropertyView::arrayCount
   */
  int64_t arrayCount() const noexcept { return _count; }

  /**
   * @copydoc PropertyView::normalized
   */
  bool normalized() const noexcept { return true; }

  /**
   * @copydoc PropertyView::offset
   */
  std::optional<PropertyArrayView<NormalizedType>> offset() const noexcept {
    if (!_offset) {
      return std::nullopt;
    }

    return PropertyArrayView<NormalizedType>(
        std::span<const std::byte>(_offset->data(), _offset->size()));
  }

  /**
   * @copydoc PropertyView::scale
   */
  std::optional<PropertyArrayView<NormalizedType>> scale() const noexcept {
    if (!_scale) {
      return std::nullopt;
    }

    return PropertyArrayView<NormalizedType>(
        std::span<const std::byte>(_scale->data(), _scale->size()));
  }

  /**
   * @copydoc PropertyView::max
   */
  std::optional<PropertyArrayView<NormalizedType>> max() const noexcept {
    if (!_max) {
      return std::nullopt;
    }

    return PropertyArrayView<NormalizedType>(
        std::span<const std::byte>(_max->data(), _max->size()));
  }

  /**
   * @copydoc PropertyView::min
   */
  std::optional<PropertyArrayView<NormalizedType>> min() const noexcept {
    if (!_min) {
      return std::nullopt;
    }

    return PropertyArrayView<NormalizedType>(
        std::span<const std::byte>(_min->data(), _min->size()));
  }

  /**
   * @copydoc PropertyView::required
   */
  bool required() const noexcept { return _required; }

  /**
   * @copydoc PropertyView::noData
   */
  std::optional<PropertyArrayView<ElementType>> noData() const noexcept {
    if (!_noData) {
      return std::nullopt;
    }

    return PropertyArrayView<ElementType>(
        std::span<const std::byte>(_noData->data(), _noData->size()));
  }

  /**
   * @copydoc PropertyView::defaultValue
   */
  std::optional<PropertyArrayView<NormalizedType>>
  defaultValue() const noexcept {
    if (!_defaultValue) {
      return std::nullopt;
    }

    return PropertyArrayView<NormalizedType>(std::span<const std::byte>(
        _defaultValue->data(),
        _defaultValue->size()));
  }

protected:
  PropertyViewStatusType _status;

private:
  std::optional<std::string> _name;
  std::optional<std::string> _semantic;
  std::optional<std::string> _description;

  int64_t _count;

  std::optional<std::vector<std::byte>> _offset;
  std::optional<std::vector<std::byte>> _scale;
  std::optional<std::vector<std::byte>> _max;
  std::optional<std::vector<std::byte>> _min;

  bool _required;
  std::optional<std::vector<std::byte>> _noData;
  std::optional<std::vector<std::byte>> _defaultValue;

  using PropertyDefinitionType = std::
      variant<ClassProperty, PropertyTableProperty, PropertyTextureProperty>;
  void getNumericPropertyValues(const PropertyDefinitionType& inProperty) {
    std::visit(
        [this](auto property) {
          if (property.offset) {
            if (_count > 0) {
              _offset = getArrayValue<NormalizedType>(*property.offset);
            }
            if (!_offset || getCount<NormalizedType>(_offset) != _count) {
              // The value was specified but something went wrong.
              _status = PropertyViewStatus::ErrorInvalidOffset;
              return;
            }
          }

          if (property.scale) {
            if (_count > 0) {
              _scale = getArrayValue<NormalizedType>(*property.scale);
            }
            if (!_scale || getCount<NormalizedType>(_scale) != _count) {
              // The value was specified but something went wrong.
              _status = PropertyViewStatus::ErrorInvalidScale;
              return;
            }
          }

          if (property.max) {
            if (_count > 0) {
              _max = getArrayValue<NormalizedType>(*property.max);
            }
            if (!_max || getCount<NormalizedType>(_max) != _count) {
              // The value was specified but something went wrong.
              _status = PropertyViewStatus::ErrorInvalidMax;
              return;
            }
          }

          if (property.min) {
            if (_count > 0) {
              _min = getArrayValue<NormalizedType>(*property.min);
            }
            if (!_min || getCount<NormalizedType>(_min) != _count) {
              // The value was specified but something went wrong.
              _status = PropertyViewStatus::ErrorInvalidMin;
              return;
            }
          }
        },
        inProperty);
  }

  template <typename T>
  static std::optional<std::vector<std::byte>>
  getArrayValue(const CesiumUtility::JsonValue& jsonValue) {
    if (!jsonValue.isArray()) {
      return std::nullopt;
    }

    const CesiumUtility::JsonValue::Array& array = jsonValue.getArray();
    std::vector<T> values;
    values.reserve(array.size());

    if constexpr (IsMetadataScalar<T>::value) {
      for (size_t i = 0; i < array.size(); i++) {
        std::optional<T> element = getScalar<T>(array[i]);
        if (!element) {
          return std::nullopt;
        }

        values.push_back(*element);
      }
    }

    if constexpr (IsMetadataVecN<T>::value) {
      for (size_t i = 0; i < array.size(); i++) {
        std::optional<T> element = getVecN<T>(array[i]);
        if (!element) {
          return std::nullopt;
        }

        values.push_back(*element);
      }
    }

    if constexpr (IsMetadataMatN<T>::value) {
      for (size_t i = 0; i < array.size(); i++) {
        std::optional<T> element = getMatN<T>(array[i]);
        if (!element) {
          return std::nullopt;
        }

        values.push_back(*element);
      }
    }

    std::vector<std::byte> result(values.size() * sizeof(T));
    std::memcpy(result.data(), values.data(), result.size());

    return result;
  }
};

/**
 * @brief Represents a boolean array metadata property in
 * EXT_structural_metadata.
 */
template <> class PropertyView<PropertyArrayView<bool>> {
public:
  /**
   * @brief Constructs an empty property instance.
   */
  PropertyView()
      : _status(PropertyViewStatus::ErrorNonexistentProperty),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _count(0),
        _required(false),
        _defaultValue(),
        _size(0) {}

  /**
   * @brief Constructs a property instance from a class definition only.
   */
  PropertyView(const ClassProperty& classProperty)
      : _status(
            validateArrayPropertyType<PropertyArrayView<bool>>(classProperty)),
        _name(classProperty.name),
        _semantic(classProperty.semantic),
        _description(classProperty.description),
        _count(classProperty.count ? *classProperty.count : 0),
        _required(classProperty.required),
        _defaultValue(),
        _size(0) {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    if (classProperty.defaultProperty) {
      if (!_required) {
        _defaultValue =
            getBooleanArrayValue(*classProperty.defaultProperty, _size);
      }

      if (_size == 0 || (_count > 0 && _size != _count)) {
        _status = PropertyViewStatus::ErrorInvalidDefaultValue;
        return;
      }
    }
  }

protected:
  /**
   * @brief Constructs an invalid instance for an erroneous property.
   *
   * @param status The value of {@link PropertyViewStatus} indicating the error with the property.
   */
  PropertyView(PropertyViewStatusType status)
      : _status(status),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _count(0),
        _required(false),
        _defaultValue(),
        _size(0) {}

  /**
   * @brief Constructs a property instance from a property table property and
   * its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyTableProperty& /*property*/)
      : PropertyView(classProperty) {}

public:
  /**
   * @copydoc PropertyView::status
   */
  PropertyViewStatusType status() const noexcept { return _status; }

  /**
   * @copydoc PropertyView::name
   */
  const std::optional<std::string>& name() const noexcept { return _name; }

  /**
   * @copydoc PropertyView::semantic
   */
  const std::optional<std::string>& semantic() const noexcept {
    return _semantic;
  }

  /**
   * @copydoc PropertyView::description
   */
  const std::optional<std::string>& description() const noexcept {
    return _description;
  }

  /**
   * @copydoc PropertyView::arrayCount
   */
  int64_t arrayCount() const noexcept { return _count; }

  /**
   * @copydoc PropertyView::normalized
   */
  bool normalized() const noexcept { return false; }

  /**
   * @copydoc PropertyView::offset
   */
  std::optional<PropertyArrayView<bool>> offset() const noexcept {
    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::scale
   */
  std::optional<PropertyArrayView<bool>> scale() const noexcept {
    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::max
   */
  std::optional<PropertyArrayView<bool>> max() const noexcept {
    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::min
   */
  std::optional<PropertyArrayView<bool>> min() const noexcept {
    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::required
   */
  bool required() const noexcept { return _required; }

  /**
   * @copydoc PropertyView::noData
   */
  std::optional<PropertyArrayView<bool>> noData() const noexcept {
    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::defaultValue
   */
  std::optional<PropertyArrayView<bool>> defaultValue() const noexcept {
    if (_size > 0) {
      return PropertyArrayView<bool>(
          std::span<const std::byte>(
              _defaultValue.data(),
              _defaultValue.size()),
          /* bitOffset = */ 0,
          _size);
    }

    return std::nullopt;
  }

protected:
  PropertyViewStatusType _status;

private:
  std::optional<std::string> _name;
  std::optional<std::string> _semantic;
  std::optional<std::string> _description;

  int64_t _count;
  bool _required;

  std::vector<std::byte> _defaultValue;
  int64_t _size;

  static std::vector<std::byte> getBooleanArrayValue(
      const CesiumUtility::JsonValue& jsonValue,
      int64_t& size) {
    if (!jsonValue.isArray()) {
      return std::vector<std::byte>();
    }

    const CesiumUtility::JsonValue::Array& array = jsonValue.getArray();
    std::vector<std::byte> values;
    values.reserve((array.size() / 8) + 1);

    size_t byteIndex = 0;
    uint8_t bitIndex = 0;

    for (size_t i = 0; i < array.size(); i++, size++) {
      if (!array[i].isBool()) {
        size = 0;
        return values;
      }

      if (values.size() < byteIndex - 1) {
        values.push_back(std::byte(0));
      }

      std::byte value = std::byte(array[i].getBool() ? 1 : 0);
      value = value << bitIndex;

      values[byteIndex] |= value;

      bitIndex++;
      if (bitIndex > 7) {
        byteIndex++;
        bitIndex = 0;
      }
    }

    return values;
  }
};

/**
 * @brief Represents a string array metadata property in
 * EXT_structural_metadata.
 */
template <> class PropertyView<PropertyArrayView<std::string_view>> {
public:
  /**
   * @brief Constructs an empty property instance.
   */
  PropertyView()
      : _status(PropertyViewStatus::ErrorNonexistentProperty),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _count(0),
        _required(false),
        _noData(),
        _defaultValue() {}

  /**
   * @brief Constructs a property instance from a class definition only.
   */
  PropertyView(const ClassProperty& classProperty)
      : _status(validateArrayPropertyType<PropertyArrayView<std::string_view>>(
            classProperty)),
        _name(classProperty.name),
        _semantic(classProperty.semantic),
        _description(classProperty.description),
        _count(classProperty.count ? *classProperty.count : 0),
        _required(classProperty.required),
        _noData(),
        _defaultValue() {
    if (_status != PropertyViewStatus::Valid) {
      return;
    }

    if (classProperty.noData) {
      if (!_required) {
        _noData = getStringArrayValue(*classProperty.noData);
      }

      if (_noData.size == 0 || (_count > 0 && _noData.size != _count)) {
        _status = PropertyViewStatus::ErrorInvalidNoDataValue;
        return;
      }
    }

    if (classProperty.defaultProperty) {
      if (!_required) {
        _defaultValue = getStringArrayValue(*classProperty.defaultProperty);
      }

      if (_defaultValue.size == 0 ||
          (_count > 0 && _defaultValue.size != _count)) {
        // The value was specified but something went wrong.
        _status = PropertyViewStatus::ErrorInvalidDefaultValue;
        return;
      }
    }
  }

protected:
  /**
   * @brief Constructs an invalid instance for an erroneous property.
   *
   * @param status The value of {@link PropertyViewStatus} indicating the error with the property.
   */
  PropertyView(PropertyViewStatusType status)
      : _status(status),
        _name(std::nullopt),
        _semantic(std::nullopt),
        _description(std::nullopt),
        _count(0),
        _required(false),
        _noData(),
        _defaultValue() {}

  /**
   * @brief Constructs a property instance from a property table property
   * and its class definition.
   */
  PropertyView(
      const ClassProperty& classProperty,
      const PropertyTableProperty& /*property*/)
      : PropertyView(classProperty) {}

public:
  /**
   * @copydoc PropertyView::status
   */
  PropertyViewStatusType status() const noexcept { return _status; }

  /**
   * @copydoc PropertyView::name
   */
  const std::optional<std::string>& name() const noexcept { return _name; }

  /**
   * @copydoc PropertyView::semantic
   */
  const std::optional<std::string>& semantic() const noexcept {
    return _semantic;
  }

  /**
   * @copydoc PropertyView::description
   */
  const std::optional<std::string>& description() const noexcept {
    return _description;
  }

  /**
   * @copydoc PropertyView::arrayCount
   */
  int64_t arrayCount() const noexcept { return _count; }

  /**
   * @copydoc PropertyView::normalized
   */
  bool normalized() const noexcept { return false; }

  /**
   * @copydoc PropertyView::offset
   */
  std::optional<PropertyArrayView<std::string_view>> offset() const noexcept {
    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::scale
   */
  std::optional<PropertyArrayView<std::string_view>> scale() const noexcept {
    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::max
   */
  std::optional<PropertyArrayView<std::string_view>> max() const noexcept {
    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::min
   */
  std::optional<PropertyArrayView<std::string_view>> min() const noexcept {
    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::required
   */
  bool required() const noexcept { return _required; }

  /**
   * @copydoc PropertyView::noData
   */
  std::optional<PropertyArrayView<std::string_view>> noData() const noexcept {
    if (_noData.size > 0) {
      return PropertyArrayView<std::string_view>(
          std::span<const std::byte>(_noData.data.data(), _noData.data.size()),
          std::span<const std::byte>(
              _noData.offsets.data(),
              _noData.offsets.size()),
          _noData.offsetType,
          _noData.size);
    }

    return std::nullopt;
  }

  /**
   * @copydoc PropertyView::defaultValue
   */
  std::optional<PropertyArrayView<std::string_view>>
  defaultValue() const noexcept {
    if (_defaultValue.size > 0) {
      return PropertyArrayView<std::string_view>(
          std::span<const std::byte>(
              _defaultValue.data.data(),
              _defaultValue.data.size()),
          std::span<const std::byte>(
              _defaultValue.offsets.data(),
              _defaultValue.offsets.size()),
          _defaultValue.offsetType,
          _defaultValue.size);
    }

    return std::nullopt;
  }

protected:
  PropertyViewStatusType _status;

private:
  std::optional<std::string> _name;
  std::optional<std::string> _semantic;
  std::optional<std::string> _description;

  int64_t _count;
  bool _required;

  struct StringArrayValue {
    std::vector<std::byte> data;
    std::vector<std::byte> offsets;
    PropertyComponentType offsetType = PropertyComponentType::None;
    int64_t size = 0;
  };

  StringArrayValue _noData;
  StringArrayValue _defaultValue;

  static StringArrayValue
  getStringArrayValue(const CesiumUtility::JsonValue& jsonValue) {
    StringArrayValue result;
    if (!jsonValue.isArray()) {
      return result;
    }

    std::vector<std::string> strings;
    std::vector<uint64_t> stringOffsets;

    const auto array = jsonValue.getArray();
    strings.reserve(array.size());
    stringOffsets.reserve(array.size() + 1);
    stringOffsets.push_back(static_cast<uint64_t>(0));

    for (size_t i = 0; i < array.size(); i++) {
      if (!array[i].isString()) {
        // The entire array is invalidated; return.
        return result;
      }

      const std::string& string = array[i].getString();
      strings.push_back(string);
      stringOffsets.push_back(stringOffsets[i] + string.size());
    }

    uint64_t totalLength = stringOffsets.back();
    result.data.resize(totalLength);
    for (size_t i = 0; i < strings.size(); ++i) {
      std::memcpy(
          result.data.data() + stringOffsets[i],
          strings[i].data(),
          strings[i].size());
    };

    if (totalLength <= std::numeric_limits<uint8_t>::max()) {
      result.offsets = narrowOffsetsBuffer<uint8_t>(stringOffsets);
      result.offsetType = PropertyComponentType::Uint8;
    } else if (totalLength <= std::numeric_limits<uint16_t>::max()) {
      result.offsets = narrowOffsetsBuffer<uint16_t>(stringOffsets);
      result.offsetType = PropertyComponentType::Uint16;
    } else if (totalLength <= std::numeric_limits<uint32_t>::max()) {
      result.offsets = narrowOffsetsBuffer<uint32_t>(stringOffsets);
      result.offsetType = PropertyComponentType::Uint32;
    } else {
      result.offsets.resize(stringOffsets.size() * sizeof(uint64_t));
      std::memcpy(
          result.offsets.data(),
          stringOffsets.data(),
          result.offsets.size());
      result.offsetType = PropertyComponentType::Uint64;
    }

    result.size = static_cast<int64_t>(strings.size());

    return result;
  }

  template <typename T>
  static std::vector<std::byte>
  narrowOffsetsBuffer(std::vector<uint64_t> offsets) {
    std::vector<std::byte> result(offsets.size() * sizeof(T));
    size_t bufferOffset = 0;
    for (size_t i = 0; i < offsets.size(); i++, bufferOffset += sizeof(T)) {
      T offset = static_cast<T>(offsets[i]);
      std::memcpy(result.data() + bufferOffset, &offset, sizeof(T));
    }

    return result;
  }
};

} // namespace CesiumGltf
