/*
 * 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 "gazebo/common/Assert.hh"
#include "gazebo/common/Console.hh"
#include "gazebo/common/Exception.hh"
#include "gazebo/math/Vector3.hh"

#include "gazebo/transport/Publisher.hh"

#include "gazebo/physics/Collision.hh"
#include "gazebo/physics/ContactManager.hh"
#include "gazebo/physics/Entity.hh"
#include "gazebo/physics/MapShape.hh"
#include "gazebo/physics/Model.hh"
#include "gazebo/physics/PhysicsTypes.hh"
#include "gazebo/physics/PhysicsFactory.hh"
#include "gazebo/physics/SurfaceParams.hh"
#include "gazebo/physics/World.hh"

#include "gazebo/physics/dart/DARTScrewJoint.hh"
#include "gazebo/physics/dart/DARTHingeJoint.hh"
#include "gazebo/physics/dart/DARTHinge2Joint.hh"
#include "gazebo/physics/dart/DARTSliderJoint.hh"
#include "gazebo/physics/dart/DARTBallJoint.hh"
#include "gazebo/physics/dart/DARTUniversalJoint.hh"
#include "gazebo/physics/dart/DARTFixedJoint.hh"

#include "gazebo/physics/dart/DARTRayShape.hh"
#include "gazebo/physics/dart/DARTBoxShape.hh"
#include "gazebo/physics/dart/DARTSphereShape.hh"
#include "gazebo/physics/dart/DARTCylinderShape.hh"
#include "gazebo/physics/dart/DARTPlaneShape.hh"
#include "gazebo/physics/dart/DARTMeshShape.hh"
#include "gazebo/physics/dart/DARTPolylineShape.hh"
#include "gazebo/physics/dart/DARTMultiRayShape.hh"
#include "gazebo/physics/dart/DARTHeightmapShape.hh"

#include "gazebo/physics/dart/DARTModel.hh"
#include "gazebo/physics/dart/DARTLink.hh"

#include "gazebo/physics/dart/DARTPhysics.hh"

#include "gazebo/physics/dart/DARTPhysicsPrivate.hh"

using namespace gazebo;
using namespace physics;

GZ_REGISTER_PHYSICS_ENGINE("dart", DARTPhysics)

//////////////////////////////////////////////////
DARTPhysics::DARTPhysics(WorldPtr _world)
    : PhysicsEngine(_world), dataPtr(new DARTPhysicsPrivate())
{
}

//////////////////////////////////////////////////
DARTPhysics::~DARTPhysics()
{
  delete this->dataPtr;
  this->dataPtr = nullptr;
}

//////////////////////////////////////////////////
void DARTPhysics::Load(sdf::ElementPtr _sdf)
{
  PhysicsEngine::Load(_sdf);

  // Gravity
  auto g = this->world->Gravity();
  // ODEPhysics checks this, so we will too.
  if (g == ignition::math::Vector3d::Zero)
    gzwarn << "Gravity vector is (0, 0, 0). Objects will float.\n";
  this->dataPtr->dtWorld->setGravity(Eigen::Vector3d(g.X(), g.Y(), g.Z()));

  // Time step
  // double timeStep = this->sdf->GetValueDouble("time_step");
  // this->dartWorld->setTimeStep(timeStep);

  // TODO: Elements for dart settings
  // sdf::ElementPtr dartElem = this->sdf->GetElement("dart");
  // this->stepTimeDouble = dartElem->GetElement("dt")->GetValueDouble();
}

//////////////////////////////////////////////////
void DARTPhysics::Init()
{
  // this->dartWorld->initialize();
}

//////////////////////////////////////////////////
void DARTPhysics::Fini()
{
  PhysicsEngine::Fini();
}

//////////////////////////////////////////////////
void DARTPhysics::Reset()
{
  boost::recursive_mutex::scoped_lock lock(*this->physicsUpdateMutex);

  // Restore state all the models
  unsigned int modelCount = this->world->GetModelCount();
  DARTModelPtr dartModelIt;

  for (unsigned int i = 0; i < modelCount; ++i)
  {
    dartModelIt =
      boost::dynamic_pointer_cast<DARTModel>(this->world->GetModel(i));
    GZ_ASSERT(dartModelIt.get(), "dartModelIt pointer is NULL");

    dartModelIt->RestoreState();
  }
}

//////////////////////////////////////////////////
void DARTPhysics::InitForThread()
{
}

//////////////////////////////////////////////////
void DARTPhysics::UpdateCollision()
{
  this->contactManager->ResetCount();

  dart::constraint::ConstraintSolver *dtConstraintSolver =
      this->dataPtr->dtWorld->getConstraintSolver();
  dart::collision::CollisionDetector *dtCollisionDetector =
      dtConstraintSolver->getCollisionDetector();
  int numContacts = dtCollisionDetector->getNumContacts();

  for (int i = 0; i < numContacts; ++i)
  {
    const dart::collision::Contact &dtContact =
        dtCollisionDetector->getContact(i);
    dart::dynamics::BodyNode *dtBodyNode1 = dtContact.bodyNode1;
    dart::dynamics::BodyNode *dtBodyNode2 = dtContact.bodyNode2;

    DARTLinkPtr dartLink1 = this->FindDARTLink(dtBodyNode1);
    DARTLinkPtr dartLink2 = this->FindDARTLink(dtBodyNode2);

    GZ_ASSERT(dartLink1.get() != NULL, "dartLink1 in collision pare is NULL");
    GZ_ASSERT(dartLink2.get() != NULL, "dartLink2 in collision pare is NULL");

    unsigned int colIndex = 0;
    CollisionPtr collisionPtr1 = dartLink1->GetCollision(colIndex);
    CollisionPtr collisionPtr2 = dartLink2->GetCollision(colIndex);

    // Add a new contact to the manager. This will return NULL if no one is
    // listening for contact information.
    Contact *contactFeedback = this->GetContactManager()->NewContact(
                                 collisionPtr1.get(), collisionPtr2.get(),
                                 this->world->GetSimTime());

    if (!contactFeedback)
      continue;

    math::Pose body1Pose = dartLink1->GetWorldPose();
    math::Pose body2Pose = dartLink2->GetWorldPose();
    math::Vector3 localForce1;
    math::Vector3 localForce2;
    math::Vector3 localTorque1;
    math::Vector3 localTorque2;

    // calculate force in world frame
    Eigen::Vector3d force = dtContact.force;

    // calculate torque in world frame
    Eigen::Vector3d torqueA =
        (dtContact.point -
         dtBodyNode1->getTransform().translation()).cross(force);
    Eigen::Vector3d torqueB =
        (dtContact.point -
         dtBodyNode2->getTransform().translation()).cross(-force);

    // Convert from world to link frame
    localForce1 = body1Pose.rot.RotateVectorReverse(
        DARTTypes::ConvVec3(force));
    localForce2 = body2Pose.rot.RotateVectorReverse(
        DARTTypes::ConvVec3(-force));
    localTorque1 = body1Pose.rot.RotateVectorReverse(
        DARTTypes::ConvVec3(torqueA));
    localTorque2 = body2Pose.rot.RotateVectorReverse(
        DARTTypes::ConvVec3(torqueB));

    contactFeedback->positions[0] = DARTTypes::ConvVec3(dtContact.point);
    contactFeedback->normals[0] = DARTTypes::ConvVec3(dtContact.normal);
    contactFeedback->depths[0] = dtContact.penetrationDepth;

    if (!dartLink1->IsStatic())
    {
      contactFeedback->wrench[0].body1Force = localForce1;
      contactFeedback->wrench[0].body1Torque = localTorque1;
    }

    if (!dartLink2->IsStatic())
    {
      contactFeedback->wrench[0].body2Force = localForce2;
      contactFeedback->wrench[0].body2Torque = localTorque2;
    }

    ++contactFeedback->count;
  }
}

//////////////////////////////////////////////////
void DARTPhysics::UpdatePhysics()
{
  // need to lock, otherwise might conflict with world resetting
  boost::recursive_mutex::scoped_lock lock(*this->physicsUpdateMutex);

  // common::Time currTime =  this->world->GetRealTime();

  this->dataPtr->dtWorld->setTimeStep(this->maxStepSize);
  this->dataPtr->dtWorld->step();

  // Update all the transformation of DART's links to gazebo's links
  // TODO: How to visit all the links in the world?
  unsigned int modelCount = this->world->GetModelCount();
  ModelPtr modelItr;

  for (unsigned int i = 0; i < modelCount; ++i)
  {
    modelItr = this->world->GetModel(i);
    // TODO: need to improve speed
    Link_V links = modelItr->GetLinks();
    unsigned int linkCount = links.size();
    DARTLinkPtr dartLinkItr;

    for (unsigned int j = 0; j < linkCount; ++j)
    {
      dartLinkItr
          = boost::dynamic_pointer_cast<DARTLink>(links.at(j));
      dartLinkItr->updateDirtyPoseFromDARTTransformation();
    }
  }

  // this->lastUpdateTime = currTime;
}

//////////////////////////////////////////////////
std::string DARTPhysics::GetType() const
{
  return "dart";
}

//////////////////////////////////////////////////
void DARTPhysics::SetSeed(uint32_t /*_seed*/)
{
  gzwarn << "Not implemented yet in DART.\n";
}

