/*
 * 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.
 *
 */
/*
 * Desc: Projector
 * Author: Jared Duke, John Hsu, Nate Koenig
 */
#include <boost/algorithm/string.hpp>
#include <boost/bind.hpp>

#include "gazebo/rendering/RTShaderSystem.hh"

#include "gazebo/rendering/Conversions.hh"
#include "gazebo/rendering/Visual.hh"
#include "gazebo/rendering/Scene.hh"
#include "gazebo/rendering/Projector.hh"

using namespace gazebo;
using namespace rendering;

typedef std::map<std::string, Ogre::Pass*> OgrePassMap;
typedef OgrePassMap::iterator OgrePassMapIterator;

/////////////////////////////////////////////////
Projector::Projector(rendering::VisualPtr _parent)
{
  this->visual = _parent;

  this->node = transport::NodePtr(new transport::Node());
  this->node->Init(this->visual->GetScene()->Name());
}

/////////////////////////////////////////////////
Projector::~Projector()
{
  this->SetEnabled(false);
  // Ogre cleanup
  Ogre::Root::getSingletonPtr()->removeFrameListener(&this->projector);
}

/////////////////////////////////////////////////
void Projector::Load(const std::string &_name,
                     const ignition::math::Pose3d &_pose,
                     const std::string &_textureName,
                     const double _nearClip,
                     const double _farClip,
                     const double _fov)
{
  std::string topicName = std::string("~/") + _name;

  boost::replace_all(topicName, "::", "/");
  this->controlSub = this->node->Subscribe(topicName, &Projector::OnMsg, this);

  if (!this->visual)
    return;

  int retryCount = 0;

  // Initialize the projector
  while (!this->projector.initialized && retryCount < 10)
  {
    // init
    this->projector.Init(this->visual, _textureName, _nearClip, _farClip, _fov);

    // set the projector pose relative to body
    this->projector.SetPose(_pose);

    if (!this->projector.initialized)
    {
      gzwarn << "starting projector failed, retrying in 1 sec.\n";
      common::Time::MSleep(1000);
      ++retryCount;
    }
  }

  // Add the projector as an Ogre frame listener
  Ogre::Root::getSingletonPtr()->addFrameListener(&this->projector);

  this->projector.SetEnabled(true);

  // Start projector
  /*this->add_model_event_ = gazebo::event::Events::ConnectWorldUpdateStart(
    boost::bind(&Projector::ToggleProjector, this, true));
    */
}

/////////////////////////////////////////////////
void Projector::Load(sdf::ElementPtr _sdf)
{
  ignition::math::Pose3d pose;
  std::string textureName;
  double nearClip = 0.1;
  double farClip = 10.0;
  double fov = M_PI * 0.25;

  if (_sdf->HasElement("pose"))
    pose = _sdf->Get<ignition::math::Pose3d>("pose");

  if (_sdf->HasElement("texture_name"))
    textureName = _sdf->Get<std::string>("texture_name");

  if (_sdf->HasElement("near_clip"))
    nearClip = _sdf->Get<double>("near_clip");

  if (_sdf->HasElement("far_clip"))
    farClip = _sdf->Get<double>("far_clip");

  if (_sdf->HasElement("fov"))
    fov = _sdf->Get<double>("fov");

  this->Load(_sdf->Get<std::string>("name"), pose, textureName,
             nearClip, farClip, fov);
}

/////////////////////////////////////////////////
void Projector::Load(const msgs::Projector &_msg)
{
  ignition::math::Pose3d pose;
  std::string textureName;
  double nearClip = 0.1;
  double farClip = 10.0;
  double fov = M_PI * 0.25;

  if (_msg.has_pose())
    pose = msgs::ConvertIgn(_msg.pose());

  if (_msg.has_texture())
    textureName = _msg.texture();

  if (_msg.has_near_clip())
    nearClip = _msg.near_clip();

  if (_msg.has_far_clip())
    farClip = _msg.far_clip();

  if (_msg.has_fov())
    fov = _msg.fov();

  this->Load(_msg.name(), pose, textureName, nearClip, farClip, fov);
}

