/*****************************************************************************
 * $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$
 ****************************************************************************/
// because of namespace problems with Component, put these include first
// Monitoring includes
#include <MonitoringManagerFactory.h>
#include <SimulatorFactory.h>

#include <MonitorIn.hxx>
#include <Translation.h>
#include <MultiComponent.h>

#include "GenerateModel.h"
#include <Component.h>
#include <Log.h>
#include <Core.h>
#include <Application.h>
#include <MeshComponent.h>
#include <ExtensionManager.h>
#include <Property.h>

#include <vtkSelectionNode.h>
#include <vtkIdTypeArray.h>

#include <QFileInfo>
#include <QDateTime>
#include <QFileDialog>


using namespace camitk;

// -------------------- GenerateModel --------------------
GenerateModel::GenerateModel(ActionExtension* extension) : Action(extension) {
    setName("Generate Model");
    setDescription("Generates a model from a mesh: mml, pml and corresponding lml are generated.<p>The current points selected by picking (<tt>Picked Selection</tt>) will be considered as fixed nodes.");
    setComponentClassName("MeshComponent");
    setFamily("Mesh Processing");
    addTag("Build Biomechanical Model");

    addParameter(new Property("Selection Is Constrained", true, "Use the current point selection to add a null displacement constraint to the model. If this is true, all the currently selected nodes will be fixed during the simulation", ""));

    addParameter(new Property("Gravity", QVector3D(-9.81, 0.0, 0.0), "Direction of the simulated gravity, X- is the default", "acceleration in m/s^2"));

    addParameter(new Property("SOFA Only", false, "Generate the model using only the SOFA simulator (without the help of MML", ""));
}


// --------------- apply -------------------
Action::ApplyStatus GenerateModel::apply() {
    // set waiting cursor
    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));

    //-- get the mesh, prepare the filename
    targetMesh = dynamic_cast<MeshComponent*>(getTargets().last());
    QString originalFilename = targetMesh->getFileName();
    if (originalFilename.isEmpty()) {
        // ask the user
        originalFilename = QFileDialog::getSaveFileName(nullptr, "Generate Model", "", "ModelingML (*.mml)");
        if (originalFilename.isNull()) {
            QApplication::restoreOverrideCursor();
            CAMITK_WARNING(tr("Filename required. Action Aborted."))
            return ABORTED;
        }
    }
    QString baseFilename = QFileInfo(originalFilename).absolutePath() + "/" + QFileInfo(originalFilename).completeBaseName();

    //-- 0) get the selected node index into an handy list
    fixedDOFIndex.clear();
    vtkSmartPointer<vtkSelectionNode> currentlySelected = targetMesh->getSelection("Picked Selection");
    if (currentlySelected != nullptr) {
        vtkSmartPointer<vtkIdTypeArray> idList = vtkIdTypeArray::SafeDownCast(currentlySelected->GetSelectionList());
        // if the selection is not empty
        if (idList != nullptr) {
            for (vtkIdType i = 0; i < idList->GetNumberOfTuples(); i++) {
                fixedDOFIndex.insert(idList->GetValue(i));
            }
        }
    }

    //-- 1) save the selected component as a .msh (to simplify .scn)
    targetMesh->setFileName(baseFilename + "-model.msh");
    Application::save(targetMesh);
    targetMesh->setFileName(originalFilename);   // just to avoid a discrepancy in case of another saveAs

    //-- 2) save the corresponding .scn file
    saveSOFAFile(baseFilename);

    //-- 3) save LML, PML, and MML
    if (!getParameterValue("SOFA Only").toBool()) {
        saveMMLFiles(baseFilename);
    }

    // restore the normal cursor
    QApplication::restoreOverrideCursor();

    return SUCCESS;
}

