// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default

#include "qhelpsearchquerywidget.h"

#include <QtCore/qabstractitemmodel.h>
#include <QtCore/qstringlist.h>
#include <QtGui/qevent.h>
#include <QtWidgets/qcompleter.h>
#include <QtWidgets/qlabel.h>
#include <QtWidgets/qlayout.h>
#include <QtWidgets/qlineedit.h>
#include <QtWidgets/qpushbutton.h>
#include <QtWidgets/qtoolbutton.h>

QT_BEGIN_NAMESPACE

class QHelpSearchQueryWidgetPrivate : public QObject
{
public:
    struct QueryHistory {
        explicit QueryHistory() : curQuery(-1) {}
        QStringList queries;
        int curQuery = 0;
    };

    class CompleterModel : public QAbstractListModel
    {
    public:
        explicit CompleterModel(QObject *parent) : QAbstractListModel(parent) { }

        int rowCount(const QModelIndex &parent = QModelIndex()) const override
        {
            return parent.isValid() ? 0 : termList.size();
        }

        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
        {
            if (!index.isValid() || index.row() >= termList.size()||
                (role != Qt::EditRole && role != Qt::DisplayRole))
                return {};
            return termList.at(index.row());
        }

        void addTerm(const QString &term)
        {
            if (!termList.contains(term)) {
                beginResetModel();
                termList.append(term);
                endResetModel();
            }
        }

    private:
        QStringList termList;
    };

    QHelpSearchQueryWidgetPrivate() : m_searchCompleter(new CompleterModel(this), this) {}

    void retranslate()
    {
        m_searchLabel->setText(QHelpSearchQueryWidget::tr("Search for:"));
        m_searchButton->setText(QHelpSearchQueryWidget::tr("Search"));
#if QT_CONFIG(tooltip)
        m_prevQueryButton->setToolTip(QHelpSearchQueryWidget::tr("Previous search"));
        m_nextQueryButton->setToolTip(QHelpSearchQueryWidget::tr("Next search"));
#endif
    }

    void saveQuery(const QString &query)
    {
        // We only add the query to the list if it is different from the last one.
        if (!m_queries.queries.isEmpty() && m_queries.queries.last() == query)
            return;

        m_queries.queries.append(query);
        static_cast<CompleterModel *>(m_searchCompleter.model())->addTerm(query);
    }

    void nextOrPrevQuery(int maxOrMinIndex, int addend, QToolButton *thisButton,
        QToolButton *otherButton)
    {
        m_lineEdit->clear();

        // Otherwise, the respective button would be disabled.
        Q_ASSERT(m_queries.curQuery != maxOrMinIndex);

        m_queries.curQuery = qBound(0, m_queries.curQuery + addend, m_queries.queries.size() - 1);
        const QString &query = m_queries.queries.at(m_queries.curQuery);
        m_lineEdit->setText(query);

        if (m_queries.curQuery == maxOrMinIndex)
            thisButton->setEnabled(false);
        otherButton->setEnabled(true);
    }

    void enableOrDisableToolButtons()
    {
        m_prevQueryButton->setEnabled(m_queries.curQuery > 0);
        m_nextQueryButton->setEnabled(m_queries.curQuery < m_queries.queries.size() - 1);
    }

    bool eventFilter(QObject *ob, QEvent *event) override
    {
        if (event->type() == QEvent::KeyPress) {
            QKeyEvent *const keyEvent = static_cast<QKeyEvent *>(event);
            if (keyEvent->key() == Qt::Key_Down) {
                if (m_queries.curQuery + 1 < m_queries.queries.size())
                    nextQuery();
                return true;
            }
            if (keyEvent->key() == Qt::Key_Up) {
                if (m_queries.curQuery > 0)
                    prevQuery();
                return true;
            }
        }
        return QObject::eventFilter(ob, event);
    }

    void searchRequested()
    {
        saveQuery(m_lineEdit->text());
        m_queries.curQuery = m_queries.queries.size() - 1;
        if (m_queries.curQuery > 0)
            m_prevQueryButton->setEnabled(true);
        m_nextQueryButton->setEnabled(false);
    }

    void nextQuery()
    {
        nextOrPrevQuery(m_queries.queries.size() - 1, 1, m_nextQueryButton, m_prevQueryButton);
    }

    void prevQuery() { nextOrPrevQuery(0, -1, m_prevQueryButton, m_nextQueryButton); }

    QLabel *m_searchLabel = nullptr;
    QPushButton *m_searchButton = nullptr;
    QLineEdit *m_lineEdit = nullptr;
    QToolButton *m_nextQueryButton = nullptr;
    QToolButton *m_prevQueryButton = nullptr;
    QueryHistory m_queries;
    QCompleter m_searchCompleter;
    bool m_compactMode = false;
};

/*!
    \class QHelpSearchQueryWidget
    \since 4.4
    \inmodule QtHelp
    \brief The QHelpSearchQueryWidget class provides a simple line edit or
    an advanced widget to enable the user to input a search term in a
    standardized input mask.
*/

/*!
    \fn void QHelpSearchQueryWidget::search()

    This signal is emitted when a the user has the search button invoked.
    After receiving the signal you can ask the QHelpSearchQueryWidget for the
    search input that you may pass to the QHelpSearchEngine::search() function.
*/

