/*
 * 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/internal/imu_based_pose_extrapolator.h"

#include <algorithm>

#include "absl/memory/memory.h"
#include "cartographer/mapping/internal/3d/imu_integration.h"
#include "cartographer/mapping/internal/3d/rotation_parameterization.h"
#include "cartographer/mapping/internal/eigen_quaterniond_from_two_vectors.h"
#include "cartographer/mapping/internal/optimization/ceres_pose.h"
#include "cartographer/mapping/internal/optimization/cost_functions/acceleration_cost_function_3d.h"
#include "cartographer/mapping/internal/optimization/cost_functions/rotation_cost_function_3d.h"
#include "cartographer/mapping/internal/optimization/cost_functions/spa_cost_function_3d.h"
#include "cartographer/mapping/pose_graph_interface.h"
#include "cartographer/transform/transform.h"
#include "glog/logging.h"

namespace cartographer {
namespace mapping {

using ::cartographer::transform::TimestampedTransform;

ImuBasedPoseExtrapolator::ImuBasedPoseExtrapolator(
    const proto::ImuBasedPoseExtrapolatorOptions& options)
    : options_(options),
      solver_options_(
          common::CreateCeresSolverOptions(options_.solver_options())) {}

ImuBasedPoseExtrapolator::~ImuBasedPoseExtrapolator() {
  LOG(INFO) << "Number of iterations for pose extrapolation:";
  LOG(INFO) << num_iterations_hist_.ToString(10);
}

std::unique_ptr<PoseExtrapolatorInterface>
ImuBasedPoseExtrapolator::InitializeWithImu(
    const proto::ImuBasedPoseExtrapolatorOptions& options,
    const std::vector<sensor::ImuData>& imu_data,
    const std::vector<transform::TimestampedTransform>& initial_poses) {
  CHECK(!imu_data.empty());
  LOG(INFO) << options.DebugString();
  auto extrapolator = absl::make_unique<ImuBasedPoseExtrapolator>(options);
  std::copy(imu_data.begin(), imu_data.end(),
            std::back_inserter(extrapolator->imu_data_));
  if (!initial_poses.empty()) {
    for (const auto& pose : initial_poses) {
      if (pose.time > imu_data.front().time) {
        extrapolator->AddPose(pose.time, pose.transform);
      }
    }
  } else {
    extrapolator->AddPose(
        imu_data.back().time,
        transform::Rigid3d::Rotation(FromTwoVectors(
            imu_data.back().linear_acceleration, Eigen::Vector3d::UnitZ())));
  }
  return extrapolator;
}

common::Time ImuBasedPoseExtrapolator::GetLastPoseTime() const {
  if (timed_pose_queue_.empty()) {
    return common::Time::min();
  }
  return timed_pose_queue_.back().time;
}

common::Time ImuBasedPoseExtrapolator::GetLastExtrapolatedTime() const {
  return last_extrapolated_time_;
}

void ImuBasedPoseExtrapolator::AddPose(const common::Time time,
                                       const transform::Rigid3d& pose) {
  timed_pose_queue_.push_back(TimestampedTransform{time, pose});
  while (timed_pose_queue_.size() > 3 &&
         timed_pose_queue_[1].time <=
             time - common::FromSeconds(options_.pose_queue_duration())) {
    if (!previous_solution_.empty()) {
      CHECK_EQ(timed_pose_queue_.front().time, previous_solution_.front().time);
      previous_solution_.pop_front();
    }
    timed_pose_queue_.pop_front();
  }
  TrimImuData();
}

void ImuBasedPoseExtrapolator::AddImuData(const sensor::ImuData& imu_data) {
  CHECK(timed_pose_queue_.empty() ||
        imu_data.time >= timed_pose_queue_.back().time);
  imu_data_.push_back(imu_data);
  TrimImuData();
}

void ImuBasedPoseExtrapolator::AddOdometryData(
    const sensor::OdometryData& odometry_data) {
  CHECK(timed_pose_queue_.empty() ||
        odometry_data.time >= timed_pose_queue_.back().time);
  odometry_data_.push_back(odometry_data);
  TrimOdometryData();
}

ImuBasedPoseExtrapolator::ExtrapolationResult
ImuBasedPoseExtrapolator::ExtrapolatePosesWithGravity(
    const std::vector<common::Time>& times) {
  const auto& time = times.back();
  const auto& newest_timed_pose = timed_pose_queue_.back();
  CHECK_GE(time, newest_timed_pose.time);
  CHECK_GE(times.size(), 1);
  last_extrapolated_time_ = time;

  if (timed_pose_queue_.size() < 3 ||
      common::ToSeconds(time - newest_timed_pose.time) < 1e-6) {
    return ExtrapolationResult{
        std::vector<transform::Rigid3f>(
            times.size() - 1, timed_pose_queue_.back().transform.cast<float>()),
        timed_pose_queue_.back().transform, Eigen::Vector3d::Zero(),
        timed_pose_queue_.back().transform.rotation()};
  }

  ceres::Problem::Options problem_options;
  ceres::Problem problem(problem_options);

  // Track gravity alignment over time and use this as a frame here so that
  // we can estimate the gravity alignment of the current pose.
  optimization::CeresPose gravity_from_local(
      gravity_from_local_, nullptr,
      absl::make_unique<ceres::QuaternionParameterization>(), &problem);
  // Use deque so addresses stay constant during problem formulation.
  std::deque<optimization::CeresPose> nodes;
  std::vector<common::Time> node_times;

  for (size_t i = 0; i < timed_pose_queue_.size(); i++) {
    const bool is_last = (i == (timed_pose_queue_.size() - 1));
    const auto& timed_pose = timed_pose_queue_[i];
    node_times.push_back(timed_pose.time);

    transform::Rigid3d gravity_from_node;
    // Use the last scan match result (timed_pose_queue_.back()) for
    // initialization here instead of the last result from the optimization.
    // This keeps poses from slowly drifting apart due to lack of feedback
    // from the scan matching here.
    if (!previous_solution_.empty() && !is_last) {
      CHECK_GT(previous_solution_.size(), i);
      CHECK_EQ(timed_pose.time, previous_solution_[i].time);
      gravity_from_node = previous_solution_[i].transform;
    } else {
      gravity_from_node = gravity_from_local_ * timed_pose.transform;
    }

    if (is_last) {
      nodes.emplace_back(gravity_from_node, nullptr,
                         absl::make_unique<ceres::AutoDiffLocalParameterization<
                             ConstantYawQuaternionPlus, 4, 2>>(),
                         &problem);
      problem.SetParameterBlockConstant(nodes.back().translation());
    } else {
      nodes.emplace_back(gravity_from_node, nullptr,
                         absl::make_unique<ceres::QuaternionParameterization>(),
                         &problem);
    }
  }

  double gravity_constant = 9.8;
  bool fix_gravity = false;
  if (options_.gravity_constant() > 0) {
    fix_gravity = true;
    gravity_constant = options_.gravity_constant();
  }

  auto imu_it_prev_prev = imu_data_.begin();
  while (imu_it_prev_prev != std::prev(imu_data_.end()) &&
         std::next(imu_it_prev_prev)->time <= timed_pose_queue_.back().time) {
    ++imu_it_prev_prev;
  }

  const TimestampedTransform prev_gravity_from_tracking =
      TimestampedTransform{node_times.back(), nodes.back().ToRigid()};
  const TimestampedTransform prev_prev_gravity_from_tracking =
      TimestampedTransform{node_times.at(node_times.size() - 2),
                           nodes.at(nodes.size() - 2).ToRigid()};
  const transform::Rigid3d initial_estimate =
      ExtrapolatePoseWithImu<double>(
          prev_gravity_from_tracking.transform, prev_gravity_from_tracking.time,
          prev_prev_gravity_from_tracking.transform,
          prev_prev_gravity_from_tracking.time,
          gravity_constant * Eigen::Vector3d::UnitZ(), time, imu_data_,
          &imu_it_prev_prev)
          .pose;
  nodes.emplace_back(initial_estimate, nullptr,
                     absl::make_unique<ceres::QuaternionParameterization>(),
                     &problem);
  node_times.push_back(time);

  // Add cost functions for node constraints.
  for (size_t i = 0; i < timed_pose_queue_.size(); i++) {
    const auto& timed_pose = timed_pose_queue_[i];
    problem.AddResidualBlock(
        optimization::SpaCostFunction3D::CreateAutoDiffCostFunction(
            PoseGraphInterface::Constraint::Pose{
                timed_pose.transform, options_.pose_translation_weight(),
                options_.pose_rotation_weight()}),
        nullptr /* loss function */, gravity_from_local.rotation(),
        gravity_from_local.translation(), nodes.at(i).rotation(),
        nodes.at(i).translation());
  }

  CHECK(!imu_data_.empty());
  CHECK_LE(imu_data_.front().time, timed_pose_queue_.front().time);

  std::array<double, 4> imu_calibration{{1., 0., 0., 0.}};

  problem.AddParameterBlock(imu_calibration.data(), 4,
                            new ceres::QuaternionParameterization());
  problem.SetParameterBlockConstant(imu_calibration.data());

  auto imu_it = imu_data_.begin();
  CHECK(imu_data_.size() == 1 ||
        std::next(imu_it)->time > timed_pose_queue_.front().time);

  transform::Rigid3d last_node_odometry;
  common::Time last_node_odometry_time;

  for (size_t i = 1; i < nodes.size(); i++) {
    const common::Time first_time = node_times[i - 1];
    const common::Time second_time = node_times[i];

    auto imu_it2 = imu_it;
    const IntegrateImuResult<double> result =
        IntegrateImu(imu_data_, first_time, second_time, &imu_it);
    if ((i + 1) < nodes.size()) {
      const common::Time third_time = node_times[i + 1];
      const common::Duration first_duration = second_time - first_time;
      const common::Duration second_duration = third_time - second_time;
      const common::Time first_center = first_time + first_duration / 2;
      const common::Time second_center = second_time + second_duration / 2;
      const IntegrateImuResult<double> result_to_first_center =
          IntegrateImu(imu_data_, first_time, first_center, &imu_it2);
      const IntegrateImuResult<double> result_center_to_center =
          IntegrateImu(imu_data_, first_center, second_center, &imu_it2);
      // 'delta_velocity' is the change in velocity from the point in time
      // halfway between the first and second poses to halfway between
      // second and third pose. It is computed from IMU data and still
      // contains a delta due to gravity. The orientation of this vector is
      // in the IMU frame at the second pose.
      const Eigen::Vector3d delta_velocity =
          (result.delta_rotation.inverse() *
           result_to_first_center.delta_rotation) *
          result_center_to_center.delta_velocity;
      problem.AddResidualBlock(
          AccelerationCostFunction3D::CreateAutoDiffCostFunction(
              options_.imu_acceleration_weight(), delta_velocity,
              common::ToSeconds(first_duration),
              common::ToSeconds(second_duration)),
          nullptr /* loss function */, nodes.at(i).rotation(),
          nodes.at(i - 1).translation(), nodes.at(i).translation(),
          nodes.at(i + 1).translation(), &gravity_constant,
          imu_calibration.data());
      // TODO(danielsievers): Fix gravity in CostFunction.
      if (fix_gravity) {
        problem.SetParameterBlockConstant(&gravity_constant);
      } else {
        // Force gravity constant to be positive.
        problem.SetParameterLowerBound(&gravity_constant, 0, 0.0);
      }
    }
    problem.AddResidualBlock(
        RotationCostFunction3D::CreateAutoDiffCostFunction(
            options_.imu_rotation_weight(), result.delta_rotation),
        nullptr /* loss function */, nodes.at(i - 1).rotation(),
        nodes.at(i).rotation(), imu_calibration.data());

    // Add a relative pose constraint based on the odometry (if available).
    if (HasOdometryDataForTime(first_time) &&
        HasOdometryDataForTime(second_time)) {
      // Here keep track of last node odometry to avoid double computation.
      // Do this if first loop, or if there were some missing odometry nodes
      // then recalculate.
      if (i == 1 || last_node_odometry_time != first_time) {
        last_node_odometry = InterpolateOdometry(first_time);
        last_node_odometry_time = first_time;
      }
      const transform::Rigid3d current_node_odometry =
          InterpolateOdometry(second_time);
      const transform::Rigid3d relative_odometry =
          CalculateOdometryBetweenNodes(last_node_odometry,
                                        current_node_odometry);

      problem.AddResidualBlock(
          optimization::SpaCostFunction3D::CreateAutoDiffCostFunction(
              PoseGraphInterface::Constraint::Pose{
                  relative_odometry, options_.odometry_translation_weight(),
                  options_.odometry_rotation_weight()}),
          nullptr /* loss function */, nodes.at(i - 1).rotation(),
          nodes.at(i - 1).translation(), nodes.at(i).rotation(),
          nodes.at(i).translation());
      // Use the current node odometry in the next iteration
      last_node_odometry = current_node_odometry;
      last_node_odometry_time = second_time;
    }
  }

  // Solve.
  ceres::Solver::Summary summary;
  ceres::Solve(solver_options_, &problem, &summary);
  LOG_IF_EVERY_N(INFO, !fix_gravity, 20) << "Gravity was: " << gravity_constant;

  const auto gravity_estimate = nodes.back().ToRigid().rotation();

  const auto& last_pose = timed_pose_queue_.back();
  const auto extrapolated_pose = TimestampedTransform{
      time, last_pose.transform *
                nodes.at(nodes.size() - 2).ToRigid().inverse() *
                nodes.back().ToRigid()};

  num_iterations_hist_.Add(summary.iterations.size());

  gravity_from_local_ = gravity_from_local.ToRigid();

  previous_solution_.clear();
  for (size_t i = 0; i < nodes.size(); ++i) {
    previous_solution_.push_back(
        TimestampedTransform{node_times.at(i), nodes.at(i).ToRigid()});
  }

  const Eigen::Vector3d current_velocity =
      (extrapolated_pose.transform.translation() -
       last_pose.transform.translation()) /
      common::ToSeconds(time - last_pose.time);

  return ExtrapolationResult{
      InterpolatePoses(last_pose, extrapolated_pose, times.begin(),
                       std::prev(times.end())),
      extrapolated_pose.transform, current_velocity, gravity_estimate};
}

