/****************************************************************************
** Copyright (c) 2016, Fougue SAS <https://www.fougue.pro>
** SPDX-License-Identifier: BSD-2-Clause
****************************************************************************/

#include "commands_file.h"

#include "../base/application.h"
#include "../base/io_system.h"
#include "../base/task_manager.h"
#include "../gui/gui_application.h"
#include "../qtcommon/filepath_conv.h"
#include "../qtcommon/qstring_conv.h"
#include "app_module.h"
#include "recent_files.h"
#include "theme.h"

#include <algorithm>
#include <cassert>
#include <fmt/format.h>
#include <QtCore/QtDebug>
#include <QtCore/QElapsedTimer>
#include <QtCore/QMimeData>
#include <QtGui/QDragEnterEvent>
#include <QtGui/QDropEvent>
#include <QtWidgets/QApplication>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QMenu>

namespace Mayo {

namespace {

QString fileFilter(IO::Format format)
{
    if (format == IO::Format_Unknown)
        return {};

    QString filter;
    for (std::string_view suffix : IO::formatFileSuffixes(format)) {
        if (suffix.data() != IO::formatFileSuffixes(format).front().data())
            filter += " ";

        const QString qsuffix = to_QString(suffix);
        filter += "*." + qsuffix;
#ifdef Q_OS_UNIX
        filter += " *." + qsuffix.toUpper();
#endif
    }

    //: %1 is the format identifier and %2 is the file filters string
    return Command::tr("%1 files(%2)")
            .arg(to_QString(IO::formatIdentifier(format)))
            .arg(filter);
}

// TODO: move in Options
struct ImportExportSettings {
    FilePath openDir;
    QString selectedFilter;

    static ImportExportSettings load()
    {
        return {
            AppModule::get()->properties()->lastOpenDir.value(),
            to_QString(AppModule::get()->properties()->lastSelectedFormatFilter.value())
        };
    }

    static void save(const ImportExportSettings& sets)
    {
        AppModule::get()->properties()->lastOpenDir.setValue(sets.openDir);
        AppModule::get()->properties()->lastSelectedFormatFilter.setValue(to_stdString(sets.selectedFilter));
    }
};

struct OpenFileNames {
    std::vector<FilePath> listFilepath;
    ImportExportSettings lastIoSettings;

    enum class GetOption { One, Many };

