// Copyright 2019 The Draco Authors.
//
// Licensed 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 "draco/scene/scene_utils.h"

#include <string>
#include <utility>

#ifdef DRACO_TRANSCODER_SUPPORTED
#include "draco/core/bounding_box.h"
#include "draco/core/draco_test_base.h"
#include "draco/core/draco_test_utils.h"
#include "draco/io/texture_io.h"
#include "draco/scene/scene_indices.h"

namespace {

using draco::MeshIndex;
using draco::MeshInstanceIndex;

void AssertMatrixNear(const Eigen::Matrix4d &a, const Eigen::Matrix4d &b,
                      float tolerance) {
  Eigen::Matrix4d diff = a - b;
  ASSERT_NEAR(diff.norm(), 0.f, tolerance) << a << " vs " << b;
}

// TODO(fgalligan): Re-factor this code with gltf_encoder_test.
void CompareScenes(const draco::Scene *scene0, const draco::Scene *scene1) {
  ASSERT_EQ(scene0->NumMeshGroups(), scene1->NumMeshGroups());
  ASSERT_EQ(scene0->NumMeshes(), scene1->NumMeshes());
  ASSERT_EQ(scene0->GetMaterialLibrary().NumMaterials(),
            scene1->GetMaterialLibrary().NumMaterials());
  ASSERT_EQ(scene0->NumAnimations(), scene1->NumAnimations());
  ASSERT_EQ(scene0->NumSkins(), scene1->NumSkins());
  for (draco::AnimationIndex i(0); i < scene0->NumAnimations(); ++i) {
    const draco::Animation *const animation0 = scene0->GetAnimation(i);
    const draco::Animation *const animation1 = scene1->GetAnimation(i);
    ASSERT_NE(animation0, nullptr);
    ASSERT_NE(animation1, nullptr);
    ASSERT_EQ(animation0->NumSamplers(), animation1->NumSamplers());
    ASSERT_EQ(animation0->NumChannels(), animation1->NumChannels());
    ASSERT_EQ(animation0->NumNodeAnimationData(),
              animation1->NumNodeAnimationData());
  }
}

TEST(SceneUtilsTest, TestComputeAllInstances) {
  // Tests that we can compute all instances in an input scene along with their
  // transformations.

  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(scene->NumMeshes(), 4);

  // Compute mesh instances.
  const auto instances = draco::SceneUtils::ComputeAllInstances(*scene);
  ASSERT_EQ(instances.size(), 5);

  // Check base mesh indices.
  ASSERT_EQ(instances[MeshInstanceIndex(0)].mesh_index, 0);
  ASSERT_EQ(instances[MeshInstanceIndex(1)].mesh_index, 1);
  ASSERT_EQ(instances[MeshInstanceIndex(2)].mesh_index, 2);
  ASSERT_EQ(instances[MeshInstanceIndex(3)].mesh_index, 3);
  ASSERT_EQ(instances[MeshInstanceIndex(4)].mesh_index, 3);

  // Check scene node indices.
  ASSERT_EQ(instances[MeshInstanceIndex(0)].scene_node_index, 0);
  ASSERT_EQ(instances[MeshInstanceIndex(1)].scene_node_index, 0);
  ASSERT_EQ(instances[MeshInstanceIndex(2)].scene_node_index, 0);
  ASSERT_EQ(instances[MeshInstanceIndex(3)].scene_node_index, 4);
  ASSERT_EQ(instances[MeshInstanceIndex(4)].scene_node_index, 2);

  // Check indices of meshes in mesh group.
  ASSERT_EQ(instances[MeshInstanceIndex(0)].mesh_group_mesh_index, 0);
  ASSERT_EQ(instances[MeshInstanceIndex(1)].mesh_group_mesh_index, 1);
  ASSERT_EQ(instances[MeshInstanceIndex(2)].mesh_group_mesh_index, 2);
  ASSERT_EQ(instances[MeshInstanceIndex(3)].mesh_group_mesh_index, 0);
  ASSERT_EQ(instances[MeshInstanceIndex(4)].mesh_group_mesh_index, 0);

  // The first three instances should have identity transformation.
  for (MeshInstanceIndex i(0); i < 3; ++i) {
    AssertMatrixNear(instances[i].transform, Eigen::Matrix4d::Identity(),
                     1e-6f);
  }

  // Fourth and fifth instances are transformed.
  Eigen::Matrix4d expected_transform = Eigen::Matrix4d::Identity();
  // Expected translation.
  expected_transform(0, 3) = -1.352329969406128;
  expected_transform(1, 3) = 0.4277220070362091;
  expected_transform(2, 3) = -2.98022992950564e-8;

  // Expected rotation.
  Eigen::Matrix4d expected_rotation = Eigen::Matrix4d::Identity();
  expected_rotation.block<3, 3>(0, 0) =
      Eigen::Quaterniond(-0.9960774183273317, -0.0, -0.0, 0.08848590403795243)
          .normalized()
          .toRotationMatrix();
  expected_transform = expected_transform * expected_rotation;

  AssertMatrixNear(instances[MeshInstanceIndex(3)].transform,
                   expected_transform, 1e-6f);

  // Last instance differs only in the translation part in X axis.
  expected_transform(0, 3) = 1.432669997215271;

  AssertMatrixNear(instances[MeshInstanceIndex(4)].transform,
                   expected_transform, 1e-6f);
}

TEST(SceneUtilsTest, TestComputeAllInstancesWithShiftedGeometryRoot) {
  // Tests that we can compute all instances in an input scene along with their
  // transformations. This scene has light and camera nodes before the geometry
  // node.
  auto scene = draco::ReadSceneFromTestFile(
      "SphereWithCircleTexture/sphere_with_circle_texture.gltf");
  ASSERT_NE(scene, nullptr);

  // There is one base mesh.
  ASSERT_EQ(scene->NumMeshes(), 1);

  // There is a single mesh instance.
  const auto instances = draco::SceneUtils::ComputeAllInstances(*scene);
  ASSERT_EQ(instances.size(), 1);
  ASSERT_EQ(instances[MeshInstanceIndex(0)].mesh_index, 0);

  // There is no transformation.
  AssertMatrixNear(instances[MeshInstanceIndex(0)].transform,
                   Eigen::Matrix4d::Identity(), 1e-6);
}

TEST(SceneUtilsTest, TestNumMeshInstances) {
  // Tests that we can compute mesh instance counts for all base meshes in an
  // input scene.

  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(scene->NumMeshes(), 4);

  const auto num_mesh_instances = draco::SceneUtils::NumMeshInstances(*scene);
  ASSERT_EQ(num_mesh_instances.size(), 4);
  ASSERT_EQ(num_mesh_instances[draco::MeshIndex(0)], 1);
  ASSERT_EQ(num_mesh_instances[draco::MeshIndex(1)], 1);
  ASSERT_EQ(num_mesh_instances[draco::MeshIndex(2)], 1);
  ASSERT_EQ(num_mesh_instances[draco::MeshIndex(3)], 2);
}

TEST(SceneUtilsTest, TestNumFacesOnScene) {
  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(draco::SceneUtils::NumFacesOnBaseMeshes(*scene), 2856);
  ASSERT_EQ(draco::SceneUtils::NumFacesOnInstancedMeshes(*scene), 3624);
}

TEST(SceneUtilsTest, TestNumPointsOnScene) {
  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(draco::SceneUtils::NumPointsOnBaseMeshes(*scene), 2978);
  ASSERT_EQ(draco::SceneUtils::NumPointsOnInstancedMeshes(*scene), 3564);
}

TEST(SceneUtilsTest, TestNumPositionsOnScene) {
  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnBaseMeshes(
                *scene, draco::GeometryAttribute::POSITION),
            1572);
  ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnInstancedMeshes(
                *scene, draco::GeometryAttribute::POSITION),
            1960);
}

