#include "mainwindow.hpp"
#include "ui_mainwindow.h"
#include "browsertab.hpp"
#include "dialogs/settingsdialog.hpp"
#include <cassert>
#include <QMessageBox>
#include <memory>
#include <QShortcut>
#include <QKeySequence>
#include <QFile>
#include <QTextStream>
#include <QFileDialog>
#include <QInputDialog>
#include <QMouseEvent>

#include "ioutil.hpp"
#include "kristall.hpp"
#include "widgets/browsertabbar.hpp"

#include "dialogs/certificatemanagementdialog.hpp"

MainWindow::MainWindow(QApplication * app, QWidget *parent) :
    QMainWindow(parent),
    application(app),
    ui(new Ui::MainWindow),
    url_status(new ElideLabel(this)),
    file_size(new QLabel(this)),
    file_cached(new QLabel(this)),
    file_mime(new QLabel(this)),
    load_time(new QLabel(this))
{
    this->setAttribute(Qt::WA_DeleteOnClose);

    ui->setupUi(this);

    connect( // connect with "this" as context, so the connection will die when the window is destroyed
        kristall::globals().localization.get(), &Localization::translationChanged,
        this, [this]() { this->ui->retranslateUi(this); },
        Qt::DirectConnection
    );

    this->url_status->setElideMode(Qt::ElideMiddle);

    this->statusBar()->addWidget(this->url_status);
    this->statusBar()->addPermanentWidget(this->file_cached);
    this->statusBar()->addPermanentWidget(this->file_mime);
    this->statusBar()->addPermanentWidget(this->file_size);
    this->statusBar()->addPermanentWidget(this->load_time);

    ui->favourites_view->setModel(&kristall::globals().favourites);

    this->ui->outline_window->setVisible(false);
    this->ui->history_window->setVisible(false);
    this->ui->bookmarks_window->setVisible(false);

    for(QDockWidget * dock : findChildren<QDockWidget *>())
    {
        QAction * act = dock->toggleViewAction();
        act->setShortcut(dock->property("_shortcut").toString());
        // act->setIcon(dock->windowIcon());

        this->ui->menuView->addAction(act);
    }

    connect(this->ui->menuNavigation, &QMenu::aboutToShow, [this]() {
        BrowserTab * tab = this->curTab();
        if(tab != nullptr) {
            ui->actionAdd_to_favourites->setChecked(kristall::globals().favourites.containsUrl(tab->current_location));
        }
    });

    connect(this->ui->menuView, &QMenu::aboutToShow, [this]() {
        for(QAction * act : this->ui->menuView->actions())
        {
            auto * dock = qvariant_cast<QDockWidget*>(act->data());
            if(dock != nullptr) {
                act->setChecked(dock->isVisible());
            }
        }
    });

    {
        QShortcut * sc = new QShortcut(QKeySequence("Ctrl+L"), this);
        connect(sc, &QShortcut::activated, this, &MainWindow::on_focus_inputbar);
    }

    {
        std::string prefix = "Alt+";
        for (char tab = '0'; tab <= '9'; ++tab) {
            std::string shortcut = prefix + tab;
            QShortcut * sc = new QShortcut(QKeySequence(shortcut.c_str()), this);
            connect(sc, &QShortcut::activated, this, [this, tab]()
            {
                // 1-9 goes from the first to the n-th tab, 0 goes to the last one
                setCurrentTabIndex((tab == '0'
                    ? this->ui->browser_tabs->count()
                    : tab-'0') - 1);
            });
        }
    }

    {
        QShortcut * sc = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_PageDown), this);
        connect(sc, &QShortcut::activated, this, [this](){
           int i = this->currentTabIndex();

           if (i + 1 >= this->ui->browser_tabs->count())
               i = 0;
           else
               i++;

           this->setCurrentTabIndex(i);
        });
    }

    {
        QShortcut * sc = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_PageUp), this);
        connect(sc, &QShortcut::activated, this, [this](){
           int i = this->currentTabIndex();

           if (!i)
               i = this->ui->browser_tabs->count() - 1;
           else
               i--;

           this->setCurrentTabIndex(i);
        });
    }

    this->ui->favourites_view->setContextMenuPolicy(Qt::CustomContextMenu);
    this->ui->history_view->setContextMenuPolicy(Qt::CustomContextMenu);

    connect(this->ui->browser_tabs->tab_bar, &BrowserTabBar::on_newTabClicked, this, [this]() {
        this->addEmptyTab(true, true);
    });

    kristall::registerAppWindow(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

BrowserTab * MainWindow::addEmptyTab(bool focus_new, bool load_default)
{
    BrowserTab * tab = new BrowserTab(this);

    connect(tab, &BrowserTab::destroyed, this, &MainWindow::on_tab_closed);
    connect(tab, &BrowserTab::titleChanged, this, &MainWindow::on_tab_titleChanged);
    connect(tab, &BrowserTab::fileLoaded, this, &MainWindow::on_tab_fileLoaded);
    connect(tab, &BrowserTab::requestStateChanged, this, &MainWindow::on_tab_requestStateChanged);

    int index = this->ui->browser_tabs->addTab(tab, "Page");

    if(focus_new) {
        this->setCurrentTabIndex(index);
    }

    if(load_default) {
        tab->navigateTo(QUrl(kristall::globals().options.start_page), BrowserTab::PushImmediate);
        tab->focusUrlBar();
    } else {
        tab->navigateTo(QUrl("about:blank"), BrowserTab::DontPush);
    }

    return tab;
}

BrowserTab * MainWindow::addNewTab(bool focus_new, QUrl const & url, bool lazyload, QString defaultTitle)
{
    auto tab = addEmptyTab(focus_new, false);

    if (lazyload)
    {
        tab->current_location = url;
        tab->lazy_loading = true;
    }

    tab->navigateTo(url, BrowserTab::PushImmediate);

    if (!defaultTitle.isEmpty())
    {
        tab->page_title = defaultTitle;
        emit tab->titleChanged(defaultTitle);
    }

    return tab;
}

BrowserTab * MainWindow::curTab() const
{
    // Was getting irritated writing this out all the time
    return qobject_cast<BrowserTab*>(this->ui->browser_tabs->currentWidget());
}

BrowserTab * MainWindow::tabAt(int index) const
{
    return qobject_cast<BrowserTab*>(this->ui->browser_tabs->widget(index));
}

int MainWindow::tabCount() const
{
    return this->ui->browser_tabs->count();
}

void MainWindow::setUrlPreview(const QUrl &url)
{
    if(url.isValid()) {
        auto str = url.toString();
        if(str.length() > 300) {
            str = str.mid(0, 300) + "...";
        }
        this->previewing_url = true;
        this->url_status->setText(str);
        return;
    }

    this->previewing_url = false;
    this->url_status->setText(this->request_status);
}

void MainWindow::setRequestState(RequestState state)
{
    switch (state)
    {
    case RequestState::Started:
    {
        this->request_status = tr("Looking up...");
    } break;

    case RequestState::StartedWeb:
    {
        this->request_status = tr("Loading webpage...");
    } break;

    case RequestState::HostFound:
    {
        this->request_status = tr("Connecting...");
    } break;

    case RequestState::Connected:
    {
        this->request_status = tr("Downloading...");
    } break;

    default:
    {
        this->request_status = "";
    } break;
    }

    if (!this->previewing_url)
        this->url_status->setText(this->request_status);
}

void MainWindow::viewPageSource()
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        tab->openSourceView();
    }
}

