/**********************************************************************
  AutoOptTool - Automatic Optimization Tool for Avogadro

  Copyright (C) 2007,2008 by Marcus D. Hanwell
  Copyright (C) 2007 by Geoffrey R. Hutchison
  Copyright (C) 2007 by Benoit Jacob
  Copyright (C) 2008 by Tim Vandermeersch

  This file is part of the Avogadro molecular editor project.
  For more information, see <http://avogadro.cc/>

  Some code is based on Open Babel
  For more information, see <http://openbabel.sourceforge.net/>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation version 2 of the License.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
 ***********************************************************************/

#include "autoopttool.h"
#include <avogadro/navigate.h>
#include <avogadro/primitive.h>
#include <avogadro/atom.h>
#include <avogadro/bond.h>
#include <avogadro/color.h>
#include <avogadro/glwidget.h>
#include <avogadro/camera.h>
#include <avogadro/painter.h>
#include <avogadro/toolgroup.h>
#include <avogadro/primitivelist.h>

#include <openbabel/obiter.h>
#include <openbabel/mol.h>
#include <openbabel/obconversion.h>

#include <QtCore/QDebug>
#include <QtCore/QtPlugin>
#include <QtGui/QLabel>
#include <QtGui/QVBoxLayout>

using namespace std;
using namespace OpenBabel;
using namespace Eigen;

namespace Avogadro {

  AutoOptTool::AutoOptTool(QObject *parent) : Tool(parent), m_clickedAtom(0),
  m_leftButtonPressed(false), m_midButtonPressed(false), m_rightButtonPressed(false),
  m_running(false), m_block(false), m_setupFailed(false), m_timerId(0) ,m_toolGroup(0),
  m_settingsWidget(0), m_thread(0), m_lastEnergy(0.0)
  {
    QAction *action = activateAction();
    action->setIcon(QIcon(QString::fromUtf8(":/autoopttool/autoopttool.png")));
    action->setToolTip(tr("Auto Optimization Tool\n\n"
          "Navigation Functions when clicking in empty space.\n"
          "Left Mouse: \tRotate Space\n"
          "Middle Mouse: Zoom Space\n"
          "Right Mouse: \tMove Space\n"
          "Double-Click: \t Reset View\n\n"
          "When running:\n"
          "Left Mouse: \tClick and drag atoms to move them."));
    // An OBConverison object must be instantiated before the
    // FindForceField call will work.
    OBConversion conv; Q_UNUSED(conv);
    m_forceField = OBForceField::FindForceField( "UFF" );
    // Check that the force field exists and was initialised OK
    if (!m_forceField) {
      // We can't do anything is the force field cannot be found - OB issue
      emit setupFailed();
      return;
    }
    m_thread = new AutoOptThread;
    connect(m_thread, SIGNAL(finished(bool)), this, SLOT(finished(bool)));
    connect(m_thread, SIGNAL(setupDone()),    this, SLOT(setupDone()));
    connect(m_thread, SIGNAL(setupFailed()),  this, SLOT(setupFailed()));
    connect(m_thread, SIGNAL(setupSucces()),  this, SLOT(setupSucces()));

    OBPlugin::ListAsVector("forcefields", "ids", m_forceFieldList);
    //action->setShortcut(Qt::Key_F10);
  }

  AutoOptTool::~AutoOptTool()
  {
    if (m_thread) {
      m_thread->exit();
      m_thread->wait();
      delete m_thread;
      m_thread = 0;
    }
    if (m_settingsWidget)
      m_settingsWidget->deleteLater();
  }

  int AutoOptTool::usefulness() const
  {
    return 10;
  }

  void AutoOptTool::translate(GLWidget *widget, const Eigen::Vector3d &what,
                              const QPoint &from, const QPoint &to) const
  {
    // Translate the selected atoms in the x and y sense of the view
    Vector3d fromPos = widget->camera()->unProject(from, what);
    Vector3d toPos = widget->camera()->unProject(to, what);

    Vector3d atomTranslation = toPos - fromPos;

    if (widget->selectedPrimitives().size())
    {
      foreach(Primitive *p, widget->selectedPrimitives())
      {
        if (p->type() == Primitive::AtomType)
        {
          Atom *a = static_cast<Atom *>(p);
          a->setPos(atomTranslation + *a->pos());
          a->update();
        }
      }
    }

    if (m_clickedAtom)
    {
      m_clickedAtom->setPos(atomTranslation + *m_clickedAtom->pos());
      m_clickedAtom->update();
    }
  }