//////////////////////////////////////////////////
ModelPtr DARTPhysics::CreateModel(BasePtr _parent)
{
  DARTModelPtr model(new DARTModel(_parent));

  return model;
}

//////////////////////////////////////////////////
LinkPtr DARTPhysics::CreateLink(ModelPtr _parent)
{
  if (_parent == NULL)
  {
    gzerr << "Link must have a parent in DART.\n";
    return LinkPtr();
  }

  DARTLinkPtr link(new DARTLink(_parent));
  link->SetWorld(_parent->GetWorld());

  return link;
}

//////////////////////////////////////////////////
CollisionPtr DARTPhysics::CreateCollision(const std::string &_type,
                                          LinkPtr _body)
{
  DARTCollisionPtr collision(new DARTCollision(_body));
  ShapePtr shape = this->CreateShape(_type, collision);
  collision->SetShape(shape);
  shape->SetWorld(_body->GetWorld());
  return collision;
}

//////////////////////////////////////////////////
ShapePtr DARTPhysics::CreateShape(const std::string &_type,
                                    CollisionPtr _collision)
{
  ShapePtr shape;
  DARTCollisionPtr collision =
    boost::dynamic_pointer_cast<DARTCollision>(_collision);

  if (_type == "sphere")
    shape.reset(new DARTSphereShape(collision));
  else if (_type == "plane")
    shape.reset(new DARTPlaneShape(collision));
  else if (_type == "box")
    shape.reset(new DARTBoxShape(collision));
  else if (_type == "cylinder")
    shape.reset(new DARTCylinderShape(collision));
  else if (_type == "multiray")
    shape.reset(new DARTMultiRayShape(collision));
  else if (_type == "mesh" || _type == "trimesh")
    shape.reset(new DARTMeshShape(collision));
  else if (_type == "polyline")
    shape.reset(new DARTPolylineShape(collision));
  else if (_type == "heightmap")
    shape.reset(new DARTHeightmapShape(collision));
  else if (_type == "map" || _type == "image")
    shape.reset(new MapShape(collision));
  else if (_type == "ray")
    if (_collision)
      shape.reset(new DARTRayShape(collision));
    else
      shape.reset(new DARTRayShape(this->world->GetPhysicsEngine()));
  else
    gzerr << "Unable to create collision of type[" << _type << "]\n";

  return shape;
}