void MainWindow::updateWindowTitle()
{
    BrowserTab * tab = this->curTab();
    if (tab == nullptr || tab->page_title.isEmpty())
    {
        this->setWindowTitle(tr("Kristall"));
        return;
    }
    this->setWindowTitle(tr("%0 - %1").arg(tab->page_title, tr("Kristall")));
}

void MainWindow::setUiDensity(UIDensity density, bool previewing)
{
    // If we are previewing, we only update the current tab.
    // If not, we update all tabs as it means user accepted the settings
    // dialog.

    if (previewing)
    {
        if (not this->curTab())
            return;
        this->curTab()->setUiDensity(density);
    }
    else
    {
        for (int i = 0; i < this->ui->browser_tabs->count(); ++i)
            this->tabAt(i)->setUiDensity(density);
    }
}

QString MainWindow::newGroupDialog()
{
    QInputDialog dialog { this };

    dialog.setInputMode(QInputDialog::TextInput);
    dialog.setLabelText(tr("Enter name of the new group:"));

    if(dialog.exec() != QDialog::Accepted)
        return QString { };

    kristall::globals().favourites.addGroup(dialog.textValue());

    return dialog.textValue();
}

void MainWindow::applySettings()
{
    // Flag open tabs for re-render so theme
    // changes are instantly applied.
    for (int i = 0; i < this->ui->browser_tabs->count(); ++i)
    {
        BrowserTab *t = this->tabAt(i);
        t->refreshOptionalToolbarItems();
        t->refreshToolbarIcons();
        t->needs_rerender = true;
    }

    // Re-render the currently-open tab if we have one.
    BrowserTab * tab = this->curTab();
    if (tab)
        tab->rerenderPage();

    // Update new-tab button visibility.
    this->ui->browser_tabs->tab_bar->new_tab_btn->setVisible(kristall::globals().options.enable_newtab_btn);
}