TEST(SceneUtilsTest, TestNumNormalsOnScene) {
  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnBaseMeshes(
                *scene, draco::GeometryAttribute::NORMAL),
            1252);
  ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnInstancedMeshes(
                *scene, draco::GeometryAttribute::NORMAL),
            1612);
}

TEST(SceneUtilsTest, TestNumColorsOnScene) {
  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnBaseMeshes(
                *scene, draco::GeometryAttribute::COLOR),
            0);
  ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnInstancedMeshes(
                *scene, draco::GeometryAttribute::COLOR),
            0);
}

TEST(SceneUtilsTest, TestComputeBoundingBox) {
  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);
  const draco::BoundingBox bbox = draco::SceneUtils::ComputeBoundingBox(*scene);
  const draco::Vector3f min_point = bbox.GetMinPoint();
  const draco::Vector3f max_point = bbox.GetMaxPoint();
  constexpr float tolerance = 1e-4f;
  EXPECT_NEAR(min_point[0], -2.43091, tolerance);
  EXPECT_NEAR(min_point[1], +0.00145, tolerance);
  EXPECT_NEAR(min_point[2], -1.39600, tolerance);
  EXPECT_NEAR(max_point[0], +2.43800, tolerance);
  EXPECT_NEAR(max_point[1], +2.58437, tolerance);
  EXPECT_NEAR(max_point[2], +1.39600, tolerance);
}

