////////////////////////////////////////////////////////////////////////////////////////
//
//  Copyright 2026 OVITO GmbH, Germany
//
//  This file is part of OVITO (Open Visualization Tool).
//
//  OVITO is free software; you can redistribute it and/or modify it either under the
//  terms of the GNU General Public License version 3 as published by the Free Software
//  Foundation (the "GPL") or, at your option, under the terms of the MIT License.
//  If you do not alter this notice, a recipient may use your version of this
//  file under either the GPL or the MIT License.
//
//  You should have received a copy of the GPL along with this program in a
//  file LICENSE.GPL.txt.  You should have received a copy of the MIT License along
//  with this program in a file LICENSE.MIT.txt
//
//  This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
//  either express or implied. See the GPL or the MIT License for the specific language
//  governing rights and limitations.
//
////////////////////////////////////////////////////////////////////////////////////////

#include <ovito/gui/desktop/GUI.h>
#include <ovito/gui/desktop/actions/WidgetActionManager.h>
#include <ovito/gui/desktop/mainwin/MainWindow.h>
#include <ovito/gui/desktop/mainwin/RecentFilesList.h>
#include <ovito/gui/desktop/dialogs/ApplicationSettingsDialog.h>
#include <ovito/gui/desktop/dialogs/ImportFileDialog.h>
#include <ovito/gui/desktop/dialogs/ImportRemoteFileDialog.h>
#include <ovito/gui/desktop/dialogs/FileExporterSettingsDialog.h>
#include <ovito/gui/desktop/dialogs/MessageDialog.h>
#include <ovito/gui/desktop/dialogs/SystemInformationDialog.h>
#include <ovito/gui/desktop/utilities/concurrent/ProgressDialog.h>
#include <ovito/core/app/PluginManager.h>
#include <ovito/core/app/Application.h>
#include <ovito/core/dataset/DataSetContainer.h>
#include <ovito/core/dataset/io/FileImporter.h>
#include <ovito/core/dataset/io/FileExporter.h>
#include <ovito/core/dataset/scene/SelectionSet.h>
#include <ovito/core/dataset/scene/ScenePreparation.h>
#include <ovito/core/dataset/animation/AnimationSettings.h>

