#pragma once

#include "Library.h"

#include <CesiumGeospatial/BoundingRegion.h>
#include <CesiumGeospatial/Ellipsoid.h>
#include <CesiumGeospatial/GlobeRectangle.h>

#include <glm/fwd.hpp>

#include <optional>
#include <string>
#include <string_view>
#include <vector>

namespace CesiumGltf {
struct Buffer;
struct Model;
struct Node;
} // namespace CesiumGltf

namespace CesiumGeometry {
class Ray;
} // namespace CesiumGeometry

namespace CesiumGltfContent {
/**
 * A collection of utility functions that are used to process and transform a
 * gltf model
 */
struct CESIUMGLTFCONTENT_API GltfUtilities {
  /**
   * @brief Gets the transformation matrix for a given node.
   *
   * This returns the node's local transform as-is. It does not incorporate
   * transforms from any of the node's ancestors.
   *
   * @param node The node from which to get the transformation matrix.
   * @return The transformation matrix, or std::nullopt if the node's
   * transformation is invalid, .e.g, because it has a matrix with fewer than
   * 16 elements in it.
   */
  static std::optional<glm::dmat4x4>
  getNodeTransform(const CesiumGltf::Node& node);

  /**
   * @brief Sets the transformation matrix for a given node.
   *
   * This sets only the local transform of the node. It does not affect the
   * transforms of any ancestor or descendant nodes, if present.
   *
   * @param node The node on which to set the transformation matrix.
   * @param newTransform The new transformation matrix.
   */
  static void
  setNodeTransform(CesiumGltf::Node& node, const glm::dmat4x4& newTransform);

  /**
   * @brief Applies the glTF's RTC_CENTER, if any, to the given transform.
   *
   * If the glTF has a `CESIUM_RTC` extension, this function will multiply the
   * given matrix with the (translation) matrix that is created from the
   * `RTC_CENTER` in the. If the given model does not have this extension, then
   * this function will return the `rootTransform` unchanged.
   *
   * @param model The glTF model
   * @param rootTransform The matrix that will be multiplied with the transform
   * @return The result of multiplying the `RTC_CENTER` with the
   * `rootTransform`.
   */
  static glm::dmat4x4 applyRtcCenter(
      const CesiumGltf::Model& gltf,
      const glm::dmat4x4& rootTransform);

  /**
   * @brief Applies the glTF's `gltfUpAxis`, if any, to the given transform.
   *
   * By default, the up-axis of a glTF model will the the Y-axis.
   *
   * If the tileset that contained the model had the `asset.gltfUpAxis` string
   * property, then the information about the up-axis has been stored in as a
   * number property called `gltfUpAxis` in the `extras` of the given model.
   *
   * Depending on whether this value is `CesiumGeometry::Axis::X`, `Y`, or `Z`,
   * the given matrix will be multiplied with a matrix that converts the
   * respective axis to be the Z-axis, as required by the 3D Tiles standard.
   *
   * @param model The glTF model
   * @param rootTransform The matrix that will be multiplied with the transform
   * @return The result of multiplying the `rootTransform` with the
   * `gltfUpAxis`.
   */
  static glm::dmat4x4 applyGltfUpAxisTransform(
      const CesiumGltf::Model& model,
      const glm::dmat4x4& rootTransform);

  /**
   * @brief Computes a bounding region from the vertex positions in a glTF
   * model.
   *
   * If the glTF model spans the anti-meridian, the west and east longitude
   * values will be in the usual -PI to PI range, but east will have a smaller
   * value than west.
   *
   * If the glTF contains no geometry, the returned region's rectangle
   * will be {@link GlobeRectangle::EMPTY}, its minimum height will be 1.0, and
   * its maximum height will be -1.0 (the minimum will be greater than the
   * maximum).
   *
   * @param gltf The model.
   * @param transform The transform from model coordinates to ECEF coordinates.
   * @return The computed bounding region.
   */
  static CesiumGeospatial::BoundingRegion computeBoundingRegion(
      const CesiumGltf::Model& gltf,
      const glm::dmat4& transform,
      const CesiumGeospatial::Ellipsoid& ellipsoid CESIUM_DEFAULT_ELLIPSOID);

  /**
   * @brief Parse the copyright field of a glTF model and return the individual
   * credits.
   *
   * Credits are read from the glTF's asset.copyright field. This method assumes
   * that individual credits are separated by semicolons.
   *
   * @param gltf The model.
   * @return The credits from the glTF.
   */
  static std::vector<std::string_view>
  parseGltfCopyright(const CesiumGltf::Model& gltf);