TEST(SceneUtilsTest, TestComputeMeshInstanceBoundingBox) {
  auto scene = draco::ReadSceneFromTestFile(
      "SphereWithCircleTexture/sphere_with_circle_texture.gltf");
  ASSERT_NE(scene, nullptr);
  const draco::BoundingBox scene_bbox =
      draco::SceneUtils::ComputeBoundingBox(*scene);
  const auto instances = draco::SceneUtils::ComputeAllInstances(*scene);
  ASSERT_EQ(instances.size(), 1);
  const draco::BoundingBox mesh_bbox =
      draco::SceneUtils::ComputeMeshInstanceBoundingBox(
          *scene, instances[draco::MeshInstanceIndex(0)]);
  ASSERT_EQ(scene_bbox.GetMinPoint(), mesh_bbox.GetMinPoint());
  ASSERT_EQ(scene_bbox.GetMaxPoint(), mesh_bbox.GetMaxPoint());
}

TEST(SceneUtilsTest, TestMeshToSceneZeroMaterials) {
  const std::string filename = "cube_att.obj";
  std::unique_ptr<draco::Mesh> mesh = draco::ReadMeshFromTestFile(filename);
  ASSERT_NE(mesh, nullptr);
  ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 0);

  DRACO_ASSIGN_OR_ASSERT(const std::unique_ptr<draco::Scene> scene_from_mesh,
                         draco::SceneUtils::MeshToScene(std::move(mesh)));
  ASSERT_NE(scene_from_mesh, nullptr);
  ASSERT_EQ(scene_from_mesh->NumMeshes(), 1);
  ASSERT_EQ(scene_from_mesh->GetMaterialLibrary().NumMaterials(), 1);
  ASSERT_EQ(scene_from_mesh->NumMeshGroups(), 1);
  const draco::MeshGroup *const mesh_group =
      scene_from_mesh->GetMeshGroup(draco::MeshGroupIndex(0));
  ASSERT_EQ(mesh_group->NumMeshIndices(), 1);
  ASSERT_EQ(mesh_group->NumMaterialIndices(), 1);
}

TEST(SceneUtilsTest, TestMeshToSceneOneMaterial) {
  const std::string filename =
      "SphereWithCircleTexture/sphere_with_circle_texture.gltf";
  auto scene = draco::ReadSceneFromTestFile(filename);
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1);

  std::unique_ptr<draco::Mesh> mesh = draco::ReadMeshFromTestFile(filename);
  ASSERT_NE(mesh, nullptr);
  ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1);

  DRACO_ASSIGN_OR_ASSERT(const std::unique_ptr<draco::Scene> scene_from_mesh,
                         draco::SceneUtils::MeshToScene(std::move(mesh)));
  ASSERT_NE(scene_from_mesh, nullptr);
  ASSERT_EQ(scene_from_mesh->NumMeshes(), 1);
  ASSERT_EQ(scene_from_mesh->GetMaterialLibrary().NumMaterials(), 1);
  ASSERT_EQ(scene_from_mesh->NumMeshGroups(), 1);
  const draco::MeshGroup *const mesh_group =
      scene_from_mesh->GetMeshGroup(draco::MeshGroupIndex(0));
  ASSERT_EQ(mesh_group->NumMeshIndices(), 1);
  ASSERT_EQ(mesh_group->NumMaterialIndices(), 1);

  CompareScenes(scene.get(), scene_from_mesh.get());
}