  QUndoCommand* AutoOptTool::mousePressEvent(GLWidget *widget,
                                             QMouseEvent *event)
  {
    m_glwidget = widget;
    m_lastDraggingPosition = event->pos();

    m_leftButtonPressed = (event->buttons() & Qt::LeftButton
        && event->modifiers() == Qt::NoModifier);
    // On the Mac, either use a three-button mouse
    // or hold down the Shift key
    m_midButtonPressed = ((event->buttons() & Qt::MidButton) ||
        (event->buttons() & Qt::LeftButton && event->modifiers()
         & Qt::ShiftModifier));
    // Hold down the Command key (ControlModifier in Qt notation) for right button
    m_rightButtonPressed = ((event->buttons() & Qt::RightButton) ||
        (event->buttons() & Qt::LeftButton &&
         (event->modifiers() == Qt::ControlModifier ||
          event->modifiers() == Qt::MetaModifier)));

    m_clickedAtom = widget->computeClickedAtom(event->pos());
    if(m_clickedAtom != 0 && m_leftButtonPressed && m_running)
    {
      event->accept();
      if (m_forceField->GetConstraints().IsIgnored(m_clickedAtom->index()+1) &&
          !m_ignoredMovable->isChecked()) {
        m_clickedAtom = 0;
      }
      else if (m_forceField->GetConstraints().IsFixed(m_clickedAtom->index()+1) &&
               !m_fixedMovable->isChecked()) {
        m_clickedAtom = 0;
      }

      if (m_clickedAtom)
        m_forceField->SetFixAtom(m_clickedAtom->index()+1);
    }

    widget->update();
    return 0;
  }

  QUndoCommand* AutoOptTool::mouseReleaseEvent(GLWidget *widget, QMouseEvent *)
  {
    m_glwidget = widget;

    m_leftButtonPressed = false;
    m_midButtonPressed = false;
    m_rightButtonPressed = false;

    m_clickedAtom = 0;
    m_forceField->UnsetFixAtom();

    widget->update();
    return 0;
  }

  QUndoCommand* AutoOptTool::mouseMoveEvent(GLWidget *widget,
                                            QMouseEvent *event)
  {
    m_glwidget = widget;
    if(!widget->molecule())
      return 0;

    // Get the currently selected atoms from the view
    PrimitiveList currentSelection = widget->selectedPrimitives();

    // Manipulation can be performed in two ways - centred on an individual atom

    if (m_clickedAtom && m_running) {
      if (m_leftButtonPressed) {
        event->accept();
        // translate the molecule following mouse movement
        Vector3d begin = widget->camera()->project(*m_clickedAtom->pos());
        QPoint point = QPoint(begin.x(), begin.y());
        translate(widget, *m_clickedAtom->pos(),
                  point/*m_lastDraggingPosition*/, event->pos());
      }
    }

    m_lastDraggingPosition = event->pos();
    widget->update();

    return 0;
  }

  QUndoCommand* AutoOptTool::mouseDoubleClickEvent(GLWidget*, QMouseEvent*)
  {
    return 0;
  }

  QUndoCommand* AutoOptTool::wheelEvent(GLWidget*, QWheelEvent*)
  {
    return 0;
  }

