//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/IO/PythonImport.cpp
//! @brief     Implements Python import functions in namespace IO.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2018
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#ifdef BORNAGAIN_PYTHON

#include "GUI/View/IO/PythonImport.h"
#include "BABuild.h"
#include "Base/Util/Assert.h"
#include "Base/Util/SysUtil.h"
#include "GUI/Model/FromCore/ItemizeSample.h"
#include "GUI/Model/FromCore/ItemizeSimulation.h"
#include "GUI/Model/Project/ProjectUtil.h"
#include "GUI/Model/Util/Path.h"
#include "GUI/Model/Util/String.h"
#include "GUI/View/Info/ComboSelectorDialog.h"
#include "GUI/View/Info/DetailedMessageBox.h"
#include "GUI/View/Info/MessageBox.h"
#include "GUI/View/Widget/AppConfig.h"
#include "PyCore/Embed/PyInterpreter.h"
#include "PyCore/Embed/PyObjectPtr.h"
#include "Sample/Multilayer/Sample.h"
#include <QApplication>
#include <QFileDialog>
#include <QMessageBox>
#include <QTextStream>

namespace {

//! Returns directory with BornAgain library. If PYTHONPATH is not empty,
//! Returns an empty string.

std::string bornagainDir()
{
    std::string pythonPath = Base::System::getenv("PYTHONPATH");
    return pythonPath.empty() ? BABuild::buildLibDir() : "";
}

//! Returns a name from the list which looks like a function name intended for sample
//! creation.

QString getCandidate(const QStringList& funcNames)
{
    if (funcNames.isEmpty())
        return "";

    for (auto str : funcNames) {
        QString name = str.toLower();
        if (name.contains("sample") || name.contains("sample"))
            return str;
    }

    return funcNames.front();
}

//! Read content of text file and returns it as a multi-line string.
//! Pop-ups warning dialog in the case of failure.

QString readFile(const QString& fname)
{
    QFile file(fname);
    if (file.open(QIODevice::ReadOnly | QIODevice::Text))
        return QTextStream(&file).readAll();

    const QString message = "Cannot read the file. \n\n"
                            "PythonImportAssistant::readFile -> Error: Cannot open the file '"
                            + fname + "' for reading.";
    GUI::Message::warning("File read failure.", message);
    return {};
}

//! Creates a multi-layer by executing a funcName in embedded Python.
//! Function is supposed to be in code provided by 'snippet'.

std::unique_ptr<Sample> code2sample(const QString& snippet, const QString& funcName)
{
    QApplication::setOverrideCursor(Qt::WaitCursor);

    PyInterpreter::initialize();

    PyObjectPtr pySample{PyInterpreter::BornAgain::callScriptFunction(
        funcName.toStdString(), snippet.toStdString(), bornagainDir())};

    std::unique_ptr<Sample> sample{BA_SWIG_sampleFromPyObject(pySample.get())};

    pySample.discard();

    QApplication::restoreOverrideCursor();

    return sample;
}

//! Lets user select a function name which generates a MultiLayer.

QString selectPySampleFunction(const QStringList& funcNames)
{
    QString result;

    if (funcNames.empty()) {
        QString message("Python code does not contain any functions.\n\n");
        GUI::Message::warning("Python failure", message);
    } else {
        ComboSelectorDialog dialog;
        dialog.addItems(funcNames, getCandidate(funcNames));
        dialog.setTextTop("Python code contains several functions. "
                          "Please select the function intended to produce a valid sample:");
        dialog.setTextBottom(
            "Please select a valid function in combo-box and press OK to continue.");
        if (dialog.exec() == QDialog::Accepted)
            result = dialog.currentText();
    }

    return result;
}

//! Returns the name of function which might generate a MultiLayer in Python code snippet.
//! Pop-ups dialog and asks user for help in the case of doubts.

QString getPySampleFunctionName(const QString& snippet)
{
    QApplication::setOverrideCursor(Qt::WaitCursor);

    PyInterpreter::initialize();

    std::vector<std::string> funcs_res{
        PyInterpreter::BornAgain::listOfFunctions(snippet.toStdString(), bornagainDir())};

    QApplication::restoreOverrideCursor();

    if (funcs_res.empty()) {
        QString message("Exception thrown while acquiring functions from Python code.\n\n");
        DetailedMessageBox(gApp->mainWindow, "Python failure", message, "").exec();
        return "";
    }

    QStringList funcList{GUI::Util::String::fromStdStrings(funcs_res)};
    return selectPySampleFunction(funcList);
}

SampleItem* itemizeSample(const Sample& sample)
{
    try {
        auto* newItem = GUI::FromCore::itemizeSample(sample);

        QString message("Importing sample from Python script was successful.\n\n"
                        "Check SampleView for new sample and material editor for new materials.");
        GUI::Message::information("Python import", message);
        return newItem;
    } catch (const std::exception& ex) {
        QString message("Exception thrown while trying to build GUI models.\n"
                        "GUI models might be in inconsistent state.\n\n");
        QString details = QString::fromStdString(std::string(ex.what()));
        DetailedMessageBox(gApp->mainWindow, "GUI::ObjectBuilder failure", message, details).exec();
        return nullptr;
    }
}

} // namespace


SampleItem* IO::Py::importSample()
{
    QString fname =
        QFileDialog::getOpenFileName(gApp->mainWindow, "Open Python script",
                                     gApp->script_import_dir, "Python scripts (*.py)", nullptr);

    if (fname.isEmpty())
        return {};

    QString snippet = readFile(fname);
    if (snippet.isEmpty())
        return {};

    QString funcName = getPySampleFunctionName(snippet);
    if (funcName.isEmpty())
        return {};

    std::unique_ptr<Sample> sample;
    try {
        sample = code2sample(snippet, funcName);

        if (!sample)
            throw std::runtime_error("Import did not yield a MultiLayer object");

        if (sample->name() == "Unnamed")
            sample->setName(GUI::Path::baseName(fname).toStdString());

    } catch (const std::exception& ex) {
        QApplication::restoreOverrideCursor();
        QMessageBox msgbox(QMessageBox::Warning, "BornAgain: failed import", QString(ex.what()),
                           QMessageBox::Ok, nullptr);
        msgbox.exec();
        return {};
    }

    return ::itemizeSample(*sample);
}

#endif // BORNAGAIN_PYTHON
