/* SPDX-FileCopyrightText: 2020 Blender Authors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

#include "BKE_armature.hh"

#include "BLI_listbase.h"
#include "BLI_math_matrix.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.h"
#include "BLI_string.h"

#include "DNA_armature_types.h"

#include "testing/testing.h"

namespace blender::bke::tests {

static const float FLOAT_EPSILON = 1.2e-7;

static const float SCALE_EPSILON = 3.71e-5;
static const float ORTHO_EPSILON = 5e-5;

/** Test that the matrix is orthogonal, i.e. has no scale or shear within acceptable precision. */
static double EXPECT_M3_ORTHOGONAL(const float mat[3][3],
                                   double epsilon_scale,
                                   double epsilon_ortho)
{
  /* Do the checks in double precision to avoid precision issues in the checks themselves. */
  double3x3 dmat;
  copy_m3d_m3(dmat.ptr(), mat);

  /* Check individual axis scaling. */
  EXPECT_NEAR(math::length(dmat[0]), 1.0, epsilon_scale);
  EXPECT_NEAR(math::length(dmat[1]), 1.0, epsilon_scale);
  EXPECT_NEAR(math::length(dmat[2]), 1.0, epsilon_scale);

  /* Check orthogonality. */
  EXPECT_NEAR(dot_v3v3_db(dmat[0], dmat[1]), 0.0, epsilon_ortho);
  EXPECT_NEAR(dot_v3v3_db(dmat[0], dmat[2]), 0.0, epsilon_ortho);
  EXPECT_NEAR(dot_v3v3_db(dmat[1], dmat[2]), 0.0, epsilon_ortho);

  /* Check determinant to detect flipping and as a secondary volume change check. */
  double determinant = math::determinant(dmat);

  EXPECT_NEAR(determinant, 1.0, epsilon_ortho);

  return determinant;
}

TEST(mat3_vec_to_roll, UnitMatrix)
{
  float unit_matrix[3][3];
  float roll;

  unit_m3(unit_matrix);

  /* Any vector with a unit matrix should return zero roll. */
  mat3_vec_to_roll(unit_matrix, unit_matrix[0], &roll);
  EXPECT_FLOAT_EQ(0.0f, roll);

  mat3_vec_to_roll(unit_matrix, unit_matrix[1], &roll);
  EXPECT_FLOAT_EQ(0.0f, roll);

  mat3_vec_to_roll(unit_matrix, unit_matrix[2], &roll);
  EXPECT_FLOAT_EQ(0.0f, roll);

  {
    /* Non-unit vector. */
    float vector[3] = {1.0f, 1.0f, 1.0f};
    mat3_vec_to_roll(unit_matrix, vector, &roll);
    EXPECT_NEAR(0.0f, roll, FLOAT_EPSILON);

    /* Normalized version of the above vector. */
    normalize_v3(vector);
    mat3_vec_to_roll(unit_matrix, vector, &roll);
    EXPECT_NEAR(0.0f, roll, FLOAT_EPSILON);
  }
}

TEST(mat3_vec_to_roll, Rotationmatrix)
{
  float rotation_matrix[3][3];
  float roll;

  const float rot_around_x[3] = {1.234f, 0.0f, 0.0f};
  eul_to_mat3(rotation_matrix, rot_around_x);

  {
    const float unit_axis_x[3] = {1.0f, 0.0f, 0.0f};
    mat3_vec_to_roll(rotation_matrix, unit_axis_x, &roll);
    EXPECT_NEAR(1.234f, roll, FLOAT_EPSILON);
  }

  {
    const float unit_axis_y[3] = {0.0f, 1.0f, 0.0f};
    mat3_vec_to_roll(rotation_matrix, unit_axis_y, &roll);
    EXPECT_NEAR(0, roll, FLOAT_EPSILON);
  }

  {
    const float unit_axis_z[3] = {0.0f, 0.0f, 1.0f};
    mat3_vec_to_roll(rotation_matrix, unit_axis_z, &roll);
    EXPECT_NEAR(0, roll, FLOAT_EPSILON);
  }

  {
    const float between_x_and_y[3] = {1.0f, 1.0f, 0.0f};
    mat3_vec_to_roll(rotation_matrix, between_x_and_y, &roll);
    EXPECT_NEAR(0.57158958f, roll, FLOAT_EPSILON);
  }
}