  /**
   * @brief Merges all of the glTF's buffers into a single buffer (the first
   * one).
   *
   * This is useful when writing the glTF as a GLB, which supports only a single
   * embedded buffer.
   *
   * @param gltf The glTF in which to merge buffers.
   */
  static void collapseToSingleBuffer(CesiumGltf::Model& gltf);

  /**
   * @brief Copies the content of one {@link Buffer} to the end of another,
   * updates all {@link BufferView} instances to refer to the destination
   * buffer, and clears the contents of the original buffer.
   *
   * The source buffer is not removed, but it has a `byteLength` of zero after
   * this function completes.
   *
   * @param gltf The glTF model to modify.
   * @param destination The destination Buffer into which to move content.
   * @param source The source Buffer from which to move content.
   */
  static void moveBufferContent(
      CesiumGltf::Model& gltf,
      CesiumGltf::Buffer& destination,
      CesiumGltf::Buffer& source);

  static void removeUnusedTextures(
      CesiumGltf::Model& gltf,
      const std::vector<int32_t>& extraUsedTextureIndices = {});
  static void removeUnusedSamplers(
      CesiumGltf::Model& gltf,
      const std::vector<int32_t>& extraUsedSamplerIndices = {});
  static void removeUnusedImages(
      CesiumGltf::Model& gltf,
      const std::vector<int32_t>& extraUsedImageIndices = {});
  static void removeUnusedAccessors(
      CesiumGltf::Model& gltf,
      const std::vector<int32_t>& extraUsedAccessorIndices = {});
  static void removeUnusedBufferViews(
      CesiumGltf::Model& gltf,
      const std::vector<int32_t>& extraUsedBufferViewIndices = {});
  static void removeUnusedBuffers(
      CesiumGltf::Model& gltf,
      const std::vector<int32_t>& extraUsedBufferIndices = {});
  static void removeUnusedMeshes(
      CesiumGltf::Model& gltf,
      const std::vector<int32_t>& extraUsedMeshIndices = {});
  static void removeUnusedMaterials(
      CesiumGltf::Model& gltf,
      const std::vector<int32_t>& extraUsedMaterialIndices = {});

  /**
   * @brief Shrink buffers by removing any sections that are not referenced by
   * any BufferView.
   *
   * @param gltf The glTF to modify.
   */
  static void compactBuffers(CesiumGltf::Model& gltf);

  /**
   * @brief Shrink a buffer by removing any sections that are not referenced by
   * any BufferView.
   *
   * @param gltf The glTF to modify.
   * @param bufferIndex The index of the buffer to compact.
   */
  static void compactBuffer(CesiumGltf::Model& gltf, int32_t bufferIndex);

  /**
   * @brief Data describing a hit from a ray / gltf intersection test
   */
  struct RayGltfHit {
    /**
     * @brief Hit point in primitive space
     */
    glm::dvec3 primitivePoint = {};

    /**
     * @brief Transformation from primitive to world space
     */
    glm::dmat4x4 primitiveToWorld = {};

    /**
     * @brief Hit point in world space
     */
    glm::dvec3 worldPoint = {};

    /**
     * @brief Square dist from intersection ray origin to world point
     */
    double rayToWorldPointDistanceSq = -1.0;

    /**
     * @brief ID of the glTF mesh that was hit
     */
    int32_t meshId = -1;

    /**
     * @brief ID of the glTF primitive that was hit
     */
    int32_t primitiveId = -1;
  };

  /**
   * @brief Hit result data for intersectRayGltfModel
   */
  struct IntersectResult {
    /**
     * @brief Optional hit result, if an intersection occurred
     */
    std::optional<RayGltfHit> hit;

    /**
     * @brief Warnings encountered when traversing the glTF model
     */
    std::vector<std::string> warnings{};
  };

  /**
   * @brief Intersects a ray with a glTF model and returns the first
   * intersection point.
   *
   * Supports all mesh primitive modes.
   * Points and lines are assumed to have no area, and are ignored
   *
   * @param ray A ray in world space.
   * @param gltf The glTF model to intersect.
   * @param cullBackFaces Ignore triangles that face away from ray. Front faces
   * use CCW winding order.
   * @param gltfTransform Optional matrix to apply to entire gltf model.
   * @param return IntersectResult describing outcome
   */
  static IntersectResult intersectRayGltfModel(
      const CesiumGeometry::Ray& ray,
      const CesiumGltf::Model& gltf,
      bool cullBackFaces = true,
      const glm::dmat4x4& gltfTransform = glm::dmat4(1.0));
};
} // namespace CesiumGltfContent
