//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Loader/PyImportAssistant.cpp
//! @brief     Implements class PyImportAssistant.
//!
//! @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/Loader/PyImportAssistant.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"     // listOfFunctions
#include "PyCore/Sample/ImportMultiLayer.h" // createMultiLayerFromPython
#include "Sample/Multilayer/MultiLayer.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();
}

//! Lets user to select Python file on disk.

QString fnameToOpen()
{
    QString result = QFileDialog::getOpenFileName(
        gApp->mainWindow, "Open python script", gApp->script_import_dir, "Python scripts (*.py)",
        nullptr,
        gApp->useNativeFileDialog() ? QFileDialog::Options() : QFileDialog::DontUseNativeDialog);

    return result;
}

//! 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\nPyImportAssistant::readFile -> Error: Can' t open the file '"
        + fname + "' for reading.";
    GUI::Message::warning(gApp->mainWindow, "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<MultiLayer> code2sample(const QString& snippet, const QString& funcName)
{
    QApplication::setOverrideCursor(Qt::WaitCursor);

    void* pObj = PyInterpreter::pyscript2object(snippet.toStdString(), funcName.toStdString(),
                                                "MultiLayer", bornagainDir());

    QApplication::restoreOverrideCursor();

    return std::unique_ptr<MultiLayer>{reinterpret_cast<MultiLayer*>(pObj)->clone()};
}

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

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

    if (funcNames.empty()) {
        QString message("Python code doesn't contain any functions.\n\n");
        GUI::Message::warning(gApp->mainWindow, "Python failure", message);
    } else {
        ComboSelectorDialog dialog;
        dialog.addItems(funcNames, getCandidate(funcNames));
        dialog.setTextTop("Python code contains a few functions. Do you know by chance, "
                          "which one is intended to produce a valid MultiLayer?");
        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);

    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 MultiLayer& sample)
{
    try {
        auto* newItem = GUI::FromCore::itemizeSample(sample);

        QString message("Seems that import was successful.\n\n"
                        "Check SampleView for new sample and material editor for new materials.");
        GUI::Message::information(gApp->mainWindow, "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* PyImportAssistant::importMultiLayer()
{
    auto fname = fnameToOpen();

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

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

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

    std::unique_ptr<MultiLayer> sample;
    try {
        sample = code2sample(snippet, funcName);
        if (!sample)
            throw std::runtime_error("Import did not yield 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