  bool AutoOptTool::paint(GLWidget *widget)
  {
    QPoint labelPos(10, 10);
    QPoint debugPos(10, 50);
    glColor3f(1.0,1.0,1.0);
    if (m_running) {
      if (m_setupFailed) {
        widget->painter()->drawText(labelPos,
                                    tr("AutoOpt: Could not setup force field...."));
      }
      else {
        double energy = m_forceField->Energy(false);
        if (m_forceField->GetUnit().find("kcal") != string::npos)
          energy *= KCAL_TO_KJ;
        widget->molecule()->setEnergy(energy);
        widget->painter()->drawText(labelPos,
            tr("AutoOpt: E = %1 %2 (dE = %3)").arg(energy).
            arg("kJ/mol").
            arg( fabs(m_lastEnergy - energy) ));
        widget->painter()->drawText(debugPos,
                                    tr("Num Constraints: %1").arg(m_forceField->GetConstraints().Size()));
        m_lastEnergy = energy;
      }
    }

    m_glwidget = widget;
    if(m_leftButtonPressed) {
      if(m_running && m_clickedAtom) {
        // Don't highlight the atom on right mouse unless there is a selection
        double renderRadius = widget->radius(m_clickedAtom);
        renderRadius += 0.10;
        glEnable(GL_BLEND);
        widget->painter()->setColor(1.0, 0.3, 0.3, 0.7);
        widget->painter()->drawSphere(m_clickedAtom->pos(), renderRadius);
        glDisable(GL_BLEND);
      }
    }
    return true;
  }

  QWidget* AutoOptTool::settingsWidget()
  {
    if(!m_settingsWidget) {
      m_settingsWidget = new QWidget;

      QLabel* labelFF = new QLabel(tr("Force Field:"));

      m_comboFF = new QComboBox(m_settingsWidget);
      for (unsigned int i = 0; i < m_forceFieldList.size(); ++i)
        m_comboFF->addItem(m_forceFieldList[i].c_str());

      // find UFF for the default item
      int currentFF = m_comboFF->findText("UFF");
      if (currentFF != -1) // couldn't find it, go for index 0
        m_comboFF->setCurrentIndex(currentFF);

      QGridLayout* grid = new QGridLayout;
      grid->addWidget(labelFF, 0, 0, Qt::AlignRight);
      grid->addWidget(m_comboFF, 0, 1, Qt::AlignLeft);

      QLabel* labelSteps = new QLabel(tr("Steps per Update:"));
      labelSteps->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);

      m_stepsSpinBox = new QSpinBox(m_settingsWidget);
      m_stepsSpinBox->setMinimum(1);
      m_stepsSpinBox->setMaximum(100);
      m_stepsSpinBox->setValue(4);

      grid->addWidget(labelSteps, 1, 0, Qt::AlignRight);
      grid->addWidget(m_stepsSpinBox, 1, 1, Qt::AlignLeft);

      QLabel* labelAlg = new QLabel(tr("Algorithm:"));

      m_comboAlgorithm = new QComboBox(m_settingsWidget);
      m_comboAlgorithm->addItem(tr("Steepest Descent"));
      m_comboAlgorithm->addItem(tr("Conjugate Gradients"));
      m_comboAlgorithm->addItem(tr("Molecular Dynamics (300K)"));
      m_comboAlgorithm->addItem(tr("Molecular Dynamics (600K)"));
      m_comboAlgorithm->addItem(tr("Molecular Dynamics (900K)"));

      m_buttonStartStop = new QPushButton(tr("Start"), m_settingsWidget);

      m_fixedMovable = new QCheckBox(tr("Fixed atoms are movable"),
                                     m_settingsWidget);
      m_ignoredMovable = new QCheckBox(tr("Ignored atoms are movable"),
                                       m_settingsWidget);

      QVBoxLayout* layout = new QVBoxLayout();
      layout->addLayout(grid); // force field, steps and labels

      layout->addWidget(labelAlg);
      layout->addWidget(m_comboAlgorithm);
      layout->addWidget(m_fixedMovable);
      layout->addWidget(m_ignoredMovable);
      layout->addWidget(m_buttonStartStop);
      layout->addStretch(1);
      m_settingsWidget->setLayout(layout);

      // Connect the start/stop button
      connect(m_buttonStartStop, SIGNAL(clicked()), this, SLOT(toggle()));

      connect(m_settingsWidget, SIGNAL(destroyed()),
              this, SLOT(settingsWidgetDestroyed()));

      // Check the force field is there, if not disable the start button
      if (!m_forceField)
        m_buttonStartStop->setEnabled(false);
    }