TEST(SceneUtilsTest, TestMeshToSceneMultipleMaterials) {
  const std::string filename = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf";
  auto scene = draco::ReadSceneFromTestFile(filename);
  ASSERT_NE(scene, nullptr);

  std::unique_ptr<draco::Mesh> mesh = draco::ReadMeshFromTestFile(filename);
  ASSERT_NE(mesh, nullptr);
  ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 4);

  DRACO_ASSIGN_OR_ASSERT(const std::unique_ptr<draco::Scene> scene_from_mesh,
                         draco::SceneUtils::MeshToScene(std::move(mesh)));
  ASSERT_NE(scene_from_mesh, nullptr);
  ASSERT_EQ(scene_from_mesh->NumMeshes(), 4);
  ASSERT_EQ(scene_from_mesh->GetMaterialLibrary().NumMaterials(), 4);
  ASSERT_EQ(scene_from_mesh->NumMeshGroups(), 1);
  const draco::MeshGroup *const mesh_group =
      scene_from_mesh->GetMeshGroup(draco::MeshGroupIndex(0));
  ASSERT_EQ(mesh_group->NumMeshIndices(), 4);
  ASSERT_EQ(mesh_group->NumMaterialIndices(), 4);

  // Unfortunately we can't CompareScenes(scene.get(), scene_from_mesh.get()),
  // because scene has two mesh groups and scene_from_mesh has only one.
}

TEST(SceneUtilsTest, TestInstantiateMeshWithIdentityTransformation) {
  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);

  // Compute scene mesh instances.
  const auto instances = draco::SceneUtils::ComputeAllInstances(*scene);
  ASSERT_EQ(instances.size(), 5);

  // Check instantiation of mesh with identity transformation.
  const draco::SceneUtils::MeshInstance instance =
      instances[MeshInstanceIndex(0)];
  ASSERT_EQ(instance.transform, Eigen::Matrix4d::Identity());

  // Instantiate this mesh instance.
  DRACO_ASSIGN_OR_ASSERT(auto mesh,
                         draco::SceneUtils::InstantiateMesh(*scene, instance));
  const draco::Mesh &base_mesh = scene->GetMesh(instance.mesh_index);

  // Check that bounding box of the instanced mesh is same as box of base mesh.
  const draco::BoundingBox instanced_bbox = mesh->ComputeBoundingBox();
  const draco::BoundingBox base_bbox = base_mesh.ComputeBoundingBox();
  ASSERT_EQ(instanced_bbox.GetMinPoint()[0], base_bbox.GetMinPoint()[0]);
  ASSERT_EQ(instanced_bbox.GetMinPoint()[1], base_bbox.GetMinPoint()[1]);
  ASSERT_EQ(instanced_bbox.GetMinPoint()[2], base_bbox.GetMinPoint()[2]);
  ASSERT_EQ(instanced_bbox.GetMaxPoint()[0], base_bbox.GetMaxPoint()[0]);
  ASSERT_EQ(instanced_bbox.GetMaxPoint()[1], base_bbox.GetMaxPoint()[1]);
  ASSERT_EQ(instanced_bbox.GetMaxPoint()[2], base_bbox.GetMaxPoint()[2]);
}

