/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2018 Univ. Grenoble Alpes, CNRS, TIMC-IMAG UMR 5525 (GMCAO)
 *
 * 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$
 ****************************************************************************/

// -- Qt dependencies
#include <QFileDialog>
#include <QTextStream>
#include <QDate>

// -- Qt State Machine stuff
#include <QFinalState>

// -- CamiTK Core stuff
#include <MainWindow.h>
#include <Application.h>
#include <MedicalImageViewer.h>
#include <Core.h>
#include <ExtensionManager.h>
#include <Explorer.h>
#include <Log.h>

using namespace camitk;
// -- CamiTK Application Local stuff
#include "ActionStateMachine.h"
#include "ActionStateViewer.h"
#include "ActionTransition.h"

// ---------------------- constructor ----------------------------
ActionStateMachine::ActionStateMachine(int& argc, char** argv, QString inputFileName, QString outputDirectory) :
    Application("camitk-actionstatemachine", argc, argv) {

    setProperty("Log to File", true);
    setProperty("Logger Level", InterfaceLogger::TRACE);

    statesMap = new QMap<QString, ActionState*>();

    // Atributes initialization
    mainWindow = NULL;
    name = QString("Action State Machine");

    // CamiTK Application main window
    initMainWindow();

    // Read parameters or ask for scxml file name and output save directory
    QString filename;
    if (inputFileName.isEmpty()) {
        filename = QFileDialog::getOpenFileName(nullptr, tr("Please select an xml file"));
    }
    else {
        filename = inputFileName;
    }

    // Get the name of the saving directory
    if (outputDirectory.isEmpty()) {
        saveDirectory = QFileDialog::getExistingDirectory(this->mainWindow, tr("Please select a directory where to save log and component files"));
    }
    else {
        saveDirectory = QDir(outputDirectory);
    }

    // check the given file
    this->checkSCXMLFile(filename);

    QDateTime now = QDateTime::currentDateTime();
    // NOTE on Windows ":" (colon) is reserved by the system for separating drive names, therefore hh:mm:ss cannot be used
    QString newDir = now.toString("yyyy-MM-dd") + "T" + now.toString("hh-mm-ss");

    if (! saveDirectory.mkdir(newDir))  {
        saveDirectory = QFileDialog::getExistingDirectory(this->mainWindow, tr("Please select a directory where to save log and component files"));
        saveDirectory.mkdir(newDir);
    }
    saveDirectory.cd(newDir);

    // parse XML file and store actions and transitions in states
    name = this->parseSCXMLTree();
    mainWindow->setWindowSubtitle(name);

    // Prepare the output directory
    QFile::copy(":/logXSL", saveDirectory.absolutePath() + "/log2html.xsl");

    // Starting the Machine !!

    //create the main timer
    startTime = new QTime();
    startTime->start();

    (*logStream) << "<?xml version='1.0' encoding='UTF-8' ?>" << endl;
    (*logStream) << "<?xml-stylesheet type='text/xsl' href='log2html.xsl'?>" << endl << endl;
    (*logStream) << "<application xmlns='http://ujf-grenoble.fr/camitk/logApplication'>" << endl;
    (*logStream) << "\t<name>" << name << "</name>" << endl;
    (*logStream) << "\t<inputXmlFile>" << filename << "</inputXmlFile>" << endl;
    (*logStream) << "\t<startDate>" << now.toString("yyyy-MM-dd") << "</startDate>" << endl;
    (*logStream) << "\t<startTime>" << startTime->toString("hh:mm:ss:zzz") << "</startTime>" << endl;

    machine.start();


}

// ---------------------- autoNext ----------------------------
void ActionStateMachine::autoNext() {
    // if autonext is called, then this is an automatic test, there won't be any user to close the dialog
    setProperty("Message Box Level", InterfaceLogger::NONE);

    // machine.configuration() returns all the current state, the first on is the active state
    ActionState* currentState = qobject_cast<ActionState*>(machine.configuration().toList().first());

    // loop until no more state is available
    while (currentState != nullptr && currentState->transitions().size() > 0) {
        // look for the "Next" or "Quit" transition
        auto it = currentState->transitions().begin();
        bool foundNextState = false;
        while (it != currentState->transitions().end() && !foundNextState) { // && nextState==nullptr) {
            // all transition in this state machine are ActionTransition instances...
            ActionTransition* currentActionTransition = qobject_cast<ActionTransition*>(*it);

            //... in which we can look for the "Next" or "Quit" transition
            if (currentActionTransition->isNamed("Next") || currentActionTransition->isNamed("Quit")) {
                // activate the transition (simulate a click on the button)
                currentActionTransition->autoNext();  // activating the "Quit" transition enter the final state, which make the state machine to emit the finished() signal
                foundNextState = true;
            }
            ++it;
        }
        // get the new current active state
        currentState = dynamic_cast<ActionState*>(machine.configuration().toList().first());
    }
    while (currentState != nullptr);

}

