/*
 * Copyright (C) 2015 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 <functional>

#include <ignition/common/Profiler.hh>

#include <gazebo/common/Events.hh>
#include <gazebo/common/Assert.hh>
#include <gazebo/common/Console.hh>

#include <gazebo/physics/World.hh>
#include <gazebo/physics/Model.hh>

#include "plugins/TransporterPluginPrivate.hh"
#include "plugins/TransporterPlugin.hh"

using namespace gazebo;

// Register the plugin
GZ_REGISTER_WORLD_PLUGIN(TransporterPlugin)

/////////////////////////////////////////////////
TransporterPlugin::TransporterPlugin()
  : dataPtr(new TransporterPluginPrivate)
{
}

/////////////////////////////////////////////////
TransporterPlugin::~TransporterPlugin()
{
}

/////////////////////////////////////////////////
void TransporterPlugin::Load(physics::WorldPtr _world, sdf::ElementPtr _sdf)
{
  bool hasManualActivation = false;

  GZ_ASSERT(_world, "TransporterPlugin world pointer is NULL");
  GZ_ASSERT(_sdf, "TransporterPlugin sdf pointer is NULL");

  this->dataPtr->world = _world;

  // Read each pad element
  sdf::ElementPtr padElem = _sdf->GetElement("pad");
  while (padElem)
  {
    // Create a new pad
    auto pad = std::make_shared<TransporterPluginPrivate::Pad>();

    // Set the pad's name and destination
    pad->name = padElem->Get<std::string>("name");
    pad->dest = padElem->Get<std::string>("destination");

    // Check that a pad does not exist
    if (this->dataPtr->pads.find(pad->name) != this->dataPtr->pads.end())
    {
      gzerr << "Transporter pad with name[" << pad->name << "] already exists."
        << "The duplicate pad will not be loaded\n";
      continue;
    }

    // Set the pad's activation type. The default is auto activation.
    if (padElem->HasElement("activation"))
    {
      pad->autoActivation = padElem->Get<std::string>("activation") == "auto";

      // Store that the user has at least one pad with manual activation.
      // This info is used to trigger a warning message below.
      if (!pad->autoActivation)
        hasManualActivation = true;
    }
    else
    {
      pad->autoActivation = true;
    }

    // Read the outgoing information
    sdf::ElementPtr outElem = padElem->GetElement("outgoing");
    pad->outgoingBox.Min() = outElem->Get<ignition::math::Vector3d>("min");
    pad->outgoingBox.Max() = outElem->Get<ignition::math::Vector3d>("max");

    // Read the incoming information
    sdf::ElementPtr inElem = padElem->GetElement("incoming");
    pad->incomingPose = inElem->Get<ignition::math::Pose3d>("pose");

    // Store the pad
    this->dataPtr->pads[pad->name] = pad;

    // Get the next pad element
    padElem = padElem->GetNextElement("pad");
  }

  // Connect to the update event
  this->dataPtr->updateConnection = event::Events::ConnectWorldUpdateBegin(
      std::bind(&TransporterPlugin::Update, this));

  // Listen on the activation topic, if present. This topic is used for
  // manual activation.
  if (_sdf->HasElement("activation_topic"))
  {
    // Create and initialize the node.
    this->dataPtr->node = transport::NodePtr(new transport::Node());
    this->dataPtr->node->Init(_world->Name());

    // Subscribe to the activation topic.
    this->dataPtr->activationSub = this->dataPtr->node->Subscribe(
        _sdf->Get<std::string>("activation_topic"),
        &TransporterPlugin::OnActivation, this);
  }
  else if (hasManualActivation)
  {
    gzerr << "Manual activation of a transporter pad has been requested, "
      << "but no <activation_topic> is present in the plugin's SDF.\n";
  }
}

/////////////////////////////////////////////////
void TransporterPlugin::OnActivation(ConstGzStringPtr &_msg)
{
  // Find the pad
  auto const &iter = this->dataPtr->pads.find(_msg->data());
  if (iter != this->dataPtr->pads.end())
  {
    // Activate the pad.
    std::lock_guard<std::mutex> lock(this->dataPtr->padMutex);
    iter->second->activated = true;
  }
  else
  {
    gzwarn << "Unknown transporter pad[" << _msg->data() << "]\n";
  }
}

/////////////////////////////////////////////////
void TransporterPlugin::Update()
{
  IGN_PROFILE("TransporterPlugin::Update");
  IGN_PROFILE_BEGIN("Update");
  // Get all the models
  physics::Model_V models = this->dataPtr->world->Models();

  std::lock_guard<std::mutex> lock(this->dataPtr->padMutex);

  // Process each model.
  for (auto const &model : models)
  {
    // Skip models that are static
    if (model->IsStatic())
      continue;

    // Get the model's pose
    ignition::math::Pose3d modelPose = model->WorldPose();

    // Iterate over all pads
    for (auto const &padIter : this->dataPtr->pads)
    {
      // Check if the model is in the pad's outgoing box.
      if (padIter.second->outgoingBox.Contains(modelPose.Pos()))
      {
        // Get the destination pad
        auto const &destIter = this->dataPtr->pads.find(padIter.second->dest);

        // Make sure we can transport the model
        if (destIter != this->dataPtr->pads.end() &&
            (padIter.second->autoActivation || padIter.second->activated))
        {
          // Move the model
          model->SetWorldPose(destIter->second->incomingPose);

          // Deactivate the pad. This is used by manually activated pads.
          padIter.second->activated = false;
        }
      }
    }
  }
  IGN_PROFILE_END();
}
