/*
 * Copyright (C) 2012 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 <gtest/gtest.h>
#include "gazebo/physics/physics.hh"
#include "gazebo/physics/Joint.hh"
#include "gazebo/test/helper_physics_generator.hh"
#include "test/integration/joint_test.hh"

#define TOL 1e-6
#define TOL_CONT 2.0

using namespace gazebo;

//////////////////////////////////////////////////
void JointTest::JointCreationDestructionTest(const std::string &_physicsEngine)
{
  /// \TODO: Disable for now until functionality is implemented
  /// bullet collision parameters needs tweaking
  if (_physicsEngine == "bullet")
  {
    gzerr << "Aborting test for bullet, see issue #590.\n";
    return;
  }
  /// \TODO: simbody not complete for this test
  if (_physicsEngine == "simbody")
  {
    gzerr << "Aborting test for Simbody, see issue #862.\n";
    return;
  }
  /// \TODO: dart not complete for this test
  if (_physicsEngine == "dart")
  {
    gzerr << "Aborting test for DART, see issue #2469.\n";
    return;
  }

  // Load our inertial test world
  Load("worlds/joint_test.world", true, _physicsEngine);

  // Get a pointer to the world, make sure world loads
  physics::WorldPtr world = physics::get_world("default");
  ASSERT_TRUE(world != NULL);

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

  // create some fake links
  physics::ModelPtr model = world->ModelByName("model_1");
  ASSERT_TRUE(model != NULL);
  physics::LinkPtr link = model->GetLink("link_1");
  ASSERT_TRUE(link != NULL);

  physics::LinkPtr parentLink;
  physics::LinkPtr childLink(link);
  physics::JointPtr joint;
  ignition::math::Vector3d axis(1, 0, 0);
  std::string jointName = "world_" + childLink->GetName() + "_joint";
  double lower = -M_PI;

  double residentLast = 0, shareLast = 0;
  double residentCur = 0, shareCur = 0;

  // The memory footprint on osx can take around 190 cycles to stabilize.
  // So this test gives 250 cycles to stabilize and then verifies stability
  // for another 250.
  unsigned int cyclesMax = 500;
  unsigned int cyclesStabilize = cyclesMax / 2;
  for (unsigned int i = 0; i < cyclesMax; ++i)
  {
    // try creating a joint
    joint = model->CreateJoint(jointName, "revolute", parentLink, childLink);
    joint->Init();
    joint->SetLowerLimit(0, lower);
    joint->SetAxis(0, axis);

    // remove the joint
    model->RemoveJoint(jointName);

    world->Step(200);

    this->GetMemInfo(residentCur, shareCur);

    // give it 2 cycles to stabilize
    if (i > cyclesStabilize)
    {
      EXPECT_LE(residentCur, residentLast);
      EXPECT_LE(shareCur, shareLast);
    }
    // gzdbg << "memory res[" << residentCur
    //       << "] shr[" << shareCur
    //       << "] res[" << residentLast
    //       << "] shr[" << shareLast
    //       << "]\n";
    residentLast = residentCur;
    shareLast = shareCur;
  }
}

//////////////////////////////////////////////////
void JointTest::GetInertiaRatio(const std::string &_physicsEngine)
{
  // Load our inertia ratio world
  Load("worlds/inertia_ratio.world", true, _physicsEngine);

  // Get a pointer to the world, make sure world loads
  physics::WorldPtr world = physics::get_world("default");
  ASSERT_TRUE(world != NULL);

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

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

  {
    physics::JointPtr joint = model->GetJoint("lower_joint");
    ASSERT_TRUE(joint != NULL);

    EXPECT_NEAR(joint->GetInertiaRatio(0), 3125, 1e-2);
    EXPECT_NEAR(joint->InertiaRatio(ignition::math::Vector3d::UnitX), 3125,
        1e-2);
    EXPECT_NEAR(joint->InertiaRatio(ignition::math::Vector3d::UnitY), 87.50,
        1e-2);
  }
}

//////////////////////////////////////////////////
void JointTest::SpringDamperTest(const std::string &_physicsEngine)
{
  /// bullet collision parameters needs tweaking
  if (_physicsEngine == "bullet")
  {
    gzerr << "Aborting test for bullet, see issue #887.\n";
    return;
  }
  if (_physicsEngine == "dart")
  {
    gzerr << "Aborting test for " << _physicsEngine
          << ", see issue #975.\n";
    return;
  }

  // Load our inertial test world
  Load("worlds/spring_damper_test.world", true, _physicsEngine);

  // Get a pointer to the world, make sure world loads
  physics::WorldPtr world = physics::get_world("default");
  ASSERT_TRUE(world != NULL);

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

  // All models should oscillate with the same frequency
  physics::ModelPtr modelPrismatic = world->ModelByName("model_3_prismatic");
  physics::ModelPtr modelRevolute = world->ModelByName("model_3_revolute");
  physics::ModelPtr modelPlugin =
    world->ModelByName("model_4_prismatic_plugin");
  physics::ModelPtr modelContact = world->ModelByName("model_5_soft_contact");
  physics::ModelPtr modelPrismatic2 =
    world->ModelByName("model_6_prismatic_sdf");
  physics::ModelPtr modelRevolute2 = world->ModelByName("model_7_revolute_sdf");

  ASSERT_TRUE(modelPrismatic != NULL);
  ASSERT_TRUE(modelRevolute != NULL);
  ASSERT_TRUE(modelPlugin != NULL);
  ASSERT_TRUE(modelContact != NULL);
  ASSERT_TRUE(modelPrismatic2 != NULL);
  ASSERT_TRUE(modelRevolute2 != NULL);

  physics::LinkPtr linkPrismatic = modelPrismatic->GetLink("link_1");
  physics::LinkPtr linkRevolute = modelRevolute->GetLink("link_1");
  physics::LinkPtr linkPluginExplicit = modelPlugin->GetLink("link_1");
  physics::LinkPtr linkPluginImplicit = modelPlugin->GetLink("link_2");
  physics::LinkPtr linkContact = modelContact->GetLink("link_1");
  physics::LinkPtr linkPrismatic2 = modelPrismatic2->GetLink("link");
  physics::LinkPtr linkRevolute2 = modelRevolute2->GetLink("link");

  ASSERT_TRUE(linkPrismatic != NULL);
  ASSERT_TRUE(linkRevolute != NULL);
  ASSERT_TRUE(linkPluginExplicit != NULL);
  ASSERT_TRUE(linkPluginImplicit != NULL);
  ASSERT_TRUE(linkContact != NULL);
  ASSERT_TRUE(linkPrismatic2 != NULL);
  ASSERT_TRUE(linkRevolute2 != NULL);

  physics::JointPtr jointPluginImplicit = modelPlugin->GetJoint("joint_1");
  ASSERT_TRUE(jointPluginImplicit != NULL);

  int cyclesPrismatic = 0;
  int cyclesRevolute = 0;
  int cyclesPluginExplicit = 0;
  int cyclesPluginImplicit = 0;
  int cyclesContact = 0;
  int cyclesPrismatic2 = 0;
  int cyclesRevolute2 = 0;

  double velPrismatic = 1.0;
  double velRevolute = 1.0;
  double velPluginExplicit = 1.0;
  double velPluginImplicit = 1.0;
  double velContact = 1.0;
  double velPrismatic2 = 1.0;
  double velRevolute2 = 1.0;
  const double vT = 0.01;

  double energyPluginImplicit0 = linkPluginImplicit->GetWorldEnergy()
        + jointPluginImplicit->GetWorldEnergyPotentialSpring(0);

  // check number of oscillations for each of the setup.  They should all
  // be the same.
  // run 5000 steps, at which point, contact is the first one to damp out
  // and lose it's oscillatory behavior due to larger dissipation in
  // contact behavior.
  for (int i = 0; i < 5000; ++i)
  {
    world->Step(1);

    // count up and down cycles
    if (linkPrismatic->WorldLinearVel().Z() > vT && velPrismatic < -vT)
    {
      cyclesPrismatic++;
      velPrismatic = 1.0;
    }
    else if (linkPrismatic->WorldLinearVel().Z() < -vT && velPrismatic > vT)
    {
      cyclesPrismatic++;
      velPrismatic = -1.0;
    }
    if (-linkRevolute->RelativeAngularVel().Y() > vT && velRevolute < -vT)
    {
      cyclesRevolute++;
      velRevolute = 1.0;
    }
    else if (-linkRevolute->RelativeAngularVel().Y() < -vT && velRevolute > vT)
    {
      cyclesRevolute++;
      velRevolute = -1.0;
    }
    if (linkPluginExplicit->WorldLinearVel().Z() > vT &&
        velPluginExplicit < -vT)
    {
      cyclesPluginExplicit++;
      velPluginExplicit = 1.0;
    }
    else if (linkPluginExplicit->WorldLinearVel().Z() < -vT &&
             velPluginExplicit > vT)
    {
      cyclesPluginExplicit++;
      velPluginExplicit = -1.0;
    }
    if (linkPluginImplicit->WorldLinearVel().Z() > vT &&
             velPluginImplicit < -vT)
    {
      cyclesPluginImplicit++;
      velPluginImplicit = 1.0;
    }
    else if (linkPluginImplicit->WorldLinearVel().Z() < -vT &&
             velPluginImplicit > vT)
    {
      cyclesPluginImplicit++;
      velPluginImplicit = -1.0;
    }
    if (linkContact->WorldLinearVel().Z() > vT && velContact < -vT)
    {
      cyclesContact++;
      velContact = 1.0;
    }
    else if (linkContact->WorldLinearVel().Z() < -vT && velContact > vT)
    {
      cyclesContact++;
      velContact = -1.0;
    }
    if (linkPrismatic2->WorldLinearVel().Z() > vT && velPrismatic2 < -vT)
    {
      cyclesPrismatic2++;
      velPrismatic2 = 1.0;
    }
    else if (linkPrismatic2->WorldLinearVel().Z() < -vT && velPrismatic2 > vT)
    {
      cyclesPrismatic2++;
      velPrismatic2 = -1.0;
    }
    if (-linkRevolute2->RelativeAngularVel().Y() > vT && velRevolute2 < -vT)
    {
      cyclesRevolute2++;
      velRevolute2 = 1.0;
    }
    else if (-linkRevolute2->RelativeAngularVel().Y() < -vT &&
        velRevolute2 > vT)
    {
      cyclesRevolute2++;
      velRevolute2 = -1.0;
    }

    double energy = linkPluginImplicit->GetWorldEnergy() +
                   jointPluginImplicit->GetWorldEnergyPotentialSpring(0);
    if (_physicsEngine.compare("dart") == 0)
    {
      if (i == 0)
      {
        gzerr << _physicsEngine
              << " has reduced accuracy for spring energy conservation"
              << ", see #975"
              << std::endl;
      }
      EXPECT_NEAR(energy / energyPluginImplicit0, 1.0, 2e-2);
    }
    else
    {
      EXPECT_NEAR(energy / energyPluginImplicit0, 1.0, 1e-3);
    }
    // gzdbg << i << "\n";
    // gzdbg << cyclesPrismatic << " : "
    //       << linkPrismatic->GetWorldLinearVel() << "\n";
    // gzdbg << cyclesRevolute << " : "
    //       << linkRevolute->GetRelativeAngularVel() << "\n";
    // gzdbg << cyclesContact << " : "
    //       << linkContact->GetWorldLinearVel() << "\n";
  }
  if (_physicsEngine.compare("ode") == 0)
  {
    gzdbg << "Extra tests for ode" << std::endl;
    EXPECT_EQ(cyclesContact,        17);
  }
  if (_physicsEngine.compare("dart") == 0)
  {
    gzerr << _physicsEngine
          << " doesn't support joint limit stiffness"
          << ", see #975"
          << std::endl;
  }
  else
  {
    EXPECT_EQ(cyclesPrismatic,      17);
    EXPECT_EQ(cyclesRevolute,       17);
  }
  EXPECT_EQ(cyclesPluginExplicit, 17);
  EXPECT_EQ(cyclesPluginImplicit, 17);
  EXPECT_EQ(cyclesPrismatic2,     17);
  EXPECT_EQ(cyclesRevolute2,      17);
}

//////////////////////////////////////////////////
void JointTest::DynamicJointVisualization(const std::string &_physicsEngine)
{
  /// \TODO: simbody not complete for this test
  if (_physicsEngine == "simbody")
  {
    gzerr << "Aborting test for "
          << _physicsEngine
          << ", see issue #862."
          << std::endl;
    return;
  }
  // Load empty world
  Load("worlds/empty.world", true, _physicsEngine);

  // Get a pointer to the world, make sure world loads
  physics::WorldPtr world = physics::get_world("default");
  ASSERT_TRUE(world != NULL);

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

  // Spawn two boxes
  SpawnBox("box1", ignition::math::Vector3d(1, 1, 1),
      ignition::math::Vector3d(1, 0, 0.5),
      ignition::math::Vector3d::Zero, false);
  SpawnBox("box2", ignition::math::Vector3d(1, 1, 1),
      ignition::math::Vector3d(-1, 0, 0.5),
      ignition::math::Vector3d::Zero, false);

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

  // Get link pointers
  physics::LinkPtr parent = model->GetLink();
  physics::LinkPtr child  = model2->GetLink();

  // Create dynamic joint
  std::string name = "dynamic_joint_unique";
  physics::JointPtr joint;
  joint = model->CreateJoint(name, "revolute", parent, child);
  joint->Init();

  // Get model joints (used for visualization)
  physics::Joint_V joints = model->GetJoints();
  bool jointFound = false;
  for (auto const &joint : joints)
  {
    if (joint->GetName() == name)
    {
      jointFound = true;
      break;
    }
  }
  EXPECT_TRUE(jointFound);

  // Try to create the joint with the same name
  EXPECT_TRUE(model->CreateJoint(name, "revolute", parent, child) == NULL);

  // step to let joint creation finish before removing it
  world->Step(1000);

  // test remove joint
  EXPECT_TRUE(model->RemoveJoint(name));
  joint = model->GetJoint(name);
  EXPECT_TRUE(joint == NULL);

  // test that removing a non-existent joint will not break gazebo
  EXPECT_FALSE(model->RemoveJoint("this_joint_doees_not_exist"));
}

//////////////////////////////////////////////////
TEST_F(JointTest, joint_SDF14)
{
  Load("worlds/SDF_1_4.world");

  physics::WorldPtr world = physics::get_world("default");
  ASSERT_TRUE(world != NULL);

  physics::PhysicsEnginePtr physics = world->Physics();
  ASSERT_TRUE(physics != NULL);

  int i = 0;
  while (!this->HasEntity("joint14_model") && i < 20)
  {
    common::Time::MSleep(100);
    ++i;
  }

  if (i > 20)
    gzthrow("Unable to get joint14_model");

  physics::PhysicsEnginePtr physicsEngine = world->Physics();
  EXPECT_TRUE(physicsEngine != NULL);
  physics::ModelPtr model = world->ModelByName("joint14_model");
  EXPECT_TRUE(model != NULL);
  physics::LinkPtr link1 = model->GetLink("body1");
  EXPECT_TRUE(link1 != NULL);
  physics::LinkPtr link2 = model->GetLink("body2");
  EXPECT_TRUE(link2 != NULL);

  EXPECT_EQ(model->GetJointCount(), 1u);
  physics::JointPtr joint = model->GetJoint("joint14_revolute_joint");
  EXPECT_TRUE(joint != NULL);

  physics::LinkPtr parent = joint->GetParent();
  EXPECT_TRUE(parent != NULL);
  physics::LinkPtr child = joint->GetChild();
  EXPECT_TRUE(child != NULL);
  EXPECT_EQ(parent->GetName(), "body2");
  EXPECT_EQ(child->GetName(), "body1");
}

TEST_P(JointTest, JointCreationDestructionTest)
{
  JointCreationDestructionTest(this->physicsEngine);
}

TEST_P(JointTest, GetInertiaRatio)
{
  GetInertiaRatio(this->physicsEngine);
}

TEST_P(JointTest, SpringDamperTest)
{
  SpringDamperTest(this->physicsEngine);
}

TEST_P(JointTest, DynamicJointVisualization)
{
  DynamicJointVisualization(this->physicsEngine);
}

INSTANTIATE_TEST_CASE_P(PhysicsEngines, JointTest,
  ::testing::Combine(PHYSICS_ENGINE_VALUES,
  ::testing::Values("")),);  // NOLINT

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