// ---------------------- initMainWindow ----------------------------
void ActionStateMachine::initMainWindow() {

    mainWindow = new MainWindow(name);
    mainWindow->setCentralViewer(MedicalImageViewer::getInstance());
    mainWindow->addDockViewer(Qt::RightDockWidgetArea, ActionStateViewer::getActionStateViewer());
    mainWindow->addDockViewer(Qt::LeftDockWidgetArea, Explorer::getInstance());

    // never show the toolbar
    MedicalImageViewer::getInstance()->setToolbarAutoVisibility(false);

    mainWindow->showStatusBar(true);
    this->setMainWindow(mainWindow);

    // force no redirection to console (to see error message if they occur)
    mainWindow->redirectToConsole(false);

    // set the specific actions state machine icon
    mainWindow->setWindowIcon(QPixmap(":/applicationIcon"));
}

// ---------------------- checkSCXMLFile ----------------------------
void ActionStateMachine::checkSCXMLFile(QString filename) {
    QString msg;
    QDomDocument doc;

    //read the file
    QFile file(filename);
    if (!file.open(QIODevice::ReadOnly)) {
        msg = tr("File not found: the file %1 was not found").arg(filename);
        CAMITK_ERROR(msg)
        throw AbortException(msg.toStdString());
    }
    if (!doc.setContent(&file)) {
        file.close();
        msg = tr("File %1 have no valid root (not a valid XML file format)").arg(filename);
        CAMITK_ERROR(msg)
        throw AbortException(msg.toStdString());
    }

    QString rootName = doc.documentElement().nodeName();
    if (rootName != QString("scxml")) {
        file.close();
        msg = tr("File %1 is not a SCXML file (%2: root node not equals to \"scxml\")").arg(filename, getDomNodeLocation(doc.documentElement()));
        CAMITK_ERROR(msg)
        throw AbortException(msg.toStdString());
    }

    // Ok, after all this checking, the file seems to be good looking,
    // set it as the right xml doc...
    this->scxmlDoc = doc;

    file.close();
}

// ---------------------- parseSCXMLTree ----------------------------
QString ActionStateMachine::parseSCXMLTree() {
    // Get the name of the application
    QString applicationName = "";
    QDomElement docElem = scxmlDoc.documentElement();
    applicationName = docElem.attribute("applicationName");

    QString logFileName = this->saveDirectory.absolutePath() + "/log.xml";
    logFile = new QFile(logFileName);
    logFile->open(QIODevice::WriteOnly | QIODevice::Text);
    logStream = new QTextStream(logFile);

    // Get the states node
    QDomNodeList theStates = scxmlDoc.elementsByTagName("state");

    // Create ActionStates
    createAllActionStates(theStates);

    // Now that all the states are created and stored in the map,
    // create the transitions in between.
    createTransitions(theStates);

    // Set the initial state (specified in the scxml element with initial attribute).
    QString initialStateName = scxmlDoc.documentElement().attribute("initial");
    ActionState* initialState = statesMap->find(initialStateName).value();
    machine.setInitialState(initialState);
    ActionStateViewer::getActionStateViewer()->setState(initialState);

    // Connect the end of the machine with the end of the application
    QObject::connect(&machine, SIGNAL(finished()), QApplication::instance(), SLOT(quit()));

    // and connect the about to quit with some final cleanup method (recommended instead using destructor to cleanup)
    QObject::connect(this, SIGNAL(aboutToQuit()), this, SLOT(finished()));

    // Ok, Actions and Wizard are created. We do not need the file any more.
    // Clean up:
    delete statesMap;

    return applicationName;
}

