/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2025 Univ. Grenoble Alpes, CNRS, Grenoble INP - UGA, TIMC, 38000 Grenoble, France
 *
 * Visit http://camitk.imag.fr for more information
 *
 * This file is part of CamiTK.
 *
 * CamiTK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * CamiTK 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 Lesser General Public License version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with CamiTK.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $CAMITK_LICENCE_END$
 ****************************************************************************/
#include "ExternalShaker.h"

#include <Application.h>
#include <Property.h>
#include <ActionWidget.h>

using namespace camitk;

// use an external library
#include "ShakerLib.h"

// --------------- Constructor -------------------
ExternalShaker::ExternalShaker(ActionExtension* extension) : Action(extension) {
    // Setting name, description and input component
    setName("External Shaker");
    setDescription("The shaker motor. It uses the shakerlib external library to move the mesh in all directions!");
    setComponentClassName("MeshComponent");

    // Setting classification family and tags
    setFamily("Tutorial");
    addTag("Animation");
    addTag("Demo");

    // Setting the action's parameters
    Property* intensity = new Property(tr("Shaking Intensity"), QVariant(1.0), tr("Max intensity for the sshhhhaakkiinggg..."), tr("%age of the bounding radius"));
    intensity->setAttribute("minimum", 0.0);
    intensity->setAttribute("maximum", 100.0);
    addParameter(intensity);

    addParameter(new Property(tr("Show Displacement"), QVariant(false), tr("Display the displacements in a color scale"), tr("boolean")));

    // automatically update the properties as soon as the user change something
    // => any modifications in the GUI are automatically applied to the property modified
    // as there is a timer calling the animate method, this means that the modification
    // will be automatically taken into account by the animate() method
    setAutoUpdateProperties(true);

    // create the timer
    processTimer = new QTimer(this);
    connect(processTimer, SIGNAL(timeout()), this, SLOT(animate()));
    // create the mutex that ensure animate has finished all its work before it is called again
    mutex = new QMutex();
}

// --------------- destructor -------------------
ExternalShaker::~ExternalShaker() {
    // Do not do anything yet.
    // Delete stuff if you create stuff
    // (except if you use smart pointers of course !!)
    delete processTimer;
}

// --------------- apply -------------------
Action::ApplyStatus ExternalShaker::apply() {
    // simulate on/off button
    if (processTimer->isActive()) {
        processTimer->stop();
    }
    else {
        processTimer->start(0);
        elapsedTime = 0.0;
        simulationCount = 0;
        currentTarget = dynamic_cast<MeshComponent*>(getTargets().last());

        // initialize velocity data
        initialPosition = vtkSmartPointer<vtkPoints>::New();
        initialPosition->DeepCopy(currentTarget->getPointSet()->GetPoints());

        // check and update the display
        bool showDisplacement = getParameterValue("Show Displacement").toBool();

        if (!showDisplacement) {
            currentTarget->setDataRepresentationOff();
        }
        else {
            // initialize displacement is 0.0
            vtkSmartPointer<vtkDoubleArray> displacement = vtkSmartPointer<vtkDoubleArray>::New();
            displacement->SetName("Displacement");
            displacement->SetNumberOfValues(currentTarget->getPointSet()->GetNumberOfPoints());
            for (vtkIdType i = 0; i < currentTarget->getPointSet()->GetNumberOfPoints(); ++i) {
                displacement->SetValue(i, 0.0);
            }

            currentTarget->addPointData(displacement->GetName(), displacement);
        }

        Property* fps = new Property(tr("FPS"), QVariant(0.0), tr("Frame Per Second (including display)"), tr("Hz"));
        fps->setReadOnly(true);
        currentTarget->addProperty(fps);
    }

    return SUCCESS;
}

// --------------- animate -------------------
void ExternalShaker::animate() {

    // if component was closed before the timer was stop, do it now
    if (!Application::isAlive(currentTarget)) {
        processTimer->stop();
        return;
    }

    // if theres is no current animation (mutex is a kind of thread safe guard)
    if (mutex->tryLock()) {
        // timer to compute the elapsed time for the FPS
        QElapsedTimer fpsTimer;
        fpsTimer.start();

        // Get the parameters
        double shakingIntensity = getParameterValue("Shaking Intensity").toDouble() / 100.0;

        // Move all the points
        float* position = (float*) currentTarget->getPointSet()->GetPoints()->GetData()->GetVoidPointer(0);
        ShakerLib::randomDisplacement(position, (unsigned int) currentTarget->getPointSet()->GetNumberOfPoints(), currentTarget->getBoundingRadius()*shakingIntensity);

        // update vtk structures
        currentTarget->getPointSet()->Modified();
        // update component status so as to warn the user when closing the shaked object
        currentTarget->setModified();

        // update displacement value
        if (getParameterValue("Show Displacement").toBool()) {
            vtkSmartPointer<vtkDoubleArray> displacement = vtkSmartPointer<vtkDoubleArray>::New();
            displacement->SetName("Displacement");
            displacement->SetNumberOfValues(currentTarget->getPointSet()->GetNumberOfPoints());

            for (vtkIdType id = 0; id < currentTarget->getPointSet()->GetNumberOfPoints(); id++) {
                double displacementVec[3];
                initialPosition->GetPoint(id, displacementVec);

                for (unsigned int j = 0; j < 3; j++)  {
                    displacementVec[j] -= position[id * 3 + j];
                }

                displacement->SetValue(id, sqrt(displacementVec[0]*displacementVec[0] + displacementVec[1]*displacementVec[1] + displacementVec[2]*displacementVec[2]));
            }

            // will replace the previous
            currentTarget->addPointData(displacement->GetName(), displacement);
        }

        // refresh the view
        currentTarget->refresh();

        elapsedTime += fpsTimer.elapsed();
        simulationCount++;
        currentTarget->setProperty("FPS", elapsedTime * 1000.0 / (double)simulationCount);

        mutex->unlock();
    }

}