TEST(SceneUtilsTest, TestInstantiateMesh) {
  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);

  // Compute scene mesh instances.
  const auto instances = draco::SceneUtils::ComputeAllInstances(*scene);
  ASSERT_EQ(instances.size(), 5);

  // Check instantiation of mesh with identity transformation.
  const draco::SceneUtils::MeshInstance instance =
      instances[MeshInstanceIndex(3)];
  ASSERT_NE(instance.transform, Eigen::Matrix4d::Identity());

  // Instantiate this mesh instance.
  DRACO_ASSIGN_OR_ASSERT(auto mesh,
                         draco::SceneUtils::InstantiateMesh(*scene, instance));
  const draco::Mesh &base_mesh = scene->GetMesh(instance.mesh_index);

  // Check bounding box of the base mesh.
  constexpr float tolerance = 1e-4f;
  const draco::BoundingBox base_bbox = base_mesh.ComputeBoundingBox();
  EXPECT_NEAR(base_bbox.GetMinPoint()[0], -0.42780, tolerance);
  EXPECT_NEAR(base_bbox.GetMinPoint()[1], -0.42780, tolerance);
  EXPECT_NEAR(base_bbox.GetMinPoint()[2], -1.05800, tolerance);
  EXPECT_NEAR(base_bbox.GetMaxPoint()[0], +0.42780, tolerance);
  EXPECT_NEAR(base_bbox.GetMaxPoint()[1], +0.42780, tolerance);
  EXPECT_NEAR(base_bbox.GetMaxPoint()[2], +1.05800, tolerance);

  // Check bounding box of the instanced mesh. It should differ.
  const draco::BoundingBox instanced_bbox = mesh->ComputeBoundingBox();
  EXPECT_NEAR(instanced_bbox.GetMinPoint()[0], -1.77860, tolerance);
  EXPECT_NEAR(instanced_bbox.GetMinPoint()[1], +0.00145, tolerance);
  EXPECT_NEAR(instanced_bbox.GetMinPoint()[2], -1.05800, tolerance);
  EXPECT_NEAR(instanced_bbox.GetMaxPoint()[0], -0.92606, tolerance);
  EXPECT_NEAR(instanced_bbox.GetMaxPoint()[1], +0.85399, tolerance);
  EXPECT_NEAR(instanced_bbox.GetMaxPoint()[2], +1.05800, tolerance);
}

TEST(SceneUtilsTest, TestCleanupEmptyMeshGroup) {
  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(scene->NumMeshes(), 4);
  ASSERT_EQ(scene->NumMeshGroups(), 2);
  ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 5);
  ASSERT_EQ(scene->GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(),
            draco::MeshGroupIndex(0));

  // Invalidate references to the three truck body parts in mesh group.
  draco::MeshGroup &mesh_group = *scene->GetMeshGroup(draco::MeshGroupIndex(0));
  mesh_group.SetMeshIndex(0, draco::kInvalidMeshIndex);
  mesh_group.SetMeshIndex(1, draco::kInvalidMeshIndex);
  mesh_group.SetMeshIndex(2, draco::kInvalidMeshIndex);

  // Cleanup scene.
  draco::SceneUtils::Cleanup(scene.get());

  // Check cleaned up scene.
  ASSERT_EQ(scene->NumMeshes(), 1);
  ASSERT_EQ(scene->NumMeshGroups(), 1);
  ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 2);
  ASSERT_EQ(scene->GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(),
            draco::kInvalidMeshGroupIndex);
}

TEST(SceneUtilsTest, TestCleanupUnreferencedMeshGroup) {
  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(scene->NumMeshes(), 4);
  ASSERT_EQ(scene->NumMeshGroups(), 2);
  ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 5);

  // Invalidate references to truck axle mesh group.
  scene->GetNode(draco::SceneNodeIndex(2))
      ->SetMeshGroupIndex(draco::kInvalidMeshGroupIndex);
  scene->GetNode(draco::SceneNodeIndex(4))
      ->SetMeshGroupIndex(draco::kInvalidMeshGroupIndex);

  // Cleanup scene.
  draco::SceneUtils::Cleanup(scene.get());

  // Check cleaned up scene.
  ASSERT_EQ(scene->NumMeshes(), 3);
  ASSERT_EQ(scene->NumMeshGroups(), 1);
  ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 3);
}

