#include "vsearcher.h"

#include <QtWidgets>
#include <QCoreApplication>

#include "vlineedit.h"
#include "utils/vutils.h"
#include "utils/viconutils.h"
#include "vsearch.h"
#include "vsearchresulttree.h"
#include "vmainwindow.h"
#include "veditarea.h"
#include "vdirectorytree.h"
#include "vdirectory.h"
#include "vnotebookselector.h"
#include "vnotebook.h"
#include "vnote.h"
#include "vconfigmanager.h"
#include "vexplorer.h"
#include "vconstants.h"

extern VMainWindow *g_mainWin;

extern VNote *g_vnote;

extern VConfigManager *g_config;

VSearcher::VSearcher(QWidget *p_parent)
    : QWidget(p_parent),
      m_initialized(false),
      m_uiInitialized(false),
      m_inSearch(false),
      m_askedToStop(false),
      m_search(this)
{
    qRegisterMetaType<QList<QSharedPointer<VSearchResultItem>>>("QList<QSharedPointer<VSearchResultItem>>");
}

void VSearcher::setupUI()
{
    if (m_uiInitialized) {
        return;
    }

    m_uiInitialized = true;

    // Search button.
    m_searchBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/search.svg"), "", this);
    m_searchBtn->setToolTip(tr("Search"));
    m_searchBtn->setProperty("FlatBtn", true);
    connect(m_searchBtn, &QPushButton::clicked,
            this, &VSearcher::startSearch);

    // Clear button.
    m_clearBtn = new QPushButton(VIconUtils::buttonDangerIcon(":/resources/icons/clear_search.svg"), "", this);
    m_clearBtn->setToolTip(tr("Clear Results"));
    m_clearBtn->setProperty("FlatBtn", true);
    connect(m_clearBtn, &QPushButton::clicked,
            this, [this]() {
                m_results->clearResults();
            });

    // Advanced button.
    m_advBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/search_advanced.svg"),
                               "",
                               this);
    m_advBtn->setToolTip(tr("Advanced Settings"));
    m_advBtn->setProperty("FlatBtn", true);
    m_advBtn->setCheckable(true);
    connect(m_advBtn, &QPushButton::toggled,
            this, [this](bool p_checked) {
                m_advWidget->setVisible(p_checked);
            });

    // Console button.
    m_consoleBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/search_console.svg"),
                                   "",
                                   this);
    m_consoleBtn->setToolTip(tr("Console"));
    m_consoleBtn->setProperty("FlatBtn", true);
    m_consoleBtn->setCheckable(true);
    connect(m_consoleBtn, &QPushButton::toggled,
            this, [this](bool p_checked) {
                m_consoleEdit->setVisible(p_checked);
            });

    m_numLabel = new QLabel(this);

    QHBoxLayout *btnLayout = new QHBoxLayout();
    btnLayout->addWidget(m_searchBtn);
    btnLayout->addWidget(m_clearBtn);
    btnLayout->addWidget(m_advBtn);
    btnLayout->addWidget(m_consoleBtn);
    btnLayout->addStretch();
    btnLayout->addWidget(m_numLabel);
    btnLayout->setContentsMargins(0, 0, 0, 0);

    // Keyword.
    m_keywordCB = VUtils::getComboBox(this);
    m_keywordCB->setEditable(true);
    m_keywordCB->setLineEdit(new VLineEdit(this));
    m_keywordCB->setToolTip(tr("Keywords to search for"));
    m_keywordCB->lineEdit()->setPlaceholderText(tr("Supports space, &&, and ||"));
    m_keywordCB->lineEdit()->setProperty("EmbeddedEdit", true);
    connect(m_keywordCB, &QComboBox::currentTextChanged,
            this, &VSearcher::handleInputChanged);
    connect(m_keywordCB->lineEdit(), &QLineEdit::returnPressed,
            this, &VSearcher::animateSearchClick);
    m_keywordCB->completer()->setCaseSensitivity(Qt::CaseSensitive);

    // Scope.
    m_searchScopeCB = VUtils::getComboBox(this);
    m_searchScopeCB->setToolTip(tr("Scope to search"));
    connect(m_searchScopeCB, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
            this, &VSearcher::handleInputChanged);

    // Object.
    m_searchObjectCB = VUtils::getComboBox(this);
    m_searchObjectCB->setToolTip(tr("Object to search"));
    connect(m_searchObjectCB, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
            this, &VSearcher::handleInputChanged);

    // Target.
    m_searchTargetCB = VUtils::getComboBox(this);
    m_searchTargetCB->setToolTip(tr("Target to search"));
    connect(m_searchTargetCB, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
            this, &VSearcher::handleInputChanged);

    // Pattern.
    m_filePatternCB = VUtils::getComboBox(this);
    m_filePatternCB->setEditable(true);
    m_filePatternCB->setLineEdit(new VLineEdit(this));
    m_filePatternCB->setToolTip(tr("Wildcard pattern to filter the files to be searched"));
    m_filePatternCB->lineEdit()->setProperty("EmbeddedEdit", true);
    m_filePatternCB->completer()->setCaseSensitivity(Qt::CaseSensitive);

    // Engine.
    m_searchEngineCB = VUtils::getComboBox(this);
    m_searchEngineCB->setToolTip(tr("Engine to execute the search"));

    // Case sensitive.
    m_caseSensitiveCB = new QCheckBox(tr("&Case sensitive"), this);

    // Whole word only.
    m_wholeWordOnlyCB = new QCheckBox(tr("&Whole word only"), this);

    // Fuzzy search.
    m_fuzzyCB = new QCheckBox(tr("&Fuzzy search"), this);
    m_fuzzyCB->setToolTip(tr("Not available for content search"));
    connect(m_fuzzyCB, &QCheckBox::stateChanged,
            this, [this](int p_state) {
                bool checked = p_state == Qt::Checked;
                m_wholeWordOnlyCB->setEnabled(!checked);
            });

    // Regular expression.
    m_regularExpressionCB = new QCheckBox(tr("Re&gular expression"), this);
    connect(m_regularExpressionCB, &QCheckBox::stateChanged,
            this, [this](int p_state) {
                bool checked = p_state == Qt::Checked;
                m_wholeWordOnlyCB->setEnabled(!checked);
                m_fuzzyCB->setEnabled(!checked);
            });

    QFormLayout *advLayout = VUtils::getFormLayout();
    advLayout->addRow(tr("File pattern:"), m_filePatternCB);
    advLayout->addRow(tr("Engine:"), m_searchEngineCB);
    advLayout->addRow(m_caseSensitiveCB);
    advLayout->addRow(m_wholeWordOnlyCB);
    advLayout->addRow(m_fuzzyCB);
    advLayout->addRow(m_regularExpressionCB);
    advLayout->setContentsMargins(0, 0, 0, 0);

    m_advWidget = new QWidget(this);
    m_advWidget->setLayout(advLayout);
    m_advWidget->hide();

    // Progress bar.
    m_proBar = new QProgressBar(this);
    m_proBar->setRange(0, 0);

    // Cancel button.
    m_cancelBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/close.svg"),
                                  "",
                                  this);
    m_cancelBtn->setToolTip(tr("Cancel"));
    m_cancelBtn->setProperty("FlatBtn", true);
    connect(m_cancelBtn, &QPushButton::clicked,
            this, [this]() {
                if (m_inSearch) {
                    appendLogLine(tr("Cancelling the search..."));
                    m_askedToStop = true;
                    m_search.stop();
                }
            });

    QHBoxLayout *proLayout = new QHBoxLayout();
    proLayout->addWidget(m_proBar);
    proLayout->addWidget(m_cancelBtn);
    proLayout->setContentsMargins(0, 0, 0, 0);

    // Console.
    m_consoleEdit = new QPlainTextEdit(this);
    m_consoleEdit->setReadOnly(true);
    m_consoleEdit->setLineWrapMode(QPlainTextEdit::WidgetWidth);
    m_consoleEdit->setProperty("LineEdit", true);
    m_consoleEdit->setPlaceholderText(tr("Output logs will be shown here"));
    m_consoleEdit->setMaximumHeight(m_searchScopeCB->height() * 2);
    m_consoleEdit->hide();

    // List.
    m_results = new VSearchResultTree(this);
    m_results->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
    connect(m_results, &VSearchResultTree::countChanged,
            this, [this](int p_count) {
                m_clearBtn->setEnabled(p_count > 0);
                updateNumLabel(p_count);
            });

    QShortcut *expandShortcut = new QShortcut(QKeySequence(Shortcut::c_expand), this);
    expandShortcut->setContext(Qt::WidgetWithChildrenShortcut);
    connect(expandShortcut, &QShortcut::activated,
            m_results, &VSearchResultTree::expandCollapseAll);

    QFormLayout *formLayout = VUtils::getFormLayout();
    formLayout->addRow(tr("Keywords:"), m_keywordCB);
    formLayout->addRow(tr("Scope:"), m_searchScopeCB);
    formLayout->addRow(tr("Object:"), m_searchObjectCB);
    formLayout->addRow(tr("Target:"), m_searchTargetCB);
    formLayout->setContentsMargins(0, 0, 0, 0);

    QVBoxLayout *mainLayout = new QVBoxLayout();
    mainLayout->addLayout(btnLayout);
    mainLayout->addLayout(formLayout);
    mainLayout->addWidget(m_advWidget);
    mainLayout->addLayout(proLayout);
    mainLayout->addWidget(m_consoleEdit);
    mainLayout->addWidget(m_results);
    mainLayout->setContentsMargins(3, 0, 3, 0);

    setLayout(mainLayout);
}

