#include "linkdialog.h"

#include <entities/note.h>
#include <entities/tag.h>
#include <utils/gui.h>
#include <utils/misc.h>

#include <QClipboard>
#include <QDebug>
#include <QFileDialog>
#include <QHeaderView>
#include <QKeyEvent>
#include <QLocale>
#include <QMenu>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QRegularExpression>
#include <QTimer>
#include <QTreeWidget>

#include "entities/notefolder.h"
#include "mainwindow.h"
#include "services/scriptingservice.h"
#include "services/settingsservice.h"
#include "ui_linkdialog.h"
#include "widgets/navigationwidget.h"

#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0))
#include <memory>
#endif

namespace {
enum NoteListDataRoles {
    NoteIdRole = Qt::UserRole,
    NoteNameRole,
    NoteModifiedRole,
};

QString noteTagLookupKey(const QString &subFolderPath, const QString &noteName) {
    return subFolderPath + QStringLiteral("/") + noteName;
}

class NoteListTreeWidgetItem : public QTreeWidgetItem {
   public:
    using QTreeWidgetItem::QTreeWidgetItem;

    bool operator<(const QTreeWidgetItem &other) const override {
        if (treeWidget() != nullptr && treeWidget()->sortColumn() == 3) {
            const QDateTime thisModified = data(3, NoteModifiedRole).toDateTime();
            const QDateTime otherModified = other.data(3, NoteModifiedRole).toDateTime();

            return thisModified < otherModified;
        }

        return QTreeWidgetItem::operator<(other);
    }
};
}    // namespace

LinkDialog::LinkDialog(int page, const QString &dialogTitle, QWidget *parent)
    : MasterDialog(parent), ui(new Ui::LinkDialog) {
    ui->setupUi(this);
    afterSetupUI();
    ui->tabWidget->setCurrentIndex(page);
    on_tabWidget_currentChanged(page);
    ui->downloadProgressBar->hide();
    _markdownTextEdit = new QOwnNotesMarkdownTextEdit();
    _networkManager = new QNetworkAccessManager(this);
    QObject::connect(_networkManager, SIGNAL(finished(QNetworkReply *)), this,
                     SLOT(slotReplyFinished(QNetworkReply *)));

    // Show the wiki-link checkbox only when wiki-link support is enabled
    ui->wikiLinkCheckBox->setVisible(Note::isWikiLinkSupportEnabled());

    // disallow ] characters, because they will break Markdown links
    ui->nameLineEdit->setValidator(
        new QRegularExpressionValidator(QRegularExpression(R"([^\]]*)")));
    firstVisibleNoteListRow = 0;

    if (!dialogTitle.isEmpty()) {
        this->setWindowTitle(dialogTitle);
    }

    ui->searchLineEdit->installEventFilter(this);
    ui->headingSearchLineEdit->installEventFilter(this);
    ui->notesListWidget->installEventFilter(this);
    ui->notesListWidget->setRootIsDecorated(false);

    // Change the search icon between dark and light mode
    const QString searchIconFileName =
        SettingsService().value(QStringLiteral("darkModeColors")).toBool()
            ? QStringLiteral("search-notes-dark.svg")
            : QStringLiteral("search-notes.svg");
    static const QRegularExpression searchIconRegex(
        QStringLiteral("background-image: url\\(:.+\\);"));

    QString searchLineEditStyleSheet = ui->searchLineEdit->styleSheet();
    searchLineEditStyleSheet.replace(
        searchIconRegex,
        QStringLiteral("background-image: url(:/images/%1);").arg(searchIconFileName));
    ui->searchLineEdit->setStyleSheet(searchLineEditStyleSheet);

    QString headingSearchLineEditStyleSheet = ui->headingSearchLineEdit->styleSheet();
    headingSearchLineEditStyleSheet.replace(
        searchIconRegex,
        QStringLiteral("background-image: url(:/images/%1);").arg(searchIconFileName));
    ui->headingSearchLineEdit->setStyleSheet(headingSearchLineEditStyleSheet);

    const bool showSubfolders = NoteFolder::isCurrentShowSubfolders();
    ui->notesListWidget->setColumnHidden(1, !showSubfolders);
    ui->notesListWidget->setSortingEnabled(true);
    ui->notesListWidget->sortByColumn(3, Qt::DescendingOrder);
    ui->notesListWidget->header()->setSortIndicatorShown(true);
    ui->notesListWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch);
    ui->notesListWidget->header()->setSectionResizeMode(1, QHeaderView::Interactive);
    ui->notesListWidget->header()->setSectionResizeMode(2, QHeaderView::Interactive);
    ui->notesListWidget->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
    ui->notesListWidget->header()->setStretchLastSection(false);
    ui->notesListWidget->setColumnWidth(1, 180);
    ui->notesListWidget->setColumnWidth(2, 180);
    Utils::Gui::initTreeWidgetHeaderOrderPersistence(
        ui->notesListWidget, QStringLiteral("LinkDialog/notesListWidgetHeaderOrder"));
    const auto tagNamesByNoteFilePath = Tag::fetchAllNamesByNoteFilePath();

    Q_FOREACH (Note note, Note::fetchAll()) {
        const QString noteName = note.getName();
        const QString subFolderPath = note.relativeNoteSubFolderPath();
        const QString tagText =
            tagNamesByNoteFilePath.value(noteTagLookupKey(subFolderPath, noteName))
                .join(QStringLiteral(", "));
        const QDateTime modified = note.getFileLastModified();
        const QString modifiedDisplay = QLocale().toString(modified, QLocale::ShortFormat);
        auto *item = new NoteListTreeWidgetItem(ui->notesListWidget);
        item->setText(0, noteName);
        item->setText(1, subFolderPath);
        item->setText(2, tagText);
        item->setText(3, modifiedDisplay);
        item->setToolTip(2, tagText);
        item->setData(0, NoteIdRole, note.getId());
        item->setData(0, NoteNameRole, noteName);
        item->setData(3, NoteModifiedRole, modified);
    }

    if (ui->notesListWidget->topLevelItemCount() > 0) {
        ui->notesListWidget->setCurrentItem(ui->notesListWidget->topLevelItem(0));
    }

    if (page == LinkDialog::TextLinkPage) {
        QClipboard *clipboard = QApplication::clipboard();
        const QString text = clipboard->text().remove(QStringLiteral("\n")).trimmed();
        const QUrl url(text);

        // set text from clipboard
        if (url.isValid() && !url.scheme().isEmpty()) {
            ui->urlEdit->setText(text);
        }
    }

    setupFileUrlMenu();
}