//////////////////////////////////////////////////
JointPtr DARTPhysics::CreateJoint(const std::string &_type, ModelPtr _parent)
{
  JointPtr joint;

  if (_type == "prismatic")
    joint.reset(new DARTSliderJoint(_parent));
  else if (_type == "screw")
    joint.reset(new DARTScrewJoint(_parent));
  else if (_type == "revolute")
    joint.reset(new DARTHingeJoint(_parent));
  else if (_type == "revolute2")
    joint.reset(new DARTHinge2Joint(_parent));
  else if (_type == "ball")
    joint.reset(new DARTBallJoint(_parent));
  else if (_type == "universal")
    joint.reset(new DARTUniversalJoint(_parent));
  else if (_type == "fixed")
    joint.reset(new DARTFixedJoint(_parent));
  else
    gzerr << "Unable to create joint of type[" << _type << "]";

  return joint;
}

//////////////////////////////////////////////////
void DARTPhysics::SetGravity(const gazebo::math::Vector3 &_gravity)
{
  this->world->SetGravitySDF(_gravity.Ign());
  this->dataPtr->dtWorld->setGravity(
    Eigen::Vector3d(_gravity.x, _gravity.y, _gravity.z));
}

//////////////////////////////////////////////////
void DARTPhysics::DebugPrint() const
{
  gzwarn << "Not implemented in DART.\n";
}

//////////////////////////////////////////////////
boost::any DARTPhysics::GetParam(const std::string &_key) const
{
  boost::any value;
  this->GetParam(_key, value);
  return value;
}