// ---------------------- finished ----------------------------
void ActionStateMachine::finished() {
    QTime endTime = QTime::currentTime();

    (*logStream) << "\t<endTime>" << endTime.toString("hh:mm:ss:zzz") << "</endTime>" << endl;
    (*logStream) << "\t<timeEnlapsed unit='ms'>" << (startTime->elapsed()) << "</timeEnlapsed>" << endl;
    (*logStream) << "</application>" << endl;
    logFile->close();
}

// ---------------------- createAllActionStates ----------------------------
void ActionStateMachine::createAllActionStates(QDomNodeList nodeList) {
    int nbStates = nodeList.size();
    if (nbStates < 1) {
        QString msg = tr("No state and no camitk-action!");
        CAMITK_ERROR(msg)
        throw AbortException(msg.toStdString());
    }

    for (int nb = 0; nb < nbStates; nb++) {
        QDomElement element = nodeList.item(nb).toElement();
        if (! element.isNull()) {
            QString elementName = element.nodeName();
            if (elementName == QString("state")) {
                QString stateId = element.attribute("id");
                QString stateDescription = element.elementsByTagName("camitk:description").item(0).toElement().text();
                ActionState* state = new ActionState(&machine, stateId, stateDescription, logStream);
                if (element.elementsByTagName("camitk:action").size()  > 0) {
                    QDomElement actionElement = element.elementsByTagName("camitk:action").item(0).toElement();
                    // Find the name of the action
                    QString camiTKActionName = actionElement.elementsByTagName("camitk:name").item(0).toElement().text();
                    if (camiTKActionName == "Save") {
                        delete (state);
                        state = new SaveActionState(&machine, stateId, stateDescription, saveDirectory.absolutePath(), logStream);
                        setCamiTKSaveAction((SaveActionState*) state, actionElement);
                    }
                    else {
                        setCamiTKAction(state, actionElement);
                    }
                }
                QDomNodeList alternativeDescriptions = element.elementsByTagName("camitk:altDescription");
                for (int i = 0; i < alternativeDescriptions.size(); i++) {
                    QDomElement altDescElmt = alternativeDescriptions.item(i).toElement();
                    QString altDescText = altDescElmt.text();
                    QString altDescCond =  altDescElmt.attribute("previousStatus", "");
                    QVector<Action::ApplyStatus> statusList = stringToStatus(altDescCond);

                    state->setAlternativeDesc(altDescText, statusList);

                }
                statesMap->insert(stateId, state);
            }
        }
    }
    // Do not forget the final state
    finalState = new QFinalState();
    machine.addState(finalState);

    //-- check that all input component exists as output components
    // 1. build the map
    QMap <ActionState*, QString> inputNames;
    QMap <ActionState*, QString> outputNames;
    QMap <QString, ActionState* >::const_iterator it = statesMap->constBegin();
    while (it != statesMap->constEnd()) {
        foreach (QString inputCompName, it.value()->getInputComponents().keys()) {
            if (inputNames.values().contains(inputCompName)) {
                CAMITK_WARNING(tr("Warning: same input \"%1\" for more than one state. It might be wrong.\n").arg(inputCompName))
            }
            else {
                inputNames.insert(it.value(), inputCompName);
            }
        }
        foreach (QString outputCompName, it.value()->getOutputComponents().keys()) {
            if (outputNames.values().contains(outputCompName)) {
                QString msg = tr("Error: same output name \"%1\" for more than one state. Please rename the output of \"%2\"").arg(outputCompName, it.value()->getName());
                CAMITK_ERROR(msg)
                throw AbortException(msg.toStdString());
            }
            else {
                outputNames.insert(it.value(), outputCompName);
            }
        }
        ++it;
    }
    // 2. check
    QMap <ActionState*, QString>::const_iterator itAS = inputNames.constBegin();
    while (itAS != inputNames.constEnd()) {
        if (!outputNames.values().contains(itAS.value())) {
            QString msg = tr("Error: Cannot find output name \"%1\" for input of \"%2\".").arg(itAS.value(), itAS.key()->getName());
            CAMITK_ERROR(msg)
            throw AbortException(msg.toStdString());
        }
        ++itAS;
    }

}