TEST(SceneUtilsTest, TestCleanupInvalidMeshIndex) {
  auto scene =
      draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(scene->NumMeshes(), 4);
  ASSERT_EQ(scene->NumMeshGroups(), 2);
  ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 5);
  ASSERT_EQ(scene->GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(),
            draco::MeshGroupIndex(0));

  // Invalidate references to two truck body parts in mesh group.
  draco::MeshGroup &mesh_group = *scene->GetMeshGroup(draco::MeshGroupIndex(0));
  ASSERT_EQ(mesh_group.NumMeshIndices(), 3);
  mesh_group.SetMeshIndex(0, draco::kInvalidMeshIndex);
  mesh_group.SetMeshIndex(2, draco::kInvalidMeshIndex);

  // Cleanup scene.
  draco::SceneUtils::Cleanup(scene.get());

  // Check cleaned up scene.
  ASSERT_EQ(scene->NumMeshes(), 2);
  ASSERT_EQ(scene->NumMeshGroups(), 2);
  ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 3);
  ASSERT_EQ(scene->GetMeshGroup(draco::MeshGroupIndex(0))->NumMeshIndices(), 1);
}

TEST(SceneUtilsTest, TestDeduplicateMeshGroups) {
  // Input scene has four different mesh groups but only two of them should
  // contain unique set of meshes.
  auto scene =
      draco::ReadSceneFromTestFile("DuplicateMeshes/duplicate_meshes.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(scene->NumMeshes(), 1);
  ASSERT_EQ(scene->NumMeshGroups(), 4);
  ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 7);

  draco::SceneUtils::DeduplicateMeshGroups(scene.get());

  // Check deduplicated scene.
  ASSERT_EQ(scene->NumMeshes(), 1);
  ASSERT_EQ(scene->NumMeshGroups(), 2);
  ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 7);
}

TEST(SceneUtilsTest, TestCleanupUnusedTexCoords) {
  // The glTF file has two tex coords that are unused because the materials do
  // not reference any textures.
  auto scene = draco::ReadSceneFromTestFile("UnusedTexCoords/NoTextures.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(scene->GetMesh(draco::MeshIndex(0))
                .NumNamedAttributes(draco::GeometryAttribute::TEX_COORD),
            2);

  // Cleanup scene.
  draco::SceneUtils::Cleanup(scene.get());
  ASSERT_EQ(scene->GetMesh(draco::MeshIndex(0))
                .NumNamedAttributes(draco::GeometryAttribute::TEX_COORD),
            0);
}

TEST(SceneUtilsTest, TestComputeGlobalNodeTransform) {
  // Tests that we can compute global transformation of scene nodes.

  auto scene = draco::ReadSceneFromTestFile("simple_skin.gltf");
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(scene->NumNodes(), 3);

  // Compute and check global node transforms.
  constexpr float kTolerance = 1e-6;
  // clang-format off
  AssertMatrixNear(draco::SceneUtils::ComputeGlobalNodeTransform(
                       *scene, draco::SceneNodeIndex(0)),
                   Eigen::Matrix4d::Identity(),
                   kTolerance);
  AssertMatrixNear(draco::SceneUtils::ComputeGlobalNodeTransform(
                       *scene, draco::SceneNodeIndex(1)),
                   Eigen::Matrix4d{{1.0, 0.0, 0.0, 0.0},
                                   {0.0, 1.0, 0.0, 1.0},
                                   {0.0, 0.0, 1.0, 0.0},
                                   {0.0, 0.0, 0.0, 1.0}},
                   kTolerance);
  AssertMatrixNear(draco::SceneUtils::ComputeGlobalNodeTransform(
                       *scene, draco::SceneNodeIndex(2)),
                   Eigen::Matrix4d{{1.0, 0.0, 0.0, 0.0},
                                   {0.0, 1.0, 0.0, 1.0},
                                   {0.0, 0.0, 1.0, 0.0},
                                   {0.0, 0.0, 0.0, 1.0}},
                   kTolerance);
  // clang-format on
}

