/*****************************************************************************
 * $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 <iostream>
#include <string>
using namespace std;

#include <pml/PhysicalModel.h>
#include <pml/MultiComponent.h>
#include <pml/StructuralComponent.h>
#include <pml/Atom.h>
#include <math.h>
#include <limits.h>
#include <float.h>

// -------------------- ProgramArg ------------------------
// Code inspired from ProgVals "Thinking in C++, 2nd Edition, Volume 2", chapter 4
// by Bruce Eckel & Chuck Allison, (c) 2001 MindView, Inc.
// Available at www.BruceEckel.com.
// Program values can be changed by command lineclass ProgVals
#include <map>
#include <iostream>
#include <string>

class ProgramArg : public std::map<std::string, std::string> {
public:
    ProgramArg(std::string defaults[][2], unsigned int sz) {
        for (unsigned int i = 0; i < sz; i++) {
            insert(std::pair<const std::string, std::string&>(defaults[i][0], defaults[i][1]));
        }
    };

    void parse(int argc, char* argv[], std::string usage, int offset = 1) {
        for (int i = offset; i < argc; i++) {
            string flag(argv[i]);
            unsigned int equal = flag.find('=');
            if (equal == string::npos) {
                cerr << "Command line error: " << argv[i] << endl << usage << endl;
                continue; // Next argument
            }
            string name = flag.substr(0, equal);
            string value = flag.substr(equal + 1);
            if (find(name) == end()) {
                cerr << name << endl << usage << endl;
                continue; // Next argument
            }
            operator[](name) = value;
        }
    };

    void print(std::ostream& out = std::cout) {
        out << "Argument values:" << endl;
        for (iterator it = begin(); it != end(); ++it) {
            out << (*it).first << " = " << (*it).second << endl;
        }
    };
};

string defaultsArg[][2] = {
    { "-f", "none" },
    { "-o", "none" },
    { "-name", "selected" },
    { "-xMin", "none" },
    { "-xMax", "none" },
    { "-yMin", "none" },
    { "-yMax", "none" },
    { "-zMin", "none" },
    { "-zMax", "none" },
};

const char* usage = "usage:\n"
                    "select -f=file.pml [-o=out.pml] [-name=n] [-xMin=val] [-xMax=val] [-yMin=val] [-yMax=val] [-zMin=val] [-zMax=val]\n"
                    "Create a new PML document which contains a new structural component containing only\n"
                    "atoms which positions are in the defined bounding box\n"
                    "(Note no space around '=')\n"
                    "Where mandatory options are: \n"
                    "f\tsource pml document\n"
                    "And optional options are:\n"
                    "o\tdestination pml document\n"
                    "name\tname of the create SC\n"
                    "xMin\tmin included value for x\n"
                    "xMax\tmax included value for x\n"
                    "... the same for y and z\n"
                    "Note that if you do not provide value in a given direction, it means no selection has to be made\n";

// global ProgramArgument
ProgramArg argVal(defaultsArg, sizeof defaultsArg / sizeof* defaultsArg);

// arguments/options
string srcFile;
string output;
string name;
double xMin, xMax;
bool xMinProvided, xMaxProvided;
double yMin, yMax;
bool yMinProvided, yMaxProvided;
double zMin, zMax;
bool zMinProvided, zMaxProvided;

// -------------------- processArg ------------------------
void processArg() {
    srcFile = argVal["-f"];
    if (srcFile == "none") {
        cerr << "Argument error: -f argument is mandatory" << usage << endl;
        exit(-1);
    }

    output = argVal["-o"];
    if (output == "none") {
        output = srcFile;
        unsigned int pLast = output.rfind(".");
        if (pLast != string::npos) {
            output.erase(pLast);
            output += "-selected.pml";
        }
        else {
            output = "selected.pml";
        }
    }

    name = argVal["-name"];

    xMinProvided = (argVal["-xMin"] != "none");
    xMaxProvided = (argVal["-xMax"] != "none");
    yMinProvided = (argVal["-yMin"] != "none");
    yMaxProvided = (argVal["-yMax"] != "none");
    zMinProvided = (argVal["-zMin"] != "none");
    zMaxProvided = (argVal["-zMax"] != "none");
    if (xMinProvided) {
        xMin = atof(argVal["-xMin"].c_str());
    }
    else {
        xMin = -DBL_MAX;
    }
    if (xMaxProvided) {
        xMax = atof(argVal["-xMax"].c_str());
    }
    else {
        xMax = DBL_MAX;
    }
    if (yMinProvided) {
        yMin = atof(argVal["-yMin"].c_str());
    }
    else {
        yMin = -DBL_MAX;
    }
    if (yMaxProvided) {
        yMax = atof(argVal["-yMax"].c_str());
    }
    else {
        yMax = DBL_MAX;
    }
    if (zMinProvided) {
        zMin = atof(argVal["-zMin"].c_str());
    }
    else {
        zMin = -DBL_MAX;
    }
    if (zMaxProvided) {
        zMax = atof(argVal["-zMax"].c_str());
    }
    else {
        zMax = DBL_MAX;
    }
}

// -------------------- printArg ------------------------
void printArg() {
    cout << "select from " << srcFile << " to " << output << " in SC \"" << name << "\"" << endl;
    cout << "using selection:"
         << " x in [" << xMin << "," << xMax << "],"
         << " y in [" << yMin << "," << yMax << "],"
         << " z in [" << zMin << "," << zMax << "],";
    cout << endl;
}

// -------------------- main ------------------------
int main(int argc, char** argv) {


    // Initialize and parse command line values
    // before any code that uses pvals is called:
    argVal.parse(argc, argv, usage);

    processArg();
    printArg();


    try {
        // read the pml
        cout << "-> please wait while reading " << srcFile << " for selection" << endl;

        PhysicalModel* pm = new PhysicalModel(srcFile.c_str());

        // create the new component
        StructuralComponent* selection;
        selection = new StructuralComponent(pm, name);
        Cell* selectionC = new Cell(pm, StructureProperties::POLY_VERTEX);
        selection->addStructure(selectionC);

        cout << "-> please wait while selecting...";

        unsigned int nbSelected = 0;
        StructuralComponent* srcA = pm->getAtoms();
        Atom* a;
        double pos[3];

        for (unsigned int i = 0; i < srcA->getNumberOfStructures(); i++) {
            // get the src position
            a = (Atom*) srcA->getStructure(i);
            a->getPosition(pos);
            // should it be selected
            if (pos[0] >= xMin && pos[0] <= xMax && pos[1] >= yMin && pos[1] <= yMax && pos[2] >= zMin && pos[2] <= zMax) {
                selectionC->addStructureIfNotIn(a);
                nbSelected++;
            }
        }

        cout << " " << nbSelected << " selected" << endl;

        // 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);

        // save the result
        cout << "-> please wait while saving " << output << " " << endl;

        ofstream outputFile(output.c_str());
        pm->setName(pm->getName() + " + selection");
        // do not optimize output (do not touch cell and atom id)
        pm->xmlPrint(outputFile, false);

        // -- Select only the cells where atoms are all in the selection
        pm->getInformativeComponents()->removeSubComponent(selection);
        std::vector<Cell*> toRemove;
        // exclusive components
        for (unsigned  i = 0; i < pm->getExclusiveComponents()->getNumberOfCells(); i++) {
            Cell* c = pm->getCell(i);
            bool allInSelected = true;
            unsigned int j = 0;
            // check if all atoms of c are selected
            while (allInSelected && j < c->getNumberOfStructures()) {
                allInSelected = allInSelected && selectionC->isStructureIn(c->getStructure(j));
                j++;
            }
            // if some atoms of c are not selected, mark c to be deleted
            if (!allInSelected) {
                toRemove.push_back(c);
            }
        }
        // informative components
        for (unsigned  i = 0; i < pm->getInformativeComponents()->getNumberOfCells(); i++) {
            Cell* c = pm->getCell(i);
            bool allInSelected = true;
            unsigned int j = 0;
            // check if all atoms of c are selected
            while (allInSelected && j < c->getNumberOfStructures()) {
                allInSelected = allInSelected && selectionC->isStructureIn(c->getStructure(j));
                j++;
            }
            // if some atoms of c are not selected, mark c to be deleted
            if (!allInSelected) {
                toRemove.push_back(c);
            }
        }
        // now remove
        std::vector <StructuralComponent*> cUsedIn;
        for (unsigned int i = 0; i < toRemove.size(); i++) {
            cUsedIn = toRemove[i]->getAllStructuralComponents();
            for (unsigned int j = 0; j < cUsedIn.size(); j++) {
                cUsedIn[j]->removeStructure(toRemove[i]);
            }
        }
        // now set the atoms
        pm->setAtoms(selectionC, false);

        // print pm in another document
        string output2 = output;
        unsigned int pLast = output2.rfind(".");
        if (pLast != string::npos) {
            output2.erase(pLast);
            output2 += "-selected-only.pml";
        }
        else {
            output2 = "selected-only.pml";
        }
        ofstream outputFile2(output2.c_str());
        pm->setName(pm->getName() + " + selected only");
        pm->xmlPrint(outputFile2, false);

        // print the list of selected atoms as a lml target
        string outputTLName = name + ".txt";
        cout << "-> please wait while saving " << outputTLName << " " << endl;
        ofstream outputTargetList(outputTLName.c_str());
        for (unsigned int i = 0; i < selectionC->getNumberOfStructures(); i++) {
            outputTargetList << selectionC->getStructure(i)->getIndex();
            if (i < selectionC->getNumberOfStructures() - 1) {
                outputTargetList << ",";
            }
        }
        outputTargetList.close();

        delete pm;
    }
    catch (const PMLAbortException& ae) {
        cout << "AbortException: Physical model aborted:" << endl ;
        cout << ae.what() << endl;
    }
}