// ---------------------- createTransitions ----------------------------
void ActionStateMachine::createTransitions(QDomNodeList nodeList)  {
    int nbStates = nodeList.size();
    for (int stateNumber = 0; stateNumber < nbStates; stateNumber++) {
        // For each state ...
        QDomElement element = nodeList.item(stateNumber).toElement();

        // Find its name (id attribute)...
        QString stateId = element.attribute("id");
        // ... and get the corresponding created state in the map:
        ActionState* state = (statesMap->find(stateId)).value();
        // Check if this state is final
        bool isFinal = (element.attribute("final", "false").toLower() == QString("true"));

        // Find its transitions (transition children nodes)
        QDomNodeList transitionsList = element.elementsByTagName("transition");
        for (int transNb = 0; transNb < transitionsList.size(); transNb++) {
            QDomElement transElement = transitionsList.item(transNb).toElement();
            if (! transElement.isNull()) {
                QString eventName  = transElement.attribute("event");
                QString targetName = transElement.attribute("target");

                // Apply the previous action during this transition ?
                QString applyActionStr = transElement.attribute("applyAction", "true");
                bool applyAction = (applyActionStr.toLower() == "true");

                // Find disable conditions
                QString disableConds = transElement.attribute("disable", "");
                QVector<Action::ApplyStatus> disableConditions = stringToStatus(disableConds);

                // Find the corresponding target state
                QMap <QString, ActionState* >::iterator it = statesMap->find(targetName);
                if (it == statesMap->end()) {
                    QString msg = tr(R"(XML Parsing: %1 in state "%2": cannot find target "%3" for event "%4")")
                                  .arg(getDomNodeLocation(transitionsList.item(transNb)),
                                       state->getName(),
                                       targetName,
                                       eventName);

                    CAMITK_ERROR(msg)
                    throw AbortException(msg.toStdString());
                }
                ActionState* target = it.value();

                // Create the actual transition:
                ActionTransition* trans = state->addActionTransition(eventName, target, applyAction, disableConditions);

                // Check if the transition should close some components...
                QDomNodeList toClose = transElement.elementsByTagName("camitk:close");
                if (toClose.size() > 0) {
                    QDomNodeList compsToClose = toClose.item(0).toElement().elementsByTagName("camitk:component");
                    for (int idx = 0; idx < compsToClose.size(); idx++) {
                        QDomElement oneComp = compsToClose.item(idx).toElement();
                        QString forceOn = oneComp.attribute("force", "false");
                        QString compName = oneComp.attribute("name");
                        QString compType = oneComp.attribute("type");
                        trans->addComponentToClose(compName, compType, QVariant(forceOn).toBool());
                    }
                }
            }
        }

        if (isFinal) {
            state->addActionTransition("Quit", finalState, false);
        }
    }

}

// ---------------------- stringToStatus ----------------------------
QVector<Action::ApplyStatus> ActionStateMachine::stringToStatus(QString listOfStatus) {
    QVector<Action::ApplyStatus> statusList;
    statusList.clear();

    QStringList statusStringList = listOfStatus.split(" ");
    for (QStringList::iterator it = statusStringList.begin(); it != statusStringList.end(); it++) {
        if ((*it) == "SUCCESS") {
            statusList.append(Action::SUCCESS);
        }
        else if ((*it) == "ERROR") {
            statusList.append(Action::ERROR);
        }
        else if ((*it) == "WARNING") {
            statusList.append(Action::WARNING);
        }
        else if ((*it) == "ABORTED") {
            statusList.append(Action::ABORTED);
        }
        else if ((*it) == "TRIGGERED") {
            statusList.append(Action::TRIGGERED);
        }
    }

    return statusList;
}