// --------------- saveSOFAFile -------------------
void GenerateModel::saveSOFAFile(QString baseFilename) {
    // largely inspired (i.e. completely copied) from liver.scn demo file
    QString SOFAFileName = baseFilename + "-model.scn";
    QString mshFilename = baseFilename + "-model.msh";
    QVector3D gravity = getParameterValue("Gravity").value<QVector3D>();

    std::ofstream scnFile(SOFAFileName.toUtf8());

    scnFile << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << endl;
    scnFile << "<!-- Created by " << Core::shortVersion() << " -->" << endl;
    QDateTime now = QDateTime::currentDateTime();
    scnFile << "<!-- " << now.toString().toStdString() << " " << now.toSecsSinceEpoch() << "-->" << endl;

    scnFile << "<Node name=\"root\" gravity=\"" << gravity.x() << " " << gravity.y() << " " << gravity.z() << "\" dt=\"0.02\" showBehaviorModels=\"0\" showCollisionModels=\"0\" showMappings=\"0\" showForceFields=\"0\">" << endl;

    scnFile << "  <DefaultPipeline name=\"CollisionPipeline\"  verbose=\"0\" />" << endl;
    scnFile << "  <BruteForceDetection name=\"N2\" />" << endl;
    scnFile << "  <DefaultContactManager name=\"collision response\"  response=\"default\"/>" << endl;
    scnFile << "  <Node name=\"" << targetMesh->getName().toStdString() << "\" gravity=\"0 -9.81 0\" depend=\"topo dofs\">" << endl;
    scnFile << "    <EulerImplicitSolver name=\"cg_odesolver\"  printLog=\"0\" />" << endl;
    scnFile << "    <CGLinearSolver template=\"GraphScattered\" name=\"linear solver\"  iterations=\"25\"  tolerance=\"1e-09\" threshold=\"1e-09\" />" << endl;
    scnFile << "    <MeshGmshLoader name=\"loader\"  filename=\"" << mshFilename.toStdString() << "\" />" << endl;
    scnFile << "    <TetrahedronSetTopologyContainer name=\"topo\" position=\"@loader.position\"  edges=\"@loader.edges\"  triangles=\"@loader.triangles\"  tetrahedra=\"@loader.tetras\"/>" << endl;
    scnFile << "    <MechanicalObject template=\"Vec3d\" name=\"dofs\"  position=\"0 0 0\"  velocity=\"0 0 0\"  force=\"0 0 0\"  derivX=\"0 0 0\"  free_position=\"0 0 0\"  free_velocity=\"0 0 0\"  restScale=\"1\" />" << endl;
    scnFile << "    <TetrahedronSetGeometryAlgorithms name=\"tetraGeo\" />" << endl;
    scnFile << "    <DiagonalMass template=\"Vec3d\" name=\"computed using mass density\"  massDensity=\"1\" />" << endl;
    scnFile << "    <TetrahedralCorotationalFEMForceField template=\"Vec3d\" name=\"FEM\"  method=\"large\"  poissonRatio=\"0.4\"  youngModulus=\"5000\"  computeGlobalMatrix=\"0\" />" << endl;

    if (getParameterValue("SOFA Only").toBool() && getParameterValue("Selection Is Constrained").toBool() && fixedDOFIndex.size() > 0) {
        // Without LML, you would have to do this:
        scnFile << "    <FixedConstraint template=\"Vec3d\" name=\"FixedConstraint\" indices=\"";
        std::set<int>::const_iterator pos;
        // preincrement and predecrement are usually faster than postincrement and postdecrement...
        for (pos = fixedDOFIndex.begin(); pos != fixedDOFIndex.end(); ++pos) {
            scnFile << *pos << " ";
        }

        scnFile << "\" />" << endl;
    }

    scnFile << "  </Node>" << endl;
    scnFile << "</Node>" << endl;
    scnFile.close();

}