int MainWindow::currentTabIndex()
{
    return this->ui->browser_tabs->currentIndex();
}

void MainWindow::setCurrentTabIndex(int index)
{
    this->ui->browser_tabs->setCurrentIndex(index);
}


void MainWindow::mousePressEvent(QMouseEvent *event)
{
    QMainWindow::mousePressEvent(event);

    BrowserTab * tab = this->curTab();
    if (tab == nullptr) return;

    // Navigate back/forward on mouse buttons 4/5
    if (event->buttons() == Qt::ForwardButton &&
        tab->history.oneForward(tab->current_history_index).isValid())
    {
        tab->navOneForward();
    }
    else if (event->buttons() == Qt::BackButton &&
        tab->history.oneBackward(tab->current_history_index).isValid())
    {
        tab->navOneBackward();
    }
}

void MainWindow::closeEvent(QCloseEvent *event)
{
    if(kristall::getWindowCount() == 1) {
        kristall::saveSession();
    }
    event->accept();
}

void MainWindow::on_tab_closed()
{
    // If the user wants, we close the window together with the last tab.
    // tabCount() might be 1 here, as the tab is still counted as "open"
    if(kristall::globals().options.close_window_with_last_tab and (this->tabCount() <= 1))
    {
        this->close();
    }
}

void MainWindow::on_browser_tabs_currentChanged(int index)
{
    if(index >= 0) {
        BrowserTab * tab = this->tabAt(index);

        if(tab != nullptr) {
            this->ui->outline_view->setModel(&tab->outline);
            this->ui->outline_view->expandAll();

            this->ui->history_view->setModel(&tab->history);

            this->setFileStatus(tab->current_stats);

            if (tab->needs_rerender)
            {
                tab->rerenderPage();
            }
            else
            {
                tab->refreshFavButton();
            }

            if (tab->lazy_loading)
            {
                tab->reloadPage();
            }

            this->setRequestState(tab->request_state);
        } else {
            this->ui->outline_view->setModel(nullptr);
            this->ui->history_view->setModel(nullptr);
            this->setFileStatus(DocumentStats { });
            this->setRequestState(RequestState::None);
        }
    } else {
        this->ui->outline_view->setModel(nullptr);
        this->ui->history_view->setModel(nullptr);
        this->setFileStatus(DocumentStats { });
        this->setRequestState(RequestState::None);
    }
    updateWindowTitle();
}

void MainWindow::on_browser_tabs_tabCloseRequested(int index)
{
    delete tabAt(index);
}

