/*
 * Copyright (C) 2012-2016 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 <boost/bind.hpp>

#include "gazebo/rendering/Heightmap.hh"
#include "gazebo/rendering/Scene.hh"
#include "gazebo/rendering/UserCamera.hh"

#include "gazebo/gui/GuiEvents.hh"
#include "gazebo/gui/GuiIface.hh"
#include "gazebo/gui/MouseEventHandler.hh"
#include "gazebo/gui/terrain/TerrainEditorPalette.hh"
#include "gazebo/gui/terrain/TerrainEditorPalettePrivate.hh"

using namespace gazebo;
using namespace gui;

/////////////////////////////////////////////////
TerrainEditorPalette::TerrainEditorPalette(QWidget *_parent)
    : QWidget(_parent),
      dataPtr(new TerrainEditorPalettePrivate())
{
  QVBoxLayout *mainLayout = new QVBoxLayout;

  // Create the button to raise terrain
  QPushButton *raiseButton = new QPushButton("Raise", this);
  raiseButton->setStatusTip(tr("Left-mouse press to raise terrain."));
  raiseButton->setCheckable(true);
  raiseButton->setChecked(false);
  connect(raiseButton, SIGNAL(toggled(bool)), this, SLOT(OnRaise(bool)));

  // Create the button to lower terrain
  QPushButton *lowerButton = new QPushButton("Lower", this);
  lowerButton->setStatusTip(tr("Left-mouse press to lower terrain."));
  lowerButton->setCheckable(true);
  lowerButton->setChecked(false);
  connect(lowerButton, SIGNAL(toggled(bool)), this, SLOT(OnLower(bool)));

  // Create the button to flatten terrain
  QPushButton *flattenButton = new QPushButton("Flatten", this);
  flattenButton->setStatusTip(tr("Left-mouse press to flatten terrain."));
  flattenButton->setCheckable(true);
  flattenButton->setChecked(false);
  connect(flattenButton, SIGNAL(toggled(bool)), this, SLOT(OnFlatten(bool)));

  // Create the button to roughen terrain
  QPushButton *heightButton = new QPushButton("Pick Height", this);
  heightButton->setStatusTip(
      tr("Left-mouse press to select a terrain height."));
  heightButton->setCheckable(true);
  heightButton->setChecked(false);
  connect(heightButton, SIGNAL(toggled(bool)), this, SLOT(OnPickHeight(bool)));

  QButtonGroup *buttonGroup = new QButtonGroup;
  buttonGroup->addButton(raiseButton);
  buttonGroup->addButton(lowerButton);
  buttonGroup->addButton(flattenButton);
  buttonGroup->addButton(heightButton);

  // Create the layout to hold all the buttons
  QGridLayout *buttonLayout = new QGridLayout;
  buttonLayout->addWidget(raiseButton, 0, 0);
  buttonLayout->addWidget(lowerButton, 0, 1);
  buttonLayout->addWidget(flattenButton, 1, 0);
  buttonLayout->addWidget(heightButton, 1, 1);

  // Add a save button
  QPushButton *saveButton = new QPushButton("Save Image", this);
  saveButton->setStatusTip(tr("Save terrain as a PNG."));
  connect(saveButton, SIGNAL(clicked()), this, SLOT(OnSave()));

  // Create a slider to control the outer size of the brush
  this->dataPtr->outsideRadiusSlider = new QSlider(this);
  this->dataPtr->outsideRadiusSlider->setRange(1, 100000);
  this->dataPtr->outsideRadiusSlider->setTickInterval(1);
  this->dataPtr->outsideRadiusSlider->setOrientation(Qt::Horizontal);
  this->dataPtr->outsideRadiusSlider->setValue(10);
  connect(this->dataPtr->outsideRadiusSlider, SIGNAL(valueChanged(int)),
      this, SLOT(OnOutsideRadiusSlider(int)));

  this->dataPtr->outsideRadiusSpin = new QDoubleSpinBox(this);
  this->dataPtr->outsideRadiusSpin->setRange(0, 1.0);
  this->dataPtr->outsideRadiusSpin->setSingleStep(0.001);
  this->dataPtr->outsideRadiusSpin->setDecimals(3);
  connect(this->dataPtr->outsideRadiusSpin, SIGNAL(valueChanged(double)),
      this, SLOT(OnOutsideRadiusSpin(double)));

  // Create a layout to hold the outer brush size slider and its label
  QHBoxLayout *outsideRadiusSpinLayout = new QHBoxLayout;
  outsideRadiusSpinLayout->addWidget(new QLabel(tr("Outside radius: ")));
  outsideRadiusSpinLayout->addStretch(2);
  outsideRadiusSpinLayout->addWidget(this->dataPtr->outsideRadiusSpin);

  QVBoxLayout *outsideRadiusLayout = new QVBoxLayout;
  outsideRadiusLayout->addLayout(outsideRadiusSpinLayout);
  outsideRadiusLayout->addWidget(this->dataPtr->outsideRadiusSlider);


  // Create a slider to control the inner size of the brush
  this->dataPtr->insideRadiusSlider = new QSlider(this);
  this->dataPtr->insideRadiusSlider->setRange(0, 100000);
  this->dataPtr->insideRadiusSlider->setTickInterval(1);
  this->dataPtr->insideRadiusSlider->setOrientation(Qt::Horizontal);
  this->dataPtr->insideRadiusSlider->setValue(10);
  connect(this->dataPtr->insideRadiusSlider, SIGNAL(valueChanged(int)),
      this, SLOT(OnInsideRadiusSlider(int)));

  this->dataPtr->insideRadiusSpin = new QDoubleSpinBox(this);
  this->dataPtr->insideRadiusSpin->setRange(0, 1.0);
  this->dataPtr->insideRadiusSpin->setSingleStep(0.001);
  this->dataPtr->insideRadiusSpin->setDecimals(3);
  connect(this->dataPtr->insideRadiusSpin, SIGNAL(valueChanged(double)),
      this, SLOT(OnInsideRadiusSpin(double)));

  // Create a layout to hold the inner brush size slider and its label
  QHBoxLayout *insideRadiusSpinLayout = new QHBoxLayout;
  insideRadiusSpinLayout->addWidget(new QLabel(tr("Inside radius: ")));
  insideRadiusSpinLayout->addStretch(2);
  insideRadiusSpinLayout->addWidget(this->dataPtr->insideRadiusSpin);

  QVBoxLayout *insideRadiusLayout = new QVBoxLayout;
  insideRadiusLayout->addLayout(insideRadiusSpinLayout);
  insideRadiusLayout->addWidget(this->dataPtr->insideRadiusSlider);

  // Create a slider to control the weight of the brush
  this->dataPtr->weightSlider = new QSlider(this);
  this->dataPtr->weightSlider->setRange(1, 10000);
  this->dataPtr->weightSlider->setTickInterval(1);
  this->dataPtr->weightSlider->setOrientation(Qt::Horizontal);
  this->dataPtr->weightSlider->setValue(10);
  connect(this->dataPtr->weightSlider, SIGNAL(valueChanged(int)),
      this, SLOT(OnWeightSlider(int)));

  this->dataPtr->weightSpin = new QDoubleSpinBox(this);
  this->dataPtr->weightSpin->setRange(.01, 1.0);
  this->dataPtr->weightSpin->setSingleStep(.1);
  connect(this->dataPtr->weightSpin, SIGNAL(valueChanged(double)),
      this, SLOT(OnWeightSpin(double)));


  // Create a layout to hold the brush weight slider and its label
  QHBoxLayout *weightSpinLayout = new QHBoxLayout;
  weightSpinLayout->addWidget(new QLabel(tr("Weight: ")));
  weightSpinLayout->addStretch(2);
  weightSpinLayout->addWidget(this->dataPtr->weightSpin);

  QVBoxLayout *weightLayout = new QVBoxLayout;
  weightLayout->addLayout(weightSpinLayout);
  weightLayout->addWidget(this->dataPtr->weightSlider);


  // Create a slider to control the weight of the brush
  this->dataPtr->heightSlider = new QSlider(this);
  this->dataPtr->heightSlider->setRange(1, 100);
  this->dataPtr->heightSlider->setTickInterval(1);
  this->dataPtr->heightSlider->setOrientation(Qt::Horizontal);
  this->dataPtr->heightSlider->setValue(10);
  connect(this->dataPtr->heightSlider, SIGNAL(valueChanged(int)),
      this, SLOT(OnHeightSlider(int)));

  this->dataPtr->heightSpin = new QDoubleSpinBox(this);
  connect(this->dataPtr->heightSpin, SIGNAL(valueChanged(double)),
      this, SLOT(OnHeightSpin(double)));

  // Create a layout to hold the brush height slider and its label
  QHBoxLayout *heightSpinLayout = new QHBoxLayout;
  heightSpinLayout->addWidget(new QLabel(tr("Height: ")));
  heightSpinLayout->addStretch(2);
  heightSpinLayout->addWidget(this->dataPtr->heightSpin);

  QVBoxLayout *heightLayout = new QVBoxLayout;
  heightLayout->setContentsMargins(0, 0, 0, 0);
  heightLayout->addLayout(heightSpinLayout);
  heightLayout->addWidget(this->dataPtr->heightSlider);


  // Add all the layouts and widgets to the main layout
  mainLayout->addLayout(buttonLayout);
  mainLayout->addLayout(outsideRadiusLayout);
  mainLayout->addLayout(insideRadiusLayout);
  mainLayout->addLayout(weightLayout);
  mainLayout->addLayout(heightLayout);
  mainLayout->addStretch(1);
  mainLayout->addWidget(saveButton);

  mainLayout->setAlignment(Qt::AlignTop | Qt::AlignHCenter);

  this->setObjectName("terrainEditorPalette");
  this->setLayout(mainLayout);
}

/////////////////////////////////////////////////
TerrainEditorPalette::~TerrainEditorPalette()
{
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnRaise(bool _toggle)
{
  this->SetState(_toggle ? "raise" : std::string());
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnLower(bool _toggle)
{
  this->SetState(_toggle ? "lower" : std::string());
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnFlatten(bool _toggle)
{
  this->SetState(_toggle ? "flatten" : std::string());
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnPickHeight(bool /*_toggle*/)
{
}