LinkDialog::~LinkDialog() {
    delete ui;
    delete _markdownTextEdit;
}

void LinkDialog::on_searchLineEdit_textChanged(const QString &arg1) {
    // search notes when at least 2 characters were entered
    if (arg1.size() >= 2) {
        QVector<QString> noteNameList = Note::searchAsNameList(arg1, true);
        this->firstVisibleNoteListRow = -1;

        for (int i = 0; i < this->ui->notesListWidget->topLevelItemCount(); ++i) {
            QTreeWidgetItem *item = this->ui->notesListWidget->topLevelItem(i);
            if (noteNameList.indexOf(item->data(0, NoteNameRole).toString()) < 0) {
                item->setHidden(true);
            } else {
                if (this->firstVisibleNoteListRow < 0) {
                    this->firstVisibleNoteListRow = i;
                }
                item->setHidden(false);
            }
        }
    } else {    // show all items otherwise
        this->firstVisibleNoteListRow = 0;

        for (int i = 0; i < this->ui->notesListWidget->topLevelItemCount(); ++i) {
            QTreeWidgetItem *item = this->ui->notesListWidget->topLevelItem(i);
            item->setHidden(false);
        }
    }
}

QString LinkDialog::getSelectedNoteName() const {
    return ui->notesListWidget->currentItem() != nullptr
               ? ui->notesListWidget->currentItem()->data(0, NoteNameRole).toString()
               : QString();
}

Note LinkDialog::getSelectedNote() const {
    if (ui->notesListWidget->currentItem() == nullptr) {
        return {};
    }

    const int noteId = ui->notesListWidget->currentItem()->data(0, NoteIdRole).toInt();

    return Note::fetch(noteId);
}

QString LinkDialog::getSelectedHeading() const {
    // Trim the heading text, in case there are trailing carriage return characters leaking in
    // Windows
    return ui->headingListWidget->selectedItems().isEmpty()
               ? ""
               : ui->headingListWidget->currentItem()->text().trimmed();
}

QString LinkDialog::getURL() const {
    QString url = ui->urlEdit->text().trimmed();

    if (!url.isEmpty() && !url.contains(QStringLiteral("://")) &&
        !url.startsWith(QStringLiteral("./"))) {
        url = QStringLiteral("https://") + url;
    }

    return url;
}

void LinkDialog::setURL(const QString &text) { ui->urlEdit->setText(text); }

QString LinkDialog::getLinkName() const { return ui->nameLineEdit->text().trimmed(); }

void LinkDialog::setLinkName(const QString &text) { ui->nameLineEdit->setText(text); }