/////////////////////////////////////////////////
void Projector::SetEnabled(bool _enabled)
{
  this->projector.SetEnabled(_enabled);
}

/////////////////////////////////////////////////
void Projector::SetTexture(const std::string &_textureName)
{
  this->projector.SetTexture(_textureName);
}

/////////////////////////////////////////////////
void Projector::Toggle()
{
  // if not headless
  /*if (this->projector.initialized)
  {
    this->projector.SetEnabled(!this->projector.enabled);
  }
  else
    gzwarn << "could not start projector, toggle failed\n";
    */
}

/////////////////////////////////////////////////
VisualPtr Projector::GetParent()
{
  return this->visual;
}

/////////////////////////////////////////////////
Projector::ProjectorFrameListener::ProjectorFrameListener()
{
  this->enabled = false;
  this->initialized = false;
  this->usingShaders = false;

  this->node = NULL;
  this->filterNode = NULL;
  this->projectorQuery = NULL;
  this->frustum = NULL;
  this->filterFrustum = NULL;

  this->nodeName = "Projector";
  this->filterNodeName = "ProjectorFilter";
}

/////////////////////////////////////////////////
Projector::ProjectorFrameListener::~ProjectorFrameListener()
{
  this->RemovePassFromMaterials();

  if (this->filterNode)
  {
    this->filterNode->detachObject(this->filterFrustum);
    this->node->removeAndDestroyChild(this->filterNodeName);
    this->filterNode = NULL;
  }

  if (this->node)
  {
    this->node->detachObject(this->frustum);
    Ogre::SceneNode *n = this->visual->GetSceneNode();
    if (n)
      n->removeAndDestroyChild(this->nodeName);
    this->node = NULL;
  }

  delete this->frustum;
  delete this->filterFrustum;
  this->frustum = NULL;
  this->filterFrustum = NULL;

  if (this->projectorQuery)
    this->sceneMgr->destroyQuery(this->projectorQuery);

  this->visual.reset();
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::Init(VisualPtr _visual,
                                             const std::string &_textureName,
                                             double _near,
                                             double _far,
                                             double _fov)
{
  if (this->initialized)
    return;

  if (_textureName.empty())
  {
    gzerr << "Projector is missing a texture\n";
    return;
  }

  this->visual = _visual;

  this->nodeName = this->visual->Name() + "_Projector";
  this->filterNodeName = this->visual->Name() + "_ProjectorFilter";

  this->frustum = new Ogre::Frustum();
  this->filterFrustum = new Ogre::Frustum();
  this->filterFrustum->setProjectionType(Ogre::PT_ORTHOGRAPHIC);

  this->sceneMgr = this->visual->GetScene()->OgreSceneManager();
  this->projectorQuery = this->sceneMgr->createPlaneBoundedVolumeQuery(
      Ogre::PlaneBoundedVolumeList());

  this->SetSceneNode();
  this->SetTexture(_textureName);
  this->SetFrustumClipDistance(_near, _far);
  this->SetFrustumFOV(_fov);

  this->initialized = true;
}

/////////////////////////////////////////////////
bool Projector::ProjectorFrameListener::frameStarted(
    const Ogre::FrameEvent &/*_evt*/)
{
  if (!this->initialized || !this->enabled || this->textureName.empty())
    return true;

  this->AddPassToVisibleMaterials();

  return true;
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::SetEnabled(bool _enabled)
{
  this->enabled = _enabled;
  if (!this->enabled)
    this->RemovePassFromMaterials();
  rendering::RTShaderSystem::Instance()->UpdateShaders();
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::SetUsingShaders(bool _usingShaders)
{
  this->usingShaders = _usingShaders;
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::SetSceneNode()
{
  if (this->filterNode)
  {
    this->filterNode->detachObject(this->filterFrustum);
    this->node->removeAndDestroyChild(this->filterNodeName);
    this->filterNode = NULL;
  }

  if (this->node)
  {
    this->node->detachObject(this->frustum);
    this->visual->GetSceneNode()->removeAndDestroyChild(this->nodeName);
    this->node = NULL;
  }

  this->node = this->visual->GetSceneNode()->createChildSceneNode(
      this->nodeName);

  this->filterNode = this->node->createChildSceneNode(
      this->filterNodeName);

  if (this->node)
    this->node->attachObject(this->frustum);

  if (this->filterNode)
  {
    this->filterNode->attachObject(this->filterFrustum);
    this->filterNode->setOrientation(
      Ogre::Quaternion(Ogre::Degree(90), Ogre::Vector3::UNIT_Y));
  }
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::SetPose(
  const ignition::math::Pose3d &_pose)
{
  Ogre::Quaternion ogreQuaternion = Conversions::Convert(_pose.Rot());
  Ogre::Vector3 ogreVec = Conversions::Convert(_pose.Pos());
  Ogre::Quaternion offsetQuaternion;

  this->node->setPosition(ogreVec);
  this->node->setOrientation(ogreQuaternion);
  this->filterNode->setPosition(ogreVec);

  offsetQuaternion = Ogre::Quaternion(Ogre::Degree(90), Ogre::Vector3::UNIT_Y);
  this->filterNode->setOrientation(offsetQuaternion);
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::SetTexture(
    const std::string &_textureName)
{
  this->textureName = _textureName;
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::SetFrustumClipDistance(double _near,
                                                               double _far)
{
  this->frustum->setNearClipDistance(_near);
  this->filterFrustum->setNearClipDistance(_near);
  this->frustum->setFarClipDistance(_far);
  this->filterFrustum->setFarClipDistance(_far);
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::SetFrustumFOV(double _fov)
{
  this->frustum->setFOVy(Ogre::Radian(_fov));
  this->filterFrustum->setFOVy(Ogre::Radian(_fov));
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::AddPassToAllMaterials()
{
  std::list<std::string> allMaterials;

  Ogre::SceneManager::MovableObjectIterator it =
    this->sceneMgr->getMovableObjectIterator("Entity");

  while (it.hasMoreElements())
  {
    Ogre::Entity* entity = dynamic_cast<Ogre::Entity*>(it.getNext());
    if (entity && entity->getName().find("visual") != std::string::npos)

    for (unsigned int i = 0; i < entity->getNumSubEntities(); i++)
    {
      allMaterials.push_back(entity->getSubEntity(i)->getMaterialName());
    }
  }

  this->AddPassToMaterials(allMaterials);
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::AddPassToVisibleMaterials()
{
  std::list<std::string> newVisibleMaterials;
  Ogre::PlaneBoundedVolumeList volumeList;

  volumeList.push_back(this->frustum->getPlaneBoundedVolume());

  this->projectorQuery->setVolumes(volumeList);
  Ogre::SceneQueryResult result = this->projectorQuery->execute();

  // Find all visible materials
  Ogre::SceneQueryResultMovableList::iterator it;
  for (it = result.movables.begin(); it != result.movables.end(); ++it)
  {
    Ogre::Entity *entity = dynamic_cast<Ogre::Entity*>(*it);
    if (entity && entity->getName().find("visual") != std::string::npos)
    {
      for (unsigned int i = 0; i < entity->getNumSubEntities(); i++)
      {
        newVisibleMaterials.push_back(
          entity->getSubEntity(i)->getMaterialName());
      }
    }
  }

  this->AddPassToMaterials(newVisibleMaterials);
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::AddPassToMaterials(
    std::list<std::string> &_matList)
{
  _matList.remove("");
  _matList.unique();

  std::string invisibleMaterial;
  std::list<std::string>::iterator visibleMaterial;

  // Loop through all existing passes, removing those for materials
  //   not in the newlist and skipping pass creation for those in the
  //   newlist that have already been created
  OgrePassMapIterator used = projectorTargets.begin();
  while (used != projectorTargets.end())
  {
    visibleMaterial = std::find(_matList.begin(), _matList.end(), used->first);

    // Remove the pass if it applies to a material not in the new list
    if (visibleMaterial == _matList.end())
    {
      invisibleMaterial = used->first;
      ++used;
      this->RemovePassFromMaterial(invisibleMaterial);
    }
    // Otherwise remove it from the list of passes to be added
    else
    {
      _matList.remove(used->first);
      ++used;
    }
  }

  if (!_matList.empty())
  {
    // Add pass for new materials
    while (!_matList.empty())
    {
      this->AddPassToMaterial(_matList.front());
      _matList.erase(_matList.begin());
    }

    RTShaderSystem::Instance()->UpdateShaders();
  }
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::AddPassToMaterial(
    const std::string &_matName)
{
  if (this->projectorTargets.find(_matName) != this->projectorTargets.end())
  {
    return;
  }

  Ogre::MaterialPtr mat = static_cast<Ogre::MaterialPtr>(
    Ogre::MaterialManager::getSingleton().getByName(_matName));
  Ogre::Pass *pass = mat->getTechnique(0)->createPass();

  if (this->usingShaders)
  {
    Ogre::Matrix4 viewProj = this->frustum->getProjectionMatrix() *
                             this->frustum->getViewMatrix();

    pass->setVertexProgram("Gazebo/TextureProjectionVP");

    // pass->setFragmentProgram("GazeboWorlds/TexProjectionFP");
    Ogre::GpuProgramParametersSharedPtr vsParams =
      pass->getVertexProgramParameters();

    Ogre::GpuProgramParametersSharedPtr psParams =
      pass->getFragmentProgramParameters();

    // vsParams->setNamedAutoConstant(
    //   "worldViewProjMatrix",
    //   GpuProgramParameters::ACT_WORLDVIEWPROJ_MATRIX);
    // vsParams->setNamedAutoConstant(
    //   "worldMatrix",GpuProgramParameters::ACT_WORLD_MATRIX);
    // vsParams->setNamedConstant("texViewProjMatrix", viewProj);

    vsParams->setNamedAutoConstant(
      "worldMatrix", Ogre::GpuProgramParameters::ACT_WORLD_MATRIX);

    vsParams->setNamedConstant("texProjMatrix", viewProj);

    // psParams->setNamedConstant("projMap", viewProj);

    pass->setVertexProgramParameters(vsParams);

    // pass->setFragmentProgramParameters(psParams);
  }

  pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA);
  pass->setDepthBias(1);
  pass->setLightingEnabled(false);

  Ogre::TextureUnitState *texState =
    pass->createTextureUnitState(this->textureName);
  texState->setProjectiveTexturing(true, this->frustum);
  texState->setTextureAddressingMode(Ogre::TextureUnitState::TAM_BORDER);
  texState->setTextureFiltering(Ogre::TFO_ANISOTROPIC);
  texState->setTextureBorderColour(Ogre::ColourValue(0.0, 0.0, 0.0, 0.0));
  texState->setColourOperation(Ogre::LBO_ALPHA_BLEND);

  texState = pass->createTextureUnitState("projection_filter.png");
  texState->setProjectiveTexturing(true, this->filterFrustum);
  texState->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP);
  texState->setTextureFiltering(Ogre::TFO_NONE);

  this->projectorTargets[_matName] = pass;
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::RemovePassFromMaterials()
{
  for (OgrePassMap::const_iterator it = this->projectorTargets.begin();
      it != this->projectorTargets.end(); ++it)
  {
    it->second->getParent()->removePass(it->second->getIndex());
  }
  this->projectorTargets.clear();
}

/////////////////////////////////////////////////
void Projector::ProjectorFrameListener::RemovePassFromMaterial(
    const std::string &_matName)
{
  this->projectorTargets[_matName]->getParent()->removePass(
    this->projectorTargets[_matName]->getIndex());
  this->projectorTargets.erase(this->projectorTargets.find(_matName));
}

/////////////////////////////////////////////////
void Projector::OnMsg(ConstProjectorPtr &_msg)
{
  if (_msg->has_enabled())
    this->projector.SetEnabled(_msg->enabled());
}