void VSearcher::initUIFields()
{
    VSearchConfig config = VSearchConfig::fromConfig(g_config->getSearchOptions());

    // Scope.
    m_searchScopeCB->addItem(tr("Opened Notes"), VSearchConfig::OpenedNotes);
    m_searchScopeCB->addItem(tr("Current Folder"), VSearchConfig::CurrentFolder);
    m_searchScopeCB->addItem(tr("Current Notebook"), VSearchConfig::CurrentNotebook);
    m_searchScopeCB->addItem(tr("All Notebooks"), VSearchConfig::AllNotebooks);
    m_searchScopeCB->addItem(tr("Explorer Directory"), VSearchConfig::ExplorerDirectory);
    m_searchScopeCB->setCurrentIndex(m_searchScopeCB->findData(config.m_scope));

    // Object.
    m_searchObjectCB->addItem(tr("Name"), VSearchConfig::Name);
    m_searchObjectCB->addItem(tr("Content"), VSearchConfig::Content);
    m_searchObjectCB->addItem(tr("Tag"), VSearchConfig::Tag);
    m_searchObjectCB->addItem(tr("Path"), VSearchConfig::Path);
    m_searchObjectCB->setCurrentIndex(m_searchObjectCB->findData(config.m_object));

    // Target.
    m_searchTargetCB->addItem(tr("Note"), VSearchConfig::Note);
    m_searchTargetCB->addItem(tr("Folder"), VSearchConfig::Folder);
    m_searchTargetCB->addItem(tr("Notebook"), VSearchConfig::Notebook);
    m_searchTargetCB->addItem(tr("Note/Folder/Notebook"),
                              VSearchConfig::Note
                              | VSearchConfig:: Folder
                              | VSearchConfig::Notebook);
    m_searchTargetCB->setCurrentIndex(m_searchTargetCB->findData(config.m_target));

    // Engine.
    m_searchEngineCB->addItem(tr("Internal"), VSearchConfig::Internal);
    m_searchEngineCB->setCurrentIndex(m_searchEngineCB->findData(config.m_engine));

    // Pattern.
    m_filePatternCB->setCurrentText(config.m_pattern);

    m_caseSensitiveCB->setChecked(config.m_option & VSearchConfig::CaseSensitive);
    m_wholeWordOnlyCB->setChecked(config.m_option & VSearchConfig::WholeWordOnly);
    m_fuzzyCB->setChecked(config.m_option & VSearchConfig::Fuzzy);
    m_regularExpressionCB->setChecked(config.m_option & VSearchConfig::RegularExpression);

    setProgressVisible(false);

    m_clearBtn->setEnabled(false);
}

