#include "MainWindow.hh"
#include "PropertyModel.hh"
#include "VivyDocumentView.hh"
#include "AboutWindow.hh"
#include "VivyFileIconProvider.hh"
#include "../Lib/Utils.hh"
#include "../VivyApplication.hh"

#define DCL_MENU(menu, name) [[maybe_unused]] QMenu *menu##Menu = menuBar()->addMenu(name);

#define DCL_ACTION(method, name, tip, menu)                              \
    [[maybe_unused]] QAction *method##Act = new QAction(tr(name), this); \
    method##Act->setStatusTip(tr(tip));                                  \
    menu##Menu->addAction(method##Act);                                  \
    connect(method##Act, &QAction::triggered, this, &MainWindow::method);

#define ACTION_ADD_ICON(action, icon) action##Act->setIcon(QIcon(icon));

#define ACTION_ADD_SHORTCUT(action, shortcut) action##Act->setShortcut(shortcut);

using namespace Vivy;

MainWindow::MainWindow() noexcept
    : QMainWindow()
{
    setWindowIcon(QIcon(VIVY_ICON_APP));
    setWindowTitle("Vivy");
    setDocumentMode(true);
    setDockNestingEnabled(true);
    setTabShape(QTabWidget::Rounded);

    /* Some declarations */
    DCL_MENU(file, "&File");
    DCL_MENU(edit, "&Edit");
    DCL_MENU(viewTmp, "&View");
    viewMenu = viewTmpMenu; // Save the view menu
    DCL_MENU(simulate, "&Simulate");
    DCL_MENU(help, "&Help");

    DCL_ACTION(newDocument, "&New document", "Create a new document", file);
    DCL_ACTION(openDocument, "&Open document", "Open a document", file);
    DCL_ACTION(saveFile, "&Save file", "Save the current document", file);
    DCL_ACTION(saveFileAs, "&Save file as", "Save the current document as", file);
    DCL_ACTION(renameFile, "&Rename the file", "Rename the current document", file);
    fileMenu->addSeparator();
    DCL_ACTION(loadSubDocumentAudio, "&Load audio", "Load an audio file as a sub-document", file);
    DCL_ACTION(loadSubDocumentVideo, "&Load video", "Load a vide file as a sub-document", file);
    DCL_ACTION(loadSubDocumentAss, "&Load ASS", "Load an ASS file as a sub-document", file);

    DCL_ACTION(openDialogHelp, "&About", "Open the help dialog", help);

    ACTION_ADD_ICON(newDocument, VIVY_ICON_NEW);
    ACTION_ADD_ICON(openDocument, VIVY_ICON_OPEN);
    ACTION_ADD_ICON(saveFile, VIVY_ICON_SAVE);
    ACTION_ADD_ICON(saveFileAs, VIVY_ICON_SAVE_AS);
    ACTION_ADD_ICON(renameFile, VIVY_ICON_RENAME);
    ACTION_ADD_ICON(openDialogHelp, VIVY_ICON_ABOUT);

    ACTION_ADD_SHORTCUT(newDocument, QKeySequence::New);
    ACTION_ADD_SHORTCUT(openDocument, QKeySequence::Open);
    ACTION_ADD_SHORTCUT(saveFile, QKeySequence::Save);

    // Custom useFakeVim action
    {
        editMenu->addSeparator();
        QAction *useFakeVim = new QAction(tr("Use FakeVim"), this);
        useFakeVim->setStatusTip("Use FakeVim with integrated text editors");
        useFakeVim->setCheckable(true);
        editMenu->addAction(useFakeVim);
        connect(useFakeVim, &QAction::toggled, this, [this](bool checked) noexcept -> void {
            const bool oldState = vivyApp->getUseFakeVimEditor();
            if (oldState != checked) {
                vivyApp->setUseFakeVimEditor(checked);
                updateFakeVimUsage(checked);
            }
        });
    }

    // Setup the tabs to display the documents
    documents = new QTabWidget(this);
    documents->setMovable(true);
    documents->setTabsClosable(true);
    documents->setElideMode(Qt::ElideRight);
    documents->setUsesScrollButtons(true);
    documents->setDocumentMode(true);
    documents->setTabBarAutoHide(true);
    connect(documents, &QTabWidget::tabCloseRequested, this,
            [this](int index) noexcept -> void { closeDocument(index); });
    connect(documents, &QTabWidget::tabBarDoubleClicked, this, &MainWindow::openProperties);
    setCentralWidget(documents);
    centralWidget()->setContentsMargins(0, 0, 0, 0);
    setContentsMargins(0, 0, 0, 0);

    // Enable/disable actions depending on the context
    saveFileAct->setEnabled(false);
    saveFileAsAct->setEnabled(false);
    loadSubDocumentAssAct->setEnabled(false);
    loadSubDocumentVideoAct->setEnabled(false);
    loadSubDocumentAudioAct->setEnabled(false);

    // Enable "Save As" action
    auto enableSaveAsOnDocument = [this](auto *widget, int index) noexcept -> void {
        if (index >= 0) {
            const auto doc  = static_cast<AbstractDocumentView *>(documents->widget(index));
            const auto type = doc->getType();
            widget->setEnabled(type == AbstractDocumentView::Type::Vivy ||
                               type == AbstractDocumentView::Type::Script);
        } else {
            widget->setEnabled(false);
        }
    };

    // Enable actions if the document is save-able
    auto enableSaveOnDocument = [this](auto *widget, int index) noexcept -> void {
        if (index >= 0) {
            const auto doc  = static_cast<AbstractDocumentView *>(documents->widget(index));
            const auto type = doc->getType();
            const bool isVivyMemoryDoc =
                (type == AbstractDocumentView::Type::Vivy) &&
                dynamic_cast<VivyDocument *>(doc->getDocument())
                    ->checkDocumentOption(VivyDocument::MemoryDocumentCreation);
            widget->setEnabled((!isVivyMemoryDoc) && (type == AbstractDocumentView::Type::Vivy ||
                                                      type == AbstractDocumentView::Type::Script));
        } else {
            widget->setEnabled(false);
        }
    };

    // Enable load sub-document on sub-document able objects
    auto enableLoadSubOnDocument = [this](auto *widget, int index) noexcept -> void {
        if (index >= 0) {
            auto type = static_cast<AbstractDocumentView *>(documents->widget(index))->getType();
            widget->setEnabled(type == AbstractDocumentView::Type::Vivy);
        } else {
            widget->setEnabled(false);
        }
    };

    {
#define CONNECT_ENABLE(act, func) \
    connect(documents, &QTabWidget::currentChanged, act, std::bind_front(func, act));

        connect(documents, &QTabWidget::currentChanged, this,
                [this](int) noexcept -> void { documentViewActionsChanged(); });

        CONNECT_ENABLE(saveFileAct, enableSaveOnDocument);
        CONNECT_ENABLE(saveFileAsAct, enableSaveAsOnDocument);

        CONNECT_ENABLE(loadSubDocumentAssAct, enableLoadSubOnDocument);
        CONNECT_ENABLE(loadSubDocumentVideoAct, enableLoadSubOnDocument);
        CONNECT_ENABLE(loadSubDocumentAudioAct, enableLoadSubOnDocument);

#undef CONNECT_ENABLE
    }

    // Main window has finished its construction
    statusBar()->showMessage("QSimulate has started");

    // Minimal size...
    setMinimumHeight(400);
    setMinimumWidth(600);

    // Always a new empty document
    newDocument();
}

void
MainWindow::closeEvent(QCloseEvent *event) noexcept
{
    qDebug() << "Closing the main window!";
    forEachViews<VivyDocumentView>(
        [](VivyDocumentView *view, int) { view->closeDocument(); }); // XXX
    // event->accept();
    QMainWindow::closeEvent(event);
}

void
MainWindow::updateFakeVimUsage(bool yes) noexcept
{
    forEachViews<AbstractDocumentView>([yes](AbstractDocumentView *docView, int) noexcept -> void {
        const bool documentExists = docView && docView->getDocument();
        const bool isScriptEditor =
            documentExists && (docView->getDocument()->getType() == AbstractDocument::Type::Script);

        if (isScriptEditor)
            static_cast<ScriptDocumentView *>(docView)->setUseFakeVimEditor(yes);
    });
}

void
MainWindow::openProperties(int index) noexcept
{
    if (index < 0) {
        // TODO: May may want to do something when the user is clicking where
        // no tab bar is openned (like open document?)
        return;
    }

    qDebug().nospace() << "Tab n°" << index << " was double clicked";
    AbstractDocumentView *current = getTab(index);
    current->openProperties();
}

void
MainWindow::openDialogHelp() noexcept
{
    if (aboutWindowMutex.try_lock() && aboutWindow == nullptr) {
        aboutWindow = new AboutWindow(this);
        QEventLoop loop;
        connect(aboutWindow, &AboutWindow::closed, &loop, &QEventLoop::quit);
        aboutWindow->show();
        loop.exec();
        delete aboutWindow;
        aboutWindow = nullptr;
        aboutWindowMutex.unlock();
    }
}

AbstractDocument *
MainWindow::getCurrentDocument() const
{
    return getCurrentDocumentView()->getDocument();
}

void
MainWindow::saveFile() noexcept
{
    try {
        const auto document = getCurrentDocument();
        document->save();
    }

    catch (const std::runtime_error &e) {
        qCritical() << "Failed to save current document:" << e.what();
    }
}

void
MainWindow::renameFile() noexcept
{
    try {
        const auto docView = getCurrentDocumentView();
        auto document      = docView->getDocument();
        const QString filename =
            dialogSaveFileName("Select the target file to rename the current file to",
                               QDir::homePath(), Utils::getVivyDocumentFileSuffixFilter());

        if (filename.isEmpty())
            throw std::runtime_error("No filename passed");

        else
            document->rename(filename); // Kubat: Save is called by rename!
    }

    catch (const std::runtime_error &e) {
        qCritical() << "Failed to save current document:" << e.what();
    }
}

void
MainWindow::saveFileAs() noexcept
{
    try {
        const auto docView = getCurrentDocumentView();
        auto document      = docView->getDocument();
        const QString filename =
            dialogSaveFileName("Select the target file to save into", QDir::homePath(),
                               Utils::getVivyDocumentFileSuffixFilter());

        if (filename.isEmpty())
            throw std::runtime_error("No filename passed");

        else
            document->copy(filename); // Kubat: Save is called by copy!
    }

    catch (const std::runtime_error &e) {
        qCritical() << "Failed to save current document:" << e.what();
    }
}

void
MainWindow::closeDocument(AbstractDocumentView *const view) noexcept
{
    if (view == nullptr || !view->getDocument())
        return;

    forEachViews<AbstractDocumentView>(
        [this, view](AbstractDocumentView *documentView, int index) noexcept -> void {
            const bool documentExists = documentView && documentView->getDocument();
            if (documentExists && (*documentView->getDocument()) == (*view->getDocument()))
                closeDocument(index);
        });
}

void
MainWindow::closeDocument(int index) noexcept
{
    if (index < 0)
        return;

    auto *documentToClose = static_cast<AbstractDocumentView *>(documents->widget(index));
    documents->removeTab(index);
    disconnect(documentToClose, &AbstractDocumentView::viewActionsChanged, this,
               &MainWindow::documentViewActionsChanged);

    if (documentToClose) {
        qDebug() << "Delete document view" << documentToClose->getDocumentTabName();
        documentToClose->closeDocument();
        delete documentToClose;
    }
}

void
MainWindow::newDocument() noexcept
{
    try {
        addTab(new VivyDocumentView(
            vivyApp->documentStore.newDocument(VivyDocument::UntouchedByDefault), documents));
    } catch (const std::runtime_error &e) {
        qCritical() << "Failed to create a new empty document:" << e.what();
    }
}

void
MainWindow::openDocument() noexcept
{
    const QString filename = dialogOpenFileName("Select a document to open", QDir::homePath(),
                                                Utils::getAnyTopLevelDocumentFileSuffixFilter());
    if (filename.isEmpty()) {
        qWarning() << "Found an empty filename, don't open a file";
        return;
    }

    const QFileInfo fileInfo(filename);
    Utils::DocumentType fileType;

    if (!Utils::detectDocumentType(fileInfo, &fileType)) {
        qWarning() << "Failed to detect file type for" << filename;
        return;
    }

    // Handle the different types here
    try {
        if (fileType == Utils::DocumentType::Vivy)
            addTab(new VivyDocumentView(vivyApp->documentStore.loadDocument(filename), documents));

        else if (fileType == Utils::DocumentType::VivyScript) {
            auto scriptDocument = vivyApp->scriptStore.loadDocument(filename);
            auto errorTuple     = vivyApp->scriptStore.executeScript(scriptDocument->getUuid());
            ScriptDocumentView *newView = new ScriptDocumentView(scriptDocument, documents);

            if (errorTuple.has_value()) {
                const auto &[line, desc] = errorTuple.value();
                emit newView->luaErrorFound(line, QString::fromUtf8(desc.c_str()));
            }

            addTab(newView);
        }
    } catch (const std::runtime_error &e) {
        qCritical() << "Failed to load document" << filename << "with error:" << e.what();
    }
}

void
MainWindow::loadSubDocumentAss() noexcept
{
    withOpenFileNameDialog<VivyDocumentView, VivyDocument>(
        "Select an ASS document to load", Utils::getAssFileSuffixFilter(),
        [](VivyDocumentView *view, VivyDocument *doc, const QString &filename) noexcept -> void {
            doc->setAssSubDocument(filename);
            view->loadAssView();
        });
}

void
MainWindow::loadSubDocumentVideo() noexcept
{
    withOpenFileNameDialog<VivyDocumentView, VivyDocument>(
        "Select a video document to load", Utils::getVideoFileSuffixFilter(),
        [](VivyDocumentView *view, VivyDocument *doc, const QString &filename) noexcept -> void {
            doc->setVideoSubDocument(filename);
            view->loadVideoView();
        });
}

void
MainWindow::loadSubDocumentAudio() noexcept
{
    withOpenFileNameDialog<VivyDocumentView, VivyDocument>(
        "Select an audio document to load",
        Utils::getAudioFileSuffixFilter() + QStringLiteral(";;") +
            Utils::getVideoFileSuffixFilter(),
        [](VivyDocumentView *view, VivyDocument *doc, const QString &filename) noexcept -> void {
            doc->setAudioSubDocument(filename);
            view->loadAudioView();
        });
}

void
MainWindow::addTab(AbstractDocumentView *const tab)
{
    int index = -1;
    if (const int untouched_index = findFirstUntouchedDocument(); untouched_index >= 0) {
        closeDocument(untouched_index);
        index = documents->insertTab(untouched_index, tab, tab->getDocumentTabIcon(),
                                     tab->getDocumentTabName());
    } else {
        index = documents->addTab(tab, tab->getDocumentTabIcon(), tab->getDocumentTabName());
    }

    documents->setTabToolTip(index, tab->getDocumentTabToolTip());
    documents->setCurrentIndex(index);
    connect(tab, &AbstractDocumentView::viewActionsChanged, this,
            &MainWindow::documentViewActionsChanged);
    connect(tab, &AbstractDocumentView::documentPropertyChanged, this,
            [this, tab]() noexcept -> void {
                int tabIndex = getIndexOfTab(tab);
                if (tabIndex < 0) {
                    qDebug() << "Tab was not found!";
                } else {
                    qDebug() << "Tab was found at index" << tabIndex;
                    documents->setTabToolTip(tabIndex, tab->getDocumentTabToolTip());
                    documents->setTabText(tabIndex, tab->getDocumentTabName());
                }
            });
    documentViewActionsChanged();
    qDebug() << "View constructed successfully";
}

int
MainWindow::getIndexOfTab(const AbstractDocumentView *const viewA) const noexcept
{
    if (viewA == nullptr)
        return -1;

    const AbstractDocument *const docA = viewA->getDocument();
    if (docA == nullptr)
        return -1;

    int returnIndex = -1;
    forEachViews<AbstractDocumentView>(
        [docA, &returnIndex](AbstractDocumentView *viewB, int index) noexcept -> void {
            if (viewB == nullptr)
                return;

            const AbstractDocument *const docB = viewB->getDocument();
            if (docB == nullptr)
                return;

            if (*docB == *docA)
                returnIndex = index;
        });

    return returnIndex;
}

AbstractDocumentView *
MainWindow::getCurrentDocumentView() const
{
    if (AbstractDocumentView *currentView =
            static_cast<AbstractDocumentView *>(documents->currentWidget())) {
        return currentView;
    }

    else {
        throw std::runtime_error("no current document");
    }
}

AbstractDocumentView *
MainWindow::getTab(const int index) const noexcept
{
    if (index > documents->count())
        return nullptr;
    return static_cast<AbstractDocumentView *>(documents->widget(index));
}

int
MainWindow::findFirstUntouchedDocument() const noexcept
{
    int returnIndex = -1;
    forEachViews<VivyDocumentView>(
        [&returnIndex](VivyDocumentView *view, int index) noexcept -> void {
            if (view->getDocument()->checkDocumentOption(VivyDocument::UntouchedByDefault))
                returnIndex = index;
        });

    return returnIndex;
}

void
MainWindow::documentViewActionsChanged() noexcept
{
    qInfo() << "Document view action changed";
    viewMenu->clear();

    // Change document view menu if we have a document
    try {
        viewMenu->addActions(getCurrentDocumentView()->getViewsActions());
    } catch (const std::runtime_error &e) {
        qInfo() << "No view to display:" << e.what();
    }
}

static inline QString
executeDialog(MainWindow *const self, QFileDialog *const dialog) noexcept
{
    bool dialogAccepted = false;
    std::unique_ptr<VivyFileIconProvider> iconProvider(new VivyFileIconProvider());

    dialog->setIconProvider(iconProvider.get());
    QFileDialog::connect(dialog, &QFileDialog::accepted, self,
                         [&dialogAccepted]() noexcept -> void { dialogAccepted = true; });

    dialog->exec();

    if (!dialogAccepted)
        return QStringLiteral("");

    const QStringList resList = dialog->selectedFiles();
    if (resList.size() != 1) {
        qCritical() << "You must select only one file";
        return QStringLiteral("");
    }

    return resList.at(0);
}

QString
MainWindow::dialogOpenFileName(const QString &title, const QString &folder,
                               const QString &filter) noexcept
{
    QFileDialog dialog(this, title, folder, filter);
    dialog.setOption(QFileDialog::ReadOnly);
    dialog.setAcceptMode(QFileDialog::AcceptOpen);
    dialog.setFileMode(QFileDialog::ExistingFile);
    return executeDialog(this, &dialog);
}

QString
MainWindow::dialogSaveFileName(const QString &title, const QString &folder,
                               const QString &filter) noexcept
{
    QFileDialog dialog(this, title, folder, filter);
    dialog.setOption(QFileDialog::ReadOnly, false);
    dialog.setAcceptMode(QFileDialog::AcceptSave);
    return executeDialog(this, &dialog);
}