// --------------- saveMMLFiles -------------------
void GenerateModel::saveMMLFiles(QString baseFilename) {
    //-- Generate LML
    QString lmlFilename;
    if (fixedDOFIndex.size() > 0) {
        Loads l;
        Translation* t = new Translation();
        std::set<int>::const_iterator pos;
        for (pos = fixedDOFIndex.begin(); pos != fixedDOFIndex.end(); ++pos) {
            t->addTarget(*pos);
        }
        t->setUnit(TranslationUnit::MM());
        Direction d;
        d.setNullX();
        d.setNullY();
        d.setNullZ();
        t->setDirection(d);
        t->addValueEvent(1, 0.0);
        l.addLoad(t);

        lmlFilename = baseFilename + "-boundary-condition.lml";
        std::ofstream lmlofs(lmlFilename.toStdString().c_str());
        l.xmlPrint(lmlofs);
        lmlofs.close();
    }

    //-- Generate MML
    // generate a default runable mml using this .scn
    // dt and refresh
    QString pmlFilename = QFileInfo(baseFilename).completeBaseName() + "-model.pml"; // in the mml, the pml file name is relative
    mml::TimeParameter dt = mml::TimeParameter(0.1, "s");
    mml::TimeParameter refresh = mml::TimeParameter(0.1, "s");
    mml::MonitoringIn mmlIn(dt, refresh, "sofa");

    // add the simulator file
    QString SOFAFileName = baseFilename + "-model.scn";
    mmlIn.simulatorFile(QFileInfo(SOFAFileName).fileName().toStdString().c_str());

    // add stopping criteria
    mml::Position notALotOfMovement(mml::Threshold(0.1), "mm");
    notALotOfMovement.method().scope(mml::Scope::Any);
    mml::Time enoughTimeToStart(mml::MinThreshold(0.5), "s");
    enoughTimeToStart.method().scope(mml::Scope::Any);
    mml::MultipleCriteria multipleCriteria(mml::LogicalOp::And);
    multipleCriteria.criteria().push_back(notALotOfMovement);
    multipleCriteria.criteria().push_back(enoughTimeToStart);
    mml::StoppingCriteria stoppingCriteria;
    stoppingCriteria.multipleCriteria(multipleCriteria);
    mmlIn.stoppingCriteria(stoppingCriteria);

    //TODO
    // add monitors
    //mml::Monitor displacement(mml::TimeParameter(0.0,"s"),mml::TimeParameter(1000000.0,"s"),mml::MonitorType::PointSetDistance,1,QFileInfo(pmlFilename).fileName().toStdString().c_str());//TODO change!
    //mml::Monitors monitors;
    //monitors.monitor().push_back(displacement);
    //mmlIn.monitors(monitors);

    // add lml
    if (fixedDOFIndex.size() > 0) {
        // in the mml the lml fileName is relative
        mmlIn.lmlFile(QFileInfo(lmlFilename).fileName().toStdString().c_str());
    }

    xml_schema::namespace_infomap map;
    map[""].name = "";
    map[""].schema = "";

    QString mmlFilename = baseFilename + "-model.mml";
    std::ofstream mmlofs(mmlFilename.toStdString().c_str());
    mml::monitoringIn(mmlofs, mmlIn, map);
    mmlofs.close();

    //-- Automatically generate proper PML from SCN
    // (this is a temporary hack) read the mml file to create the proper pml
    // this hack should become a call to a static method to transform a sofa scn to a pml
    MonitoringManager* mml = MonitoringManagerFactory::createManager(mmlFilename.toStdString().c_str());

    if (mml != nullptr) {
        // Modify mml to use pml file instead of sofa
        mmlIn.simulatorFile().reset();
        mmlIn.pmlFile(pmlFilename.toStdString().c_str());
        // reopen for rewrite
        mmlofs.open(mmlFilename.toStdString().c_str(), std::ios_base::trunc);
        mml::monitoringIn(mmlofs, mmlIn, map);
        mmlofs.close();

        // Modify the pml to have the selected nodes fixed
        QString completePmlFilename = baseFilename + "-model.pml";
        PhysicalModel* pml = new PhysicalModel(completePmlFilename.toStdString().c_str());

        // add the informative components containing the fixed dof
        if (fixedDOFIndex.size() > 0 && getParameterValue("Selection Is Constrained").toBool()) {
            StructuralComponent* fixedDof = new StructuralComponent(pml, "Fixed Dof");
            std::set<int>::const_iterator pos;
            for (pos = fixedDOFIndex.begin(); pos != fixedDOFIndex.end(); ++pos) {
                fixedDof->addStructure(pml->getAtom(*pos));
            }
            pml->getInformativeComponents()->addSubComponent(fixedDof);
        }

        // reopen for rewrite
        std::ofstream pmlofs(completePmlFilename.toStdString().c_str(), std::ios_base::trunc);
        pml->xmlPrint(pmlofs);
        pmlofs.close();

        Application::open(mmlFilename);
    }
    else {
        CAMITK_WARNING(tr("Generating Error: problem while generating the model: Sofa Simulator not supported"))
    }
}