//////////////////////////////////////////////////
bool DARTPhysics::GetParam(const std::string &_key, boost::any &_value) const
{
  if (!this->sdf->HasElement("dart"))
  {
    return PhysicsEngine::GetParam(_key, _value);
  }
  sdf::ElementPtr dartElem = this->sdf->GetElement("dart");
  // physics dart element not yet added to sdformat
  // GZ_ASSERT(dartElem != NULL, "DART SDF element does not exist");

  if (_key == "max_contacts")
  {
    _value = dartElem->GetElement("max_contacts")->Get<int>();
  }
  else if (_key == "min_step_size")
  {
    _value = dartElem->GetElement("solver")->Get<double>("min_step_size");
  }
  else
  {
    return PhysicsEngine::GetParam(_key, _value);
  }

  return true;
}

//////////////////////////////////////////////////
bool DARTPhysics::SetParam(const std::string &_key, const boost::any &_value)
{
  /// \TODO fill this out, see issue #1115
  try
  {
    if (_key == "max_contacts")
    {
      int value = boost::any_cast<int>(_value);
      gzerr << "Setting [" << _key << "] in DART to [" << value
            << "] not yet supported.\n";
    }
    else if (_key == "min_step_size")
    {
      double value = boost::any_cast<double>(_value);
      gzerr << "Setting [" << _key << "] in DART to [" << value
            << "] not yet supported.\n";
    }
    else
    {
      if (_key == "max_step_size")
      {
        this->dataPtr->dtWorld->setTimeStep(boost::any_cast<double>(_value));
      }
      return PhysicsEngine::SetParam(_key, _value);
    }
  }
  catch(boost::bad_any_cast &e)
  {
    gzerr << "DARTPhysics::SetParam(" << _key << ") boost::any_cast error: "
          << e.what() << std::endl;
    return false;
  }
  return true;
}

//////////////////////////////////////////////////
dart::simulation::World *DARTPhysics::GetDARTWorld()
{
  return this->dataPtr->dtWorld;
}

//////////////////////////////////////////////////
void DARTPhysics::OnRequest(ConstRequestPtr &_msg)
{
  msgs::Response response;
  response.set_id(_msg->id());
  response.set_request(_msg->request());
  response.set_response("success");
  std::string *serializedData = response.mutable_serialized_data();

  if (_msg->request() == "physics_info")
  {
    msgs::Physics physicsMsg;
    physicsMsg.set_type(msgs::Physics::DART);
    physicsMsg.mutable_gravity()->CopyFrom(
      msgs::Convert(this->world->Gravity()));
    physicsMsg.mutable_magnetic_field()->CopyFrom(
      msgs::Convert(this->MagneticField()));
    physicsMsg.set_enable_physics(this->world->GetEnablePhysicsEngine());
    physicsMsg.set_real_time_update_rate(this->realTimeUpdateRate);
    physicsMsg.set_real_time_factor(this->targetRealTimeFactor);
    physicsMsg.set_max_step_size(this->maxStepSize);

    response.set_type(physicsMsg.GetTypeName());
    physicsMsg.SerializeToString(serializedData);
    this->responsePub->Publish(response);
  }
}

//////////////////////////////////////////////////
void DARTPhysics::OnPhysicsMsg(ConstPhysicsPtr& _msg)
{
  // Parent class handles many generic parameters
  // This should be done first so that the profile settings
  // can be over-ridden by other message parameters.
  PhysicsEngine::OnPhysicsMsg(_msg);

  if (_msg->has_enable_physics())
    this->world->EnablePhysicsEngine(_msg->enable_physics());

  if (_msg->has_gravity())
    this->SetGravity(msgs::ConvertIgn(_msg->gravity()));

  if (_msg->has_real_time_factor())
    this->SetTargetRealTimeFactor(_msg->real_time_factor());

  if (_msg->has_real_time_update_rate())
  {
    this->SetRealTimeUpdateRate(_msg->real_time_update_rate());
  }

  if (_msg->has_max_step_size())
  {
    this->SetMaxStepSize(_msg->max_step_size());
  }

  /// Make sure all models get at least on update cycle.
  this->world->EnableAllModels();
}

//////////////////////////////////////////////////
DARTLinkPtr DARTPhysics::FindDARTLink(
    const dart::dynamics::BodyNode *_dtBodyNode)
{
  DARTLinkPtr res;

  const Model_V& models = this->world->GetModels();

  for (Model_V::const_iterator itModel = models.begin();
       itModel != models.end(); ++itModel)
  {
    const Link_V& links = (*itModel)->GetLinks();

    for (Link_V::const_iterator itLink = links.begin();
         itLink != links.end(); ++itLink)
    {
      DARTLinkPtr dartLink = boost::dynamic_pointer_cast<DARTLink>(*itLink);

      if (dartLink->GetDARTBodyNode() == _dtBodyNode)
      {
        res = dartLink;
        break;
      }
    }
  }

  return res;
}