// ---------------------- setCamiTKAction ----------------------------
void ActionStateMachine::setCamiTKAction(ActionState* actionState, QDomElement actionElement) {
    QString msg;
    if ((actionState == nullptr) || (actionElement.isNull())) {
        msg = tr("No Action or no state...");
        CAMITK_ERROR(msg)
        throw AbortException(msg.toStdString());
    }

    QDomNode actionNameNode = actionElement.elementsByTagName("camitk:name").item(0);
    QString actionName = actionNameNode.toElement().text();
    Action* action = Application::getAction(actionName);
    if (action == NULL) {
        msg = tr("XML parsing error: in %1, action \"%2\" is not a proper CamiTK action.").arg(getDomNodeLocation(actionNameNode), actionName);
        CAMITK_ERROR(msg)
        throw AbortException(msg.toStdString());
    }

    // Parameters
    QMap<QString, QVariant> parameters;
    parameters.clear();
    QDomNodeList domParameters = actionElement.elementsByTagName("camitk:parameters");
    if (! domParameters.isEmpty()) {
        QDomNodeList parameterList = domParameters.item(0).toElement().elementsByTagName("camitk:parameter");
        for (int i = 0; i < parameterList.size(); i++) {
            QDomElement param  = parameterList.item(i).toElement();
            QString paramName  = param.attribute("name");
            QString paramType  = param.attribute("type");
            QString paramValue = param.attribute("value");
            QVariant val;

            QStringList possibleTypes;
            possibleTypes << "int" << "bool" << "double" << "QString";


            switch (possibleTypes.indexOf(paramType)) {
                case 0: // int
                    val = QVariant(paramValue.toInt());
                    break;
                case 1: // bool
                    val = QVariant(paramValue);
                    break;
                case 2: // double
                    val = QVariant(paramValue.toDouble());
                    break;
                case 3: // QString
                    val = QVariant(paramValue);
                    break;
                default:
                    val = QVariant(paramValue);
                    break;
            }
            parameters.insert(paramName, val);
        }
    }

    // Input Component Names
    QMap<QString, QString> inputComponentNames;
    inputComponentNames.clear();
    QDomNodeList inputList = actionElement.elementsByTagName("camitk:inputs");
    if (! inputList.isEmpty()) {
        QDomNodeList inputComps = inputList.item(0).toElement().elementsByTagName("camitk:component");
        for (int i = 0; i < inputComps.size(); i++) {
            QDomElement inputComp = inputComps.item(i).toElement();
            QString compName = inputComp.attribute("name");
            QString compType = inputComp.attribute("type");
            inputComponentNames.insert(compName, compType);
        }
    }

    // Output Component Names
    QMap<QString, QString> outputComponentNames;
    outputComponentNames.clear();
    QDomNodeList outputList = actionElement.elementsByTagName("camitk:outputs");
    if (! outputList.isEmpty()) {
        QDomNodeList outputComps = outputList.item(0).toElement().elementsByTagName("camitk:component");
        for (int i = 0; i < outputComps.size(); i++) {
            QDomElement outputComp = outputComps.item(i).toElement();
            QString compName = outputComp.attribute("name");
            QString compType = outputComp.attribute("type");
            outputComponentNames.insert(compName, compType);
        }
    }

    actionState->setAction(action, parameters, inputComponentNames, outputComponentNames);
}

// ---------------------- setCamiTKSaveAction ----------------------------
void ActionStateMachine::setCamiTKSaveAction(SaveActionState* actionState, QDomElement actionElement) {

    QString msg;
    if ((actionState == nullptr) || (actionElement.isNull())) {
        msg = tr("No Action or no state...");
        CAMITK_ERROR(msg)
        throw AbortException(msg.toStdString());
    }


    // Input Component Names, Types, Extensions and Directories....
    QVector<SaveActionState::saveComponentsInfo> inputInfos;
    QDomNodeList inputList = actionElement.elementsByTagName("camitk:inputs");
    if (! inputList.isEmpty()) {
        QDomNodeList inputComps = inputList.item(0).toElement().elementsByTagName("camitk:component");
        for (int i = 0; i < inputComps.size(); i++) {
            QDomElement inputComp = inputComps.item(i).toElement();
            SaveActionState::saveComponentsInfo compInfos;
            compInfos.name = inputComp.attribute("name");
            compInfos.type = inputComp.attribute("type");
            compInfos.extension = inputComp.attribute("suffix");
            compInfos.directory = inputComp.attribute("saveDirectory");

            inputInfos.append(compInfos);
        }
    }

    actionState->setInput(inputInfos);
}

// ---------------------- getSaveDirectory ----------------------------
QString ActionStateMachine::getSaveDirectory() {
    return this->saveDirectory.absolutePath();
}

// ---------------------- getDomNodeLocation ----------------------------
QString ActionStateMachine::getDomNodeLocation(QDomNode inputNode) {
    QString msg;
    int whereInXML = inputNode.lineNumber();
    if (whereInXML > 0) {
        msg += "line " + QString::number(whereInXML);
    }
    whereInXML = inputNode.columnNumber();
    if (whereInXML > 0) {
        msg += QString(", column ") + QString::number(whereInXML) + QString(": ");
    }

    return msg;
}