void VSearcher::updateItemToComboBox(QComboBox *p_comboBox)
{
    QString text = p_comboBox->currentText();
    if (!text.isEmpty() && p_comboBox->findText(text) == -1) {
        p_comboBox->addItem(text);
    }
}

void VSearcher::setProgressVisible(bool p_visible)
{
    m_proBar->setVisible(p_visible);
    m_cancelBtn->setVisible(p_visible);
}

void VSearcher::appendLogLine(const QString &p_text)
{
    m_consoleEdit->appendPlainText(">>> " + p_text);
    m_consoleEdit->ensureCursorVisible();
    QCoreApplication::sendPostedEvents();
}

void VSearcher::showMessage(const QString &p_text) const
{
    g_mainWin->showStatusMessage(p_text);
    QCoreApplication::sendPostedEvents();
}

void VSearcher::handleInputChanged()
{
    bool readyToSearch = true;

    // Keyword.
    QString keyword = m_keywordCB->currentText();
    readyToSearch = !keyword.isEmpty();

    if (readyToSearch) {
        int obj = m_searchObjectCB->currentData().toInt();
        int target = m_searchTargetCB->currentData().toInt();
        int scope = m_searchScopeCB->currentData().toInt();

        // Other targets are only available for Name and Path.
        if (!(obj & VSearchConfig::Name)
            && !(obj & VSearchConfig::Path)
            && !(target & VSearchConfig::Note)) {
            readyToSearch = false;
        }

        if (readyToSearch
            && obj == VSearchConfig::Outline
            && scope != VSearchConfig::CurrentNotebook
            && scope != VSearchConfig::OpenedNotes) {
            // Outline is available only for CurrentNote and OpenedNotes.
            readyToSearch = false;
        }

        if (readyToSearch && scope == VSearchConfig::ExplorerDirectory) {
            if (!(obj & VSearchConfig::Name)
                && !(obj & VSearchConfig::Path)
                && !(obj & VSearchConfig::Content)) {
                readyToSearch = false;
            } else if (!(target & VSearchConfig::Note)
                && !(target & VSearchConfig::Folder)) {
                readyToSearch = false;
            }
        }
    }

    m_searchBtn->setEnabled(readyToSearch);
}