/** Generic function to test vec_roll_to_mat3_normalized. */
static double test_vec_roll_to_mat3_normalized(const float input[3],
                                               float roll,
                                               const float expected_roll_mat[3][3],
                                               bool normalize = true)
{
  float input_normalized[3];
  float roll_mat[3][3];

  if (normalize) {
    /* The vector is re-normalized to replicate the actual usage. */
    normalize_v3_v3(input_normalized, input);
  }
  else {
    copy_v3_v3(input_normalized, input);
  }

  vec_roll_to_mat3_normalized(input_normalized, roll, roll_mat);

  EXPECT_V3_NEAR(roll_mat[1], input_normalized, FLT_EPSILON);

  if (expected_roll_mat) {
    EXPECT_M3_NEAR(roll_mat, expected_roll_mat, FLT_EPSILON);
  }

  return EXPECT_M3_ORTHOGONAL(roll_mat, SCALE_EPSILON, ORTHO_EPSILON);
}

/** Binary search to test where the code switches to the most degenerate special case. */
static double find_flip_boundary(double x, double z)
{
  /* Irrational scale factor to ensure values aren't 'nice', have a lot of rounding errors,
   * and can't accidentally produce the exact result returned by the special case. */
  const double scale = M_1_PI / 10;
  double theta = x * x + z * z;
  double minv = 0, maxv = 1e-2;

  while (maxv - minv > FLT_EPSILON * 1e-3) {
    double mid = (minv + maxv) / 2;

    float roll_mat[3][3];
    float input[3] = {float(x * mid * scale),
                      -float(sqrt(1 - theta * mid * mid) * scale),
                      float(z * mid * scale)};

    normalize_v3(input);
    vec_roll_to_mat3_normalized(input, 0, roll_mat);

    /* The special case assigns exact constants rather than computing. */
    if (roll_mat[0][0] == -1 && roll_mat[0][1] == 0 && roll_mat[2][1] == 0) {
      minv = mid;
    }
    else {
      maxv = mid;
    }
  }

  return sqrt(theta) * (minv + maxv) * 0.5;
}

TEST(vec_roll_to_mat3_normalized, FlippedBoundary1)
{
  EXPECT_NEAR(find_flip_boundary(0, 1), 2.50e-4, 0.01e-4);
}

TEST(vec_roll_to_mat3_normalized, FlippedBoundary2)
{
  EXPECT_NEAR(find_flip_boundary(1, 1), 2.50e-4, 0.01e-4);
}

/* Test cases close to the -Y axis. */
TEST(vec_roll_to_mat3_normalized, Flipped1)
{
  /* If normalized_vector is -Y, simple symmetry by Z axis. */
  const float input[3] = {0.0f, -1.0f, 0.0f};
  const float expected_roll_mat[3][3] = {
      {-1.0f, 0.0f, 0.0f}, {0.0f, -1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}};
  test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat, false);
}

TEST(vec_roll_to_mat3_normalized, Flipped2)
{
  /* If normalized_vector is close to -Y and
   * it has X and Z values below a threshold,
   * simple symmetry by Z axis. */
  const float input[3] = {1e-24, -0.999999881, 0};
  const float expected_roll_mat[3][3] = {
      {-1.0f, 0.0f, 0.0f}, {0.0f, -1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}};
  test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat, false);
}

TEST(vec_roll_to_mat3_normalized, Flipped3)
{
  /* If normalized_vector is in a critical range close to -Y, apply the special case. */
  const float input[3] = {2.5e-4f, -0.999999881f, 2.5e-4f}; /* Corner Case. */
  const float expected_roll_mat[3][3] = {{0.000000f, -2.5e-4f, -1.000000f},
                                         {2.5e-4f, -0.999999881f, 2.5e-4f},
                                         {-1.000000f, -2.5e-4f, 0.000000f}};
  test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat, false);
}

/* Test 90 degree rotations. */
TEST(vec_roll_to_mat3_normalized, Rotate90_Z_CW)
{
  /* Rotate 90 around Z. */
  const float input[3] = {1, 0, 0};
  const float expected_roll_mat[3][3] = {{0, -1, 0}, {1, 0, 0}, {0, 0, 1}};
  test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
}

