/*****************************************************************************
 * $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 "CreateSC.h"

#include <PMLComponent.h>
#include <PhysicalModel.h>
#include <MultiComponent.h>

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

#include <QVector3D>
#include <qregularexpression.h>

using namespace camitk;

// -------------------- CreateSC --------------------
CreateSC::CreateSC(ActionExtension* extension) : Action(extension) {
    checkEvents = false;

    this->setName("Create Structural Component From Selection");
    this->setDescription("Generates a Structural Component from a given bounding box or current PML explorer selection");
    this->setComponentClassName("PMLComponent");
    this->setFamily("Physical Model");
    this->addTag("CreateSC");
    this->addTag("Select");

    addParameter(new Property("Name Of The New Component", "selection", tr("Name of the newly created PML component."), ""));
    addParameter(new Property("Use PML Selection", (bool) false, tr("Use the selected PML atoms and cells."), ""));
    addParameter(new Property("Use Picking Selection", (bool) false, tr("Use the piked selection."), ""));
    addParameter(new Property("Bottom Corner", QVector3D(0.0, 0.0, 0.0), tr("Bottom corner of the selection bounding box."), ""));
    addParameter(new Property("Top Corner", QVector3D(0.0, 0.0, 0.0), tr("Top corner of the selection bounding box."), ""));

    // every time a property is changed, the action will be informed, see method event(..)
    setAutoUpdateProperties(true);
}

// ---------------------- event ----------------------------
bool CreateSC::event(QEvent* e) {
    if (checkEvents && e->type() == QEvent::DynamicPropertyChange) {
        e->accept();
        QDynamicPropertyChangeEvent* changeEvent = dynamic_cast<QDynamicPropertyChangeEvent*>(e);

        if (!changeEvent) {
            return false;
        }

        // if the user change the boolean properties, update bounds
        PMLComponent* pmlComponent = dynamic_cast<PMLComponent*>(getTargets().last());

        if (pmlComponent != nullptr && (changeEvent->propertyName() == "Use PML Selection" || changeEvent->propertyName() == "Use Picking Selection")) {
            updateCorners(pmlComponent);
        }

        return true;
    }

    return QObject::event(e);
}

// --------------- getWidget -------------------
QWidget* CreateSC::getWidget() {

    //-- init the property values using the selection state
    if (getTargets().size() == 1) {
        PMLComponent* pmlComponent = dynamic_cast<PMLComponent*>(getTargets().last());

        //-- check if name exists, generate new if it does
        QString selectionName = property("Name Of The New Component").toString();

        while (pmlComponent->getPhysicalModel()->getComponentByName(selectionName.toStdString()) != nullptr) {
            // If the name contains a number then increment it
            QRegularExpression numSearch("(^|[^\\d])(\\d+)");   // we want to match as far left as possible, and when the number is at the start of the name

            // Does it have a number?
            QRegularExpressionMatch match;
            int start = selectionName.lastIndexOf(numSearch, 0, &match);
            if (match.hasMatch()) {
                // It has a number, increment it
                start = match.capturedStart(2);   // we are only interested in the second group
                QString numAsStr = match.captured(2);
                QString number = QString::number(numAsStr.toInt() + 1);
                number = number.rightJustified(numAsStr.length(), '0');
                selectionName.replace(start, numAsStr.length(), number);
            }
            else {
                // no number
                start = selectionName.lastIndexOf('.');

                if (start != -1) {
                    // has a . somewhere, e.g. it has an extension
                    selectionName.insert(start, '1');
                }
                else {
                    // no extension, just tack it on to the end
                    selectionName += '1';
                }
            }
        }

        setProperty("Name Of The New Component", selectionName);

        // update corner values depending on the selection (if this is what the user wants)
        updateCorners(pmlComponent);

        // now that we have a widget, consider all events
        checkEvents = true;
    }

    return Action::getWidget();
}

// --------------- computeBoundsOfPointIds -------------------
void CreateSC::computeBoundsOfPointIds(PMLComponent* comp, vtkSmartPointer< vtkIdTypeArray > selectedIds, double bounds[6]) {
    // compute the bounds of the selected atoms only
    Atom* a;
    double pos[3];
    PhysicalModel* pml = comp->getPhysicalModel();

    // bounds = [xmin,xmax, ymin,ymax, zmin,zmax]
    bounds[0] = bounds[2] = bounds[4] = VTK_DOUBLE_MAX;
    bounds[1] = bounds[3] = bounds[5] = -VTK_DOUBLE_MAX;

    for (vtkIdType i = 0; i < selectedIds->GetNumberOfTuples(); i++) {
        a = pml->getAtom(selectedIds->GetValue(i));

        if (a) {
            a->getPosition(pos);

            for (int j = 0; j < 3; j++) {
                if (pos[j] < bounds[2 * j]) {
                    bounds[2 * j] = pos[j];
                }

                if (pos[j] > bounds[2 * j + 1]) {
                    bounds[2 * j + 1] = pos[j];
                }
            }
        }
    }
}

// --------------- computeBoundsOfCellIds -------------------
void CreateSC::computeBoundsOfCellIds(PMLComponent* comp, vtkSmartPointer< vtkIdTypeArray > selectedCellIds, double bounds[6]) {
    vtkSmartPointer< vtkIdTypeArray > selectedIds = vtkSmartPointer<vtkIdTypeArray>::New();
    Cell* c;
    PhysicalModel* pml = comp->getPhysicalModel();

    for (vtkIdType i = 0; i < selectedCellIds->GetNumberOfTuples(); i++) {
        c = pml->getCell(selectedCellIds->GetValue(i));

        // insert all the cell atoms
        for (unsigned int j = 0; j < c->getNumberOfStructures(); j++) {
            selectedIds->InsertNextValue(c->getStructure(j)->getIndex());
        }
    }

    computeBoundsOfPointIds(comp, selectedIds, bounds);
}

// --------------- mergeBounds -------------------
void CreateSC::mergeBounds(double bounds1[6], double bounds2[6]) {
    // [xmin,xmax, ymin,ymax, zmin,zmax]
    for (int j = 0; j < 3; j++) {
        if (bounds1[2 * j] < bounds2[2 * j]) {
            // negative expansion
            bounds2[2 * j] = bounds1[2 * j];
        }

        if (bounds1[2 * j + 1] > bounds2[2 * j + 1]) {
            // position expansion
            bounds2[2 * j + 1] = bounds1[2 * j + 1];
        }
    }
}

// --------------- computeBoundsOfSelection -------------------
bool CreateSC::computeBoundsOfSelection(PMLComponent* pmlComponent, QString selectionName, double bounds[6]) {
    if (pmlComponent == nullptr) {
        return false;
    }

    // get the point selection
    vtkSmartPointer<vtkSelectionNode> selectionNode = pmlComponent->getSelection(selectionName);

    if (selectionNode) {
        vtkSmartPointer<vtkIdTypeArray> indices = vtkIdTypeArray::SafeDownCast(selectionNode->GetSelectionList());

        if (indices->GetNumberOfTuples() > 0) {
            if (selectionNode->GetFieldType() == vtkSelectionNode::POINT) {
                computeBoundsOfPointIds(pmlComponent, indices, bounds);
                return true;
            }
            else if (selectionNode->GetFieldType() == vtkSelectionNode::CELL) {
                computeBoundsOfCellIds(pmlComponent, vtkIdTypeArray::SafeDownCast(selectionNode->GetSelectionList()), bounds);
                return true;
            }
        }
    }

    return false;

}


// --------------- updateCorners -------------------
void CreateSC::updateCorners(PMLComponent* pmlComponent) {
    if (pmlComponent == nullptr) {
        return;
    }

    double bounds[6];
    // bounds = [xmin,xmax, ymin,ymax, zmin,zmax]
    bounds[0] = bounds[2] = bounds[4] = VTK_DOUBLE_MAX;
    bounds[1] = bounds[3] = bounds[5] = -VTK_DOUBLE_MAX;

    bool usePMLSelection = property("Use PML Selection").toBool();
    bool usePickingSelection = property("Use Picking Selection").toBool();
    bool boundsInitialized = false;

    // restrain to the selection
    if (usePMLSelection) {
        // get the point selection
        boundsInitialized = computeBoundsOfSelection(pmlComponent, "Selected Atoms", bounds);
        // get the cell selection
        double cellBounds[6];
        bool cellBoundsIsNotEmpty = computeBoundsOfSelection(pmlComponent, "Selected Cells", cellBounds);
        boundsInitialized = boundsInitialized || cellBoundsIsNotEmpty;
        if (cellBoundsIsNotEmpty) {
            mergeBounds(cellBounds, bounds);
        }
    }
    else if (usePickingSelection) {
        // get the point selection
        boundsInitialized = computeBoundsOfSelection(pmlComponent, "Picked Selection", bounds);
    }

    if (!boundsInitialized) {
        pmlComponent->getBounds(bounds);
    }

    setProperty("Bottom Corner", QVector3D(bounds[0], bounds[2], bounds[4]));
    setProperty("Top Corner", QVector3D(bounds[1], bounds[3], bounds[5]));
}

// --------------- apply -------------------
Action::ApplyStatus CreateSC::apply() {
    // Create Structural Component in getTargets().last()

    // set waiting cursor and status bar
    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
    Application::showStatusBarMessage("Creating New Structural Component...");
    Application::resetProgressBar();

    // use the last target
    PMLComponent* pmlComponent = dynamic_cast<PMLComponent*>(getTargets().last());
    PhysicalModel* pm = pmlComponent->getPhysicalModel();

    // update corner and selection settings
    updateCorners(pmlComponent);

    // create the new component
    StructuralComponent* selection;
    QVector3D lower = property("Bottom Corner").value<QVector3D>();
    QVector3D top = property("Top Corner").value<QVector3D>();

    QString selectionName = property("Name Of The New Component").toString();
    selection = new StructuralComponent(pm, selectionName.toStdString());
    Cell* selectionC = new Cell(pm, StructureProperties::POLY_VERTEX);
    selection->addStructure(selectionC);

    unsigned int nbSelected = 0;
    Atom* a;
    double pos[3];

    for (unsigned int i = 0; i < pm->getAtoms()->getNumberOfStructures(); i++) {
        // get the src position
        a = (Atom*) pm->getAtoms()->getStructure(i);
        a->getPosition(pos);

        // should it be selected
        if (pos[0] >= lower.x() && pos[0] <= top.x() && pos[1] >= lower.y() && pos[1] <= top.y() && pos[2] >= lower.z() && pos[2] <= top.z()) {
            selectionC->addStructureIfNotIn(a);
            nbSelected++;
        }
    }

    Application::showStatusBarMessage("Done Creating New Structural Component (" + QString::number(nbSelected) + " atoms selected");

    // add the new SC to the informative component
    if (!pm->getInformativeComponents()) {
        MultiComponent* mc = new MultiComponent(pm, "Informative Components");
        pm->setInformativeComponents(mc);
    }

    pm->getInformativeComponents()->addSubComponent(selection);

    // reinitialize prop and geometry
    pmlComponent->init();
    pmlComponent->setModified();

    Application::showStatusBarMessage("Created SC \"" + QString(selection->getName().c_str()) + "\"");

    // -- refresh to show
    refreshApplication();

    // restore the normal cursor and progress bar
    Application::resetProgressBar();
    QApplication::restoreOverrideCursor();
    return SUCCESS;
}
