//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Project/ProjectManager.cpp
//! @brief     Implements class ProjectManager
//!
//! @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)
//
//  ************************************************************************************************

#include "GUI/View/Project/ProjectManager.h"
#include "Base/Util/Assert.h"
#include "GUI/Application/ApplicationSettings.h"
#include "GUI/Model/Project/ProjectUtil.h"
#include "GUI/Support/Util/MessageService.h"
#include "GUI/View/Info/MessageBox.h"
#include "GUI/View/Info/ProjectLoadProblemDialog.h"
#include "GUI/View/Project/AutosaveController.h"
#include "GUI/View/Project/NewProjectDialog.h"
#include "GUI/View/Tool/Globals.h"
#include "GUI/View/Tool/mainwindow_constants.h"
#include <QApplication>
#include <QDateTime>
#include <QFileDialog>
#include <QMessageBox>
#include <QSettings>

namespace {

const QString S_PROJECTMANAGER = "ProjectManager";
const QString S_AUTOSAVE = "EnableAutosave";
const QString S_DEFAULTPROJECTPATH = "DefaultProjectPath";
const QString S_RECENTPROJECTS = "RecentProjects";
const QString S_LASTUSEDIMPORTDIR = "LastUsedImportDir";
const QString S_LASTUSEDIMPORFILTER1D = "LastUsedImportFilter1D";
const QString S_LASTUSEDIMPORFILTER2D = "LastUsedImportFilter2D";

} // namespace


ProjectManager* ProjectManager::s_instance = nullptr;

ProjectManager::ProjectManager(QObject* parent)
    : QObject(parent)
{
    if (s_instance != nullptr)
        throw std::runtime_error("ProjectManager::ProjectManager -> Error. Attempt to create "
                                 "ProjectManager twice.");

    s_instance = this;
}

ProjectManager::~ProjectManager()
{
    s_instance = nullptr;
    gProjectDocument.reset();
}

ProjectManager* ProjectManager::instance()
{
    if (!s_instance)
        throw std::runtime_error("ProjectManager::instance -> Error. Attempt to access "
                                 "non existing ProjectManager.");

    return s_instance;
}

//! Reads settings of ProjectManager from global settings.

void ProjectManager::readSettings()
{
    QSettings settings;
    m_workingDirectory = QDir::homePath();
    if (settings.childGroups().contains(S_PROJECTMANAGER)) {
        settings.beginGroup(S_PROJECTMANAGER);

        if (!settings.contains(S_AUTOSAVE))
            settings.setValue(S_AUTOSAVE, true);

        m_workingDirectory = settings.value(S_DEFAULTPROJECTPATH).toString();
        m_recentProjects = settings.value(S_RECENTPROJECTS).toStringList();

        if (settings.contains(S_LASTUSEDIMPORTDIR))
            m_importDirectory = settings.value(S_LASTUSEDIMPORTDIR, QString()).toString();

        m_importFilter1D = settings.value(S_LASTUSEDIMPORFILTER1D, m_importFilter1D).toString();
        m_importFilter2D = settings.value(S_LASTUSEDIMPORFILTER2D, m_importFilter2D).toString();

        setAutosaveEnabled(settings.value(S_AUTOSAVE).toBool());

        settings.endGroup();
    }
}

//! Saves settings of ProjectManager in global settings.

void ProjectManager::writeSettings()
{
    QSettings settings;
    settings.beginGroup(S_PROJECTMANAGER);
    settings.setValue(S_DEFAULTPROJECTPATH, m_workingDirectory);
    settings.setValue(S_RECENTPROJECTS, m_recentProjects);

    if (!m_importDirectory.isEmpty())
        settings.setValue(S_LASTUSEDIMPORTDIR, m_importDirectory);
    settings.setValue(S_LASTUSEDIMPORFILTER1D, m_importFilter1D);
    settings.setValue(S_LASTUSEDIMPORFILTER2D, m_importFilter2D);

    settings.endGroup();
}

//! Returns list of recent projects, validates if projects still exists on disk.

QStringList ProjectManager::recentProjects()
{
    QStringList updatedList;
    for (const QString& fileName : m_recentProjects)
        if (QFile fin(fileName); fin.exists())
            updatedList.append(fileName);
    m_recentProjects = updatedList;
    return m_recentProjects;
}

//! Returns name of the current project directory.

QString ProjectManager::projectDir() const
{
    if (gProjectDocument.has_value())
        return gProjectDocument.value()->validProjectDir();
    return "";
}

//! Returns directory name which was used by the user to import files.

QString ProjectManager::userImportDir() const
{
    if (m_importDirectory.isEmpty()) {
        if (gProjectDocument.has_value())
            return gProjectDocument.value()->userExportDir();
        return "";
    }
    return m_importDirectory;
}

QString ProjectManager::recentlyUsedImportFilter1D() const
{
    return m_importFilter1D;
}

QString ProjectManager::recentlyUsedImportFilter2D() const
{
    return m_importFilter2D;
}

//! Sets user import directory in system settings.

void ProjectManager::setImportDir(const QString& dirname)
{
    m_importDirectory = dirname;
}