void MainWindow::on_tab_titleChanged(const QString &title)
{
    auto * tab = qobject_cast<BrowserTab*>(sender());
    if(tab != nullptr) {
        int index = this->ui->browser_tabs->indexOf(tab);
        assert(index >= 0);

        QString escapedTitle = title;

        // Set the window title to full title
        if (tab == this->curTab())
        {
            updateWindowTitle();
        }

        // Set tooltip
        this->ui->browser_tabs->tab_bar->setTabToolTip(index, title);

        // Shorten lengthy titles for tab bar (45 chars max for now - we assume
        // that Gemini surfers don't usually have loads of tabs open, so the
        // limit is fairly high)
        const int MAX_TITLE_LEN = 45;
        if (escapedTitle.length() > MAX_TITLE_LEN)
        {
            escapedTitle = escapedTitle.mid(0, MAX_TITLE_LEN - 3).trimmed() + "...";
        }

        this->ui->browser_tabs->setTabText(index, escapedTitle.replace("&", "&&"));
    }
}

void MainWindow::on_tab_locationChanged(const QUrl &url)
{
    auto * tab = qobject_cast<BrowserTab*>(sender());
    if(tab != nullptr) {
        int index = this->ui->browser_tabs->indexOf(tab);
        assert(index >= 0);
        this->ui->browser_tabs->setTabToolTip(index, url.toString());
    }
}

void MainWindow::on_outline_view_clicked(const QModelIndex &index)
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        auto anchor = tab->outline.getAnchor(index);
        if(not anchor.isEmpty()) {
            tab->scrollToAnchor(anchor);
        }
    }
}

void MainWindow::on_actionSettings_triggered()
{
    SettingsDialog dialog;

    dialog.setGeminiStyle(kristall::globals().document_style);
    dialog.setProtocols(kristall::globals().protocols);
    dialog.setOptions(kristall::globals().options);
    dialog.setGeminiSslTrust(kristall::globals().trust.gemini);
    dialog.setHttpsSslTrust(kristall::globals().trust.https);
    dialog.setLocale(kristall::globals().localization->locale);

    if(dialog.exec() != QDialog::Accepted) {
        kristall::setTheme(kristall::globals().options.theme);
        kristall::globals().localization->setLocale(kristall::globals().localization->locale);
        this->setUiDensity(kristall::globals().options.ui_density, false);
        return;
    }

    kristall::globals().localization->setLocale(dialog.locale());

    kristall::globals().trust.gemini = dialog.geminiSslTrust();
    kristall::globals().trust.https = dialog.httpsSslTrust();
    kristall::globals().options = dialog.options();

    kristall::globals().protocols = dialog.protocols();
    kristall::globals().document_style = dialog.geminiStyle();

    kristall::saveLocale();
    kristall::applySettings();

    kristall::saveSettings();
}

void MainWindow::on_actionNew_Tab_triggered()
{
    this->addEmptyTab(true, true);
}

void MainWindow::on_actionQuit_triggered()
{
    kristall::saveSession();
    QApplication::quit();
}

void MainWindow::on_actionAbout_triggered()
{
    QMessageBox::about(this,
                       tr("Kristall"),
tr(R"about(Kristall %1
An OpenSource Gemini browser.
Made by Felix "xq" Queißner

This is free software. You can get the source code at
https://github.com/MasterQ32/Kristall)about").arg(QApplication::applicationVersion())
    );
}

void MainWindow::on_actionClose_Tab_triggered()
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        delete tab;
    }
}

void MainWindow::on_actionForward_triggered()
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        tab->navOneForward();
    }
}

void MainWindow::on_actionBackward_triggered()
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        tab->navOneBackward();
    }
}

void MainWindow::on_actionRoot_triggered()
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        tab->navigateToRoot();
    }
}

void MainWindow::on_actionParent_triggered()
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        tab->navigateToParent();
    }
}

void MainWindow::on_actionRefresh_triggered()
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        tab->reloadPage();
    }
}