std::vector<transform::Rigid3f> ImuBasedPoseExtrapolator::InterpolatePoses(
    const TimestampedTransform& start, const TimestampedTransform& end,
    const std::vector<common::Time>::const_iterator times_begin,
    const std::vector<common::Time>::const_iterator times_end) {
  std::vector<transform::Rigid3f> poses;
  poses.reserve(std::distance(times_begin, times_end));
  const float duration_scale = 1. / common::ToSeconds(end.time - start.time);

  const Eigen::Quaternionf start_rotation =
      Eigen::Quaternionf(start.transform.rotation());
  const Eigen::Quaternionf end_rotation =
      Eigen::Quaternionf(end.transform.rotation());
  const Eigen::Vector3f start_translation =
      start.transform.translation().cast<float>();
  const Eigen::Vector3f end_translation =
      end.transform.translation().cast<float>();

  for (auto it = times_begin; it != times_end; ++it) {
    const float factor = common::ToSeconds(*it - start.time) * duration_scale;
    const Eigen::Vector3f origin =
        start_translation + (end_translation - start_translation) * factor;
    const Eigen::Quaternionf rotation =
        start_rotation.slerp(factor, end_rotation);
    poses.emplace_back(origin, rotation);
  }
  return poses;
}

transform::Rigid3d ImuBasedPoseExtrapolator::ExtrapolatePose(
    const common::Time time) {
  return ExtrapolatePosesWithGravity(std::vector<common::Time>{time})
      .current_pose;
}

