/*
 * Copyright (c) 2008, Willow Garage, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Willow Garage, Inc. nor the names of its
 *       contributors may be used to endorse or promote products derived from
 *       this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <OgreEntity.h>
#include <OgreSceneNode.h>

#include <rviz/display_context.h>
#include <rviz/frame_manager.h>
#include <rviz/ogre_helpers/arrow.h>
#include <rviz/ogre_helpers/axes.h>
#include <rviz/ogre_helpers/shape.h>
#include <rviz/properties/color_property.h>
#include <rviz/properties/enum_property.h>
#include <rviz/properties/float_property.h>
#include <rviz/properties/quaternion_property.h>
#include <rviz/properties/string_property.h>
#include <rviz/properties/vector_property.h>
#include <rviz/selection/selection_manager.h>
#include <rviz/validate_floats.h>
#include <rviz/validate_quaternions.h>

#include <rviz/default_plugin/pose_display.h>

namespace rviz
{
class PoseDisplaySelectionHandler : public SelectionHandler
{
public:
  PoseDisplaySelectionHandler(PoseDisplay* display, DisplayContext* context)
    : SelectionHandler(context), display_(display)
  {
  }

  void createProperties(const Picked& /*obj*/, Property* parent_property) override
  {
    Property* cat = new Property("Pose " + display_->getName(), QVariant(), "", parent_property);
    properties_.push_back(cat);

    frame_property_ = new StringProperty("Frame", "", "", cat);
    frame_property_->setReadOnly(true);

    position_property_ = new VectorProperty("Position", Ogre::Vector3::ZERO, "", cat);
    position_property_->setReadOnly(true);

    orientation_property_ = new QuaternionProperty("Orientation", Ogre::Quaternion::IDENTITY, "", cat);
    orientation_property_->setReadOnly(true);
  }

  void getAABBs(const Picked& /*obj*/, V_AABB& aabbs) override
  {
    if (display_->pose_valid_)
    {
      if (display_->shape_property_->getOptionInt() == PoseDisplay::Arrow)
      {
        aabbs.push_back(display_->arrow_->getHead()->getEntity()->getWorldBoundingBox());
        aabbs.push_back(display_->arrow_->getShaft()->getEntity()->getWorldBoundingBox());
      }
      else
      {
        aabbs.push_back(display_->axes_->getXShape()->getEntity()->getWorldBoundingBox());
        aabbs.push_back(display_->axes_->getYShape()->getEntity()->getWorldBoundingBox());
        aabbs.push_back(display_->axes_->getZShape()->getEntity()->getWorldBoundingBox());
      }
    }
  }

  void setMessage(const geometry_msgs::PoseStampedConstPtr& message)
  {
    // properties_.size() should only be > 0 after createProperties()
    // and before destroyProperties(), during which frame_property_,
    // position_property_, and orientation_property_ should be valid
    // pointers.
    if (!properties_.empty())
    {
      frame_property_->setStdString(message->header.frame_id);
      position_property_->setVector(
          Ogre::Vector3(message->pose.position.x, message->pose.position.y, message->pose.position.z));
      orientation_property_->setQuaternion(
          Ogre::Quaternion(message->pose.orientation.w, message->pose.orientation.x,
                           message->pose.orientation.y, message->pose.orientation.z));
    }
  }

private:
  PoseDisplay* display_;
  StringProperty* frame_property_;
  VectorProperty* position_property_;
  QuaternionProperty* orientation_property_;
};

PoseDisplay::PoseDisplay() : pose_valid_(false)
{
  shape_property_ = new EnumProperty("Shape", "Arrow", "Shape to display the pose as.", this,
                                     SLOT(updateShapeChoice()));
  shape_property_->addOption("Arrow", Arrow);
  shape_property_->addOption("Axes", Axes);

  color_property_ = new ColorProperty("Color", QColor(255, 25, 0), "Color to draw the arrow.", this,
                                      SLOT(updateColorAndAlpha()));

  alpha_property_ = new FloatProperty("Alpha", 1, "Amount of transparency to apply to the arrow.", this,
                                      SLOT(updateColorAndAlpha()));
  alpha_property_->setMin(0);
  alpha_property_->setMax(1);

  shaft_length_property_ = new FloatProperty(
      "Shaft Length", 1, "Length of the arrow's shaft, in meters.", this, SLOT(updateArrowGeometry()));

  // aleeper: default changed from 0.1 to match change in arrow.cpp
  shaft_radius_property_ =
      new FloatProperty("Shaft Radius", 0.05, "Radius of the arrow's shaft, in meters.", this,
                        SLOT(updateArrowGeometry()));

  head_length_property_ = new FloatProperty("Head Length", 0.3, "Length of the arrow's head, in meters.",
                                            this, SLOT(updateArrowGeometry()));

  // aleeper: default changed from 0.2 to match change in arrow.cpp
  head_radius_property_ = new FloatProperty("Head Radius", 0.1, "Radius of the arrow's head, in meters.",
                                            this, SLOT(updateArrowGeometry()));

  axes_length_property_ = new FloatProperty("Axes Length", 1, "Length of each axis, in meters.", this,
                                            SLOT(updateAxisGeometry()));

  axes_radius_property_ = new FloatProperty("Axes Radius", 0.1, "Radius of each axis, in meters.", this,
                                            SLOT(updateAxisGeometry()));
}

