/*
    This file is part of Rocs.
    SPDX-FileCopyrightText: 2008-2011 Tomaz Canabrava <tomaz.canabrava@gmail.com>
    SPDX-FileCopyrightText: 2008 Ugo Sangiori <ugorox@gmail.com>
    SPDX-FileCopyrightText: 2010-2011 Wagner Reck <wagner.reck@gmail.com>
    SPDX-FileCopyrightText: 2011-2014 Andreas Cord-Landwehr <cordlandwehr@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "mainwindow.h"
#include "rocsversion.h"
#include "settings.h"

#include "grapheditorwidget.h"
#include "libgraphtheory/kernel/kernel.h"
#include "libgraphtheory/view.h"
#include "plugins/scriptapi/scriptapiwidget.h"
#include "project/project.h"
#include "ui/codeeditorwidget.h"
#include "ui/documenttypeswidget.h"
#include "ui/fileformatdialog.h"
#include "ui/journalwidget.h"
#include "ui/scriptoutputwidget.h"
#include "ui/sidedockwidget.h"
#include <KActionCollection>
#include <KActionMenu>
#include <KConfigDialog>
#include <KLocalizedString>
#include <KMessageBox>
#include <KRecentFilesAction>
#include <KTar>
#include <KToolBar>
#include <QActionGroup>
#include <QApplication>
#include <QCloseEvent>
#include <QDebug>
#include <QFileDialog>
#include <QGraphicsView>
#include <QGridLayout>
#include <QIcon>
#include <QInputDialog>
#include <QLabel>
#include <QLayout>
#include <QPointer>
#include <QPushButton>
#include <QQuickWidget>
#include <QRegularExpression>
#include <QSplitter>
#include <QToolButton>
#include <ktexteditor/configpage.h>
#include <ktexteditor/document.h>
#include <ktexteditor/editor.h>
#include <ktexteditor/view.h>
#include <kwidgetsaddons_version.h>

using namespace GraphTheory;

MainWindow::MainWindow()
    : KXmlGuiWindow()
    , m_currentProject(nullptr)
    , m_kernel(new Kernel)
    , m_codeEditorWidget(new CodeEditorWidget(this))
    , m_graphEditorWidget(new GraphEditorWidget(this))
    , m_outputWidget(new ScriptOutputWidget(this))
{
    setObjectName("RocsMainWindow");
    m_graphEditor = new GraphTheory::Editor();

    setupWidgets();
    setupActions();
    setupGUI(Keys | Save | Create);

    setupToolsPluginsAction();

    // setup kernel
    connect(m_kernel, &Kernel::message, m_outputWidget, &ScriptOutputWidget::processMessage);

    // TODO: use welcome widget instead of creating default empty project
    createProject();
    updateCaption();

    // update rocs config version
    Settings::setVersion(ROCS_VERSION_STRING);

    // disable save action from kpart, since we take care for the editor by global save action
    // here "file_save" is the action identifier from katepartui.rc
    // note that we may not use that name for our own actions
    const auto allCollections = KActionCollection::allCollections();
    for (KActionCollection *ac : allCollections) {
        if (ac->action("file_save")) {
            ac->action("file_save")->setDisabled(true);
        }
    }
}

MainWindow::~MainWindow()
{
    Settings::setVSplitterSizeTop(m_vSplitter->sizes()[0]);
    Settings::setVSplitterSizeBottom(m_vSplitter->sizes()[1]);
    Settings::setHSplitterSizeLeft(m_hSplitter->sizes()[0]);
    Settings::setHSplitterSizeRight(m_hSplitter->sizes()[1]);
    Settings::setHScriptSplitterSizeLeft(m_hScriptSplitter->sizes()[0]);
    Settings::setHScriptSplitterSizeRight(m_hScriptSplitter->sizes()[1]);
    m_recentProjects->saveEntries(Settings::self()->config()->group("RecentFiles"));

    Settings::self()->save();

    m_graphEditor->deleteLater();
    m_kernel->deleteLater();
}

void MainWindow::closeEvent(QCloseEvent *event)
{
    if (queryClose() == true) {
        event->accept();
    } else {
        event->ignore();
    }
    return;
}

void MainWindow::setupWidgets()
{
    // setup main widgets
    QWidget *sidePanel = setupSidePanel();
    QWidget *scriptPanel = setupScriptPanel();

    // splits the main window horizontally
    m_vSplitter = new QSplitter(this);
    m_vSplitter->setOrientation(Qt::Vertical);
    m_vSplitter->addWidget(m_graphEditorWidget);
    m_vSplitter->addWidget(scriptPanel);

    // horizontal arrangement
    m_hSplitter = new QSplitter(this);
    m_hSplitter->setOrientation(Qt::Horizontal);
    m_hSplitter->addWidget(m_vSplitter);
    m_hSplitter->addWidget(sidePanel);

    // set sizes for script panel
    m_hScriptSplitter->setSizes({Settings::hScriptSplitterSizeLeft(), Settings::hScriptSplitterSizeRight(), 80});

    // set sizes for vertical splitter
    m_vSplitter->setSizes({Settings::vSplitterSizeTop(), Settings::vSplitterSizeBottom()});

    // set sizes for side panel
    // the following solves the setting of the panel width if it was closed at previous session
    int panelWidth = Settings::hSplitterSizeRight();
    if (panelWidth == 0) {
        // FIXME this is only a workaround
        //  that fixes the wrong saving of hSplitterSizeRight
        panelWidth = 400;
    }

    m_hSplitter->setSizes({Settings::hSplitterSizeLeft(), panelWidth});

    setCentralWidget(m_hSplitter);
}

QWidget *MainWindow::setupScriptPanel()
{
    m_hScriptSplitter = new QSplitter(this);
    m_hScriptSplitter->setOrientation(Qt::Horizontal);

    KToolBar *executeCommands = new KToolBar(this);
    executeCommands->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
    executeCommands->setOrientation(Qt::Vertical);
    m_runScript = new QAction(QIcon::fromTheme("media-playback-start"), i18nc("@action:intoolbar Script Execution", "Run"), this);
    m_runScript->setToolTip(i18nc("@info:tooltip", "Execute currently active script on active graph document."));
    m_stopScript = new QAction(QIcon::fromTheme("process-stop"), i18nc("@action:intoolbar Script Execution", "Stop"), this);
    m_stopScript->setToolTip(i18nc("@info:tooltip", "Stop script execution."));
    m_stopScript->setEnabled(false);
    executeCommands->addAction(m_runScript);
    executeCommands->addAction(m_stopScript);
    // add actions to action collection to be able to set shortcuts on them in the ui
    actionCollection()->addAction("_runScript", m_runScript);
    actionCollection()->addAction("_stopScript", m_stopScript);

    connect(m_runScript, &QAction::triggered, this, &MainWindow::executeScript);
    connect(m_stopScript, &QAction::triggered, this, &MainWindow::stopScript);

    m_hScriptSplitter->addWidget(m_codeEditorWidget);
    m_hScriptSplitter->addWidget(m_outputWidget);

    QWidget *scriptInterface = new QWidget(this);
    scriptInterface->setLayout(new QHBoxLayout);
    scriptInterface->layout()->addWidget(m_hScriptSplitter);
    scriptInterface->layout()->addWidget(executeCommands);

    return scriptInterface;
}

QWidget *MainWindow::setupSidePanel()
{
    // add sidebar
    SidedockWidget *sideDock = new SidedockWidget(this);
    addToolBar(Qt::RightToolBarArea, sideDock->toolbar());

    // add widgets to dock
    // document property widgets
    DocumentTypesWidget *documentTypesWidget = new DocumentTypesWidget(this);
    connect(this, &MainWindow::graphDocumentChanged, documentTypesWidget, &DocumentTypesWidget::setDocument);
    sideDock->addDock(documentTypesWidget, i18n("Element Types"), QIcon::fromTheme("document-properties"));
    if (m_currentProject && m_currentProject->activeGraphDocument()) {
        documentTypesWidget->setDocument(m_currentProject->activeGraphDocument());
    }

    // Project Journal
    m_journalWidget = new JournalEditorWidget(this);
    sideDock->addDock(m_journalWidget, i18nc("@title", "Journal"), QIcon::fromTheme("story-editor"));

    // Rocs scripting API documentation
    ScriptApiWidget *apiDoc = new ScriptApiWidget(this);
    sideDock->addDock(apiDoc, i18nc("@title", "Scripting API"), QIcon::fromTheme("documentation"));

    return sideDock;
}

void MainWindow::setProject(Project *project)
{
    m_codeEditorWidget->setProject(project);
    m_graphEditorWidget->setProject(project);
    m_journalWidget->openJournal(project);
    updateCaption();

    if (m_currentProject) {
        m_currentProject->disconnect(this);
        m_currentProject->deleteLater();
    }

    connect(project,
            static_cast<void (Project::*)(GraphTheory::GraphDocumentPtr)>(&Project::activeGraphDocumentChanged),
            this,
            &MainWindow::graphDocumentChanged);
    connect(project, &Project::modifiedChanged, this, &MainWindow::updateCaption);
    m_currentProject = project;
    Q_EMIT graphDocumentChanged(m_currentProject->activeGraphDocument());
}

void MainWindow::setupActions()
{
    KStandardAction::quit(this, SLOT(quit()), actionCollection());
    KStandardAction::preferences(this, SLOT(showConfigurationDialog()), actionCollection());

    // setup graph visual editor actions and add them to mainwindow action collection
    //     m_graphEditor->setupActions(actionCollection()); //FIXME add editor actions to main action collection

    // Menu actions
    QAction *newProjectAction = new QAction(QIcon::fromTheme("document-new"), i18nc("@action:inmenu", "New Project"), this);
    newProjectAction->setShortcutContext(Qt::ApplicationShortcut);
    actionCollection()->addAction("new-project", newProjectAction);
    actionCollection()->setDefaultShortcut(newProjectAction, QKeySequence::New);
    connect(newProjectAction, &QAction::triggered, this, &MainWindow::createProject);

    QAction *projectSaveAction = new QAction(QIcon::fromTheme("document-save"), i18nc("@action:inmenu", "Save Project"), this);
    projectSaveAction->setShortcutContext(Qt::ApplicationShortcut);
    actionCollection()->addAction("save-project", projectSaveAction);
    actionCollection()->setDefaultShortcut(projectSaveAction, QKeySequence::Save);
    connect(projectSaveAction, &QAction::triggered, this, &MainWindow::saveProject);

    QAction *projectOpenAction = new QAction(QIcon::fromTheme("document-open"), i18nc("@action:inmenu", "Open Project..."), this);
    projectOpenAction->setShortcutContext(Qt::ApplicationShortcut);
    actionCollection()->addAction("open-project", projectOpenAction);
    actionCollection()->setDefaultShortcut(projectOpenAction, QKeySequence::Open);
    connect(projectOpenAction, &QAction::triggered, this, [=]() {
        openProject();
    });

    m_recentProjects = new KRecentFilesAction(QIcon("document-open"), i18nc("@action:inmenu", "Recent Projects"), this);
    connect(m_recentProjects, &KRecentFilesAction::urlSelected, this, &MainWindow::openProject);
    actionCollection()->addAction("recent-project", m_recentProjects);
    m_recentProjects->loadEntries(Settings::self()->config()->group("RecentFiles"));

    createAction("document-save-as", i18nc("@action:inmenu", "Save Project As..."), "save-project-as", SLOT(saveProjectAs()), this);
    createAction("document-new", i18nc("@action:inmenu", "New Graph Document"), "new-graph", SLOT(createGraphDocument()), this);
    createAction("document-new", i18nc("@action:inmenu", "New Script File..."), "new-script", SLOT(tryToCreateCodeDocument()), this);
    createAction("document-import", i18nc("@action:inmenu", "Import Graph..."), "import-graph", SLOT(importGraphDocument()), this);
    createAction("document-export", i18nc("@action:inmenu", "Export Graph As..."), "export-graph-as", SLOT(exportGraphDocument()), this);
    createAction("document-import", i18nc("@action:inmenu", "Import Script..."), "add-script", SLOT(importCodeDocument()), this);
    createAction("document-export", i18nc("@action:inmenu", "Export Script..."), "export-script", SLOT(exportCodeDocument()), this);
}

void MainWindow::createAction(const QByteArray &iconName, const QString &actionTitle, const QString &actionName, const char *slot, QObject *parent)
{
    QAction *action = new QAction(QIcon::fromTheme(iconName), actionTitle, parent);
    actionCollection()->addAction(actionName, action);
    connect(action, SIGNAL(triggered(bool)), parent, slot);
}

void MainWindow::showConfigurationDialog()
{
    QPointer<KConfigDialog> dialog = new KConfigDialog(this, "settings", Settings::self());
    KTextEditor::Editor *editor = KTextEditor::Editor::instance();
    for (int index = 0; index < editor->configPages(); ++index) {
        KTextEditor::ConfigPage *page = editor->configPage(index, dialog);
        dialog->addPage(page, page->name(), page->icon().name(), page->fullName());
    }
    dialog->exec();
}

void MainWindow::setupToolsPluginsAction()
{
    QList<EditorPluginInterface *> availablePlugins = m_graphEditorPluginManager.plugins();
    QList<QAction *> actions;
    int count = 0;
    for (auto plugin : availablePlugins) {
        auto *action = new QAction(plugin->displayName(), this);
        action->setData(count++);
        connect(action, &QAction::triggered, this, &MainWindow::showEditorPluginDialog);
        actions << action;
    }
    unplugActionList("tools_plugins");
    plugActionList("tools_plugins", actions);
}

void MainWindow::importCodeDocument()
{
    QString startDirectory = Settings::lastOpenedDirectory();
    QUrl fileUrl = QUrl::fromLocalFile(QFileDialog::getOpenFileName(this, i18nc("@title:window", "Import Script into Project"), startDirectory));

    if (fileUrl.isEmpty()) {
        return;
    }
    m_currentProject->importCodeDocument(fileUrl);
    Settings::setLastOpenedDirectory(startDirectory);
}

void MainWindow::exportCodeDocument()
{
    QString startDirectory = Settings::lastOpenedDirectory();
    QUrl fileUrl = QUrl::fromLocalFile(QFileDialog::getSaveFileName(this, i18nc("@title:window", "Export Script"), startDirectory, i18n("JavaScript (*.js)")));
    m_codeEditorWidget->activeDocument()->saveAs(fileUrl);
}

void MainWindow::createProject()
{
    if (!queryClose()) {
        return;
    }

    Project *project = new Project(m_graphEditor);
    project->createCodeDocument(i18n("untitled"));
    project->addGraphDocument(m_graphEditor->createDocument());
    project->setModified(false);

    setProject(project);
}

void MainWindow::saveProject()
{
    if (m_currentProject->projectUrl().isEmpty()) {
        saveProjectAs();
        return;
    }

    m_currentProject->projectSave();
    m_recentProjects->addUrl(m_currentProject->projectUrl());

    updateCaption();
}

void MainWindow::saveProjectAs()
{
    QString startDirectory = Settings::lastOpenedDirectory();
    QString file = QFileDialog::getSaveFileName(this, i18nc("@title:window", "Save Project As"), startDirectory, i18n("Rocs Projects (*.rocs)"));

    if (file.isEmpty()) {
        qCritical() << "Filename is empty and no script file was created.";
        return;
    }
    QFileInfo fi(file);
    if (fi.exists()) {
        const int btnCode =
            KMessageBox::warningContinueCancel(this,
                                               i18nc("@info", "A file named \"%1\" already exists. Are you sure you want to overwrite it?", fi.fileName()),
                                               i18nc("@title:window", "Overwrite File?"),
                                               KStandardGuiItem::overwrite());
        if (btnCode == KMessageBox::Cancel) {
            return; // cancel saving
        }
    }
    Settings::setLastOpenedDirectory(m_currentProject->projectUrl().path());
    m_currentProject->projectSaveAs(QUrl::fromLocalFile(file));
    m_recentProjects->addUrl(QUrl::fromLocalFile(file));
    updateCaption();
}

void MainWindow::openProject(const QUrl &fileName)
{
    if (!queryClose()) {
        return;
    }

    QString startDirectory = Settings::lastOpenedDirectory();
    QUrl file = fileName;
    if (file.isEmpty()) {
        // show open dialog
        file = QUrl::fromLocalFile(
            QFileDialog::getOpenFileName(this, i18nc("@title:window", "Open Project Files"), startDirectory, i18n("Rocs projects (*.rocs)")));
    }
    if (file.isEmpty()) {
        return;
    }
    Project *project = new Project(file, m_graphEditor);
    if (!project->isValid()) {
        KMessageBox::error(nullptr, i18nc("@info", "The Rocs project could not be imported because the project file could not be parsed."));
    } else {
        setProject(project);
        m_recentProjects->addUrl(file);
        updateCaption();

        Settings::setLastOpenedDirectory(file.path());
    }
}

void MainWindow::updateCaption()
{
    if (!m_currentProject) {
        return;
    }

    QString modified;
    if (m_currentProject->isModified()) {
        modified = '*';
    }

    if (m_currentProject->projectUrl().isEmpty()) {
        setCaption(i18nc("caption text for temporary project", "[ untitled ]%1", modified));
    } else {
        setCaption(QString("[ %1 ]%2").arg(m_currentProject->projectUrl().toLocalFile()).arg(modified));
    }
}

QString MainWindow::uniqueFilename(const QString &basePrefix, const QString &suffix)
{
    QFile targetFile;
    QString basePath = m_currentProject->projectUrl().path();
    QString fullSuffix = '.' + suffix;
    QString fullPrefix = basePrefix;

    if (fullPrefix.isNull()) {
        fullPrefix = m_currentProject->projectUrl().fileName().remove(QRegularExpression(".rocs*$"));
    } else if (fullPrefix.endsWith(fullSuffix)) {
        fullPrefix.remove(QRegularExpression(fullSuffix + '$'));
    }

    targetFile.setFileName(basePath + fullPrefix + fullSuffix);
    for (int i = 1; targetFile.exists(); i++) {
        targetFile.setFileName(basePath + fullPrefix + QString::number(i) + fullSuffix);
    }

    return targetFile.fileName();
}

void MainWindow::tryToCreateCodeDocument()
{
    QString basePrefix = QInputDialog::getText(this, i18n("ScriptName"), i18n("Enter the name of your new script"));
    if (basePrefix.isNull()) {
        qDebug() << "Filename is empty and no script file was created.";
        return;
    }

    QString fullPath = m_currentProject->workingDir() + QLatin1Char('/') + basePrefix + QStringLiteral(".js");
    QFileInfo file(fullPath);
    if (file.exists()) {
        KMessageBox::error(this, i18n("File already exists."));
        return;
    }

    m_currentProject->createCodeDocument(basePrefix);
}

void MainWindow::createGraphDocument()
{
    GraphDocumentPtr document = m_graphEditor->createDocument();
    m_currentProject->addGraphDocument(document);
}

bool MainWindow::queryClose()
{
    if (!m_currentProject) {
        return true;
    }
    if (!m_currentProject->isModified()) {
        return true;
    }
#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
    const int btnCode = KMessageBox::warningTwoActionsCancel(this,
#else
    const int btnCode = KMessageBox::warningYesNoCancel(this,
#endif
                                                             i18nc("@info", "Changes on your project are unsaved. Do you want to save your changes?"),
                                                             QString(),
                                                             KStandardGuiItem::save(),
                                                             KStandardGuiItem::discard());

    if (btnCode == KMessageBox::Cancel) {
        return false;
    }

#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
    if (btnCode == KMessageBox::PrimaryAction) {
#else
    if (btnCode == KMessageBox::Yes) {
#endif
        saveProject();
    }

    return true;
}

void MainWindow::quit()
{
    if (queryClose()) {
        QApplication::quit();
    }
}

void MainWindow::importGraphDocument()
{
    FileFormatDialog importer(this);
    GraphDocumentPtr document = importer.importFile();
    if (!document) {
        qWarning() << "No graph document was imported.";
        return;
    }
    m_currentProject->addGraphDocument(document);
}

void MainWindow::exportGraphDocument()
{
    FileFormatDialog exporter(this);
    exporter.exportFile(m_currentProject->activeGraphDocument());
}

void MainWindow::showEditorPluginDialog()
{
    QAction *action = qobject_cast<QAction *>(sender());

    if (!action) {
        return;
    }
    if (EditorPluginInterface *plugin = m_graphEditorPluginManager.plugins().value(action->data().toInt())) {
        plugin->showDialog(m_currentProject->activeGraphDocument());
    }
}

void MainWindow::executeScript()
{
    if (m_outputWidget->isOutputClearEnabled()) {
        m_outputWidget->clear();
    }
    QString script = m_codeEditorWidget->activeDocument()->text();
    enableStopAction();

    m_kernel->execute(m_currentProject->activeGraphDocument(), script);
}

void MainWindow::stopScript()
{
    m_kernel->stop();
    disableStopAction();
}

void MainWindow::enableStopAction()
{
    m_stopScript->setEnabled(true);
}

void MainWindow::disableStopAction()
{
    m_stopScript->setEnabled(false);
}

#include "moc_mainwindow.cpp"