Eigen::Quaterniond ImuBasedPoseExtrapolator::EstimateGravityOrientation(
    const common::Time time) {
  return ExtrapolatePosesWithGravity(std::vector<common::Time>{time})
      .gravity_from_tracking;
}

template <typename T>
void ImuBasedPoseExtrapolator::TrimDequeData(std::deque<T>* data) {
  while (data->size() > 1 && !timed_pose_queue_.empty() &&
         data->at(1).time <= timed_pose_queue_.front().time) {
    data->pop_front();
  }
}

void ImuBasedPoseExtrapolator::TrimImuData() {
  TrimDequeData<sensor::ImuData>(&imu_data_);
}

void ImuBasedPoseExtrapolator::TrimOdometryData() {
  TrimDequeData<sensor::OdometryData>(&odometry_data_);
}

// Odometry methods
bool ImuBasedPoseExtrapolator::HasOdometryData() const {
  return odometry_data_.size() >= 2;
}

bool ImuBasedPoseExtrapolator::HasOdometryDataForTime(
    const common::Time& time) const {
  return HasOdometryData() && odometry_data_.front().time < time &&
         time < odometry_data_.back().time;
}

transform::Rigid3d ImuBasedPoseExtrapolator::InterpolateOdometry(
    const common::Time& time) const {
  // Only interpolate if time is within odometry data range.
  CHECK(HasOdometryDataForTime(time))
      << "Odometry data range does not include time " << time;
  std::deque<sensor::OdometryData>::const_iterator data = std::upper_bound(
      odometry_data_.begin(), odometry_data_.end(), time,
      [](const common::Time& time, const sensor::OdometryData& odometry_data) {
        return time < odometry_data.time;
      });
  const TimestampedTransform first{std::prev(data)->time,
                                   std::prev(data)->pose};
  const TimestampedTransform second{data->time, data->pose};
  return Interpolate(first, second, time).transform;
}

transform::Rigid3d ImuBasedPoseExtrapolator::CalculateOdometryBetweenNodes(
    const transform::Rigid3d& first_node_odometry,
    const transform::Rigid3d& second_node_odometry) const {
  return first_node_odometry.inverse() * second_node_odometry;
}

}  // namespace mapping
}  // namespace cartographer