/*!
    Constructs a new search query widget with the given \a parent.
*/
QHelpSearchQueryWidget::QHelpSearchQueryWidget(QWidget *parent)
    : QWidget(parent)
    , d(new QHelpSearchQueryWidgetPrivate)
{
    QVBoxLayout *vLayout = new QVBoxLayout(this);
    vLayout->setContentsMargins({});

    QHBoxLayout* hBoxLayout = new QHBoxLayout;
    d->m_searchLabel = new QLabel(this);
    d->m_lineEdit = new QLineEdit(this);
    d->m_lineEdit->setClearButtonEnabled(true);
    d->m_lineEdit->setCompleter(&d->m_searchCompleter);
    d->m_lineEdit->installEventFilter(d);
    d->m_prevQueryButton = new QToolButton(this);
    d->m_prevQueryButton->setArrowType(Qt::LeftArrow);
    d->m_prevQueryButton->setEnabled(false);
    d->m_nextQueryButton = new QToolButton(this);
    d->m_nextQueryButton->setArrowType(Qt::RightArrow);
    d->m_nextQueryButton->setEnabled(false);
    d->m_searchButton = new QPushButton(this);
    hBoxLayout->addWidget(d->m_searchLabel);
    hBoxLayout->addWidget(d->m_lineEdit);
    hBoxLayout->addWidget(d->m_prevQueryButton);
    hBoxLayout->addWidget(d->m_nextQueryButton);
    hBoxLayout->addWidget(d->m_searchButton);

    vLayout->addLayout(hBoxLayout);

    connect(d->m_prevQueryButton, &QAbstractButton::clicked, this, [this] { d->prevQuery(); });
    connect(d->m_nextQueryButton, &QAbstractButton::clicked, this, [this] { d->nextQuery(); });
    connect(d->m_searchButton, &QAbstractButton::clicked, this, &QHelpSearchQueryWidget::search);
    connect(d->m_lineEdit, &QLineEdit::returnPressed, this, &QHelpSearchQueryWidget::search);

    d->retranslate();
    connect(this, &QHelpSearchQueryWidget::search, this, [this] { d->searchRequested(); });
    setCompactMode(true);
}

/*!
    Destroys the search query widget.
*/
QHelpSearchQueryWidget::~QHelpSearchQueryWidget()
{
    delete d;
}

/*!
    Expands the search query widget so that the extended search fields are shown.
*/
void QHelpSearchQueryWidget::expandExtendedSearch()
{
    // TODO: no extended search anymore, deprecate it?
}

/*!
    Collapses the search query widget so that only the default search field is
    shown.
*/
void QHelpSearchQueryWidget::collapseExtendedSearch()
{
    // TODO: no extended search anymore, deprecate it?
}

#if QT_DEPRECATED_SINCE(5, 9)
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
/*!
    \deprecated

    Use searchInput() instead.
*/
QList<QHelpSearchQuery> QHelpSearchQueryWidget::query() const
{
    return {{QHelpSearchQuery::DEFAULT, searchInput().split(QChar::Space, Qt::SkipEmptyParts)}};
}

/*!
    \deprecated

    Use setSearchInput() instead.
*/
void QHelpSearchQueryWidget::setQuery(const QList<QHelpSearchQuery> &queryList)
{
    if (queryList.isEmpty())
        return;

    setSearchInput(queryList.first().wordList.join(QChar::Space));
}
QT_WARNING_POP
#endif // QT_DEPRECATED_SINCE(5, 9)

/*!
    \since 5.9

    Returns a search phrase to use in combination with the
    QHelpSearchEngine::search(const QString &searchInput) function.
*/
QString QHelpSearchQueryWidget::searchInput() const
{
    if (d->m_queries.queries.isEmpty())
        return {};
    return d->m_queries.queries.last();
}

/*!
    \since 5.9

    Sets the QHelpSearchQueryWidget input field to the value specified by
    \a searchInput.

    \note The QHelpSearchEngine::search(const QString &searchInput) function has
    to be called to perform the actual search.
*/
void QHelpSearchQueryWidget::setSearchInput(const QString &searchInput)
{
    d->m_lineEdit->clear();
    d->m_lineEdit->setText(searchInput);
    d->searchRequested();
}

bool QHelpSearchQueryWidget::isCompactMode() const
{
    return d->m_compactMode;
}

void QHelpSearchQueryWidget::setCompactMode(bool on)
{
    if (d->m_compactMode != on) {
        d->m_compactMode = on;
        d->m_prevQueryButton->setVisible(!on);
        d->m_nextQueryButton->setVisible(!on);
        d->m_searchLabel->setVisible(!on);
    }
}

/*!
    \reimp
*/
void QHelpSearchQueryWidget::focusInEvent(QFocusEvent *focusEvent)
{
    if (focusEvent->reason() != Qt::MouseFocusReason) {
        d->m_lineEdit->selectAll();
        d->m_lineEdit->setFocus();
    }
}

/*!
    \reimp
*/
void QHelpSearchQueryWidget::changeEvent(QEvent *event)
{
    if (event->type() == QEvent::LanguageChange)
        d->retranslate();
    else
        QWidget::changeEvent(event);
}

QT_END_NAMESPACE