TEST(vec_roll_to_mat3_normalized, Rotate90_Z_CCW)
{
  /* Rotate 90 around Z. */
  const float input[3] = {-1, 0, 0};
  const float expected_roll_mat[3][3] = {{0, 1, 0}, {-1, 0, 0}, {0, 0, 1}};
  test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
}

TEST(vec_roll_to_mat3_normalized, Rotate90_X_CW)
{
  /* Rotate 90 around X. */
  const float input[3] = {0, 0, -1};
  const float expected_roll_mat[3][3] = {{1, 0, 0}, {0, 0, -1}, {0, 1, 0}};
  test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
}

TEST(vec_roll_to_mat3_normalized, Rotate90_X_CCW)
{
  /* Rotate 90 around X. */
  const float input[3] = {0, 0, 1};
  const float expected_roll_mat[3][3] = {{1, 0, 0}, {0, 0, 1}, {0, -1, 0}};
  test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
}

/* Test the general case when the vector is far enough from -Y. */
TEST(vec_roll_to_mat3_normalized, Generic1)
{
  const float input[3] = {1.0f, 1.0f, 1.0f}; /* Arbitrary Value. */
  const float expected_roll_mat[3][3] = {{0.788675129f, -0.577350259f, -0.211324856f},
                                         {0.577350259f, 0.577350259f, 0.577350259f},
                                         {-0.211324856f, -0.577350259f, 0.788675129f}};
  test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
}

TEST(vec_roll_to_mat3_normalized, Generic2)
{
  const float input[3] = {1.0f, -1.0f, 1.0f}; /* Arbitrary Value. */
  const float expected_roll_mat[3][3] = {{0.211324856f, -0.577350259f, -0.788675129f},
                                         {0.577350259f, -0.577350259f, 0.577350259f},
                                         {-0.788675129f, -0.577350259f, 0.211324856f}};
  test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
}

TEST(vec_roll_to_mat3_normalized, Generic3)
{
  const float input[3] = {-1.0f, -1.0f, 1.0f}; /* Arbitrary Value. */
  const float expected_roll_mat[3][3] = {{0.211324856f, 0.577350259f, 0.788675129f},
                                         {-0.577350259f, -0.577350259f, 0.577350259f},
                                         {0.788675129f, -0.577350259f, 0.211324856f}};
  test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
}

TEST(vec_roll_to_mat3_normalized, Generic4)
{
  const float input[3] = {-1.0f, -1.0f, -1.0f}; /* Arbitrary Value. */
  const float expected_roll_mat[3][3] = {{0.211324856f, 0.577350259f, -0.788675129f},
                                         {-0.577350259f, -0.577350259f, -0.577350259f},
                                         {-0.788675129f, 0.577350259f, 0.211324856f}};
  test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
}

/* Test roll. */
TEST(vec_roll_to_mat3_normalized, Roll1)
{
  const float input[3] = {1.0f, 1.0f, 1.0f}; /* Arbitrary Value. */
  const float expected_roll_mat[3][3] = {{0.211324856f, 0.577350259f, -0.788675129f},
                                         {0.577350259f, 0.577350259f, 0.577350259f},
                                         {0.788675129f, -0.577350259f, -0.211324856f}};
  test_vec_roll_to_mat3_normalized(input, float(M_PI_2), expected_roll_mat);
}

/** Test that the matrix is orthogonal for an input close to -Y. */
static double test_vec_roll_to_mat3_orthogonal(double s, double x, double z)
{
  const float input[3] = {float(x), float(s * sqrt(1 - x * x - z * z)), float(z)};

  return test_vec_roll_to_mat3_normalized(input, 0.0f, nullptr);
}

/** Test that the matrix is orthogonal for a range of inputs close to -Y. */
static void test_vec_roll_to_mat3_orthogonal(double s, double x1, double x2, double y1, double y2)
{
  const int count = 5000;
  double delta = 0;
  double tmax = 0;

  for (int i = 0; i <= count; i++) {
    double t = double(i) / count;
    double det = test_vec_roll_to_mat3_orthogonal(s, interpd(x2, x1, t), interpd(y2, y1, t));

    /* Find and report maximum error in the matrix determinant. */
    double curdelta = abs(det - 1);
    if (curdelta > delta) {
      delta = curdelta;
      tmax = t;
    }
  }

  printf("             Max determinant deviation %.10f at %f.\n", delta, tmax);
}

