/*
 * Copyright 2018 The Cartographer 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 "cartographer/mapping/pose_extrapolator.h"

#include "Eigen/Geometry"
#include "absl/memory/memory.h"
#include "cartographer/mapping/internal/eigen_quaterniond_from_two_vectors.h"
#include "cartographer/transform/rigid_transform_test_helpers.h"
#include "gtest/gtest.h"

namespace cartographer {
namespace mapping {
namespace {

constexpr double kPoseQueueDuration = 0.5f;
constexpr double kGravityTimeConstant = 0.1f;
constexpr double kExtrapolateDuration = 0.1f;
constexpr double kPrecision = 1e-8;
constexpr double kExtrapolatePrecision = 1e-2;

TEST(PoseExtrapolatorDeathTest, IncompleteInitialization) {
  PoseExtrapolator extrapolator(common::FromSeconds(kPoseQueueDuration),
                                kGravityTimeConstant);
  common::Time time = common::FromUniversal(123);
  EXPECT_DEATH(extrapolator.EstimateGravityOrientation(time), "");
  EXPECT_DEATH(extrapolator.ExtrapolatePose(time), "");
  Eigen::Vector3d acceleration(0, 0, 9);
  Eigen::Vector3d angular_velocity(0, 0, 0);
  extrapolator.AddImuData(
      sensor::ImuData{time, acceleration, angular_velocity});
  EXPECT_DEATH(extrapolator.EstimateGravityOrientation(time), "");
  EXPECT_DEATH(extrapolator.ExtrapolatePose(time), "");
}

TEST(PoseExtrapolatorDeathTest, ExtrapolateInPast) {
  PoseExtrapolator extrapolator(common::FromSeconds(kPoseQueueDuration),
                                kGravityTimeConstant);
  common::Time time_present = common::FromUniversal(123);
  transform::Rigid3d pose =
      transform::Rigid3d::Translation(Eigen::Vector3d(0.1, 0.2, 0.3));
  extrapolator.AddPose(time_present, pose);
  extrapolator.EstimateGravityOrientation(time_present);
  EXPECT_THAT(extrapolator.ExtrapolatePose(time_present),
              transform::IsNearly(pose, kPrecision));
  common::Time time_in_past = time_present - common::FromSeconds(10);
  EXPECT_DEATH(extrapolator.EstimateGravityOrientation(time_in_past), "");
  EXPECT_DEATH(extrapolator.ExtrapolatePose(time_in_past), "");
  common::Time time_in_future = time_present + common::FromSeconds(20);
  extrapolator.ExtrapolatePose(time_in_future);
  EXPECT_DEATH(extrapolator.ExtrapolatePose(time_present), "");
}

TEST(PoseExtrapolatorTest, EstimateGravityOrientationWithIMU) {
  Eigen::Vector3d initial_gravity_acceleration(1.6, 2.0, 8.0);
  Eigen::Vector3d angular_velocity(0, 0, 0);
  common::Time current_time = common::FromUniversal(123);
  sensor::ImuData imu_data{current_time, initial_gravity_acceleration,
                           angular_velocity};
  auto extrapolator = PoseExtrapolator::InitializeWithImu(
      common::FromSeconds(kPoseQueueDuration), kGravityTimeConstant, imu_data);
  Eigen::Quaterniond expected_orientation =
      FromTwoVectors(initial_gravity_acceleration, Eigen::Vector3d::UnitZ());
  EXPECT_NEAR(0.,
              extrapolator->EstimateGravityOrientation(current_time)
                  .angularDistance(expected_orientation),
              kPrecision);
  Eigen::Vector3d gravity_acceleration(1.6, 2.0, 8.0);
  for (int i = 0; i < 10; ++i) {
    current_time += common::FromSeconds(kGravityTimeConstant);
    extrapolator->AddImuData(
        sensor::ImuData{current_time, gravity_acceleration, angular_velocity});
  }
  expected_orientation =
      FromTwoVectors(gravity_acceleration, Eigen::Vector3d::UnitZ());
  EXPECT_NEAR(0.,
              extrapolator->EstimateGravityOrientation(current_time)
                  .angularDistance(expected_orientation),
              kPrecision);
}

TEST(PoseExtrapolatorTest, ExtrapolateWithPoses) {
  PoseExtrapolator extrapolator(common::FromSeconds(kPoseQueueDuration),
                                kGravityTimeConstant);
  common::Time current_time = common::FromUniversal(123);
  transform::Rigid3d current_pose =
      transform::Rigid3d::Translation(Eigen::Vector3d(0.3, 0.7, 0.2));
  Eigen::Vector3d velocity(0, 0.1, 0);
  Eigen::Vector3d angular_velocity(0.01, 0, 0.1);
  transform::Rigid3d motion_per_second(
      velocity, Eigen::AngleAxisd(angular_velocity.norm(),
                                  angular_velocity.normalized()));
  extrapolator.AddPose(current_time, current_pose);
  EXPECT_EQ(common::ToUniversal(extrapolator.GetLastPoseTime()),
            common::ToUniversal(current_time));
  EXPECT_THAT(extrapolator.ExtrapolatePose(current_time),
              transform::IsNearly(current_pose, kPrecision));
  for (int i = 0; i < 5; ++i) {
    current_time += common::FromSeconds(1);
    current_pose = current_pose * motion_per_second;
    extrapolator.AddPose(current_time, current_pose);
    EXPECT_THAT(extrapolator.ExtrapolatePose(current_time),
                transform::IsNearly(current_pose, kPrecision));
    transform::Rigid3d expected_pose =
        current_pose *
        transform::Rigid3d(
            kExtrapolateDuration * velocity,
            Eigen::AngleAxisd(kExtrapolateDuration * angular_velocity.norm(),
                              angular_velocity.normalized()));
    EXPECT_THAT(extrapolator.ExtrapolatePose(
                    current_time + common::FromSeconds(kExtrapolateDuration)),
                transform::IsNearly(expected_pose, kExtrapolatePrecision));
    EXPECT_EQ(common::ToUniversal(extrapolator.GetLastExtrapolatedTime()),
              common::ToUniversal(current_time +
                                  common::FromSeconds(kExtrapolateDuration)));
  }
  Eigen::AngleAxisd gravity_axis(
      extrapolator.EstimateGravityOrientation(current_time));
  EXPECT_NEAR(
      0.f, (gravity_axis.axis().normalized() - Eigen::Vector3d::UnitZ()).norm(),
      kExtrapolatePrecision);
}

TEST(PoseExtrapolatorTest, ExtrapolateWithIMU) {
  Eigen::Vector3d initial_gravity_acceleration(0, 0, 9.8);
  Eigen::Vector3d initial_angular_velocity(0, 0, 0);
  common::Time current_time = common::FromUniversal(123);
  sensor::ImuData imu_data{current_time, initial_gravity_acceleration,
                           initial_angular_velocity};
  auto extrapolator = PoseExtrapolator::InitializeWithImu(
      common::FromSeconds(kPoseQueueDuration), kGravityTimeConstant, imu_data);
  transform::Rigid3d current_pose =
      transform::Rigid3d::Translation(Eigen::Vector3d(0.3, 0.7, 0.2));
  // Let velocity estimation come to rest.
  for (int i = 0; i < kPoseQueueDuration + 2; ++i) {
    current_time += common::FromSeconds(1);
    extrapolator->AddImuData(sensor::ImuData{
        current_time, initial_gravity_acceleration, initial_angular_velocity});
    extrapolator->AddPose(current_time, current_pose);
  }

  Eigen::Vector3d angular_velocity_yaw(0, 0, 0.1);
  transform::Rigid3d expected_pose = current_pose;
  for (int i = 0; i < 3; ++i) {
    for (int j = 0; j < 10; ++j) {
      current_time += common::FromSeconds(kGravityTimeConstant);
      extrapolator->AddImuData(sensor::ImuData{
          current_time, initial_gravity_acceleration, angular_velocity_yaw});
      Eigen::Quaterniond expected_rotation(
          Eigen::AngleAxisd(kGravityTimeConstant * angular_velocity_yaw.norm(),
                            angular_velocity_yaw.normalized()));
      expected_pose =
          expected_pose * transform::Rigid3d::Rotation(expected_rotation);
    }
    EXPECT_THAT(extrapolator->ExtrapolatePose(current_time),
                transform::IsNearly(expected_pose, kExtrapolatePrecision));
    extrapolator->AddPose(current_time, expected_pose);
  }
  Eigen::AngleAxisd gravity_axis(
      extrapolator->EstimateGravityOrientation(current_time));
  EXPECT_NEAR(
      0.f, (gravity_axis.axis().normalized() - Eigen::Vector3d::UnitZ()).norm(),
      kPrecision);
}

}  // namespace
}  // namespace mapping
}  // namespace cartographer