    static OpenFileNames get(
            QWidget* parentWidget,
            OpenFileNames::GetOption option = OpenFileNames::GetOption::Many
        )
    {
        OpenFileNames result;
        result.lastIoSettings = ImportExportSettings::load();
        QStringList listFormatFilter;
        for (IO::Format format : AppModule::get()->ioSystem()->readerFormats())
            listFormatFilter += fileFilter(format);

        const QString allFilesFilter = Command::tr("All files(*.*)");
        listFormatFilter.append(allFilesFilter);
        if (!listFormatFilter.contains(result.lastIoSettings.selectedFilter))
            result.lastIoSettings.selectedFilter = allFilesFilter;

        const QString dlgTitle = Command::tr("Select Part File");
        const QString dlgOpenDir = filepathTo<QString>(result.lastIoSettings.openDir);
        const QString dlgFilter = listFormatFilter.join(QLatin1String(";;"));
        QString* dlgPtrSelFilter = &result.lastIoSettings.selectedFilter;
        if (option == OpenFileNames::GetOption::One) {
            const QString strFilepath =
                QFileDialog::getOpenFileName(
                    parentWidget, dlgTitle, dlgOpenDir, dlgFilter, dlgPtrSelFilter
                );
            result.listFilepath.clear();
            result.listFilepath.push_back(filepathFrom(strFilepath));
        }
        else {
            const QStringList listStrFilePath =
                QFileDialog::getOpenFileNames(
                    parentWidget, dlgTitle, dlgOpenDir, dlgFilter, dlgPtrSelFilter
                );
            result.listFilepath.clear();
            for (const QString& strFilePath : listStrFilePath)
                result.listFilepath.push_back(filepathFrom(strFilePath));
        }

        if (!result.listFilepath.empty()) {
            result.lastIoSettings.openDir = result.listFilepath.front();
            ImportExportSettings::save(result.lastIoSettings);
        }

        return result;
    }
};

QString strFilepathQuoted(const QString& filepath)
{
    for (QChar c : filepath) {
        if (c.isSpace())
            return "\"" + filepath + "\"";
    }

    return filepath;
}

std::vector<FilePath> getLocalFilePaths(const QList<QUrl>& listUrl)
{
    std::vector<FilePath> filePaths;
    for (const QUrl& url : listUrl) {
        if (url.isLocalFile())
            filePaths.push_back(filepathFrom(url.toLocalFile()));
    }

    return filePaths;
}

} // namespace


void FileCommandTools::closeDocument(IAppContext* context, Document::Identifier docId)
{
    auto app = context->guiApp()->application();
    DocumentPtr doc = app->findDocumentByIdentifier(docId);
    app->closeDocument(doc);
}

void FileCommandTools::closeAllDocuments(IAppContext* context)
{
    while (!context->guiApp()->guiDocuments().empty())
        FileCommandTools::closeDocument(context, context->currentDocument());
}

void FileCommandTools::openDocumentsFromList(IAppContext* context, gsl::span<const FilePath> listFilePath)
{
    assert(context != nullptr);
    auto app = context->guiApp()->application();
    auto appModule = AppModule::get();
    for (const FilePath& fp : listFilePath) {
        DocumentPtr docPtr = app->findDocumentByLocation(fp);
        if (docPtr.IsNull()) {
            docPtr = app->newDocument();
            docPtr->setName(fp.filename().u8string());
            docPtr->setFilePath(fp);
            // WARNING
            // Use the Document identifier instead of handle within the job function(capture)
            // Using the handle increases the ref count and the task will be released on next
            // task creation, so the document won't be destroyed
            const Document::Identifier newDocId = docPtr->identifier();
            const TaskId taskId = context->taskMgr()->newTask([=](TaskProgress* progress) {
                QElapsedTimer chrono;
                chrono.start();
                const bool okImport =
                    appModule->ioSystem()->importInDocument()
                        .targetDocument(app->findDocumentByIdentifier(newDocId))
                        .withFilepath(fp)
                        .withParametersProvider(appModule)
                        .withEntityPostProcess([appModule](TDF_Label labelEntity, TaskProgress* progress) {
                            appModule->computeBRepMesh(labelEntity, progress);
                        })
                        .withEntityPostProcessRequiredIf(&IO::formatProvidesBRep)
                        .withEntityPostProcessInfoProgress(20, Command::textIdTr("Mesh BRep shapes"))
                        .withMessenger(appModule)
                        .withTaskProgress(progress)
                    .execute();
                if (okImport)
                    appModule->emitInfo(fmt::format(Command::textIdTr("Import time: {}ms"), chrono.elapsed()));
            });
            context->taskMgr()->setTitle(taskId, fp.stem().u8string());
            context->taskMgr()->run(taskId);
            appModule->prependRecentFile(fp);
        }
        else {
            if (listFilePath.size() == 1)
                context->setCurrentDocument(docPtr->identifier());
        }
    } // endfor()
}

void FileCommandTools::openDocument(IAppContext* context, const FilePath& filePath)
{
    FileCommandTools::openDocumentsFromList(context, gsl::span<const FilePath>(&filePath, 1));
}

void FileCommandTools::importInDocument(
        IAppContext* context, const DocumentPtr& targetDoc, gsl::span<const FilePath> listFilePaths
    )
{
    // WARNING
    // Use the Document identifier instead of handle within the job function(capture)
    // Using the handle increases the ref count and the task will be released on next task creation,
    // so the document won't be destroyed
    const Document::Identifier targetDocId = targetDoc->identifier();

    // WARNING
    // Can't safely use `listFilePaths` in the lambda passed to TaskManager::newTask()
    // As the lambda will be executed in another thread the memory pointed to by `listFilePaths` may
    // have been destroyed
    // The solution is to copy the span into a local array that can be captured and safely used by
    // the lambda
    std::vector<FilePath> arrayFilePaths;
    arrayFilePaths.resize(listFilePaths.size());
    std::copy(listFilePaths.begin(), listFilePaths.end(), arrayFilePaths.begin());

    const TaskId taskId = context->taskMgr()->newTask([=](TaskProgress* progress) {
        QElapsedTimer chrono;
        chrono.start();

        auto appModule = AppModule::get();
        auto doc = appModule->application()->findDocumentByIdentifier(targetDocId);
        const bool okImport =
            appModule->ioSystem()->importInDocument()
                .targetDocument(doc)
                .withFilepaths(arrayFilePaths)
                .withParametersProvider(appModule)
                .withEntityPostProcess([appModule](TDF_Label labelEntity, TaskProgress* progress) {
                    appModule->computeBRepMesh(labelEntity, progress);
                })
                .withEntityPostProcessRequiredIf(&IO::formatProvidesBRep)
                .withEntityPostProcessInfoProgress(20, Command::textIdTr("Mesh BRep shapes"))
                .withMessenger(appModule)
                .withTaskProgress(progress)
            .execute();
        if (okImport)
            appModule->emitInfo(fmt::format(Command::textIdTr("Import time: {}ms"), chrono.elapsed()));
    });
    const QString taskTitle =
        listFilePaths.size() > 1 ?
            Command::tr("Import") :
            filepathTo<QString>(listFilePaths.front().stem())
        ;
    context->taskMgr()->setTitle(taskId, to_stdString(taskTitle));
    context->taskMgr()->run(taskId);
}

void FileCommandTools::importInDocument(
        IAppContext* context, const DocumentPtr& targetDoc, const FilePath& filePath
    )
{
    FileCommandTools::importInDocument(context, targetDoc, gsl::span<const FilePath>(&filePath, 1));
}

CommandNewDocument::CommandNewDocument(IAppContext* context)
    : Command(context)
{
    auto action = this->createAction();
    action->setText(Command::tr("New"));
    action->setToolTip(Command::tr("New Document"));
    action->setShortcut(QKeySequence::StandardKey::New);
}

void CommandNewDocument::execute()
{
    static unsigned docSequenceId = 0;
    auto docPtr = this->app()->newDocument(Document::Format::Binary);
    docPtr->setName(to_stdString(Command::tr("Anonymous%1").arg(++docSequenceId)));
}

CommandOpenDocuments::CommandOpenDocuments(IAppContext* context)
    : Command(context)
{
    auto action = this->createAction();
    action->setText(Command::tr("Open"));
    action->setToolTip(Command::tr("Open Documents"));
    action->setShortcut(QKeySequence::StandardKey::Open);

    context->widgetMain()->setAcceptDrops(true);
    context->widgetMain()->installEventFilter(this);
}

void CommandOpenDocuments::execute()
{
    const auto resFileNames = OpenFileNames::get(this->widgetMain());
    if (!resFileNames.listFilepath.empty())
        FileCommandTools::openDocumentsFromList(this->context(), resFileNames.listFilepath);
}

bool CommandOpenDocuments::eventFilter(QObject* watched, QEvent* event)
{
    if (watched == this->widgetMain()) {
        if (event->type() == QEvent::DragEnter) {
            auto dragEnterEvent = static_cast<QDragEnterEvent*>(event);
            if (dragEnterEvent->mimeData()->hasUrls())
                dragEnterEvent->acceptProposedAction();

            return true;
        }
        else if (event->type() == QEvent::Drop) {
            auto dropEvent = static_cast<QDropEvent*>(event);
            const QList<QUrl> listUrl = dropEvent->mimeData()->urls();
            dropEvent->acceptProposedAction();
            FileCommandTools::openDocumentsFromList(this->context(), getLocalFilePaths(listUrl));
            return true;
        }
        else {
            return false;
        }
    }

    return Command::eventFilter(watched, event);
}

CommandRecentFiles::CommandRecentFiles(IAppContext* context)
    : Command(context)
{
    auto action = this->createAction();
    action->setText(Command::tr("Recent files"));
}

CommandRecentFiles::CommandRecentFiles(IAppContext* context, const QMenu* containerMenu)
    : CommandRecentFiles(context)
{
    QObject::connect(containerMenu, &QMenu::aboutToShow, this, &CommandRecentFiles::recreateEntries);
}

void CommandRecentFiles::execute()
{
    // Intentionally left empty because this command is UI-driven
    // execute() is unused but required by the Command interface
}

void CommandRecentFiles::recreateEntries()
{
    QMenu* menu = this->action()->menu();
    if (!menu)
        menu = new QMenu(this->widgetMain());

    menu->clear();
    int idFile = 0;
    auto appModule = AppModule::get();
    const RecentFiles& recentFiles = appModule->properties()->recentFiles;
    for (const RecentFile& recentFile : recentFiles) {
        const QString strFilePath = filepathTo<QString>(recentFile.filepath);
        const QString strEntryRecentFile = Command::tr("%1 | %2").arg(++idFile).arg(strFilePath);
        menu->addAction(strEntryRecentFile, this, [=]{
            FileCommandTools::openDocument(this->context(), recentFile.filepath);
        });
    }

    if (!recentFiles.empty()) {
        menu->addSeparator();
        menu->addAction(Command::tr("Clear menu"), this, [=]{
            menu->clear();
            appModule->properties()->recentFiles.setValue({});
        });
    }

    this->action()->setMenu(menu);
}

CommandImportInCurrentDocument::CommandImportInCurrentDocument(IAppContext* context)
    : Command(context)
{
    auto action = this->createAction();
    action->setText(Command::tr("Import"));
    action->setToolTip(Command::tr("Import in current document"));
    action->setIcon(mayoTheme()->icon(Theme::Icon::Import));
}

void CommandImportInCurrentDocument::execute()
{
    const GuiDocument* guiDoc = this->currentGuiDocument();
    if (!guiDoc)
        return;

    const auto resFileNames = OpenFileNames::get(this->widgetMain());
    if (resFileNames.listFilepath.empty())
        return;

    FileCommandTools::importInDocument(this->context(), guiDoc->document(), resFileNames.listFilepath);
}

bool CommandImportInCurrentDocument::getEnabledStatus() const
{
    return this->app()->documentCount() != 0
           && this->context()->currentPage() == IAppContext::Page::Documents;
}

CommandExportSelectedApplicationItems::CommandExportSelectedApplicationItems(IAppContext* context)
    : Command(context)
{
    auto action = this->createAction();
    action->setText(Command::tr("Export selected items"));
    action->setToolTip(Command::tr("Export selected items"));
    action->setIcon(mayoTheme()->icon(Theme::Icon::Export));
}

void CommandExportSelectedApplicationItems::execute()
{
    auto appModule = AppModule::get();
    if (this->guiApp()->selectionModel()->selectedItems().empty()) {
        appModule->emitError(Command::textIdTr("No item selected for export"));
        return;
    }

    QStringList listWriterFileFilter;
    for (IO::Format format : appModule->ioSystem()->writerFormats())
        listWriterFileFilter.append(fileFilter(format));

    auto lastSettings = ImportExportSettings::load();
    const QString strFilepath =
        QFileDialog::getSaveFileName(
            this->widgetMain(),
            Command::tr("Select Output File"),
            filepathTo<QString>(lastSettings.openDir),
            listWriterFileFilter.join(QLatin1String(";;")),
            &lastSettings.selectedFilter
        );
    if (strFilepath.isEmpty())
        return;

    lastSettings.openDir = filepathFrom(strFilepath);
    // IMPORTANT
    //     Don't try to deduce format from the selected file filter. The native UI may change the
    //     input filters for some reason(add spaces, locale, ...)
    //     See issue https://github.com/fougue/mayo/issues/357
    const IO::Format format = appModule->ioSystem()->probeFormat(filepathFrom(strFilepath));
    const TaskId taskId = this->taskMgr()->newTask([=](TaskProgress* progress) {
        QElapsedTimer chrono;
        chrono.start();
        const bool okExport =
            appModule->ioSystem()->exportApplicationItems()
                .targetFile(filepathFrom(strFilepath))
                .targetFormat(format)
                .withItems(this->guiApp()->selectionModel()->selectedItems())
                .withParameters(appModule->findWriterParameters(format))
                .withMessenger(appModule)
                .withTaskProgress(progress)
            .execute();
        if (okExport)
            appModule->emitInfo(fmt::format(Command::textIdTr("Export time: {}ms"), chrono.elapsed()));
    });
    this->taskMgr()->setTitle(taskId, to_stdString(QFileInfo(strFilepath).fileName()));
    this->taskMgr()->run(taskId);
    ImportExportSettings::save(lastSettings);
}

bool CommandExportSelectedApplicationItems::getEnabledStatus() const
{
    return this->app()->documentCount() != 0
            && this->context()->currentPage() == IAppContext::Page::Documents;
}

CommandCloseCurrentDocument::CommandCloseCurrentDocument(IAppContext* context)
    : Command(context)
{
    auto action = this->createAction();
    action->setText(Command::tr("Close \"%1\""));
    action->setToolTip(action->text());
    action->setIcon(mayoTheme()->icon(Theme::Icon::Cross));
    action->setShortcut(QKeySequence::StandardKey::Close);

    QObject::connect(
        context, &IAppContext::currentDocumentChanged,
        this, &CommandCloseCurrentDocument::updateActionText
    );
    this->app()->signalDocumentNameChanged.connectSlot([=](const DocumentPtr& doc) {
        if (this->currentDocument() == doc->identifier())
            this->updateActionText(this->currentDocument());
    });

    this->updateActionText(-1);
}

void CommandCloseCurrentDocument::execute()
{
    FileCommandTools::closeDocument(this->context(), this->currentDocument());
}

bool CommandCloseCurrentDocument::getEnabledStatus() const
{
    return this->app()->documentCount() != 0;
}

void CommandCloseCurrentDocument::updateActionText(Document::Identifier docId)
{
    DocumentPtr docPtr = this->app()->findDocumentByIdentifier(docId);
    const QString docName = to_QString(docPtr ? docPtr->name() : std::string{});
    const QString textActionClose =
        docPtr ?
            Command::tr("Close %1").arg(strFilepathQuoted(docName)) :
            Command::tr("Close")
        ;
    this->action()->setText(textActionClose);
    this->action()->setToolTip(textActionClose);
}

CommandCloseAllDocuments::CommandCloseAllDocuments(IAppContext* context)
    : Command(context)
{
    auto action = this->createAction();
    action->setText(Command::tr("Close all"));
    action->setToolTip(Command::tr("Close all documents"));
}

void CommandCloseAllDocuments::execute()
{
    FileCommandTools::closeAllDocuments(this->context());
}

bool CommandCloseAllDocuments::getEnabledStatus() const
{
    return this->app()->documentCount() != 0;
}

CommandCloseAllDocumentsExceptCurrent::CommandCloseAllDocumentsExceptCurrent(IAppContext* context)
    : Command(context)
{
    auto action = this->createAction();
    action->setText(Command::tr("Close all except current"));
    action->setToolTip(Command::tr("Close all except current document"));

    QObject::connect(
        context, &IAppContext::currentDocumentChanged,
        this, &CommandCloseAllDocumentsExceptCurrent::updateActionText
    );
    this->app()->signalDocumentNameChanged.connectSlot([=](const DocumentPtr& doc) {
        if (this->currentDocument() == doc->identifier())
            this->updateActionText(this->currentDocument());
    });

    this->updateActionText(-1);
}

void CommandCloseAllDocumentsExceptCurrent::execute()
{
    GuiDocument* currentGuiDoc = this->currentGuiDocument();
    std::vector<GuiDocument*> vecGuiDoc;
    for (GuiDocument* guiDoc : this->guiApp()->guiDocuments())
        vecGuiDoc.push_back(guiDoc);

    for (const GuiDocument* guiDoc : vecGuiDoc) {
        if (guiDoc != currentGuiDoc)
            FileCommandTools::closeDocument(this->context(), guiDoc->document()->identifier());
    }
}

bool CommandCloseAllDocumentsExceptCurrent::getEnabledStatus() const
{
    return this->app()->documentCount() != 0;
}

void CommandCloseAllDocumentsExceptCurrent::updateActionText(Document::Identifier docId)
{
    DocumentPtr docPtr = this->app()->findDocumentByIdentifier(docId);
    const QString docName = to_QString(docPtr ? docPtr->name() : std::string{});
    const QString textActionClose =
        docPtr ?
            Command::tr("Close all except %1").arg(strFilepathQuoted(docName)) :
            Command::tr("Close all except current")
        ;
    this->action()->setText(textActionClose);
}

CommandQuitApplication::CommandQuitApplication(IAppContext* context)
    : Command(context)
{
    auto action = this->createAction();
    action->setMenuRole(QAction::QuitRole);
    action->setText(Command::tr("Quit"));
    action->setShortcut(QKeySequence::StandardKey::Quit);
}

void CommandQuitApplication::execute()
{
    FileCommandTools::closeAllDocuments(this->context());
    QApplication::quit();
}

} // namespace Mayo