void VSearcher::startSearch()
{
    if (m_inSearch) {
        return;
    }

    m_searchBtn->setEnabled(false);
    setProgressVisible(true);
    m_results->clearResults();
    m_askedToStop = false;
    m_inSearch = true;
    m_consoleEdit->clear();
    appendLogLine(tr("Search started."));

    updateItemToComboBox(m_keywordCB);
    updateItemToComboBox(m_filePatternCB);

    m_search.clear();

    QSharedPointer<VSearchConfig> config(new VSearchConfig(m_searchScopeCB->currentData().toInt(),
                                                           m_searchObjectCB->currentData().toInt(),
                                                           m_searchTargetCB->currentData().toInt(),
                                                           m_searchEngineCB->currentData().toInt(),
                                                           getSearchOption(),
                                                           m_keywordCB->currentText(),
                                                           m_filePatternCB->currentText()));
    m_search.setConfig(config);

    g_config->setSearchOptions(config->toConfig());

    QSharedPointer<VSearchResult> result;
    switch (config->m_scope) {
    case VSearchConfig::CurrentNote:
    {
        QVector<VFile *> files;
        files.append(g_mainWin->getCurrentFile());
        if (files[0]) {
            QString msg(tr("Search current note %1.").arg(files[0]->getName()));
            appendLogLine(msg);
            showMessage(msg);
        }

        result = m_search.search(files);
        break;
    }

    case VSearchConfig::OpenedNotes:
    {
        QVector<VEditTabInfo> tabs = g_mainWin->getEditArea()->getAllTabsInfo();
        QVector<VFile *> files;
        files.reserve(tabs.size());
        for (auto const & ta : tabs) {
            files.append(ta.m_editTab->getFile());
        }

        result = m_search.search(files);
        break;
    }

    case VSearchConfig::CurrentFolder:
    {
        VDirectory *dir = g_mainWin->getDirectoryTree()->currentDirectory();
        if (dir) {
            QString msg(tr("Search current folder %1.").arg(dir->getName()));
            appendLogLine(msg);
            showMessage(msg);
        }

        result = m_search.search(dir);
        break;
    }

    case VSearchConfig::CurrentNotebook:
    {
        QVector<VNotebook *> notebooks;
        notebooks.append(g_mainWin->getNotebookSelector()->currentNotebook());
        if (notebooks[0]) {
            QString msg(tr("Search current notebook %1.").arg(notebooks[0]->getName()));
            appendLogLine(msg);
            showMessage(msg);
        }

        result = m_search.search(notebooks);
        break;
    }

    case VSearchConfig::AllNotebooks:
    {
        const QVector<VNotebook *> &notebooks = g_vnote->getNotebooks();
        result = m_search.search(notebooks);
        break;
    }

    case VSearchConfig::ExplorerDirectory:
    {
        QString rootDirectory = g_mainWin->getExplorer()->getRootDirectory();
        if (!rootDirectory.isEmpty()) {
            QString msg(tr("Search Explorer directory %1.").arg(rootDirectory));
            appendLogLine(msg);
            showMessage(msg);
        }

        result = m_search.search(rootDirectory);
        break;
    }

    default:
        break;
    }

    handleSearchFinished(result);
}

