/*
 * 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 <limits>

#include "gazebo/test/ServerFixture.hh"
#include "gazebo/msgs/msgs.hh"

#include "gazebo/test/helper_physics_generator.hh"

using namespace gazebo;

// certain tests fail (with the symbody engine
// setting this to true skips those tests
bool SKIP_FAILING_TESTS = true;

// this is the test fixture
class SimEventsTest : public ServerFixture,
                      public testing::WithParamInterface<const char*>
{
  public: void SimPauseRun(const std::string &_physicsEngine);
  public: void SpawnAndDeleteModel(const std::string &_physicsEngine);
  public: void ModelInAndOutOfRegion(const std::string &_physicsEngine);
  public: void OccupiedEventSource(const std::string &_physicsEngine);
  public: void JointEventSource(const std::string &_physicsEngine);
};

// globals to exchange data between threads
boost::mutex g_mutex;
unsigned int g_event_count = 0;
std::string g_event_data;
std::string g_event_type;
std::string g_event_name;

// callback for SimEvent messages
// increment a counter and keep the data around
void ReceiveSimEvent(ConstSimEventPtr &_msg)
{
  boost::mutex::scoped_lock lock(g_mutex);
  g_event_count += 1;
  g_event_type = _msg->type();
  g_event_name = _msg->name();
  g_event_data = _msg->data();
  gzdbg << "ReceiveSimEvent " << g_event_type
        << " " << g_event_name
        << std::endl;
}

// get the count in a thread safe way
unsigned int GetEventCount()
{
  boost::mutex::scoped_lock lock(g_mutex);
  return g_event_count;
}

// get the last event type in thread safe way
std::string GetEventType()
{
  boost::mutex::scoped_lock lock(g_mutex);
  return g_event_type;
}

// get the data in a thread safe way
std::string GetEventData()
{
  boost::mutex::scoped_lock lock(g_mutex);
  return g_event_data;
}

// waits for one or multiple events. if the expected number is
// specified, then the function can return early
unsigned int WaitForNewEvent(unsigned int current,
                             unsigned int max_tries = 20,
                             unsigned int ms = 100)
{
  for (unsigned int i = 0; i < max_tries; i++)
  {
    unsigned int count = GetEventCount();
    if (count > current)
    {
      return count;
    }
    common::Time::MSleep(ms);
  }
  return GetEventCount();
}

////////////////////////////////////////////////////////////////////////
// SimPauseRun:
// Load test world, pause, run and verify that events are generated.
////////////////////////////////////////////////////////////////////////
void SimEventsTest::SimPauseRun(const std::string &_physicsEngine)
{
  Load("worlds/sim_events.world", false, _physicsEngine);
  physics::WorldPtr world = physics::get_world("default");

  // setup the callback that increments the counter everytime a
  // SimEvent is emitted.
  transport::NodePtr node = transport::NodePtr(new transport::Node());
  node->Init();
  transport::SubscriberPtr sceneSub = node->Subscribe("/gazebo/sim_events",
      &ReceiveSimEvent);

  // check that after pause, we have received a new event
  unsigned int count_before = GetEventCount();
  SetPause(true);
  unsigned int count_after = WaitForNewEvent(count_before);
  EXPECT_GT(count_after, count_before);
  // run the sim and check for event
  count_before = GetEventCount();
  SetPause(false);
  count_after = WaitForNewEvent(count_before);
  EXPECT_GT(count_after, count_before);
}

////////////////////////////////////////////////////////////////////////
// SpawnAndDeleteModel
// Load test world, add/delete models and verify that events are
// generated.
////////////////////////////////////////////////////////////////////////
void SimEventsTest::SpawnAndDeleteModel(const std::string &_physicsEngine)
{
  if (SKIP_FAILING_TESTS && _physicsEngine != "ode") return;

  Load("worlds/sim_events.world", false, _physicsEngine);
  // setup the callback that increments the counter everytime a
  // SimEvent is emitted.
  transport::NodePtr node = transport::NodePtr(new transport::Node());
  node->Init();
  transport::SubscriberPtr sceneSub = node->Subscribe("/gazebo/sim_events",
      &ReceiveSimEvent);

  unsigned int countBefore, countAfter;
  std::string name = "beer";

  countBefore = GetEventCount();
  std::string modelUri = "model://beer";
  SpawnModel(modelUri);
  countAfter = WaitForNewEvent(countBefore, 10, 100);
  EXPECT_GT(countAfter, countBefore);

  countBefore = GetEventCount();
  RemoveModel(name);
  countAfter = WaitForNewEvent(countBefore);
  EXPECT_GT(countAfter, countBefore);
  EXPECT_EQ(GetEventType(), "existence");
}


////////////////////////////////////////////////////////////////////////
// ModelInAndOutOfRegion:
// Load test world, move models and verify that events are generated.
////////////////////////////////////////////////////////////////////////
void SimEventsTest::ModelInAndOutOfRegion(const std::string &_physicsEngine)
{
  // simbody stepTo() failure
  if (SKIP_FAILING_TESTS && _physicsEngine != "ode") return;

  Load("worlds/sim_events.world", false, _physicsEngine);
  physics::WorldPtr world = physics::get_world("default");
  // setup the callback that increments the counter everytime a
  // SimEvent is emitted.
  transport::NodePtr node = transport::NodePtr(new transport::Node());
  node->Init();
  transport::SubscriberPtr sceneSub = node->Subscribe("/gazebo/sim_events",
      &ReceiveSimEvent);

  physics::ModelPtr can1 = world->ModelByName("can1");
  EXPECT_TRUE(can1 != NULL);

  unsigned int countBefore1 = GetEventCount();
  can1->SetWorldPose(ignition::math::Pose3d(0, 5, 0, 0, 0, 0));
  unsigned int countAfter1 = WaitForNewEvent(countBefore1, 10, 100);
  EXPECT_GT(countAfter1, countBefore1);

  // move can1 into the end region
  unsigned int countBefore2 = GetEventCount();
  can1->SetWorldPose(ignition::math::Pose3d(10, 10, 0, 0, 0, 0));
  unsigned int countAfter2 = WaitForNewEvent(countBefore2, 10, 100);
  EXPECT_GT(countAfter2, countBefore2);
}

////////////////////////////////////////////////////////////////////////
// OccupiedEventSource:
// Load test world, move model and verify that events are generated by
// checking the position of the elevator.
////////////////////////////////////////////////////////////////////////
void SimEventsTest::OccupiedEventSource(const std::string &_physicsEngine)
{
  // simbody stepTo() failure
  if (SKIP_FAILING_TESTS && _physicsEngine != "ode") return;

  this->Load("worlds/elevator.world", false, _physicsEngine);
  physics::WorldPtr world = physics::get_world("default");

  // Get the elevator model
  physics::ModelPtr elevatorModel = world->ModelByName("elevator");

  gzdbg << "Elevator Pose1["
        << elevatorModel->WorldPose().Pos() << "]\n";

  // Make sure the elevator is on the ground level
  EXPECT_LT(elevatorModel->WorldPose().Pos().Z(), 0.08);
  EXPECT_GT(elevatorModel->WorldPose().Pos().Z(), 0.07);

  // Spawn a box on the second floor, which should call the elevator up.
  this->SpawnBox("_my_test_box_", ignition::math::Vector3d(0.5, 0.5, 0.5),
      ignition::math::Vector3d(2, 0, 3.65), ignition::math::Vector3d::Zero);

  // Wait for elevator to move. 10 seconds is more than long enough.
  common::Time::Sleep(10);

  gzdbg << "Elevator Pose2[" << elevatorModel->WorldPose().Pos() << "]\n";

  // Make sure the elevator has moved up to the second floor.
  EXPECT_LT(elevatorModel->WorldPose().Pos().Z(), 3.08);
  EXPECT_GT(elevatorModel->WorldPose().Pos().Z(), 3.05);
}

////////////////////////////////////////////////////////////////////////
// JointEventSource:
// Load test world, rotate joint and verify that events are generated
////////////////////////////////////////////////////////////////////////
void SimEventsTest::JointEventSource(const std::string &_physicsEngine)
{
  if (SKIP_FAILING_TESTS && _physicsEngine != "ode")
    return;

  this->Load("worlds/sim_events.world", false, _physicsEngine);
  physics::WorldPtr world = physics::get_world("default");

  // Get the revoluter model
  physics::ModelPtr model = world->ModelByName("revoluter");
  physics::JointPtr joint = model->GetJoint("joint");

  // setup the callback that increments the counter everytime a
  // SimEvent is emitted.
  transport::NodePtr node = transport::NodePtr(new transport::Node());
  node->Init();
  transport::SubscriberPtr sceneSub = node->Subscribe("/gazebo/sim_events",
      &ReceiveSimEvent);

  // check that after position, we have received a new event
  unsigned int count_before = GetEventCount();

  // rotate joint
  joint->SetPosition(0, 3.1);

  // check for event
  unsigned int count_after = WaitForNewEvent(count_before);
  EXPECT_GT(count_after, count_before);

  count_before = GetEventCount();

  joint->SetVelocity(0, 3.1);
  // check for event
  count_after = WaitForNewEvent(count_before);
  EXPECT_GT(count_after, count_before);
}


// Run all test cases
INSTANTIATE_TEST_CASE_P(PhysicsEngines, SimEventsTest, PHYSICS_ENGINE_VALUES,);  // NOLINT

TEST_P(SimEventsTest, JointEventSource)
{
  JointEventSource(GetParam());
}

TEST_P(SimEventsTest, SimPauseRun)
{
  SimPauseRun(GetParam());
}

TEST_P(SimEventsTest, SpawnAndDeleteModel)
{
  SpawnAndDeleteModel(GetParam());
}

TEST_P(SimEventsTest, ModelInAnOutOfRegion)
{
  ModelInAndOutOfRegion(GetParam());
}

TEST_P(SimEventsTest, OccupiedEventSource)
{
  OccupiedEventSource(GetParam());
}

// main, where we can specify to skip certain tests
int main(int argc, char **argv)
{
  if (argc > 1)
  {
    std::string skipStr = argv[1];
    if (skipStr == "no_skip")
    {
      SKIP_FAILING_TESTS = false;
    }
  }
  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}