//! Sets user import directory in system settings.

void ProjectManager::setImportDirFromFilePath(const QString& filePath)
{
    m_importDirectory = QFileInfo(filePath).absolutePath();
}

void ProjectManager::setRecentlyUsedImportFilter1D(const QString& filter)
{
    m_importFilter1D = filter;
}

void ProjectManager::setRecentlyUsedImportFilter2D(const QString& filter)
{
    m_importFilter2D = filter;
}

bool ProjectManager::isAutosaveEnabled() const
{
    return static_cast<bool>(m_autosave);
}

AutosaveController* ProjectManager::autosaveController() const
{
    return m_autosave.get();
}

void ProjectManager::setAutosaveEnabled(bool value)
{
    if (value)
        m_autosave.reset(new AutosaveController());
    else
        m_autosave.reset();

    QSettings settings;
    settings.setValue(S_PROJECTMANAGER + "/" + S_AUTOSAVE, value);
}

//! Clears list of recent projects.

void ProjectManager::clearRecentProjects()
{
    m_recentProjects.clear();
    emit recentListModified();
}

//! Processes new project request (close old project, rise dialog for project name, create project).

ProjectDocument* ProjectManager::newProject()
{
    if (!closeCurrentProject())
        return nullptr;
    createNewProject();
    emit documentOpenedOrClosed(true);
    return gProjectDocument.value();
}

//! Processes close current project request. Call save/discard/cancel dialog, if necessary.
//! Returns false if saving was canceled.

bool ProjectManager::closeCurrentProject()
{
    if (!gProjectDocument.has_value())
        return true;

    if (gProjectDocument.value()->isModified()) {
        QMessageBox msgBox;
        msgBox.setText("The project has been modified.");
        msgBox.setInformativeText("Do you want to save your changes?");
        msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
        msgBox.setDefaultButton(QMessageBox::Save);

        switch (msgBox.exec()) {
        case QMessageBox::Save:
            if (!saveProject())
                return false;
            break;
        case QMessageBox::Discard:
            break;
        case QMessageBox::Cancel:
            return false;
        default:
            break;
        }
    }

    deleteCurrentProject();
    emit documentOpenedOrClosed(false);
    return true;
}

//! Processes save project request.

bool ProjectManager::saveProject(QString projectPullPath)
{
    if (projectPullPath.isEmpty()) {
        if (gProjectDocument.value()->hasValidNameAndPath())
            projectPullPath = gProjectDocument.value()->projectFullPath();
        else
            projectPullPath = acquireProjectPullPath();
    }

    if (projectPullPath.isEmpty())
        return false;

    gProjectDocument.value()->setProjectName(GUI::Project::Util::projectName(projectPullPath));
    gProjectDocument.value()->setProjectDir(GUI::Project::Util::projectDir(projectPullPath));

    try {
        gProjectDocument.value()->saveProjectFileWithData(projectPullPath);
    } catch (const std::exception& ex) {
        QString message = QString("Failed to save project under '%1'. \n\n").arg(projectPullPath);
        message.append("Exception was thrown.\n\n");
        message.append(ex.what());

        QMessageBox::warning(GUI::Global::mainWindow, "Error while saving project", message);
        return false;
    }
    addToRecentProjects();
    return true;
}

//! Processes 'save project as' request.

bool ProjectManager::saveProjectAs()
{
    QString projectFileName = acquireProjectPullPath();

    if (projectFileName.isEmpty())
        return false;

    return saveProject(projectFileName);
}

//! Opens existing project. If fileName is empty, will popup file selection dialog.

void ProjectManager::openProject(QString projectPullPath)
{
    if (!closeCurrentProject())
        return;

    if (projectPullPath.isEmpty()) {
        const QString ext = QString(GUI::Project::Util::projectFileExtension);
        projectPullPath = QFileDialog::getOpenFileName(
            GUI::Global::mainWindow, "Open project file", workingDirectory(),
            "BornAgain project Files (*" + ext + ")", nullptr,
            appSettings->useNativeFileDialog() ? QFileDialog::Options()
                                               : QFileDialog::DontUseNativeDialog);
        if (projectPullPath.isEmpty())
            return;
    }

    createNewProject();
    MessageService messageService;
    const auto readResult = loadProject(projectPullPath, messageService);

    if (readResult == ProjectDocument::ReadResult::ok)
        addToRecentProjects();
    else if (readResult == ProjectDocument::ReadResult::error) {
        riseProjectLoadFailedDialog(messageService);
        deleteCurrentProject();
    } else if (readResult == ProjectDocument::ReadResult::warning) {
        riseProjectLoadProblemDialog(messageService);
        addToRecentProjects();
    }
    if (gProjectDocument.has_value())
        emit documentOpenedOrClosed(true);
}

//! Calls dialog window to define project path and name.