void VSearcher::handleSearchFinished(const QSharedPointer<VSearchResult> &p_result)
{
    Q_ASSERT(m_inSearch);
    Q_ASSERT(p_result->m_state != VSearchState::Idle);

    qDebug() << "handleSearchFinished" << (int)p_result->m_state;

    QString msg;
    switch (p_result->m_state) {
    case VSearchState::Busy:
        msg = tr("Search is on going.");
        appendLogLine(msg);
        return;

    case VSearchState::Success:
        msg = tr("Search succeeded.");
        appendLogLine(msg);
        showMessage(msg);
        break;

    case VSearchState::Fail:
        msg = tr("Search failed.");
        appendLogLine(msg);
        showMessage(msg);
        VUtils::showMessage(QMessageBox::Warning,
                            tr("Warning"),
                            tr("Search failed."),
                            p_result->m_errMsg,
                            QMessageBox::Ok,
                            QMessageBox::Ok,
                            this);
        break;

    case VSearchState::Cancelled:
        Q_ASSERT(m_askedToStop);
        msg = tr("User cancelled the search. Aborted!");
        appendLogLine(msg);
        showMessage(msg);
        m_askedToStop = false;
        break;

    default:
        break;
    }

    if (p_result->m_state != VSearchState::Fail
        && p_result->hasError()) {
        VUtils::showMessage(QMessageBox::Warning,
                            tr("Warning"),
                            tr("Errors found during search."),
                            p_result->m_errMsg,
                            QMessageBox::Ok,
                            QMessageBox::Ok,
                            this);
    }

    m_search.clear();

    m_inSearch = false;
    m_searchBtn->setEnabled(true);
    setProgressVisible(false);
}

void VSearcher::animateSearchClick()
{
    m_searchBtn->animateClick();
}

int VSearcher::getSearchOption() const
{
    int ret = VSearchConfig::NoneOption;

    if (m_caseSensitiveCB->isChecked()) {
        ret |= VSearchConfig::CaseSensitive;
    }

    if (m_wholeWordOnlyCB->isChecked()) {
        ret |= VSearchConfig::WholeWordOnly;
    }

    if (m_fuzzyCB->isChecked()) {
        ret |= VSearchConfig::Fuzzy;
    }

    if (m_regularExpressionCB->isChecked()) {
        ret |= VSearchConfig::RegularExpression;
    }

    return ret;
}

void VSearcher::updateNumLabel(int p_count)
{
    m_numLabel->setText(tr("%1 Items").arg(p_count));
}

void VSearcher::focusToSearch()
{
    init();

    m_keywordCB->setFocus(Qt::OtherFocusReason);
}

void VSearcher::showNavigation()
{
    setupUI();

    VNavigationMode::showNavigation(m_results);
}

bool VSearcher::handleKeyNavigation(int p_key, bool &p_succeed)
{
    setupUI();

    return VNavigationMode::handleKeyNavigation(m_results,
                                                p_key,
                                                p_succeed);
}

void VSearcher::init()
{
    if (m_initialized) {
        return;
    }

    m_initialized = true;

    setupUI();

    initUIFields();

    handleInputChanged();

    connect(&m_search, &VSearch::resultItemAdded,
            this, [this](const QSharedPointer<VSearchResultItem> &p_item) {
                // Not sure if it works.
                QCoreApplication::sendPostedEvents(NULL, QEvent::MouseButtonRelease);
                m_results->addResultItem(p_item);
            });
    connect(&m_search, &VSearch::resultItemsAdded,
            this, [this](const QList<QSharedPointer<VSearchResultItem> > &p_items) {
                // Not sure if it works.
                QCoreApplication::sendPostedEvents(NULL, QEvent::MouseButtonRelease);
                m_results->addResultItems(p_items);
            });
    connect(&m_search, &VSearch::finished,
            this, &VSearcher::handleSearchFinished);
}

void VSearcher::showEvent(QShowEvent *p_event)
{
    init();

    QWidget::showEvent(p_event);
}
