/*
 * 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 "gazebo/common/Console.hh"
#include "gazebo/common/Assert.hh"
#include "gazebo/physics/PresetManagerPrivate.hh"
#include "gazebo/physics/PresetManager.hh"

using namespace gazebo;
using namespace physics;

//////////////////////////////////////////////////
template<typename T> bool CastAnyValue(const boost::any &_value, T &_return)
{
  try
  {
    _return = boost::any_cast<T>(_value);
  }
  catch(boost::bad_any_cast &_e)
  {
    return false;
  }
  return true;
}

//////////////////////////////////////////////////
Preset::Preset()
    : dataPtr(new PresetPrivate)
{
  GZ_ASSERT(this->dataPtr != NULL, "Data ptr NULL for Preset!");
}

//////////////////////////////////////////////////
Preset::Preset(const std::string &_name)
    : dataPtr(new PresetPrivate(_name))
{
  GZ_ASSERT(this->dataPtr != NULL, "Data ptr NULL for Preset!");
}

//////////////////////////////////////////////////
Preset::~Preset()
{
  this->dataPtr->elementSDF.reset();

  delete this->dataPtr;
  this->dataPtr = NULL;
}

//////////////////////////////////////////////////
bool Preset::SetAllPhysicsParameters(PhysicsEnginePtr _physicsEngine) const
{
  bool result = true;

  if (!_physicsEngine)
  {
    gzwarn << "Physics engine for PresetManager is NULL. PresetManager will "
           << "have no effect on simulation!" << std::endl;
    result = false;
  }
  else
  {
    for (auto const &param : this->dataPtr->parameterMap)
    {
      // disable params we know can't be set
      if (param.first != "type" &&
          !_physicsEngine->SetParam(param.first, param.second))
      {
        gzwarn << "Couldn't set parameter [" << param.first
          << "] in physics engine" << std::endl;
        result = false;
      }
    }
  }

  return result;
}

//////////////////////////////////////////////////
bool Preset::SetAllParamsFromSDF(const sdf::ElementPtr _elem)
{
  return this->SetAllParamsHelper(_elem);
}

//////////////////////////////////////////////////
bool Preset::SetAllParamsHelper(const sdf::ElementPtr _elem)
{
  bool result = true;

  if (!_elem)
  {
    return result;
  }

  // Avoid setting parameters that do not match the physics engine type set in
  // SDF
  if (_elem->GetParent() && _elem->GetParent()->GetName() == "physics" &&
      _elem->GetParent()->Get<std::string>("type") != _elem->GetName())
  {
    return result;
  }

  for (sdf::ElementPtr elem = _elem->GetFirstElement(); elem;
        elem = elem->GetNextElement())
  {
    if (elem->GetValue())
    {
      result &= this->SetParam(elem->GetName(), elem->GetAny());
    }
    result &= this->SetAllParamsHelper(elem);
  }

  return result;
}

//////////////////////////////////////////////////
std::string Preset::Name() const
{
  return this->dataPtr->name;
}

//////////////////////////////////////////////////
bool Preset::HasParam(const std::string &_key) const
{
  return this->dataPtr->parameterMap.find(_key) !=
         this->dataPtr->parameterMap.end();
}

//////////////////////////////////////////////////
bool Preset::GetParam(const std::string &_key, boost::any &_value) const
{
  bool result = true;

  if (!this->HasParam(_key))
  {
    gzwarn << "Parameter [" << _key << "] is not a member of profile ["
           << this->Name() << "]" << std::endl;
    result = false;
  }
  else
  {
    _value = this->dataPtr->parameterMap[_key];
  }

  return result;
}

//////////////////////////////////////////////////
bool Preset::SetParam(const std::string &_key, const boost::any &_value)
{
  bool result = true;

  if (_key.empty())
    result = false;
  else
    this->dataPtr->parameterMap[_key] = _value;

  return result;
}

//////////////////////////////////////////////////
sdf::ElementPtr Preset::SDF() const
{
  return this->dataPtr->elementSDF;
}

//////////////////////////////////////////////////
bool Preset::SDF(const sdf::ElementPtr _sdfElement)
{
  if (!_sdfElement)
  {
    gzwarn << "Can't add NULL SDF element to Preset" << std::endl;
    return false;
  }
  if (_sdfElement->GetName() != "physics")
  {
    gzwarn << "Can't assign non-physics element to preset profile" << std::endl;
    return false;
  }
  this->dataPtr->elementSDF = _sdfElement;
  return true;
}

//////////////////////////////////////////////////
PresetManager::PresetManager(PhysicsEnginePtr _physicsEngine,
    const sdf::ElementPtr _sdf)
    : dataPtr(new PresetManagerPrivate)
{
  GZ_ASSERT(this->dataPtr != NULL, "Data pointer NULL for PresetManager!");

  this->dataPtr->physicsEngine = _physicsEngine;
  if (!_physicsEngine || !_sdf)
  {
    gzerr << "Required arguments NULL for PresetManager" << std::endl;
    return;
  }

  // Load SDF
  if (!_sdf->HasElement("physics"))
  {
    gzerr << "PresetManager does not support preset profiles for SDF besides "
          << "physics. No profiles will be made." << std::endl;
  }

  bool defaultSet = false;
  for (sdf::ElementPtr physicsElem = _sdf->GetElement("physics"); physicsElem;
        physicsElem = physicsElem->GetNextElement("physics"))
  {
    // Get name attribute
    std::string name = this->CreateProfile(physicsElem);
    if (name.empty())
    {
      gzlog << "Empty physics profile name specified in SDF. No profile made "
            << "in PresetManager." << std::endl;
    }
    else
    {
      if (this->CurrentProfile().empty())
      {
        this->CurrentProfile(name);
      }
      if (physicsElem->HasAttribute("default"))
      {
        if (physicsElem->Get<bool>("default") && !defaultSet)
        {
          this->CurrentProfile(name);
          defaultSet = true;
        }
      }
    }
  }
}

//////////////////////////////////////////////////
PresetManager::~PresetManager()
{
  this->dataPtr->physicsEngine.reset();
  this->dataPtr->presetProfiles.clear();

  delete this->dataPtr;
  this->dataPtr = NULL;
}

//////////////////////////////////////////////////
bool PresetManager::CurrentProfile(const std::string &_name)
{
  GZ_ASSERT(this->dataPtr->physicsEngine, "Physics engine was NULL");

  if (_name.empty())
    return false;

  if (!this->HasProfile(_name))
  {
    gzwarn << "Profile [" << _name << "] not found." << std::endl;
    return false;
  }

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

    if (_name == this->CurrentProfile())
      return true;

    this->dataPtr->currentPreset = _name;

    // For now, ignore the return value of this function, since not all
    // parameters are supported
    this->CurrentPreset()->SetAllPhysicsParameters(
        this->dataPtr->physicsEngine);
  }

  return true;
}

//////////////////////////////////////////////////
std::string PresetManager::CurrentProfile() const
{
  return this->dataPtr->currentPreset;
}

//////////////////////////////////////////////////
std::vector<std::string> PresetManager::AllProfiles() const
{
  std::vector<std::string> ret;
  for (auto const &profile : this->dataPtr->presetProfiles)
  {
    ret.push_back(profile.first);
  }
  return ret;
}

//////////////////////////////////////////////////
bool PresetManager::SetProfileParam(const std::string &_profileName,
    const std::string &_key, const boost::any &_value)
{
  if (_profileName == this->CurrentProfile())
  {
    return this->SetCurrentProfileParam(_key, _value);
  }

  auto iter = this->dataPtr->presetProfiles.find(_profileName);

  if (iter == this->dataPtr->presetProfiles.end())
  {
    gzwarn << "Invalid profile name: [" << _profileName << "]" << std::endl;
    return false;
  }

  if (!iter->second.HasParam(_key))
  {
    gzwarn << "Profile [" << _profileName << "] does not have key [" << _key
           << "], so it was not set." << std::endl;
    return false;
  }

  if (!iter->second.SetParam(_key, _value))
  {
    gzwarn << "Could not set key [" << _key
           << "] for profile [" << _profileName << "]." << std::endl;
    return false;
  }
  return true;
}

//////////////////////////////////////////////////
bool PresetManager::GetProfileParam(const std::string &_name,
    const std::string &_key, boost::any &_value) const
{
  if (!this->HasProfile(_name))
  {
    return false;
  }
  return this->dataPtr->presetProfiles[_name].GetParam(_key, _value);
}

//////////////////////////////////////////////////
bool PresetManager::SetCurrentProfileParam(const std::string &_key,
    const boost::any &_value)
{
  if (this->CurrentProfile().empty() || this->CurrentPreset() == NULL)
  {
    return false;
  }

  std::lock_guard<std::mutex> lock(this->dataPtr->currentProfileMutex);
  if (!this->CurrentPreset()->HasParam(_key))
  {
    return false;
  }
  if (!this->CurrentPreset()->SetParam(_key, _value))
    return false;
  try
  {
    return this->dataPtr->physicsEngine->SetParam(_key, _value);
  }
  catch(const boost::bad_any_cast &e)
  {
    gzerr << "Couldn't set physics engine parameter[" << e.what()
          << "]" << std::endl;
    return false;
  }
  return true;
}

//////////////////////////////////////////////////
bool PresetManager::GetCurrentProfileParam(const std::string &_key,
    boost::any &_value)
{
  if (this->CurrentProfile().empty())
  {
    return false;
  }
  return this->CurrentPreset()->GetParam(_key, _value);
}

//////////////////////////////////////////////////
bool PresetManager::CreateProfile(const std::string &_name)
{
  if (_name.empty())
  {
    gzwarn << "Specified profile name was empty. Aborting." << std::endl;
    return false;
  }
  if (this->HasProfile(_name))
  {
    gzwarn << "Warning: profile [" << _name << "] already exists! Overwriting."
           << std::endl;
  }
  this->dataPtr->presetProfiles.emplace(_name, _name);

  if (!this->ProfileSDF(_name, this->dataPtr->physicsEngine->GetSDF()))
    return false;

  return true;
}

//////////////////////////////////////////////////
std::string PresetManager::CreateProfile(const sdf::ElementPtr _elem)
{
  if (!_elem || !_elem->HasAttribute("name"))
  {
    return "";
  }
  const std::string name = _elem->Get<std::string>("name");
  if (name.empty())
    return "";

  if (!this->CreateProfile(name))
    return "";

  // Make a copy of this SDF element.
  this->ProfileSDF(name, _elem);
  return name;
}

//////////////////////////////////////////////////
void PresetManager::RemoveProfile(const std::string &_name)
{
  if (!this->HasProfile(_name))
  {
    gzwarn << "Cannot remove non-existent profile [" << _name << "]"
           << std::endl;
    return;
  }

  if (_name == this->CurrentProfile())
  {
    gzmsg << "deselecting current preset " << _name << std::endl;
    this->dataPtr->currentPreset = "";
  }

  this->dataPtr->presetProfiles.erase(_name);
}

//////////////////////////////////////////////////
bool PresetManager::HasProfile(const std::string &_name) const
{
  return this->dataPtr->presetProfiles.find(_name) !=
         this->dataPtr->presetProfiles.end();
}

//////////////////////////////////////////////////
sdf::ElementPtr PresetManager::ProfileSDF(const std::string &_name) const
{
  if (_name.empty() || !this->HasProfile(_name))
  {
    gzerr << "Profile [" << _name << "] does not exist. Returning NULL "
          << "SDF pointer." << std::endl;
    return NULL;
  }

  return this->dataPtr->presetProfiles[_name].SDF();
}

//////////////////////////////////////////////////
bool PresetManager::ProfileSDF(const std::string &_name,
    const sdf::ElementPtr _sdf)
{
  if (!_sdf)
  {
    gzwarn << "Received NULL SDF element pointer in ProfileSDF" << std::endl;
    return false;
  }

  auto iter = this->dataPtr->presetProfiles.find(_name);

  if (iter == this->dataPtr->presetProfiles.end() || !iter->second.SDF(_sdf))
    return false;

  this->GeneratePresetFromSDF(_sdf, iter->second);

  if (_name == this->CurrentProfile())
  {
    std::lock_guard<std::mutex> lock(this->dataPtr->currentProfileMutex);
    this->CurrentPreset()->SetAllPhysicsParameters(
        this->dataPtr->physicsEngine);
  }

  iter->second.SetAllParamsFromSDF(_sdf);
  return true;
}

//////////////////////////////////////////////////
void PresetManager::GeneratePresetFromSDF(const sdf::ElementPtr _elem,
    Preset &_preset) const
{
  GZ_ASSERT(_elem, "NULL elem in GeneratePresetFromSDF, should never happen");

  // Check for physics engine type. If the type specified in SDF doesn't match
  // the type of the physics engine, don't add the children to the parameter
  // map. This will be changed in the future when switching the type of the
  // physics engine is allowed.
  GZ_ASSERT(this->dataPtr->physicsEngine, "No physics engine in PresetManager");
  if (_elem->GetParent() && _elem->GetParent()->GetName() == "physics" &&
      this->dataPtr->physicsEngine->GetType() != _elem->GetName())
  {
    return;
  }

  for (sdf::ElementPtr elem = _elem->GetFirstElement(); elem;
        elem = elem->GetNextElement())
  {
    if (elem->GetValue() != NULL)
    {
      _preset.SetParam(elem->GetName(), elem->GetAny());
    }
    this->GeneratePresetFromSDF(elem, _preset);
  }
}

//////////////////////////////////////////////////
void PresetManager::GenerateSDFFromPreset(const std::string &_name,
    sdf::ElementPtr &_elem) const
{
  auto iter = this->dataPtr->presetProfiles.find(_name);

  if (iter == this->dataPtr->presetProfiles.end())
  {
    gzwarn << "Profile [" << _name << "] does not exist. No SDF will be "
           << "generated." << std::endl;
    return;
  }
  _elem.reset();

  // Start with the elementSDF member variable of the preset
  _elem = iter->second.SDF()->Clone();
  GZ_ASSERT(_elem, "Null SDF pointer in preset");

  this->GenerateSDFHelper(iter->second, _elem);
  GZ_ASSERT(_elem, "Generated NULL SDF pointer");
}

//////////////////////////////////////////////////
void PresetManager::GenerateSDFHelper(const Preset &_preset,
    sdf::ElementPtr &_elem) const
{
  GZ_ASSERT(_elem, "NULL elem in GenerateSDFHelper, should never happen");

  // For each element, enforce that its equivalent in Preset has the same value
  for (sdf::ElementPtr elem = _elem->GetFirstElement(); elem;
        elem = elem->GetNextElement())
  {
    boost::any value;
    if (_preset.GetParam(elem->GetName(), value) && elem->GetValue())
    {
      // cast based on type in SDF
      if (elem->GetValue()->IsType<int>())
      {
        int v;
        if (CastAnyValue(value, v))
          elem->Set(v);
        else
          gzerr << "SDF type did not give successful cast" << std::endl;
      }
      else if (elem->GetValue()->IsType<double>())
      {
        double v;
        if (CastAnyValue(value, v))
          elem->Set(v);
        else
          gzerr << "SDF type did not give successful cast" << std::endl;
      }
      else if (elem->GetValue()->IsType<float>())
      {
        float v;
        if (CastAnyValue(value, v))
          elem->Set(v);
        else
          gzerr << "SDF type did not give successful cast" << std::endl;
      }
      else if (elem->GetValue()->IsType<bool>())
      {
        bool v;
        if (CastAnyValue(value, v))
          elem->Set(v);
        else
          gzerr << "SDF type did not give successful cast" << std::endl;
      }
      else if (elem->GetValue()->IsType<std::string>())
      {
        std::string v;
        if (CastAnyValue(value, v))
          elem->Set(v);
        else
          gzerr << "SDF type did not give successful cast" << std::endl;
      }
      else if (elem->GetValue()->IsType<ignition::math::Vector3d>())
      {
        ignition::math::Vector3d v;
        if (CastAnyValue(value, v))
        {
          gzdbg << "Vector3: " << v << std::endl;
          elem->Set(v);
        }
        else
          gzerr << "SDF type did not give successful cast" << std::endl;
#ifndef _WIN32
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
      }
      else if (elem->GetValue()->IsType<sdf::Vector3>())
      {
        math::Vector3 v;
        if (CastAnyValue(value, v))
        {
          gzdbg << "Vector3: " << v << std::endl;
          elem->Set(v);
        }
        else
          gzerr << "SDF type did not give successful cast" << std::endl;
#ifndef _WIN32
#pragma GCC diagnostic pop
#endif
      }
    }
    this->GenerateSDFHelper(_preset, elem);
  }
}

//////////////////////////////////////////////////
Preset *PresetManager::CurrentPreset() const
{
  return &this->dataPtr->presetProfiles[this->CurrentProfile()];
}