/////////////////////////////////////////////////
void TerrainEditorPalette::SetState(const std::string &_state)
{
  if (!_state.empty())
  {
    this->dataPtr->state = _state;

    // Add an event filter, which allows the TerrainEditor to capture
    // mouse events.
    MouseEventHandler::Instance()->AddPressFilter("terrain",
        boost::bind(&TerrainEditorPalette::OnMousePress, this, _1));

    MouseEventHandler::Instance()->AddMoveFilter("terrain",
        boost::bind(&TerrainEditorPalette::OnMouseMove, this, _1));
  }
  else
  {
    this->dataPtr->state.clear();

    // Remove the event filters.
    MouseEventHandler::Instance()->RemovePressFilter("terrain");
    MouseEventHandler::Instance()->RemoveMoveFilter("terrain");
  }
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnSave()
{
  // Get the active camera and scene.
  rendering::UserCameraPtr camera = gui::get_active_camera();
  rendering::ScenePtr scene = camera->GetScene();

  // Get a pointer to the heightmap, if the scen is valid.
  rendering::Heightmap *heightmap = scene ? scene->GetHeightmap() : NULL;
  common::Image img = heightmap->Image();

  // Get a filename to save to.
  // Note that file dialog static functions seem to be broken (issue #1514)
  std::string filename = QFileDialog::getSaveFileName(this,
      tr("Save Heightmap"), QString(),
      tr("PNG Files (*.png)")).toStdString();

  // Return if the user has canceled.
  if (filename.empty())
    return;

  img.SavePNG(filename);
}

/////////////////////////////////////////////////
bool TerrainEditorPalette::OnMousePress(const common::MouseEvent &_event)
{
  if (_event.Button() != common::MouseEvent::LEFT)
    return false;

  bool handled = false;

  // Get the active camera and scene.
  rendering::UserCameraPtr camera = gui::get_active_camera();
  rendering::ScenePtr scene = camera->GetScene();

  // Get a pointer to the heightmap, if the scen is valid.
  rendering::Heightmap *heightmap = scene ? scene->GetHeightmap() : NULL;

  // Only try to modify if the heightmap exists, and the LEFT mouse button
  // was used.
  if (heightmap && !_event.Shift())
  {
    handled = this->Apply(_event, camera, heightmap);
    QApplication::setOverrideCursor(QCursor(Qt::CrossCursor));
  }
  else
    QApplication::setOverrideCursor(QCursor(Qt::ArrowCursor));

  return handled;
}

/////////////////////////////////////////////////
bool TerrainEditorPalette::OnMouseMove(const common::MouseEvent &_event)
{
  if (_event.Button() != common::MouseEvent::LEFT)
    return false;

  bool handled = false;

  // Get the active camera and scene.
  rendering::UserCameraPtr camera = gui::get_active_camera();
  rendering::ScenePtr scene = camera->GetScene();

  // Get a pointer to the heightmap, if the scen is valid.
  rendering::Heightmap *heightmap = scene ? scene->GetHeightmap() : NULL;

  if (heightmap && !_event.Shift())
  {
    if (_event.Dragging())
      handled = this->Apply(_event, camera, heightmap);
    QApplication::setOverrideCursor(QCursor(Qt::CrossCursor));
  }
  else
    QApplication::setOverrideCursor(QCursor(Qt::ArrowCursor));

  return handled;
}

/////////////////////////////////////////////////
bool TerrainEditorPalette::Apply(const common::MouseEvent &_event,
    rendering::CameraPtr _camera, rendering::Heightmap *_heightmap) const
{
  bool handled = false;

  // Get the brush weight and size from the sliders.
  double weight = this->dataPtr->weightSpin->value();
  double outsideRadius = this->dataPtr->outsideRadiusSpin->value() / 10.0;
  double insideRadius = this->dataPtr->insideRadiusSpin->value() / 10.0;

  if (this->dataPtr->state == "lower")
  {
    handled = _heightmap->Lower(_camera, _event.Pos(), outsideRadius,
        insideRadius, weight);
  }
  else if (this->dataPtr->state == "raise")
  {
    handled = _heightmap->Raise(_camera, _event.Pos(), outsideRadius,
        insideRadius, weight);
  }
  else if (this->dataPtr->state == "flatten")
  {
    handled = _heightmap->Flatten(_camera, _event.Pos(), outsideRadius,
        insideRadius, weight);
  }
  else if (this->dataPtr->state == "smooth")
  {
    handled = _heightmap->Smooth(_camera, _event.Pos(), outsideRadius,
        insideRadius, weight);
  }

  return handled;
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnOutsideRadiusSpin(double _value)
{
  disconnect(this->dataPtr->outsideRadiusSlider, SIGNAL(valueChanged(int)),
      this, SLOT(OnOutsideRadiusSlider(int)));

  this->dataPtr->outsideRadiusSlider->setValue(
      static_cast<int>(rint(
        _value * this->dataPtr->outsideRadiusSlider->maximum())));

  connect(this->dataPtr->outsideRadiusSlider, SIGNAL(valueChanged(int)),
      this, SLOT(OnOutsideRadiusSlider(int)));
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnOutsideRadiusSlider(int _value)
{
  disconnect(this->dataPtr->outsideRadiusSpin, SIGNAL(valueChanged(double)),
      this, SLOT(OnOutsideRadiusSpin(double)));

  this->dataPtr->outsideRadiusSpin->setValue(_value /
      static_cast<double>(this->dataPtr->outsideRadiusSlider->maximum()));

  connect(this->dataPtr->outsideRadiusSpin, SIGNAL(valueChanged(double)),
      this, SLOT(OnOutsideRadiusSpin(double)));
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnInsideRadiusSpin(double _value)
{
  disconnect(this->dataPtr->insideRadiusSlider, SIGNAL(valueChanged(int)),
      this, SLOT(OnInsideRadiusSlider(int)));

  this->dataPtr->insideRadiusSlider->setValue(
      static_cast<int>(rint(
        _value * this->dataPtr->insideRadiusSlider->maximum())));

  connect(this->dataPtr->insideRadiusSlider, SIGNAL(valueChanged(int)),
      this, SLOT(OnInsideRadiusSlider(int)));
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnInsideRadiusSlider(int _value)
{
  disconnect(this->dataPtr->insideRadiusSpin, SIGNAL(valueChanged(double)),
      this, SLOT(OnInsideRadiusSpin(double)));


  this->dataPtr->insideRadiusSpin->setValue(_value /
      static_cast<double>(this->dataPtr->insideRadiusSlider->maximum()));

  connect(this->dataPtr->insideRadiusSpin, SIGNAL(valueChanged(double)),
      this, SLOT(OnInsideRadiusSpin(double)));
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnWeightSpin(double _value)
{
  disconnect(this->dataPtr->weightSlider, SIGNAL(valueChanged(int)),
      this, SLOT(OnWeightSlider(int)));

  this->dataPtr->weightSlider->setValue(
      static_cast<int>(rint(_value * this->dataPtr->weightSlider->maximum())));

  connect(this->dataPtr->weightSlider, SIGNAL(valueChanged(int)),
      this, SLOT(OnWeightSlider(int)));
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnWeightSlider(int _value)
{
  disconnect(this->dataPtr->weightSpin, SIGNAL(valueChanged(double)),
             this, SLOT(OnWeightSpin(double)));

  this->dataPtr->weightSpin->setValue(_value /
      static_cast<double>(this->dataPtr->weightSlider->maximum()));

  connect(this->dataPtr->weightSpin, SIGNAL(valueChanged(double)),
      this, SLOT(OnWeightSpin(double)));
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnHeightSpin(double _value)
{
  disconnect(this->dataPtr->heightSlider, SIGNAL(valueChanged(int)),
      this, SLOT(OnHeightSlider(int)));

  this->dataPtr->heightSlider->setValue(static_cast<int>(rint(_value)));

  connect(this->dataPtr->heightSlider, SIGNAL(valueChanged(int)),
      this, SLOT(OnHeightSlider(int)));
}

/////////////////////////////////////////////////
void TerrainEditorPalette::OnHeightSlider(int _value)
{
  disconnect(this->dataPtr->heightSpin, SIGNAL(valueChanged(double)),
      this, SLOT(OnHeightSpin(double)));

  this->dataPtr->heightSpin->setValue(_value / 10.0);

  connect(this->dataPtr->heightSpin, SIGNAL(valueChanged(double)),
      this, SLOT(OnHeightSpin(double)));
}