QString LinkDialog::getLinkDescription() const { return ui->descriptionLineEdit->text().trimmed(); }

bool LinkDialog::isWikiLink() const {
    return Note::isWikiLinkSupportEnabled() && ui->wikiLinkCheckBox->isChecked();
}

//
// Event filters on the NoteSearchDialog
//
bool LinkDialog::eventFilter(QObject *obj, QEvent *event) {
    if (obj == ui->searchLineEdit) {
        if (event->type() == QEvent::KeyPress) {
            auto *keyEvent = dynamic_cast<QKeyEvent *>(event);

            // set focus to the notes list if Key_Down or Key_Tab were pressed
            // in the search line edit
            if ((keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_Tab)) {
                // choose another selected item if current item is invisible
                auto item = ui->notesListWidget->currentItem();
                if ((item != nullptr) && ui->notesListWidget->currentItem()->isHidden() &&
                    (this->firstVisibleNoteListRow >= 0)) {
                    ui->notesListWidget->setCurrentItem(
                        ui->notesListWidget->topLevelItem(this->firstVisibleNoteListRow));
                }

                // give the keyboard focus to the notes list widget
                ui->notesListWidget->setFocus();
                return true;
            }
        }
        return false;
    } else if (obj == ui->headingSearchLineEdit) {
        if (event->type() == QEvent::KeyPress) {
            auto *keyEvent = dynamic_cast<QKeyEvent *>(event);

            // set focus to the notes list if Key_Down or Key_Tab were pressed
            // in the search line edit
            if ((keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_Tab)) {
                // give the keyboard focus to the heading list widget
                ui->headingListWidget->setFocus();
                return true;
            }
        }
        return false;
    } else if (obj == ui->notesListWidget) {
        if (event->type() == QEvent::KeyPress) {
            auto *keyEvent = dynamic_cast<QKeyEvent *>(event);

            // set focus to the note text edit if Key_Return or Key_Tab
            // were pressed in the notes list
            if ((keyEvent->key() == Qt::Key_Return) || (keyEvent->key() == Qt::Key_Tab)) {
                ui->headingSearchLineEdit->setFocus();

                return true;
            }
        }
        return false;
    }

    return MasterDialog::eventFilter(obj, event);
}

void LinkDialog::doAccept() {
    ui->urlEdit->clear();
    this->close();
    this->setResult(QDialog::Accepted);
}

void LinkDialog::on_notesListWidget_doubleClicked(const QModelIndex &index) {
    Q_UNUSED(index)
    doAccept();
}

void LinkDialog::on_headingListWidget_doubleClicked(const QModelIndex &index) {
    Q_UNUSED(index)
    doAccept();
}

/**
 * @brief Returns the title of an html page
 *
 * @param html
 * @return
 */
QString LinkDialog::getTitleFromHtml(const QString &html) {
    if (html.isEmpty()) {
        return {};
    }

    // parse title from webpage
    QRegularExpression regex(QStringLiteral(R"(<title.*>(.*)<\/title>)"),
                             QRegularExpression::MultilineOption |
                                 QRegularExpression::DotMatchesEverythingOption |
                                 QRegularExpression::InvertedGreedinessOption);
    QRegularExpressionMatch match = regex.match(html);
    QString title = match.captured(1);

    // decode HTML entities
    title = Utils::Misc::unescapeHtml(std::move(title));

    // replace some other characters we don't want
    title.replace(QStringLiteral("["), QStringLiteral("("))
        .replace(QStringLiteral("]"), QStringLiteral(")"))
        .replace(QStringLiteral("&#8211;"), QStringLiteral("-"))
        .replace(QStringLiteral("&#124;"), QStringLiteral("-"))
        .replace(QStringLiteral("&#038;"), QStringLiteral("&"))
        .replace(QStringLiteral("&#39;"), QStringLiteral("'"));

    // trim whitespaces and return title
    return title.simplified();
}

/**
 * Shows the download progress
 */
void LinkDialog::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
    ui->downloadProgressBar->setMaximum(static_cast<int>(bytesTotal / 1000));
    ui->downloadProgressBar->setValue(static_cast<int>(bytesReceived / 1000));
    ui->downloadProgressBar->setToolTip(Utils::Misc::toHumanReadableByteSize(bytesReceived) +
                                        " / " + Utils::Misc::toHumanReadableByteSize(bytesTotal));
}

/**
 *
 *
 * @param reply
 */