#define TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(name, s, x1, x2, y1, y2) \
  TEST(vec_roll_to_mat3_normalized, name) \
  { \
    test_vec_roll_to_mat3_orthogonal(s, x1, x2, y1, y2); \
  }

/* Moving from -Y towards X. */
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_000_005, -1, 0, 0, 3e-4, 0.005)
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_000_010, -1, 0, 0, 0.005, 0.010)
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_000_050, -1, 0, 0, 0.010, 0.050)
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_000_100, -1, 0, 0, 0.050, 0.100)
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_000_200, -1, 0, 0, 0.100, 0.200)
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_000_300, -1, 0, 0, 0.200, 0.300)

/* Moving from -Y towards X and Y. */
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_005_005, -1, 3e-4, 0.005, 3e-4, 0.005)
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_010_010, -1, 0.005, 0.010, 0.005, 0.010)
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_050_050, -1, 0.010, 0.050, 0.010, 0.050)
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_100_100, -1, 0.050, 0.100, 0.050, 0.100)
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_200_200, -1, 0.100, 0.200, 0.100, 0.200)

/* Moving from +Y towards X. */
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoP_000_005, 1, 0, 0, 0, 0.005)
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoP_000_100, 1, 0, 0, 0.005, 0.100)

/* Moving from +Y towards X and Y. */
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoP_005_005, 1, 0, 0.005, 0, 0.005)
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoP_100_100, 1, 0.005, 0.100, 0.005, 0.100)

class BKE_armature_find_selected_bones_test : public testing::Test {
 protected:
  bArmature arm = {};
  Bone bone1 = {}, bone2 = {}, bone3 = {};

  void SetUp() override
  {
    STRNCPY(bone1.name, "bone1");
    STRNCPY(bone2.name, "bone2");
    STRNCPY(bone3.name, "bone3");

    BLI_addtail(&arm.bonebase, &bone1);    /* bone1 is root bone. */
    BLI_addtail(&arm.bonebase, &bone2);    /* bone2 is root bone. */
    BLI_addtail(&bone2.childbase, &bone3); /* bone3 has bone2 as parent. */
  }
};

TEST_F(BKE_armature_find_selected_bones_test, some_bones_selected)
{
  bone1.flag = BONE_SELECTED;
  bone2.flag = 0;
  bone3.flag = BONE_SELECTED;

  std::vector<Bone *> seen_bones;
  auto callback = [&](Bone *bone) { seen_bones.push_back(bone); };

  SelectedBonesResult result = BKE_armature_find_selected_bones(&arm, callback);

  ASSERT_EQ(seen_bones.size(), 2) << "Expected 2 selected bones, got " << seen_bones.size();
  EXPECT_EQ(seen_bones[0], &bone1);
  EXPECT_EQ(seen_bones[1], &bone3);

  EXPECT_FALSE(result.all_bones_selected); /* Bone 2 was not selected. */
  EXPECT_FALSE(result.no_bones_selected);  /* Bones 1 and 3 were selected. */
}

TEST_F(BKE_armature_find_selected_bones_test, no_bones_selected)
{
  bone1.flag = bone2.flag = bone3.flag = 0;

  std::vector<Bone *> seen_bones;
  auto callback = [&](Bone *bone) { seen_bones.push_back(bone); };

  SelectedBonesResult result = BKE_armature_find_selected_bones(&arm, callback);

  EXPECT_TRUE(seen_bones.empty()) << "Expected no selected bones, got " << seen_bones.size();
  EXPECT_FALSE(result.all_bones_selected);
  EXPECT_TRUE(result.no_bones_selected);
}

TEST_F(BKE_armature_find_selected_bones_test, all_bones_selected)
{
  bone1.flag = bone2.flag = bone3.flag = BONE_SELECTED;

  std::vector<Bone *> seen_bones;
  auto callback = [&](Bone *bone) { seen_bones.push_back(bone); };

  SelectedBonesResult result = BKE_armature_find_selected_bones(&arm, callback);

  ASSERT_EQ(seen_bones.size(), 3) << "Expected 3 selected bones, got " << seen_bones.size();
  EXPECT_EQ(seen_bones[0], &bone1);
  EXPECT_EQ(seen_bones[1], &bone2);
  EXPECT_EQ(seen_bones[2], &bone3);

  EXPECT_TRUE(result.all_bones_selected);
  EXPECT_FALSE(result.no_bones_selected);
}

}  // namespace blender::bke::tests
