/*
 * Copyright (C) 2014 Open Source Robotics Foundation
 *
 * 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 <string.h>

#include "gazebo/physics/physics.hh"
#include "gazebo/test/ServerFixture.hh"
#include "gazebo/test/helper_physics_generator.hh"

using namespace gazebo;

typedef std::tuple<const char *, const char *, int> string2_int;

class WorldResetTest : public ServerFixture,
                       public ::testing::WithParamInterface<string2_int>
{
  /// \brief Test to see if model pose is reset when the world is reset
  /// \param[in] _physicsEngine Physics engine type.
  /// \param[in] _world Name of world to load
  /// \param[in] _resets Number of resets to perform in the test
  public: void ModelPose(const std::string &_physicsEngine,
                         const std::string &_world, const int _resets);

  /// \brief Test to see if nested model pose is reset when the world is reset
  /// \param[in] _physicsEngine Physics engine type.
  /// \param[in] _world Name of world to load
  /// \param[in] _resets Number of resets to perform in the test
  public: void NestedModelPose(const std::string &_physicsEngine,
                         const std::string &_world, const int _resets);

  /// \brief Test resetting different worlds
  /// \param[in] _physicsEngine Physics engine type.
  /// \param[in] _world Name of world to load
  /// \param[in] _resets Number of resets to perform in the test
  public: void WorldName(const std::string &_physicsEngine,
                         const std::string &_world, const int _resets);
};

class WorldControlResetTest : public ServerFixture,
                       public ::testing::WithParamInterface<const char *>
{
  /// \brief Test to see if model pose is reset when the world is reset through
  /// world_control topic
  /// \param[in] _physicsEngine Physics engine type.
  public: void ModelOverlapReset(const std::string &_physicsEngine);
};

/////////////////////////////////////////////////
void WorldControlResetTest::ModelOverlapReset(const std::string &_physicsEngine)
{
  if (_physicsEngine == "simbody")
  {
    gzerr << "Simbody fails this test due to issue #1957" << std::endl;
    return;
  }

  Load("worlds/empty.world", true, _physicsEngine);
  physics::WorldPtr world = physics::get_world("default");
  ASSERT_TRUE(world != NULL);

  physics::PhysicsEnginePtr physics = world->Physics();
  ASSERT_TRUE(physics != NULL);
  EXPECT_EQ(physics->GetType(), _physicsEngine);

  double dt = physics->GetMaxStepSize();
  unsigned int steps = 250;

  // Step forward, verify time increasing
  world->Step(steps);
  double simTime = world->SimTime().Double();
  EXPECT_NEAR(simTime, dt*steps, dt);

  // spawn a box with known initial pose
  ignition::math::Pose3d initialPose(1, 2, 0.0, 0, 0, 1.57);
  ignition::math::Vector3d size(1, 1, 1);
  SpawnBox("box", size, initialPose.Pos(), initialPose.Rot().Euler(), false);
  physics::ModelPtr model = world->ModelByName("box");
  ASSERT_TRUE(model != NULL);

  // spawn another box that overlaps with the previous box
  ignition::math::Pose3d initialPose2(1.2, 2, 0.5, 0, 0, 0);
  SpawnBox("box2", size, initialPose2.Pos(), initialPose2.Rot().Euler(), false);
  physics::ModelPtr model2 = world->ModelByName("box2");
  ASSERT_TRUE(model2 != NULL);

  // physics engine has not stepped yet since the boxes were spawned so they
  // should stay overlapped. Verify pose
  EXPECT_EQ(model->WorldPose(), initialPose);
  EXPECT_EQ(model2->WorldPose(), initialPose2);

  // Step forward, verify time increasing
  world->Step(steps);
  double simTime2 = world->SimTime().Double();
  EXPECT_NEAR(simTime2, simTime + dt*steps, dt);

  // physics engine has stepped, verify the models are pushed apart
  // Note: DART doesn't really mind overlapping models
  if (_physicsEngine != "dart")
  {
    EXPECT_NE(model->WorldPose(), initialPose);
    EXPECT_NE(model2->WorldPose(), initialPose2);
  }

  // Create a publisher to reset gzserver
  gazebo::transport::PublisherPtr pub =
    node->Advertise<gazebo::msgs::WorldControl>(
        "/gazebo/default/world_control");
  pub->WaitForConnection();

  // Reset world, verify time == 0
  gazebo::msgs::WorldControl msg;
  msg.mutable_reset()->set_all(true);
  pub->Publish(msg);

  int sleep = 0;
  int maxSleep = 20;
  // Wait for sim time to be reset
  while (sleep < maxSleep)
  {
    simTime = world->SimTime().Double();
    if (ignition::math::equal(simTime, 0.0, dt))
      break;
    sleep++;
    gazebo::common::Time::MSleep(100);
  }

  simTime = world->SimTime().Double();
  EXPECT_NEAR(simTime, 0.0, dt);

  // verify boxes have moved back to initial pose and they are not pushed
  // apart
  EXPECT_EQ(model->WorldPose(), initialPose);
  EXPECT_EQ(model2->WorldPose(), initialPose2);
}

/////////////////////////////////////////////////
TEST_P(WorldControlResetTest, ModelOverlapReset)
{
  ModelOverlapReset(GetParam());
}

/////////////////////////////////////////////////
void WorldResetTest::ModelPose(const std::string &_physicsEngine,
                               const std::string &_world, const int _resets)
{
  if (_physicsEngine == "simbody" &&
      _world.find("pr2") != std::string::npos)
  {
    gzerr << "Simbody fails this test with the PR2 due to issue #1672"
          << std::endl;
    return;
  }
  if (_physicsEngine == "simbody" &&
      _world.find("nested_model") != std::string::npos)
  {
    gzerr << "Simbody fails this test with nested models due to issue #1718"
          << std::endl;
    return;
  }
  if (_physicsEngine == "dart" &&
      _world.find("pr2") != std::string::npos)
  {
    gzerr << "Abort test since dart does not support ray sensor in PR2, "
          << "Please see issue #911.\n";
    return;
  }

  Load(_world, true, _physicsEngine);
  physics::WorldPtr world = physics::get_world("default");
  ASSERT_TRUE(world != NULL);

  physics::PhysicsEnginePtr physics = world->Physics();
  ASSERT_TRUE(physics != NULL);
  EXPECT_EQ(physics->GetType(), _physicsEngine);

  double dt = physics->GetMaxStepSize();
  unsigned int steps = 250;

  // Step forward, verify time increasing
  world->Step(steps);
  double simTime = world->SimTime().Double();
  EXPECT_NEAR(simTime, dt*steps, dt);

  ignition::math::Pose3d initialPose(1, 2, 0.5, 0, 0, 1.57);

  // spawn a box with known initial pose
  ignition::math::Vector3d size(1, 1, 1);
  SpawnBox("box", size, initialPose.Pos(), initialPose.Rot().Euler(), false);
  physics::ModelPtr model = world->ModelByName("box");
  ASSERT_TRUE(model != NULL);

  // verify box pose
  EXPECT_EQ(model->WorldPose(), initialPose);

  // move box to new pose
  ignition::math::Pose3d newPose(4, 5, 0.5, 0, 0, 0);
  model->SetWorldPose(newPose);
  EXPECT_EQ(model->WorldPose(), newPose);

  // Reset world repeatedly
  for (int i = 0; i < _resets; ++i)
  {
    // Reset world, verify time == 0
    world->Reset();
    simTime = world->SimTime().Double();
    EXPECT_NEAR(simTime, 0.0, dt);

    // Step forward, verify time increasing
    world->Step(steps);
    simTime = world->SimTime().Double();
    EXPECT_NEAR(simTime, dt*steps, dt);
  }

  // verify box has moved back to initial pose
  EXPECT_EQ(model->WorldPose(), initialPose);
}

/////////////////////////////////////////////////
TEST_P(WorldResetTest, ModelPose)
{
  std::string physics = std::get<0>(GetParam());

  std::string worldName = std::get<1>(GetParam());
  int resets = std::get<2>(GetParam());
  gzdbg << "Physics engine [" << physics << "] "
        << "world name [" << worldName << "] "
        << "reset count [" << resets << "]"
        << std::endl;
  ModelPose(physics, worldName, resets);
}

/////////////////////////////////////////////////
void WorldResetTest::NestedModelPose(const std::string &_physicsEngine,
                                     const std::string &_world,
                                     const int _resets)
{
  if (_physicsEngine == "simbody")
  {
    gzerr  << "Nested models are not working in simbody yet, issue #1718"
        << std::endl;
    return;
  }

  if (_physicsEngine == "dart")
  {
    gzerr  << "Nested models are not working in dart yet, issue #1833"
        << std::endl;
    return;
  }

  if (_world != "worlds/nested_model.world")
    return;

  Load(_world, true, _physicsEngine);
  physics::WorldPtr world = physics::get_world("default");
  ASSERT_TRUE(world != NULL);

  physics::PhysicsEnginePtr physics = world->Physics();
  ASSERT_TRUE(physics != NULL);
  EXPECT_EQ(physics->GetType(), _physicsEngine);

  double dt = physics->GetMaxStepSize();
  unsigned int steps = 250;

  // Step forward, verify time increasing
  world->Step(steps);
  double simTime = world->SimTime().Double();
  EXPECT_NEAR(simTime, dt*steps, dt);

  physics::ModelPtr model = world->ModelByName("model_00");
  ASSERT_TRUE(model != NULL);

  // store all initial pose
  std::list<ignition::math::Pose3d> modelPoses;
  std::list<physics::ModelPtr> models;
  models.push_back(model);
  while (!models.empty())
  {
    physics::ModelPtr m = models.front();
    modelPoses.push_back(m->WorldPose());
    models.pop_front();
    for (const auto &nested : m->NestedModels())
      models.push_back(nested);
  }

  // move model to new pose
  ignition::math::Pose3d newPose(9, 5, 2.5, 0, 0, 0);
  model->SetWorldPose(newPose);
  EXPECT_EQ(model->WorldPose(), newPose);

  // Reset world repeatedly
  for (int i = 0; i < _resets; ++i)
  {
    // Reset world, verify time == 0
    world->Reset();
    simTime = world->SimTime().Double();
    EXPECT_NEAR(simTime, 0.0, dt);

    // Step forward, verify time increasing
    world->Step(steps);
    simTime = world->SimTime().Double();
    EXPECT_NEAR(simTime, dt*steps, dt);

    // verify all nested models have moved back to initial pose
    models.clear();
    models.push_back(model);
    auto modelPosesCopy = modelPoses;
    while (!models.empty())
    {
      physics::ModelPtr m = models.front();
      EXPECT_EQ(m->WorldPose(), modelPosesCopy.front());
      models.pop_front();
      modelPosesCopy.pop_front();
      for (const auto &nested : m->NestedModels())
        models.push_back(nested);
    }
  }
}

/////////////////////////////////////////////////
TEST_P(WorldResetTest, NestedModelPose)
{
  std::string physics = std::get<0>(GetParam());

  std::string worldName = std::get<1>(GetParam());
  int resets = std::get<2>(GetParam());
  gzdbg << "Physics engine [" << physics << "] "
        << "world name [" << worldName << "] "
        << "reset count [" << resets << "]"
        << std::endl;
  NestedModelPose(physics, worldName, resets);
}

/////////////////////////////////////////////////
void WorldResetTest::WorldName(const std::string &_physicsEngine,
                               const std::string &_world, const int _resets)
{
  if (_physicsEngine == "simbody" &&
      _world.find("pr2") != std::string::npos)
  {
    gzerr << "Simbody fails this test with the PR2 due to issue #1672"
          << std::endl;
    return;
  }
  if (_physicsEngine == "simbody" &&
      _world.find("nested_model") != std::string::npos)
  {
    gzerr << "Simbody fails this test with nested models due to issue #1718"
          << std::endl;
    return;
  }
  if (_physicsEngine == "dart" &&
      _world.find("pr2") != std::string::npos)
  {
    gzerr << "Abort test since dart does not support ray sensor in PR2, "
          << "Please see issue #911.\n";
    return;
  }

  Load(_world, true, _physicsEngine);
  physics::WorldPtr world = physics::get_world("default");
  ASSERT_TRUE(world != NULL);

  physics::PhysicsEnginePtr physics = world->Physics();
  ASSERT_TRUE(physics != NULL);
  EXPECT_EQ(physics->GetType(), _physicsEngine);

  double dt = physics->GetMaxStepSize();
  unsigned int steps = 250;

  // Step forward, verify time increasing
  world->Step(steps);
  double simTime = world->SimTime().Double();
  EXPECT_NEAR(simTime, dt*steps, dt);

  // Reset world repeatedly
  for (int i = 0; i < _resets; ++i)
  {
    // Reset world, verify time == 0
    world->Reset();
    simTime = world->SimTime().Double();
    EXPECT_NEAR(simTime, 0.0, dt);

    // Step forward, verify time increasing
    world->Step(steps);
    simTime = world->SimTime().Double();
    EXPECT_NEAR(simTime, dt*steps, dt);
  }
}

/////////////////////////////////////////////////
TEST_P(WorldResetTest, WorldName)
{
  std::string physics = std::get<0>(GetParam());
  std::string worldName = std::get<1>(GetParam());
  int resets = std::get<2>(GetParam());
  gzdbg << "Physics engine [" << physics << "] "
        << "world name [" << worldName << "] "
        << "reset count [" << resets << "]"
        << std::endl;
  WorldName(physics, worldName, resets);
}

INSTANTIATE_TEST_CASE_P(PhysicsEngines, WorldControlResetTest,
    PHYSICS_ENGINE_VALUES,);  // NOLINT

INSTANTIATE_TEST_CASE_P(PhysicsEngines, WorldResetTest,
  ::testing::Combine(PHYSICS_ENGINE_VALUES,
  ::testing::Values("worlds/empty.world",
                    "worlds/pr2.world",
                    "worlds/nested_model.world"),
  ::testing::Range(1, 3)),);  // NOLINT

/////////////////////////////////////////////////
int main(int argc, char **argv)
{
  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}