    return m_settingsWidget;
  }

  void AutoOptTool::settingsWidgetDestroyed()
  {
    m_settingsWidget = 0;
  }

  void AutoOptTool::toggle()
  {
    // Toggle the timer on and off
    if (m_running)
      disable();
    else
      enable();
  }

  void AutoOptTool::enable()
  {
    // If the force field is false we have nothing and so should return
    if (!m_forceField)
      return;

    if(!m_running) {
      connect(m_glwidget->molecule(), SIGNAL(destroyed()), this, SLOT(abort()));
      m_thread->setup(m_glwidget->molecule(), m_forceField,
                      m_comboAlgorithm->currentIndex(),
                      m_stepsSpinBox->value());
      m_thread->start();
      m_running = true;
      m_buttonStartStop->setText(tr("Stop"));
      QUndoStack *stack = m_glwidget->undoStack();
      AutoOptCommand *cmd = new AutoOptCommand(m_glwidget->molecule(),this,0);
      if(stack && cmd)
        stack->push(cmd);
      else
        delete cmd;
    }
  }

  void AutoOptTool::abort()
  {
    killTimer(m_timerId);
    m_thread->quit();
    m_running = false;
  }

  void AutoOptTool::disable()
  {
    if(m_running) {
      if(m_timerId) {
        killTimer(m_timerId);
        m_timerId = 0;
      }
      m_thread->quit();
      m_running = false;
      m_setupFailed = false;
      m_buttonStartStop->setText(tr("Start"));

      m_glwidget->update(); // redraw AutoOpt label

      m_clickedAtom = 0;
      m_forceField->UnsetFixAtom();
      m_leftButtonPressed = false;
      m_midButtonPressed = false;
      m_rightButtonPressed = false;
    }
  }

  void AutoOptTool::timerEvent(QTimerEvent*)
  {
    if(m_block || m_glwidget->molecule()->numAtoms() < 2)
      return;
    else
      m_block = true;

    m_forceField =
        OBForceField::FindForceField(m_forceFieldList[m_comboFF->currentIndex()]);

    // Check that we can find our force field - if not return
    if (!m_forceField) {
      emit setupFailed();
      return;
    }

    m_thread->setup(m_glwidget->molecule(), m_forceField,
                    m_comboAlgorithm->currentIndex(),
                    m_stepsSpinBox->value());
    m_thread->update();
  }

  void AutoOptTool::finished(bool calculated)
  {
    if (m_running && calculated) {
      QList<Atom*> atoms = m_glwidget->molecule()->atoms();

      OBMol mol = m_glwidget->molecule()->OBMol();
      m_forceField->GetCoordinates( mol );
      // forces
      if (mol.HasData(OBGenericDataType::ConformerData)) {
        OBConformerData *cd = (OBConformerData*) mol.GetData(OBGenericDataType::ConformerData);
        const vector<vector<vector3> > &allForces = cd->GetForces();
        if (allForces.size()) {
          const vector<vector3> &forces = allForces[0];
          if (forces.size() == mol.NumAtoms()) {
            foreach(Atom* atom, atoms) {
              atom->setForceVector(Eigen::Vector3d(forces[atom->index()].AsArray()));
            }
          }
        }
      }
      // coordinates
      double *coordPtr = mol.GetCoordinates();
      foreach(Atom* atom, atoms) {
        atom->setPos(Eigen::Vector3d(coordPtr));
        coordPtr += 3;
      }

      if(m_clickedAtom && m_leftButtonPressed) {
        Vector3d begin = m_glwidget->camera()->project(*m_clickedAtom->pos());
        QPoint point = QPoint(begin.x(), begin.y());
        translate(m_glwidget, *m_clickedAtom->pos(), point,
                  m_lastDraggingPosition);
      }
    }

    m_glwidget->molecule()->update();
    m_glwidget->update();
    m_block = false;
  }

  void AutoOptTool::setupDone()
  {
    if(!m_timerId)
      m_timerId = startTimer(50);
  }

  void AutoOptTool::setupFailed()
  {
    m_setupFailed = true;
  }

  void AutoOptTool::setupSucces()
  {
    m_setupFailed = false;
  }

  AutoOptThread::AutoOptThread(QObject*)
  {
    m_stop = false;
    m_velocities = false;
  }

  void AutoOptThread::setup(Molecule *molecule,
                            OpenBabel::OBForceField* forceField,
                            int algorithm, int steps)
  {
    m_mutex.lock();
    m_molecule = molecule;
    m_forceField = forceField;
    m_algorithm = algorithm;
    m_steps = steps;
    m_stop = false;
    m_velocities = false;
    m_mutex.unlock();
    emit setupDone();
  }


  void AutoOptThread::run()
  {
    exec();
  }

  void AutoOptThread::update()
  {
    // If the force field is false we have nothing and so should return
    if (!m_forceField)
      return;

    m_mutex.lock();

    m_forceField->SetLogFile(NULL);
    m_forceField->SetLogLevel(OBFF_LOGLVL_NONE);

    OBMol mol = m_molecule->OBMol();

    // Ignore all atoms with atomic # less than 1
    foreach(const Atom *atom, m_molecule->atoms()) {
      if (atom->atomicNumber() < 1)
        m_forceField->GetConstraints().AddIgnore(atom->index() + 1);
    }

    if (!m_forceField->Setup(mol)) {
      m_stop = true;
      emit setupFailed();
      emit finished(false);
      m_mutex.unlock();
      return;
    }
    else
      emit setupSucces();

    m_forceField->SetConformers(mol);

    switch(m_algorithm) {
      case 0:
        m_forceField->SteepestDescent(m_steps);
        break;
      case 1:
        m_forceField->ConjugateGradients(m_steps);
        break;
      case 2:
        m_forceField->MolecularDynamicsTakeNSteps(m_steps, 300, 0.001);
        break;
      case 3:
        m_forceField->MolecularDynamicsTakeNSteps(m_steps, 600, 0.001);
        break;
      case 4:
        m_forceField->MolecularDynamicsTakeNSteps(m_steps, 900, 0.001);
        break;
    }

    m_mutex.unlock();

    emit finished(m_stop ? false : true);
  }

  void AutoOptThread::stop()
  {
    m_stop = true;
  }

  AutoOptCommand::AutoOptCommand(Molecule *molecule, AutoOptTool *tool,
                                 QUndoCommand *parent)
    : QUndoCommand(parent), m_molecule(0)
  {
    // Store the original molecule before any modifications are made
    setText(QObject::tr("AutoOpt Molecule"));
    m_moleculeCopy = *molecule;
    m_molecule = molecule;
    m_tool = tool;
  }

  void AutoOptCommand::redo()
  {
    m_moleculeCopy = *m_molecule;
  }

  void AutoOptCommand::undo()
  {
    if(m_tool)
      m_tool->disable();
    *m_molecule = m_moleculeCopy;
  }

  bool AutoOptCommand::mergeWith (const QUndoCommand *)
  {
    // Just return true to repeated calls - we have stored the original molecule
    return true;
  }

  int AutoOptCommand::id() const
  {
    return 1311387;
  }

  void AutoOptTool::writeSettings(QSettings &settings) const
  {
    Tool::writeSettings(settings);
    settings.setValue("forceField", m_comboFF->currentIndex());
    settings.setValue("algorithm", m_comboAlgorithm->currentIndex());
    settings.setValue("steps", m_stepsSpinBox->value());
    settings.setValue("fixedMovable", m_fixedMovable->checkState());
    settings.setValue("ignoredMovable", m_ignoredMovable->checkState());
  }

  void AutoOptTool::readSettings(QSettings &settings)
  {
    Tool::readSettings(settings);
    if (m_comboFF) {
      int currentFF = settings.value("forceField", -1).toInt();
      if (currentFF == -1) {
        // haven't set a default
        // find UFF for default
        currentFF = m_comboFF->findText("UFF");
        if (currentFF == -1) // couldn't find it, go for index 0
          currentFF = 0;
      }
      m_comboFF->setCurrentIndex(currentFF);
    }
    if (m_comboAlgorithm)
      m_comboAlgorithm->setCurrentIndex(settings.value("algorithm", 0).toInt());
    if (m_stepsSpinBox)
      m_stepsSpinBox->setValue(settings.value("steps", 4).toInt());
    if (m_fixedMovable) {
      m_fixedMovable->setCheckState(
            static_cast<Qt::CheckState>(settings.value("fixedMovable", 2).toInt()));
    }
    if(m_ignoredMovable) {
      m_ignoredMovable->setCheckState(
            static_cast<Qt::CheckState>(settings.value("ignoredMovable", 2).toInt()));
    }
  }

}

Q_EXPORT_PLUGIN2(autoopttool, Avogadro::AutoOptToolFactory)