namespace Ovito {

/******************************************************************************
* Handles the ACTION_QUIT command.
******************************************************************************/
void WidgetActionManager::on_Quit_triggered()
{
#ifdef Q_OS_MACOS
    // On macOS, we block attempts to the main window during the time a native dialog is open, because this destroying the parent of a native dialog can lead to a segfault.
    if(TaskManager::isNativeDialogActive()) {
        return;
    }
#endif

    mainWindow()->close();
}

/******************************************************************************
* Handles the ACTION_HELP_ABOUT command.
******************************************************************************/
void WidgetActionManager::on_HelpAbout_triggered()
{
    MessageDialog msgBox(QMessageBox::NoIcon, Application::applicationName(),
            tr("<h3>%1 (Open Visualization Tool)</h3>"
                "<p>Version %2</p>").arg(Application::applicationName()).arg(Application::applicationVersionString()),
            QMessageBox::Ok, mainWindow());
    QString informativeText = QStringLiteral(OVITO_COPYRIGHT_NOTICE) + QStringLiteral("<p><a href=\"https://www.ovito.org/\">https://www.ovito.org/</a></p>");
    // Use the dynamic properties attached to the global Application object to replace any
    // placeholders in the copyright notice with text strings generated by plugins at runtime.
    for(const QByteArray& pname : Application::instance()->dynamicPropertyNames()) {
        QString value = Application::instance()->property(pname.data()).toString();
        informativeText.replace(QStringLiteral("[[%1]]").arg(QString::fromLatin1(pname)), value);
    }
    msgBox.setInformativeText(informativeText);
    msgBox.setDefaultButton(QMessageBox::Ok);
    QPixmap icon = QApplication::windowIcon().pixmap(32 * mainWindow()->devicePixelRatio());
    icon.setDevicePixelRatio(mainWindow()->devicePixelRatio());
    msgBox.setIconPixmap(icon);
    msgBox.exec();
}

/******************************************************************************
* Handles the ACTION_HELP_SHOW_ONLINE_HELP command.
******************************************************************************/
void WidgetActionManager::on_HelpShowOnlineHelp_triggered()
{
    ActionManager::openHelpTopic(QString());
}

/******************************************************************************
* Handles the ACTION_HELP_SHOW_SCRIPTING_HELP command.
******************************************************************************/
void WidgetActionManager::on_HelpShowScriptingReference_triggered()
{
    ActionManager::openHelpTopic(QStringLiteral("python/index.html"));
}

/******************************************************************************
* Handles the ACTION_HELP_SHOW_SCRIPTING_HELP command.
******************************************************************************/
void WidgetActionManager::on_HelpRequestFeature_triggered()
{
#if defined(OVITO_BUILD_PROFESSIONAL)
    QUrl url(QStringLiteral("https://www.ovito.org/feature_request/?p"));
#elif defined(OVITO_BUILD_BASIC)
    QUrl url(QStringLiteral("https://www.ovito.org/feature_request/?b"));
#else
    QUrl url(QStringLiteral("https://www.ovito.org/feature_request/"));
#endif
    if(!QDesktopServices::openUrl(url)) {
        ui().reportError(QStringLiteral("Failed to launch browser to display the OVITO website. The requested URL was:\n%1").arg(url.toDisplayString()));
    }
}

/******************************************************************************
* Handles the ACTION_HELP_GRAPHICS_SYSINFO command.
******************************************************************************/
void WidgetActionManager::on_HelpSystemInfo_triggered()
{
    handleExceptions([&] {
        SystemInformationDialog dlg(ui(), mainWindow());
        dlg.exec();
    });
}

/******************************************************************************
* Handles the ACTION_FILE_NEW_WINDOW command.
******************************************************************************/
void WidgetActionManager::on_FileNewWindow_triggered()
{
    handleExceptions([&] {

        // Save window geometry and layout to settings store.
        mainWindow()->saveMainWindowGeometry();
        mainWindow()->saveLayout();

        MainWindow::openNewWindow();
    });
}

/******************************************************************************
* Handles the ACTION_FILE_OPEN command.
******************************************************************************/
void WidgetActionManager::on_FileOpen_triggered()
{
    handleExceptions([&] {
        ui().askForSaveChanges();

        QSettings settings;
        settings.beginGroup("file/scene");

        // Go to the last directory used.
        QString defaultPath;
        OORef<DataSet> dataSet = this->dataset();
        if(!dataSet || dataSet->filePath().isEmpty()) {
            if(HistoryFileDialog::keepWorkingDirectoryHistoryEnabled()) {
                defaultPath = settings.value("last_directory").toString();
            }
        }
        else {
            defaultPath = dataSet->filePath();
        }

        TaskManager::setNativeDialogActive(true);
        QString filename = QFileDialog::getOpenFileName(mainWindow(), tr("Load Session State"),
                defaultPath, tr("OVITO State Files (*.ovito);;All Files (*)"));
        TaskManager::setNativeDialogActive(false);
        if(filename.isEmpty())
            this_task::cancelAndThrow();

        if(HistoryFileDialog::keepWorkingDirectoryHistoryEnabled()) {
            // Remember directory for the next time...
            settings.setValue("last_directory", QFileInfo(filename).absolutePath());
        }

        OORef<DataSet> dataset = DataSet::createFromFile(filename);
        if(ui().checkLoadedDataset(dataset)) {
            datasetContainer().setCurrentSet(std::move(dataset));
            RecentFilesList::instance().addSessionFileEntry(QUrl::fromLocalFile(filename));
        }
    });
}

/******************************************************************************
* Handles the ACTION_FILE_SAVE command.
******************************************************************************/
void WidgetActionManager::on_FileSave_triggered()
{
    if(!dataset())
        return;

    // Set focus to main window to process any pending user inputs in QLineEdit widgets.
    mainWindow()->setFocus();

    handleExceptions([&] {
        ui().fileSave();
    });
}

/******************************************************************************
* Handles the ACTION_FILE_SAVEAS command.
******************************************************************************/
void WidgetActionManager::on_FileSaveAs_triggered()
{
    if(!dataset())
        return;

    // Set focus to main window to process any pending user inputs in QLineEdit widgets.
    mainWindow()->setFocus();

    handleExceptions([&] {
        ui().fileSaveAs();
    });
}

/******************************************************************************
* Handles the ACTION_SETTINGS_DIALOG command.
******************************************************************************/
void WidgetActionManager::on_Settings_triggered()
{
    ApplicationSettingsDialog dlg(ui());
    dlg.exec();
}

/******************************************************************************
* Handles the ACTION_FILE_IMPORT command.
******************************************************************************/
void WidgetActionManager::on_FileImport_triggered()
{
    if(!dataset())
        return;

    handleExceptions([&] {
        // Let the user select one or more files.
        ImportFileDialog dialog(ui(), PluginManager::instance().metaclassMembers<FileImporter>(), mainWindow(), tr("Load File"), true);
        if(dialog.exec() != QDialog::Accepted)
            return;

        // Selected importer class and sub-format name.
        const auto& [importerClass, importerFormat] = dialog.selectedFileImporter();

        // Get list of selected files.
        std::vector<QUrl> urlsToImport = dialog.urlsToImport();

        // If user accidentally tries to import a .ovito session state file, redirect to the corresponding session loading function.
        if(!importerClass && urlsToImport.size() == 1 && urlsToImport.front().fileName().endsWith(QStringLiteral(".ovito"))) {
            ui().askForSaveChanges();
            OORef<DataSet> dataset = DataSet::createFromFile(urlsToImport.front().toLocalFile());
            if(ui().checkLoadedDataset(dataset)) {
                datasetContainer().setCurrentSet(std::move(dataset));
                RecentFilesList::instance().addSessionFileEntry(urlsToImport.front());
            }
            return;
        }

        if(performTransaction(tr("Import data"), [&, importerClass=&importerClass, importerFormat=&importerFormat] {
            // Import the selected file(s).
            ui().importFiles(
                urlsToImport,
                *importerClass, *importerFormat);
        })) {
            RecentFilesList::instance().addEntry(std::move(urlsToImport), importerClass, importerFormat);
        }
    });
}

/******************************************************************************
* Handles the ACTION_FILE_REMOTE_IMPORT command.
******************************************************************************/
void WidgetActionManager::on_FileRemoteImport_triggered()
{
    if(!dataset())
        return;

    std::vector<QUrl> importedRemoteUrls;
    const FileImporterClass* remoteImporterClass = nullptr;
    QString remoteImporterFormat;
    if(performTransaction(tr("Import data"), [&] {
        const auto [importUrls, importerClass, importerFormat] = [&]() -> std::tuple<std::vector<QUrl>, const FileImporterClass*, QString> {
            // Let the user enter the URL of the remote file.
            ImportRemoteFileDialog dialog(ui(), PluginManager::instance().metaclassMembers<FileImporter>(), mainWindow(), tr("Load Remote File"));
            if(dialog.exec() != QDialog::Accepted)
                return {};

            // Selected importer class and sub-format name.
            const auto [importerClass, importerFormat] = dialog.selectedFileImporter();
            return { { dialog.urlToImport() }, std::move(importerClass), std::move(importerFormat) };
        }();

        // Import URL.
        if(!importUrls.empty()) {
            importedRemoteUrls = importUrls;
            remoteImporterClass = importerClass;
            remoteImporterFormat = importerFormat;
            ui().importFiles(std::move(importUrls), importerClass, importerFormat);
        }
    })) {
        if(!importedRemoteUrls.empty())
            RecentFilesList::instance().addEntry(std::move(importedRemoteUrls), remoteImporterClass, remoteImporterFormat);
    }
}

/******************************************************************************
* Handles the ACTION_FILE_EXPORT command.
******************************************************************************/
void WidgetActionManager::on_FileExport_triggered()
{
    handleExceptions([&]() {
        // Get the scene from which data is to be exported.
        OORef<Scene> scene = datasetContainer().activeScene();
        if(!scene)
            throw Exception(tr("There currently is no active scene that can be exported."));

        // Set focus to main window to process any pending user inputs in QLineEdit widgets.
        mainWindow()->setFocus();

        // Build filter string.
        QStringList filterStrings;
        std::vector<const FileExporterClass*> exporterTypes = PluginManager::instance().metaclassMembers<FileExporter>();
        // Filter out exporters that want to remain hidden from the user.
        std::erase_if(exporterTypes, [](const FileExporterClass* exporterClass) {
            return exporterClass->fileFilterDescription().isEmpty();
        });
        if(exporterTypes.empty())
            throw Exception(tr("This function is disabled, because no file exporter plugins have been installed."));
        std::sort(exporterTypes.begin(), exporterTypes.end(), [](const FileExporterClass* a, const FileExporterClass* b) {
            return a->fileFilterDescription().compare(b->fileFilterDescription(), Qt::CaseInsensitive) < 0;
        });
        for(const FileExporterClass* exporterClass : exporterTypes) {
#ifndef Q_OS_WIN
            filterStrings << QStringLiteral("%1 (%2)").arg(exporterClass->fileFilterDescription(), exporterClass->fileFilter());
#else
            // Workaround for bug in Windows file selection dialog (https://bugreports.qt.io/browse/QTBUG-45759)
            filterStrings << QStringLiteral("%1 (*)").arg(exporterClass->fileFilterDescription());
#endif
        }

        QSettings settings;
        settings.beginGroup("file/export");

        // Let the user select a destination file.
        HistoryFileDialog dialog(ui(), "export", mainWindow(), tr("Export Data"));
        dialog.setNameFilters(filterStrings);
        dialog.setAcceptMode(QFileDialog::AcceptSave);
        dialog.setFileMode(QFileDialog::AnyFile);

        // Go to the last directory used.
        if(HistoryFileDialog::keepWorkingDirectoryHistoryEnabled()) {
            QString lastExportDirectory = settings.value("last_export_dir").toString();
            if(!lastExportDirectory.isEmpty())
                dialog.setDirectory(lastExportDirectory);
        }
        // Select the last export filter being used ...
        QString lastExportFilter = settings.value("last_export_filter").toString();
        if(!lastExportFilter.isEmpty())
            dialog.selectNameFilter(lastExportFilter);

        if(!dialog.exec())
            return;

        QStringList files = dialog.selectedFiles();
        if(files.isEmpty())
            return;

        QString exportFile = files.front();

        if(HistoryFileDialog::keepWorkingDirectoryHistoryEnabled()) {
            // Remember directory for the next time...
            settings.setValue("last_export_dir", dialog.directory().absolutePath());
        }
        // Remember export filter for the next time...
        settings.setValue("last_export_filter", dialog.selectedNameFilter());

        // Export to selected file.
        int exportFilterIndex = filterStrings.indexOf(dialog.selectedNameFilter());
        OVITO_ASSERT(exportFilterIndex >= 0 && exportFilterIndex < exporterTypes.size());

        // Create exporter and initialize it.
        OORef<FileExporter> exporter = static_object_cast<FileExporter>(exporterTypes[exportFilterIndex]->createInstance());

        // Pass output filename to exporter.
        exporter->setOutputFilename(exportFile);

        // Wait until all output data is available for the exporter to inspect it and pick a good default export set.
        ui().scheduleOperationAfterScenePreparation(scene, tr("Waiting for pipeline computations to complete"), [this, exporter=std::move(exporter), scene=scene.get()]() {
            // If the exporter supports it, automatically choose the pipeline(s) and data object(s) to be exported.
            exporter->selectDefaultExportableData(dataset(), scene);

            // Let the user adjust the settings of the exporter.
            FileExporterSettingsDialog settingsDialog(ui(), *scene, exporter, mainWindow());
            if(settingsDialog.exec() != QDialog::Accepted)
                return;

            // Let the exporter do its work.
            Future<void> future = exporter->performExport();

            // Show a progress dialog while the operation is in progress.
            ProgressDialog::showForFuture(std::move(future), ui(), tr("Exporting to file"));
        });
    });
}

}   // End of namespace