void PoseDisplay::onInitialize()
{
  MFDClass::onInitialize();

  arrow_ = new rviz::Arrow(scene_manager_, scene_node_, shaft_length_property_->getFloat(),
                           shaft_radius_property_->getFloat(), head_length_property_->getFloat(),
                           head_radius_property_->getFloat());
  // Arrow points in -Z direction, so rotate the orientation before display.
  // TODO: is it safe to change Arrow to point in +X direction?
  arrow_->setOrientation(Ogre::Quaternion(Ogre::Degree(-90), Ogre::Vector3::UNIT_Y));

  axes_ = new rviz::Axes(scene_manager_, scene_node_, axes_length_property_->getFloat(),
                         axes_radius_property_->getFloat());

  updateShapeChoice();
  updateColorAndAlpha();

  coll_handler_.reset(new PoseDisplaySelectionHandler(this, context_));
  coll_handler_->addTrackedObjects(arrow_->getSceneNode());
  coll_handler_->addTrackedObjects(axes_->getSceneNode());
}

PoseDisplay::~PoseDisplay()
{
  if (initialized())
  {
    delete arrow_;
    delete axes_;
  }
}

void PoseDisplay::onEnable()
{
  MFDClass::onEnable();
  updateShapeVisibility();
}

void PoseDisplay::updateColorAndAlpha()
{
  Ogre::ColourValue color = color_property_->getOgreColor();
  color.a = alpha_property_->getFloat();

  arrow_->setColor(color);

  context_->queueRender();
}

void PoseDisplay::updateArrowGeometry()
{
  arrow_->set(shaft_length_property_->getFloat(), shaft_radius_property_->getFloat(),
              head_length_property_->getFloat(), head_radius_property_->getFloat());
  context_->queueRender();
}

void PoseDisplay::updateAxisGeometry()
{
  axes_->set(axes_length_property_->getFloat(), axes_radius_property_->getFloat());
  context_->queueRender();
}

void PoseDisplay::updateShapeChoice()
{
  bool use_arrow = (shape_property_->getOptionInt() == Arrow);

  color_property_->setHidden(!use_arrow);
  alpha_property_->setHidden(!use_arrow);
  shaft_length_property_->setHidden(!use_arrow);
  shaft_radius_property_->setHidden(!use_arrow);
  head_length_property_->setHidden(!use_arrow);
  head_radius_property_->setHidden(!use_arrow);

  axes_length_property_->setHidden(use_arrow);
  axes_radius_property_->setHidden(use_arrow);

  updateShapeVisibility();

  context_->queueRender();
}

void PoseDisplay::updateShapeVisibility()
{
  if (!pose_valid_)
  {
    arrow_->getSceneNode()->setVisible(false);
    axes_->getSceneNode()->setVisible(false);
  }
  else
  {
    bool use_arrow = (shape_property_->getOptionInt() == Arrow);
    arrow_->getSceneNode()->setVisible(use_arrow);
    axes_->getSceneNode()->setVisible(!use_arrow);
  }
}

void PoseDisplay::processMessage(const geometry_msgs::PoseStamped::ConstPtr& message)
{
  if (!validateFloats(*message))
  {
    setStatus(StatusProperty::Error, "Topic",
              "Message contained invalid floating point values (nans or infs)");
    return;
  }

  if (!validateQuaternions(*message))
  {
    ROS_WARN_ONCE_NAMED("quaternions",
                        "Pose '%s' contains unnormalized quaternions. "
                        "This warning will only be output once but may be true for others; "
                        "enable DEBUG messages for ros.rviz.quaternions to see more details.",
                        qPrintable(getName()));
    ROS_DEBUG_NAMED("quaternions", "Pose '%s' contains unnormalized quaternions.", qPrintable(getName()));
  }

  Ogre::Vector3 position;
  Ogre::Quaternion orientation;
  if (!context_->getFrameManager()->transform(message->header, message->pose, position, orientation))
  {
    ROS_ERROR("Error transforming pose '%s' from frame '%s' to frame '%s'", qPrintable(getName()),
              message->header.frame_id.c_str(), qPrintable(fixed_frame_));
    return;
  }

  pose_valid_ = true;
  updateShapeVisibility();

  scene_node_->setPosition(position);
  scene_node_->setOrientation(orientation);

  coll_handler_->setMessage(message);

  context_->queueRender();
}

void PoseDisplay::reset()
{
  MFDClass::reset();
  pose_valid_ = false;
  updateShapeVisibility();
}

} // namespace rviz

#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(rviz::PoseDisplay, rviz::Display)
