text: // Copyright 2020 The Fuchsia Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include "src/ui/scenic/lib/flatland/global_matrix_data.h"
  
  #include <lib/syslog/cpp/macros.h>
  
  #include <memory>
  
  #include <gmock/gmock.h>
  #include <gtest/gtest.h>
  
  #include "src/ui/scenic/lib/flatland/global_topology_data.h"
  
  #include <glm/glm.hpp>
  #include <glm/gtc/constants.hpp>
  #include <glm/gtc/type_ptr.hpp>
  #include <glm/gtx/matrix_transform_2d.hpp>
  
  namespace flatland {
  namespace test {
  
  namespace {
  
  // Helper function to generate an escher::Rectangle2D from a glm::mat3 for tests that are strictly
  // testing the conversion math.
  escher::Rectangle2D GetRectangleForMatrix(const glm::mat3& matrix) {
    // Compute the global rectangle vector and return the first entry.
    const auto rectangles = ComputeGlobalRectangles({matrix});
    EXPECT_EQ(rectangles.size(), 1ul);
    return rectangles[0];
  }
  
  }  // namespace
  
  // The following tests ensure the transform hierarchy is properly reflected in the list of global
  // rectangles.
  
  TEST(GlobalMatrixDataTest, EmptyTopologyReturnsEmptyMatrices) {
    UberStruct::InstanceMap uber_structs;
    GlobalTopologyData::TopologyVector topology_vector;
    GlobalTopologyData::ParentIndexVector parent_indices;
  
    auto global_matrices = ComputeGlobalMatrices(topology_vector, parent_indices, uber_structs);
    EXPECT_TRUE(global_matrices.empty());
  }
  
  TEST(GlobalMatrixDataTest, EmptyLocalMatricesAreIdentity) {
    UberStruct::InstanceMap uber_structs;
  
    // Make a global topology representing the following graph:
    //
    // 1:0 - 1:1
    GlobalTopologyData::TopologyVector topology_vector = {{1, 0}, {1, 1}};
    GlobalTopologyData::ParentIndexVector parent_indices = {0, 0};
  
    // The UberStruct for instance ID 1 must exist, but it contains no local matrices.
    auto uber_struct = std::make_unique<UberStruct>();
    uber_structs[1] = std::move(uber_struct);
  
    // The root matrix is set to the identity matrix, and the second inherits that.
    std::vector<glm::mat3> expected_matrices = {
        glm::mat3(),
        glm::mat3(),
    };
  
    auto global_matrices = ComputeGlobalMatrices(topology_vector, parent_indices, uber_structs);
    EXPECT_THAT(global_matrices, ::testing::ElementsAreArray(expected_matrices));
  }
  
  TEST(GlobalMatrixDataTest, GlobalMatricesIncludeParentMatrix) {
    UberStruct::InstanceMap uber_structs;
  
    // Make a global topology representing the following graph:
    //
    // 1:0 - 1:1 - 1:2
    //     \
    //       1:3 - 1:4
    GlobalTopologyData::TopologyVector topology_vector = {{1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}};
    GlobalTopologyData::ParentIndexVector parent_indices = {0, 0, 1, 0, 3};
  
    auto uber_struct = std::make_unique<UberStruct>();
  
    static const glm::vec2 kTranslation = {1.f, 2.f};
    static const float kRotation = glm::half_pi<float>();
    static const glm::vec2 kScale = {3.f, 5.f};
  
    // All transforms will get the translation from 1:0
    uber_struct->local_matrices[{1, 0}] = glm::translate(glm::mat3(), kTranslation);
  
    // The 1:1 - 1:2 branch rotates, then scales.
    uber_struct->local_matrices[{1, 1}] = glm::rotate(glm::mat3(), kRotation);
    uber_struct->local_matrices[{1, 2}] = glm::scale(glm::mat3(), kScale);
  
    // The 1:3 - 1:4 branch scales, then rotates.
    uber_struct->local_matrices[{1, 3}] = glm::scale(glm::mat3(), kScale);
    uber_struct->local_matrices[{1, 4}] = glm::rotate(glm::mat3(), kRotation);
  
    uber_structs[1] = std::move(uber_struct);
  
    // The expected matrices apply the operations in the correct order. The translation always comes
    // first, followed by the operations of the children.
    std::vector<glm::mat3> expected_matrices = {
        glm::translate(glm::mat3(), kTranslation),
        glm::rotate(glm::translate(glm::mat3(), kTranslation), kRotation),
        glm::scale(glm::rotate(glm::translate(glm::mat3(), kTranslation), kRotation), kScale),
        glm::scale(glm::translate(glm::mat3(), kTranslation), kScale),
        glm::rotate(glm::scale(glm::translate(glm::mat3(), kTranslation), kScale), kRotation),
    };
  
    auto global_matrices = ComputeGlobalMatrices(topology_vector, parent_indices, uber_structs);
    EXPECT_THAT(global_matrices, ::testing::ElementsAreArray(expected_matrices));
  }
  
  TEST(GlobalMatrixDataTest, GlobalMatricesMultipleUberStructs) {
    UberStruct::InstanceMap uber_structs;
  
    // Make a global topology representing the following graph:
    //
    // 1:0 - 2:0
    //     \
    //       1:1
    GlobalTopologyData::TopologyVector topology_vector = {{1, 0}, {2, 0}, {1, 1}};
    GlobalTopologyData::ParentIndexVector parent_indices = {0, 0, 0};
  
    auto uber_struct1 = std::make_unique<UberStruct>();
    auto uber_struct2 = std::make_unique<UberStruct>();
  
    // Each matrix scales by a different prime number to distinguish the branches.
    uber_struct1->local_matrices[{1, 0}] = glm::scale(glm::mat3(), {2.f, 2.f});
    uber_struct1->local_matrices[{1, 1}] = glm::scale(glm::mat3(), {3.f, 3.f});
  
    uber_struct2->local_matrices[{2, 0}] = glm::scale(glm::mat3(), {5.f, 5.f});
  
    uber_structs[1] = std::move(uber_struct1);
    uber_structs[2] = std::move(uber_struct2);
  
    std::vector<glm::mat3> expected_matrices = {
        glm::scale(glm::mat3(), glm::vec2(2.f)),   // 1:0 = 2
        glm::scale(glm::mat3(), glm::vec2(10.f)),  // 1:0 * 2:0 = 2 * 5 = 10
        glm::scale(glm::mat3(), glm::vec2(6.f)),   // 1:0 * 1:1 = 2 * 3 = 6
    };
  
    auto global_matrices = ComputeGlobalMatrices(topology_vector, parent_indices, uber_structs);
    EXPECT_THAT(global_matrices, ::testing::ElementsAreArray(expected_matrices));
  }
  
  // The following tests ensure that different geometric attributes (translation, rotation, scale)
  // modify the final rectangle as expected.
  
  TEST(Rectangle2DTest, ScaleAndRotate90DegreesTest) {
    const glm::vec2 extent(100.f, 50.f);
    glm::mat3 matrix = glm::rotate(glm::mat3(), glm::half_pi<float>());
    matrix = glm::scale(matrix, extent);
  
    const escher::Rectangle2D expected_rectangle(
        glm::vec2(0.f, 100.f), glm::vec2(50.f, 100.f),
        {glm::vec2(1, 0), glm::vec2(1, 1), glm::vec2(0, 1), glm::vec2(0, 0)});
  
    const auto rectangle = GetRectangleForMatrix(matrix);
    EXPECT_EQ(rectangle, expected_rectangle);
  }
  
  TEST(Rectangle2DTest, ScaleAndRotate180DegreesTest) {
    const glm::vec2 extent(100.f, 50.f);
    glm::mat3 matrix = glm::rotate(glm::mat3(), glm::pi<float>());
    matrix = glm::scale(matrix, extent);
  
    const escher::Rectangle2D expected_rectangle(
        glm::vec2(-100.f, 50.f), glm::vec2(100.f, 50.f),
        {glm::vec2(1, 1), glm::vec2(0, 1), glm::vec2(0, 0), glm::vec2(1, 0)});
  
    const auto rectangle = GetRectangleForMatrix(matrix);
    EXPECT_EQ(rectangle, expected_rectangle);
  }
  
  TEST(Rectangle2DTest, ScaleAndRotate270DegreesTest) {
    const glm::vec2 extent(100.f, 50.f);
    glm::mat3 matrix = glm::rotate(glm::mat3(), glm::three_over_two_pi<float>());
    matrix = glm::scale(matrix, extent);
  
    const escher::Rectangle2D expected_rectangle(
        glm::vec2(-50.f, 0.f), glm::vec2(50.f, 100.f),
        {glm::vec2(0, 1), glm::vec2(0, 0), glm::vec2(1, 0), glm::vec2(1, 1)});
  
    const auto rectangle = GetRectangleForMatrix(matrix);
    EXPECT_EQ(rectangle, expected_rectangle);
  }
  
  // Make sure that floating point transform values that aren't exactly
  // integers are also respected.
  TEST(Rectangle2DTest, FloatingPointTranslateAndScaleTest) {
    const glm::vec2 offset(10.9f, 20.5f);
    const glm::vec2 extent(100.3f, 200.7f);
    glm::mat3 matrix = glm::translate(glm::mat3(), offset);
    matrix = glm::scale(matrix, extent);
  
    const escher::Rectangle2D expected_rectangle(
        offset, extent, {glm::vec2(0, 0), glm::vec2(1, 0), glm::vec2(1, 1), glm::vec2(0, 1)});
  
    const auto rectangle = GetRectangleForMatrix(matrix);
    EXPECT_EQ(rectangle, expected_rectangle);
  }
  
  TEST(Rectangle2DTest, NegativeScaleTest) {
    // If both the x and y scale components are negative, this is equivalent
    // to a positive scale rotated by 180 degrees (PI radians).
    {
      const glm::vec2 extent(-10.f, -5.f);
      glm::mat3 matrix = glm::scale(glm::mat3(), extent);
  
      // These are the expected UVs for a 180 degree rotation.
      const escher::Rectangle2D expected_rectangle(
          glm::vec2(-10.f, 5.f), glm::vec2(10.f, 5.f),
          {glm::vec2(1, 1), glm::vec2(0, 1), glm::vec2(0, 0), glm::vec2(1, 0)});
  
      const auto rectangle = GetRectangleForMatrix(matrix);
      EXPECT_EQ(rectangle, expected_rectangle);
    }
  
    // If just the x scale component is negative and the y component is positive,
    // this is equivalent to a flip about the y axis (horiziontal).
    {
      const glm::vec2 extent(-10.f, 5.f);
      glm::mat3 matrix = glm::scale(glm::mat3(), extent);
  
      // These are the expected UVs for a horizontal flip.
      const escher::Rectangle2D expected_rectangle(
          glm::vec2(-10.f, 0.f), glm::vec2(10.f, 5.f),
          {glm::vec2(1, 0), glm::vec2(0, 0), glm::vec2(0, 1), glm::vec2(1, 1)});
  
      const auto rectangle = GetRectangleForMatrix(matrix);
      EXPECT_EQ(rectangle, expected_rectangle);
    }
  
    // If just the y scale component is negative and the x component is positive,
    // this is equivalent to a vertical flip about the x axis.
    {
      const glm::vec2 extent(10.f, -5.f);
      glm::mat3 matrix = glm::scale(glm::mat3(), extent);
  
      // These are the expected UVs for a vertical flip.
      const escher::Rectangle2D expected_rectangle(
          glm::vec2(0.f, 5.f), glm::vec2(10.f, 5.f),
          {glm::vec2(0, 1), glm::vec2(1, 1), glm::vec2(1, 0), glm::vec2(0, 0)});
  
      const auto rectangle = GetRectangleForMatrix(matrix);
      EXPECT_EQ(rectangle, expected_rectangle);
    }
  }
  
  // The same operations of translate/rotate/scale on a single matrix.
  TEST(Rectangle2DTest, OrderOfOperationsTest) {
    // First subtest tests swapping scaling and translation.
    {
      // Here we scale and then translate. The origin should be at (10,5) and the extent should also
      // still be (2,2) since the scale is being applied on the untranslated coordinates.
      const glm::mat3 test_1 =
          glm::scale(glm::translate(glm::mat3(), glm::vec2(10.f, 5.f)), glm::vec2(2.f, 2.f));
  
      const escher::Rectangle2D expected_rectangle_1(glm::vec2(10.f, 5.f), glm::vec2(2.f, 2.f));
  
      const auto rectangle_1 = GetRectangleForMatrix(test_1);
      EXPECT_EQ(rectangle_1, expected_rectangle_1);
  
      // Here we translate first, and then scale the translation, resulting in the origin point
      // doubling from (10, 5) to (20, 10).
      const glm::mat3 test_2 =
          glm::translate(glm::scale(glm::mat3(), glm::vec2(2.f, 2.f)), glm::vec2(10.f, 5.f));
  
      const escher::Rectangle2D expected_rectangle_2(glm::vec2(20.f, 10.f), glm::vec2(2.f, 2.f));
  
      const auto rectangle_2 = GetRectangleForMatrix(test_2);
      EXPECT_EQ(rectangle_2, expected_rectangle_2);
    }
  
    // Second subtest tests swapping translation and rotation.
    {
      // Since the rotation is applied first, the origin point rotates around (0,0) and then we
      // translate and wind up at (10, 5).
      const glm::mat3 test_1 =
          glm::rotate(glm::translate(glm::mat3(), glm::vec2(10.f, 5.f)), glm::half_pi<float>());
  
      const escher::Rectangle2D expected_rectangle_1(
          glm::vec2(10.f, 6.f), glm::vec2(1.f, 1.f),
          {glm::vec2(1, 0), glm::vec2(1, 1), glm::vec2(0, 1), glm::vec2(0, 0)});
  
      const auto rectangle_1 = GetRectangleForMatrix(test_1);
      EXPECT_EQ(rectangle_1, expected_rectangle_1);
  
      // Since we translated first here, the point goes from (0,0) to (10,5) and then rotates
      // 90 degrees counterclockwise and winds up at (-5, 10).
      const glm::mat3 test_2 =
          glm::translate(glm::rotate(glm::mat3(), glm::half_pi<float>()), glm::vec2(10.f, 5.f));
  
      const escher::Rectangle2D expected_rectangle_2(
          glm::vec2(-5.f, 11.f), glm::vec2(1.f, 1.f),
          {glm::vec2(1, 0), glm::vec2(1, 1), glm::vec2(0, 1), glm::vec2(0, 0)});
  
      const auto rectangle_2 = GetRectangleForMatrix(test_2);
      EXPECT_EQ(rectangle_2, expected_rectangle_2);
    }
  
    // Third subtest tests swapping non-uniform scaling and rotation.
    {
      // We rotate first and then scale, so the scaling isn't affected by the rotation.
      const glm::mat3 test_1 =
          glm::rotate(glm::scale(glm::mat3(), glm::vec2(9.f, 7.f)), glm::half_pi<float>());
  
      const escher::Rectangle2D expected_rectangle_1(
          glm::vec2(0.f, 7.f), glm::vec2(9.f, 7.f),
          {glm::vec2(1, 0), glm::vec2(1, 1), glm::vec2(0, 1), glm::vec2(0, 0)});
  
      const auto rectangle_1 = GetRectangleForMatrix(test_1);
      EXPECT_EQ(rectangle_1, expected_rectangle_1);
  
      // Here we scale and then rotate so the scale winds up rotated.
      const glm::mat3 test_2 =
          glm::scale(glm::rotate(glm::mat3(), glm::half_pi<float>()), glm::vec2(9.f, 7.f));
  
      const escher::Rectangle2D expected_rectangle_2(
          glm::vec2(0.f, 9.f), glm::vec2(7.f, 9.f),
          {glm::vec2(1, 0), glm::vec2(1, 1), glm::vec2(0, 1), glm::vec2(0, 0)});
  
      const auto rectangle_2 = GetRectangleForMatrix(test_2);
      EXPECT_EQ(rectangle_2, expected_rectangle_2);
    }
  }
  
  }  // namespace test
  }  // namespace flatland
  
  #undef EXPECT_VEC2
  