void LinkDialog::slotReplyFinished(QNetworkReply *reply) {
    if (reply == nullptr) {
        return;
    }

    reply->deleteLater();
    ui->downloadProgressBar->hide();

    qDebug() << "Reply from " << reply->url().path();
    QByteArray data = reply->readAll();
    qDebug() << __func__ << " - 'data.size': " << data.size();

    if (reply->error() != QNetworkReply::NoError &&
        reply->error() != QNetworkReply::OperationCanceledError) {
        qWarning() << QStringLiteral("Network error: %1").arg(reply->errorString());

        return;
    }

    if (getLinkName().isEmpty()) {
        const QString title = getTitleFromHtml(data);

        if (!title.isEmpty()) {
            setLinkName(title);
        }
    }
}

/**
 * Selects a local file to link to
 */
void LinkDialog::addFileUrl(bool relative) {
    SettingsService settings;
    // load last url
    QUrl fileUrl = settings.value(QStringLiteral("LinkDialog/lastSelectedFileUrl")).toUrl();

    if (Utils::Misc::isInPortableMode()) {
        fileUrl = QUrl(QStringLiteral("file://") +
                       Utils::Misc::prependPortableDataPathIfNeeded(Utils::Misc::removeIfStartsWith(
                           fileUrl.toLocalFile(), QStringLiteral("/"))));
    }

    fileUrl = QFileDialog::getOpenFileUrl(this, tr("Select file to link to"), fileUrl);
    QString fileUrlString = fileUrl.toString(QUrl::FullyEncoded);

    if (Utils::Misc::isInPortableMode()) {
        fileUrlString =
            QStringLiteral("file://") +
            QUrl(QStringLiteral("../") +
                 Utils::Misc::makePathRelativeToPortableDataPathIfNeeded(fileUrl.toLocalFile()))
                .toString(QUrl::FullyEncoded);
    }

    if (!fileUrlString.isEmpty()) {
        // store url for the next time
        settings.setValue(QStringLiteral("LinkDialog/lastSelectedFileUrl"), fileUrlString);

        fileUrl = QUrl(fileUrlString);
        if (relative && fileUrl.isLocalFile()) {
            auto note = MainWindow::instance()->getCurrentNote();
            // Make path relative to the current note
            QString relativePath = note.relativeFilePath(fileUrl.toLocalFile());
            fileUrlString = QStringLiteral("./") + relativePath;

            // Also set the link name if it's empty
            if (ui->nameLineEdit->text().isEmpty()) {
                QFileInfo fileInfo(relativePath);
                ui->nameLineEdit->setText(fileInfo.fileName());
            }
        }

        // write the file-url to the url text-edit
        ui->urlEdit->setText(fileUrlString);
    }
}

/**
 * Selects a local directory to link to
 */
void LinkDialog::addDirectoryUrl(bool relative) {
    SettingsService settings;
    // load last url
    QUrl directoryUrl =
        settings.value(QStringLiteral("LinkDialog/lastSelectedDirectoryUrl")).toUrl();

    if (Utils::Misc::isInPortableMode()) {
        directoryUrl =
            QUrl(QStringLiteral("file://") +
                 Utils::Misc::prependPortableDataPathIfNeeded(Utils::Misc::removeIfStartsWith(
                     directoryUrl.toLocalFile(), QStringLiteral("/"))));
    }

    directoryUrl =
        QFileDialog::getExistingDirectoryUrl(this, tr("Select directory to link to"), directoryUrl);
    QString directoryUrlString = directoryUrl.toString(QUrl::FullyEncoded);

    if (Utils::Misc::isInPortableMode()) {
        directoryUrlString =
            QStringLiteral("file://") +
            QUrl(QStringLiteral("../") + Utils::Misc::makePathRelativeToPortableDataPathIfNeeded(
                                             directoryUrl.toLocalFile()))
                .toString(QUrl::FullyEncoded);
    }

    if (!directoryUrlString.isEmpty()) {
        // store url for the next time
        settings.setValue(QStringLiteral("LinkDialog/lastSelectedDirectoryUrl"),
                          directoryUrlString);

        directoryUrl = QUrl(directoryUrlString);
        if (relative && directoryUrl.isLocalFile()) {
            auto note = MainWindow::instance()->getCurrentNote();
            // Make path relative to the current note
            QString relativePath = note.relativeFilePath(directoryUrl.toLocalFile());
            directoryUrlString = QStringLiteral("./") + relativePath;

            // Also set the link name if it's empty
            if (ui->nameLineEdit->text().isEmpty()) {
                QFileInfo directoryInfo(directoryUrl.toLocalFile());
                ui->nameLineEdit->setText(directoryInfo.fileName());
            }
        }

        // write the directory-url to the url text-edit
        ui->urlEdit->setText(directoryUrlString);
    }
}