void ProjectManager::createNewProject()
{
    if (gProjectDocument.has_value())
        throw std::runtime_error("ProjectManager::createNewProject -> Project already exists");

    gProjectDocument = new ProjectDocument();

    const QVariant defFunctionalities =
        appSettings->defaultFunctionalities(toVariant(ProjectDocument::All));
    gProjectDocument.value()->setFunctionalities(toFunctionalities(defFunctionalities));
    gProjectDocument.value()->setSingleInstrumentMode(appSettings->defaultIsSingleInstrumentMode());
    gProjectDocument.value()->setSingleSampleMode(appSettings->defaultIsSingleSampleMode());

    if (m_autosave)
        m_autosave->setDocument(gProjectDocument.value());

    gProjectDocument.value()->setProjectName("Untitled");

    connect(gProjectDocument.value(), &ProjectDocument::modifiedStateChanged, this,
            &ProjectManager::documentModified);
}

void ProjectManager::deleteCurrentProject()
{
    emit aboutToCloseDocument();
    if (m_autosave)
        m_autosave->removeAutosaveDir();
    gProjectDocument.reset();
}

//! Load project data from file name. If autosave info exists, opens dialog for project restore.

ProjectDocument::ReadResult ProjectManager::loadProject(const QString& fullPathAndName,
                                                        MessageService& messageService)
{
    auto readResult = ProjectDocument::ReadResult::ok;

    const bool useAutosave = GUI::Project::Util::hasAutosavedData(fullPathAndName);
    const QString autosaveFullPath = GUI::Project::Util::autosaveFullPath(fullPathAndName);
    if (qApp)
        QApplication::setOverrideCursor(Qt::WaitCursor);
    if (useAutosave && restoreProjectDialog(fullPathAndName, autosaveFullPath)) {
        readResult =
            gProjectDocument.value()->loadProjectFileWithData(autosaveFullPath, messageService);
        gProjectDocument.value()->setProjectFullPath(fullPathAndName);
        // restored project should be marked by '*'
        gProjectDocument.value()->setModified();
    } else {
        readResult =
            gProjectDocument.value()->loadProjectFileWithData(fullPathAndName, messageService);
    }
    if (qApp)
        QApplication::restoreOverrideCursor();
    return readResult;
}

//! Returns project file name from dialog. Returns empty string if dialog was canceled.

QString ProjectManager::acquireProjectPullPath()
{
    NewProjectDialog dialog(GUI::Global::mainWindow, workingDirectory(), untitledProjectName());

    if (dialog.exec() != QDialog::Accepted)
        return "";

    m_workingDirectory = dialog.getWorkingDirectory();

    return dialog.getProjectFileName();
}

//! Add name of the current project to the name of recent projects

void ProjectManager::addToRecentProjects()
{
    QString fileName = gProjectDocument.value()->projectFullPath();
    m_recentProjects.removeAll(fileName);
    m_recentProjects.prepend(fileName);
    while (m_recentProjects.size() > GUI::Constants::MAX_RECENT_PROJECTS)
        m_recentProjects.removeLast();

    emit recentListModified();
}

//! Returns default project path.

QString ProjectManager::workingDirectory()
{
    return m_workingDirectory;
}

//! Will return 'Untitled' if the directory with such name doesn't exist in project
//! path. Otherwise will return Untitled1, Untitled2 etc.

QString ProjectManager::untitledProjectName()
{
    QString result = "Untitled";
    QDir projectDir = workingDirectory() + "/" + result;
    if (projectDir.exists()) {
        for (size_t i = 1; i < 99; ++i) {
            result = QString("Untitled") + QString::number(i);
            projectDir.setPath(workingDirectory() + "/" + result);
            if (!projectDir.exists())
                break;
        }
    }
    return result;
}

void ProjectManager::riseProjectLoadFailedDialog(const MessageService& messageService)
{
    QString message = QString("Failed to load the project '%1' \n\n")
                          .arg(gProjectDocument.value()->projectFullPath());

    for (const auto& details : messageService.errors())
        message.append(details + "\n");

    QMessageBox::warning(GUI::Global::mainWindow, "Error while opening project file", message);
}

void ProjectManager::riseProjectLoadProblemDialog(const MessageService& messageService)
{
    ASSERT(gProjectDocument.has_value());
    auto* problemDialog =
        new ProjectLoadProblemDialog(GUI::Global::mainWindow, messageService.warnings(true),
                                     gProjectDocument.value()->documentVersion());

    problemDialog->show();
    problemDialog->raise();
}

//! Rises dialog if the project should be restored from autosave. Returns true, if yes.

bool ProjectManager::restoreProjectDialog(const QString& projectFileName,
                                          const QString autosaveName)
{
    const QString title("Recover project");
    const QString lmProject =
        QFileInfo(projectFileName).lastModified().toString("hh:mm:ss, MMMM d, yyyy");
    const QString lmAutoSave =
        QFileInfo(autosaveName).lastModified().toString("hh:mm:ss, MMMM d, yyyy");

    QString message = QString("Project '%1' contains autosaved data.\n\n"
                              "Project saved at %2\nAutosave from %3")
                          .arg(GUI::Project::Util::projectName(projectFileName))
                          .arg(lmProject)
                          .arg(lmAutoSave);

    return GUI::Message::question(GUI::Global::mainWindow, title, message,
                                  "\nDo you want to restore from autosave?\n",
                                  "Yes, please restore.", "No, keep loading original");
}