void MainWindow::on_actionAbout_Qt_triggered()
{
    QMessageBox::aboutQt(this, "Kristall");
}

void MainWindow::setFileStatus(const DocumentStats &stats)
{
    if(stats.isValid()) {
        this->file_size->setText(IoUtil::size_human(stats.file_size));
        this->file_cached->setText(stats.loaded_from_cache ? tr("(cached)") : "");
        this->file_mime->setText(stats.mime_type.toString(false));
        this->load_time->setText(tr("%1 ms").arg(stats.loading_time));
    } else {
        this->file_size->setText("");
        this->file_cached->setText("");
        this->file_mime->setText("");
        this->load_time->setText("");
    }
}

void MainWindow::on_actionSave_as_triggered()
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        QFileDialog dialog { this };
        dialog.setAcceptMode(QFileDialog::AcceptSave);
        dialog.selectFile(tab->current_location.fileName());

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

        QString fileName = dialog.selectedFiles().at(0);

        QFile file { fileName };

        if(file.open(QFile::WriteOnly))
        {
            IoUtil::writeAll(file, tab->current_buffer);
        }
        else
        {
            QMessageBox::warning(this, tr("Kristall"), QString("Could not save file:\r\n%1").arg(file.errorString()));
        }
    }
}

void MainWindow::on_actionGo_to_home_triggered()
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        tab->navigateTo(QUrl(kristall::globals().options.start_page), BrowserTab::PushImmediate);
    }
}

void MainWindow::on_actionAdd_to_favourites_triggered()
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        tab->showFavouritesPopup();
    }
}

void MainWindow::on_tab_fileLoaded(DocumentStats const & stats)
{
    auto * tab = qobject_cast<BrowserTab*>(sender());
    if(tab != nullptr) {
        int index = this->ui->browser_tabs->indexOf(tab);
        assert(index >= 0);
        if(index == this->currentTabIndex()) {
            setFileStatus(stats);
            this->ui->outline_view->expandAll();
        }
    }
}

void MainWindow::on_tab_requestStateChanged(RequestState state)
{
    auto * tab = qobject_cast<BrowserTab*>(sender());
    if(tab != nullptr) {
        int index = this->ui->browser_tabs->indexOf(tab);
        assert(index >= 0);
        if(index == this->currentTabIndex()) {
            setRequestState(state);
        }
    }
}

void MainWindow::on_focus_inputbar()
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        tab->focusUrlBar();
    }
}

void MainWindow::on_actionHelp_triggered()
{
    this->addNewTab(true, QUrl("about:help"));
}

void MainWindow::on_history_view_customContextMenuRequested(const QPoint pos)
{
    if(auto idx = this->ui->history_view->indexAt(pos); idx.isValid()) {
        BrowserTab * tab = this->curTab();
        if(tab != nullptr) {
            if(QUrl url = tab->history.get(idx); url.isValid()) {
                QMenu menu;

                connect(menu.addAction(tr("Open here")), &QAction::triggered, [tab, idx]() {
                    // We do the same thing as a double click here
                    tab->navigateBack(idx);
                });

                connect(menu.addAction(tr("Open in new tab")), &QAction::triggered, [this, url]() {
                    addNewTab(true, url);
                });

                menu.exec(this->ui->history_view->mapToGlobal(pos));
            }
        }
    }
}