TEST(SceneUtilsTest, TestIsDracoCompressionEnabled) {
  // Tests that we can determine whether any of the scene meshes have geometry
  // compression enabled.
  const std::string file = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf";
  auto scene = draco::ReadSceneFromTestFile(file);
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(scene->NumMeshes(), 4);

  // Check that the scene has geometry compression disabled by default.
  ASSERT_FALSE(draco::SceneUtils::IsDracoCompressionEnabled(*scene));

  // Check that geometry compression can be enabled.
  scene->GetMesh(MeshIndex(2)).SetCompressionEnabled(true);
  ASSERT_TRUE(draco::SceneUtils::IsDracoCompressionEnabled(*scene));
}

TEST(SceneUtilsTest, TestSetDracoCompressionOptions) {
  // Tests that geometry compression settings can be set for all scene meshes.
  const std::string file = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf";
  auto scene = draco::ReadSceneFromTestFile(file);
  ASSERT_NE(scene, nullptr);
  ASSERT_EQ(scene->NumMeshes(), 4);

  // Check that compression is initially disabled for all scene meshes.
  ASSERT_FALSE(scene->GetMesh(MeshIndex(0)).IsCompressionEnabled());
  ASSERT_FALSE(scene->GetMesh(MeshIndex(1)).IsCompressionEnabled());
  ASSERT_FALSE(scene->GetMesh(MeshIndex(2)).IsCompressionEnabled());
  ASSERT_FALSE(scene->GetMesh(MeshIndex(3)).IsCompressionEnabled());

  // Check that initially all scene meshes have default compression options.
  draco::DracoCompressionOptions defaults;
  ASSERT_EQ(scene->GetMesh(MeshIndex(0)).GetCompressionOptions(), defaults);
  ASSERT_EQ(scene->GetMesh(MeshIndex(1)).GetCompressionOptions(), defaults);
  ASSERT_EQ(scene->GetMesh(MeshIndex(2)).GetCompressionOptions(), defaults);
  ASSERT_EQ(scene->GetMesh(MeshIndex(3)).GetCompressionOptions(), defaults);

  // Check geometry compression options can be set to all scene meshes and that
  // this also enables compression for all scnene meshes.
  draco::DracoCompressionOptions options;
  options.compression_level = 10;
  options.quantization_bits_normal = 12;
  draco::SceneUtils::SetDracoCompressionOptions(&options, scene.get());
  ASSERT_TRUE(scene->GetMesh(MeshIndex(0)).IsCompressionEnabled());
  ASSERT_TRUE(scene->GetMesh(MeshIndex(1)).IsCompressionEnabled());
  ASSERT_TRUE(scene->GetMesh(MeshIndex(2)).IsCompressionEnabled());
  ASSERT_TRUE(scene->GetMesh(MeshIndex(3)).IsCompressionEnabled());
  ASSERT_EQ(scene->GetMesh(MeshIndex(0)).GetCompressionOptions(), options);
  ASSERT_EQ(scene->GetMesh(MeshIndex(1)).GetCompressionOptions(), options);
  ASSERT_EQ(scene->GetMesh(MeshIndex(2)).GetCompressionOptions(), options);
  ASSERT_EQ(scene->GetMesh(MeshIndex(3)).GetCompressionOptions(), options);

  // Check that geometry compression can be disabled for all scene meshes.
  draco::SceneUtils::SetDracoCompressionOptions(nullptr, scene.get());
  ASSERT_FALSE(scene->GetMesh(MeshIndex(0)).IsCompressionEnabled());
  ASSERT_FALSE(scene->GetMesh(MeshIndex(1)).IsCompressionEnabled());
  ASSERT_FALSE(scene->GetMesh(MeshIndex(2)).IsCompressionEnabled());
  ASSERT_FALSE(scene->GetMesh(MeshIndex(3)).IsCompressionEnabled());
}

}  // namespace

#endif  // DRACO_TRANSCODER_SUPPORTED