void LinkDialog::on_urlEdit_textChanged(const QString &arg1) {
    auto url = QUrl(arg1);

    if (!url.isValid()) {
        return;
    }

    // try to get the title of the webpage if no link name was set
    if (url.scheme().startsWith(QStringLiteral("http")) && ui->nameLineEdit->text().isEmpty()) {
        startTitleFetchRequest(url);
    }
}

void LinkDialog::setupFileUrlMenu() {
    // std::make_unique needs C++14
    auto addMenu = std::unique_ptr<QMenu>(new QMenu(this));

    QAction *addFileRelativeAction = addMenu->addAction(tr("Select file to link to (relative)"));
    addFileRelativeAction->setIcon(
        QIcon::fromTheme(QStringLiteral("document-open"),
                         QIcon(QStringLiteral(":icons/breeze-qownnotes/16x16/document-open.svg"))));
    connect(addFileRelativeAction, &QAction::triggered, this, [this]() { addFileUrl(true); });

    QAction *addFileAbsoluteAction = addMenu->addAction(tr("Select file to link to (absolute)"));
    addFileAbsoluteAction->setIcon(
        QIcon::fromTheme(QStringLiteral("document-open"),
                         QIcon(QStringLiteral(":icons/breeze-qownnotes/16x16/document-open.svg"))));
    connect(addFileAbsoluteAction, SIGNAL(triggered()), this, SLOT(addFileUrl()));

    QAction *addDirectoryRelativeAction =
        addMenu->addAction(tr("Select directory to link to (relative)"));
    addDirectoryRelativeAction->setIcon(
        QIcon::fromTheme(QStringLiteral("folder"),
                         QIcon(QStringLiteral(":icons/breeze-qownnotes/16x16/folder.svg"))));
    connect(addDirectoryRelativeAction, &QAction::triggered, this,
            [this]() { addDirectoryUrl(true); });

    QAction *addDirectoryAbsoluteAction =
        addMenu->addAction(tr("Select directory to link to (absolute)"));
    addDirectoryAbsoluteAction->setIcon(
        QIcon::fromTheme(QStringLiteral("folder"),
                         QIcon(QStringLiteral(":icons/breeze-qownnotes/16x16/folder.svg"))));
    connect(addDirectoryAbsoluteAction, SIGNAL(triggered()), this, SLOT(addDirectoryUrl()));

    ui->fileUrlButton->setMenu(addMenu.release());
}

void LinkDialog::on_buttonBox_accepted() {
    if (ui->tabWidget->currentWidget() == ui->noteTab) {
        ui->urlEdit->clear();
    }
}

void LinkDialog::on_headingSearchLineEdit_textChanged(const QString &arg1) {
    Utils::Gui::searchForTextInListWidget(ui->headingListWidget, arg1);
}

void LinkDialog::loadNoteHeadings() const {
    auto note = getSelectedNote();
    _markdownTextEdit->setPlainText(note.getNoteText());
    auto nodes = NavigationWidget::parseDocument(_markdownTextEdit->document());
    QStringList headingTexts;
    std::transform(nodes.begin(), nodes.end(), std::back_inserter(headingTexts),
                   [](const Node &node) { return node.text; });

    ui->headingListWidget->clear();
    ui->headingListWidget->addItems(headingTexts);
}

void LinkDialog::on_notesListWidget_currentItemChanged(QTreeWidgetItem *current,
                                                       QTreeWidgetItem *previous) {
    Q_UNUSED(current)
    Q_UNUSED(previous)

    loadNoteHeadings();
}

void LinkDialog::on_tabWidget_currentChanged(int index) {
    if (index == 0) {
        ui->urlEdit->setFocus();
    } else {
        ui->searchLineEdit->setFocus();
    }
}

void LinkDialog::startTitleFetchRequest(const QUrl &url) {
    const QString title =
        ScriptingService::instance()->callFetchUrlTitleHook(url.toString(QUrl::FullyEncoded));

    if (!title.isEmpty()) {
        setLinkName(title);
        return;
    }

    ui->downloadProgressBar->show();
    QNetworkRequest networkRequest(url);

#if QT_VERSION < QT_VERSION_CHECK(5, 9, 0)
    networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
#else
    networkRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, true);
#endif

#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
    // try to ensure the network is accessible
    _networkManager->setNetworkAccessible(QNetworkAccessManager::Accessible);
#endif

    QNetworkReply *reply = _networkManager->get(networkRequest);

    connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this,
            SLOT(downloadProgress(qint64, qint64)));
}

void LinkDialog::on_refreshButton_clicked() {
    ui->nameLineEdit->clear();
    on_urlEdit_textChanged(ui->urlEdit->text());
}