void MainWindow::on_favourites_view_customContextMenuRequested(const QPoint pos)
{
    if(auto idx = this->ui->favourites_view->indexAt(pos); idx.isValid()) {
        if(QUrl url = kristall::globals().favourites.getFavourite(idx).destination; url.isValid()) {
            QMenu menu;

            BrowserTab * tab = this->curTab();
            if(tab != nullptr) {
                connect(menu.addAction(tr("Open here")), &QAction::triggered, [tab, url]() {
                    tab->navigateTo(url, BrowserTab::PushImmediate);
                });
            }

            connect(menu.addAction(tr("Open in new tab")), &QAction::triggered, [this, url]() {
                addNewTab(true, url);
            });

            menu.addSeparator();

            connect(menu.addAction(tr("Relocate")), &QAction::triggered, [this, idx]() {
                QInputDialog dialog { this };

                dialog.setInputMode(QInputDialog::TextInput);
                dialog.setLabelText(tr("Enter new location of this favourite:"));
                dialog.setTextValue(kristall::globals().favourites.getFavourite(idx).destination.toString(QUrl::FullyEncoded));

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

                kristall::globals().favourites.editFavouriteDest(idx, QUrl(dialog.textValue()));
            });

            connect(menu.addAction(tr("Rename")), &QAction::triggered, [this, idx]() {
                QInputDialog dialog { this };

                dialog.setInputMode(QInputDialog::TextInput);
                dialog.setLabelText(tr("New name of this favourite:"));
                dialog.setTextValue(kristall::globals().favourites.getFavourite(idx).getTitle());

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

                kristall::globals().favourites.editFavouriteTitle(idx, dialog.textValue());
            });

            menu.addSeparator();

            connect(menu.addAction(tr("Delete")), &QAction::triggered, [idx]() {
                kristall::globals().favourites.destroyFavourite(idx);
            });

            menu.exec(this->ui->favourites_view->mapToGlobal(pos));
        }
        else if(QString group = kristall::globals().favourites.group(idx); not group.isEmpty()) {
            QMenu menu;

            connect(menu.addAction(tr("Rename group")), &QAction::triggered, [this, group]() {
                QInputDialog dialog { this };

                dialog.setInputMode(QInputDialog::TextInput);
                dialog.setLabelText(tr("New name of this group:"));
                dialog.setTextValue(group);

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

                if (!kristall::globals().favourites.renameGroup(group, dialog.textValue()))
                    QMessageBox::information(this, tr("Kristall"), tr("Rename failed: group name already in use."));
            });

            menu.addSeparator();

            connect(menu.addAction(tr("Delete group")), &QAction::triggered, [this, idx]() {
                if (QMessageBox::question(
                    this,
                    tr("Kristall"),
                    tr("Are you sure you want to delete this Favourite Group?\n"
                    "All favourites in this group will be lost.\n\n"
                    "This action cannot be undone!")
                ) != QMessageBox::Yes)
                {
                    return;
                }
                kristall::globals().favourites.deleteGroupRecursive(kristall::globals().favourites.group(idx));
            });

            menu.exec(this->ui->favourites_view->mapToGlobal(pos));
        }
    }
    else {
        QMenu menu;

        connect(menu.addAction(tr("Create new group...")), &QAction::triggered, [this]() {
            this->newGroupDialog();
        });

        menu.exec(this->ui->favourites_view->mapToGlobal(pos));
    }
}

void MainWindow::on_actionChangelog_triggered()
{
    this->addNewTab(true, QUrl("about:updates"));
}

void MainWindow::on_actionManage_Certificates_triggered()
{
    CertificateManagementDialog dialog { this };

    dialog.setIdentitySet(kristall::globals().identities);
    if(dialog.exec() != QDialog::Accepted)
        return;

    kristall::globals().identities = dialog.identitySet();

    kristall::saveSettings();
}

void MainWindow::on_actionShow_document_source_triggered()
{
    this->viewPageSource();
}

void MainWindow::on_actionNew_window_triggered()
{
    kristall::openNewWindow(false);
}

void MainWindow::on_actionClose_Window_triggered()
{
    this->close();
}

void MainWindow::on_favourites_view_activated(const QModelIndex &index)
{
    if(auto url = kristall::globals().favourites.getFavourite(index).destination; url.isValid()) {
        this->addNewTab(true, url);
    }
}

void MainWindow::on_history_view_activated(const QModelIndex &index)
{
    BrowserTab * tab = this->curTab();
    if(tab != nullptr) {
        tab->navigateBack(index);
    }
}

void MainWindow::on_outline_view_activated(const QModelIndex &index)
{
    this->on_outline_view_clicked(index);
}
