/* display_filter_expression_dialog.cpp
 *
 * Wireshark - Network traffic analyzer
 * By Gerald Combs <gerald@wireshark.org>
 * Copyright 1998 Gerald Combs
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include <algorithm>

#include "display_filter_expression_dialog.h"
#include <ui_display_filter_expression_dialog.h>

#include <epan/proto.h>
#include <epan/range.h>
#include <epan/tfs.h>
#include <wsutil/value_string.h>

#include <wsutil/utf8_entities.h>

#include <ui/qt/utils/qt_ui_utils.h>
#include "main_application.h"

#include <ui/qt/utils/variant_pointer.h>

#include <QPushButton>
#include <QDialogButtonBox>
#include <QListWidgetItem>
#include <QTreeWidgetItem>
#include <QRegularExpression>
#include <QtConcurrent>

// To do:
// - Speed up search.

enum {
    proto_type_ = 1000,
    field_type_
};

enum {
    present_op_ = 1000,
    any_eq_op_,
    all_eq_op_,
    any_ne_op_,
    all_ne_op_,
    gt_op_,
    lt_op_,
    ge_op_,
    le_op_,
    contains_op_,
    matches_op_,
    in_op_
};

static inline bool compareTreeWidgetItems(const QTreeWidgetItem *it1, const QTreeWidgetItem *it2)
{
    return *it1 < *it2;
}

static void generateProtocolTreeItems(QPromise<QTreeWidgetItem *> &promise)
{
    QList<QTreeWidgetItem *> proto_list;
    QList<QTreeWidgetItem *> *ptr_proto_list = &proto_list;

    void *proto_cookie;
    for (int proto_id = proto_get_first_protocol(&proto_cookie); proto_id != -1;
         proto_id = proto_get_next_protocol(&proto_cookie)) {
        protocol_t *protocol = find_protocol_by_id(proto_id);
        if (!proto_is_protocol_enabled(protocol)) continue;

        QTreeWidgetItem *proto_ti = new QTreeWidgetItem(proto_type_);
        QString label = QStringLiteral("%1 %2 %3")
            .arg(proto_get_protocol_short_name(protocol), UTF8_MIDDLE_DOT, proto_get_protocol_long_name(protocol));
        proto_ti->setText(0, label);
        proto_ti->setData(0, Qt::UserRole, QVariant::fromValue(proto_id));
        ptr_proto_list->append(proto_ti);
    }
    std::stable_sort(ptr_proto_list->begin(), ptr_proto_list->end(), compareTreeWidgetItems);

    foreach (QTreeWidgetItem *proto_ti, *ptr_proto_list) {
        if (promise.isCanceled()) {
            delete proto_ti;
            continue;
        }
        promise.suspendIfRequested();
        void *field_cookie;
        int proto_id = proto_ti->data(0, Qt::UserRole).toInt();

        QList <QTreeWidgetItem *> field_list;
        for (header_field_info *hfinfo = proto_get_first_protocol_field(proto_id, &field_cookie); hfinfo != NULL;
             hfinfo = proto_get_next_protocol_field(proto_id, &field_cookie)) {
            if (hfinfo->same_name_prev_id != -1) continue; // Ignore duplicate names.

            QTreeWidgetItem *field_ti = new QTreeWidgetItem(field_type_);
            QString label = QStringLiteral("%1 %2 %3").arg(hfinfo->abbrev, UTF8_MIDDLE_DOT, hfinfo->name);
            field_ti->setText(0, label);
            field_ti->setData(0, Qt::UserRole, VariantPointer<header_field_info>::asQVariant(hfinfo));
            field_list << field_ti;
        }
        std::stable_sort(field_list.begin(), field_list.end(), compareTreeWidgetItems);
        proto_ti->addChildren(field_list);
        if (!promise.addResult(proto_ti))
            delete proto_ti;
    }
}

DisplayFilterExpressionDialog::DisplayFilterExpressionDialog(QWidget *parent) :
    GeometryStateDialog(parent),
    watcher(new QFutureWatcher<QTreeWidgetItem *>(nullptr)),
    ui(new Ui::DisplayFilterExpressionDialog),
    ftype_(FT_NONE),
    field_(NULL)
{
    ui->setupUi(this);
    if (parent) loadGeometry(parent->width() * 2 / 3, parent->height());
    setAttribute(Qt::WA_DeleteOnClose, true);

    setWindowTitle(mainApp->windowTitleString(tr("Display Filter Expression")));
    setWindowIcon(mainApp->normalIcon());

    proto_initialize_all_prefixes();

    auto future = QtConcurrent::run(generateProtocolTreeItems);

    ui->fieldTreeWidget->setToolTip(ui->fieldLabel->toolTip());
    ui->searchLineEdit->setToolTip(ui->searchLabel->toolTip());
    ui->relationListWidget->setToolTip(ui->relationLabel->toolTip());
    ui->valueLineEdit->setToolTip(ui->valueLabel->toolTip());
    ui->enumListWidget->setToolTip(ui->enumLabel->toolTip());
    ui->rangeLineEdit->setToolTip(ui->rangeLabel->toolTip());

    // Relation list
    new QListWidgetItem("is present", ui->relationListWidget, present_op_);
    new QListWidgetItem("==", ui->relationListWidget, any_eq_op_);
    new QListWidgetItem("!=", ui->relationListWidget, all_ne_op_);
    new QListWidgetItem("===", ui->relationListWidget, all_eq_op_);
    new QListWidgetItem("!==", ui->relationListWidget, any_ne_op_);
    new QListWidgetItem(">", ui->relationListWidget, gt_op_);
    new QListWidgetItem("<", ui->relationListWidget, lt_op_);
    new QListWidgetItem(">=", ui->relationListWidget, ge_op_);
    new QListWidgetItem("<=", ui->relationListWidget, le_op_);
    new QListWidgetItem("contains", ui->relationListWidget, contains_op_);
    new QListWidgetItem("matches", ui->relationListWidget, matches_op_);
    new QListWidgetItem("in", ui->relationListWidget, in_op_);

    value_label_pfx_ = ui->valueLabel->text();

    connect(ui->anyRadioButton, &QAbstractButton::toggled, this, &DisplayFilterExpressionDialog::updateWidgets);
    connect(ui->allRadioButton, &QAbstractButton::toggled, this, &DisplayFilterExpressionDialog::updateWidgets);
    connect(ui->valueLineEdit, &QLineEdit::textEdited, this, &DisplayFilterExpressionDialog::updateWidgets);
    connect(ui->rangeLineEdit, &QLineEdit::textEdited, this, &DisplayFilterExpressionDialog::updateWidgets);

    updateWidgets();
    ui->searchLineEdit->setReadOnly(true);

    connect(watcher, &QFutureWatcher<QTreeWidgetItem *>::resultReadyAt, this, &DisplayFilterExpressionDialog::addTreeItem);
    connect(watcher, &QFutureWatcher<QTreeWidgetItem *>::finished, this, &DisplayFilterExpressionDialog::fillTree);
    watcher->setFuture(future);
}

DisplayFilterExpressionDialog::~DisplayFilterExpressionDialog()
{
    if (watcher)
    {
        watcher->future().cancel();
        qDeleteAll(watcher->future().results());
        watcher->waitForFinished();
        watcher->deleteLater();
    }
    delete ui;
}

void DisplayFilterExpressionDialog::addTreeItem(int result)
{
    QTreeWidgetItem *item = watcher->future().resultAt(result);
    ui->fieldTreeWidget->invisibleRootItem()->addChild(item);
}

void DisplayFilterExpressionDialog::fillTree()
{
    watcher->deleteLater();
    watcher = nullptr;

    ui->searchLineEdit->setReadOnly(false);
}

void DisplayFilterExpressionDialog::updateWidgets()
{
    bool rel_enable = field_ != NULL;

    ui->relationLabel->setEnabled(rel_enable);
    ui->relationListWidget->setEnabled(rel_enable);
    ui->hintLabel->clear();

    bool quantity_enable = false;
    bool value_enable = false;
    bool enum_enable = false;
    bool enum_multi_enable = false;
    bool range_enable = false;

    QString filter;
    if (field_) {
        filter = field_;
        QListWidgetItem *rli = ui->relationListWidget->currentItem();
        if (rli && rli->type() > all_ne_op_) {
            quantity_enable = true;
            if (ui->anyRadioButton->isChecked()) {
                filter.prepend("any ");
            }
            else if (ui->allRadioButton->isChecked()) {
                filter.prepend("all ");
            }
            else {
                ws_assert_not_reached();
            }
        }
        if (rli && rli->type() != present_op_) {
            value_enable = true;
            if (ftype_can_slice(ftype_)) {
                range_enable = true;
            }
            enum_enable = ui->enumListWidget->count() > 0;
            filter.append(QStringLiteral(" %1").arg(rli->text()));
        }
        if (value_enable && !ui->valueLineEdit->text().isEmpty()) {
            if (rli && rli->type() == in_op_) {
                filter.append(QStringLiteral(" {%1}").arg(ui->valueLineEdit->text()));
                enum_multi_enable = enum_enable;
            } else {
                if (ftype_ == FT_STRING) {
                    filter.append(QStringLiteral(" \"%1\"").arg(ui->valueLineEdit->text()));
                } else {
                    filter.append(QStringLiteral(" %1").arg(ui->valueLineEdit->text()));
                }
            }
        }
    }

    ui->quantityLabel->setEnabled(quantity_enable);
    ui->allRadioButton->setEnabled(quantity_enable);
    ui->anyRadioButton->setEnabled(quantity_enable);

    ui->valueLabel->setEnabled(value_enable);
    ui->valueLineEdit->setEnabled(value_enable);

    ui->enumLabel->setEnabled(enum_enable);
    ui->enumListWidget->setEnabled(enum_enable);
    ui->enumListWidget->setSelectionMode(enum_multi_enable ?
        QAbstractItemView::ExtendedSelection : QAbstractItemView::SingleSelection);

    ui->rangeLabel->setEnabled(range_enable);
    ui->rangeLineEdit->setEnabled(range_enable);

    ui->displayFilterLineEdit->setText(filter);

    QString hint = "<small><i>";
    if (ui->fieldTreeWidget->selectedItems().count() < 1) {
        hint.append(tr("Select a field name to get started"));
    } else if (ui->displayFilterLineEdit->syntaxState() != SyntaxLineEdit::Valid) {
        hint.append(ui->displayFilterLineEdit->syntaxErrorMessage());
    } else {
        hint.append(tr("Click OK to insert this filter"));
    }
    hint.append("</i></small>");
    ui->hintLabel->setText(hint);

    QPushButton *ok_bt = ui->buttonBox->button(QDialogButtonBox::Ok);
    if (ok_bt) {
        bool ok_enable = !(ui->displayFilterLineEdit->text().isEmpty()
                || (ui->displayFilterLineEdit->syntaxState() == SyntaxLineEdit::Invalid));
        ok_bt->setEnabled(ok_enable);
    }
}

void DisplayFilterExpressionDialog::fillEnumBooleanValues(const true_false_string *tfs)
{
    QListWidgetItem *eli = new QListWidgetItem(tfs_get_string(true, tfs), ui->enumListWidget);
    eli->setData(Qt::UserRole, QStringLiteral("1"));
    eli = new QListWidgetItem(tfs_get_string(false, tfs), ui->enumListWidget);
    eli->setData(Qt::UserRole, QStringLiteral("0"));
}

void DisplayFilterExpressionDialog::fillEnumIntValues(const _value_string *vals, int base)
{
    if (!vals) return;

    for (int i = 0; vals[i].strptr != NULL; i++) {
        QListWidgetItem *eli = new QListWidgetItem(vals[i].strptr, ui->enumListWidget);
        eli->setData(Qt::UserRole, int_to_qstring(vals[i].value, 0, base));
    }
}

void DisplayFilterExpressionDialog::fillEnumInt64Values(const _val64_string *vals64, int base)
{
    if (!vals64) return;

    for (int i = 0; vals64[i].strptr != NULL; i++) {
        QListWidgetItem *eli = new QListWidgetItem(vals64[i].strptr, ui->enumListWidget);
        eli->setData(Qt::UserRole, int_to_qstring(vals64[i].value, 0, base));
    }
}

void DisplayFilterExpressionDialog::fillEnumRangeValues(const _range_string *rvals)
{
    if (!rvals) return;

    for (int i = 0; rvals[i].strptr != NULL; i++) {
        QString range_text = rvals[i].strptr;

        // Tell the user which values are valid here. Default to value_min below.
        if (rvals[i].value_min != rvals[i].value_max) {
            range_text.append(QStringLiteral(" (%1 valid)").arg(range_to_qstring(&rvals[i])));
        }

        QListWidgetItem *eli = new QListWidgetItem(range_text, ui->enumListWidget);
        eli->setData(Qt::UserRole, QString::number(rvals[i].value_min));
    }
}

void DisplayFilterExpressionDialog::on_fieldTreeWidget_itemSelectionChanged()
{
    ftype_ = FT_NONE;
    field_ = NULL;
    QTreeWidgetItem *cur_fti = NULL;

    if (ui->fieldTreeWidget->selectedItems().count() > 0) {
        cur_fti = ui->fieldTreeWidget->selectedItems()[0];
    }
    ui->valueLineEdit->clear();
    ui->enumListWidget->clear();
    ui->rangeLineEdit->clear();

    if (cur_fti && cur_fti->type() == proto_type_) {
        ftype_ = FT_PROTOCOL;
        field_ = proto_get_protocol_filter_name(cur_fti->data(0, Qt::UserRole).toInt());
    } else if (cur_fti && cur_fti->type() == field_type_) {
        header_field_info *hfinfo = VariantPointer<header_field_info>::asPtr(cur_fti->data(0, Qt::UserRole));
        if (hfinfo) {
            ftype_ = hfinfo->type;
            field_ = hfinfo->abbrev;

            switch(ftype_) {
            case FT_BOOLEAN:
                // Let the user select the "True" and "False" values.
                fillEnumBooleanValues((const true_false_string *)hfinfo->strings);
                break;
            case FT_UINT8:
            case FT_UINT16:
            case FT_UINT24:
            case FT_UINT32:
            case FT_INT8:
            case FT_INT16:
            case FT_INT24:
            case FT_INT32:
            {
                int base;

                switch (hfinfo->display & FIELD_DISPLAY_E_MASK) {
                case BASE_HEX:
                case BASE_HEX_DEC:
                    base = 16;
                    break;
                case BASE_OCT:
                    base = 8;
                    break;
                default:
                    base = 10;
                    break;
                }
                // Let the user select from a list of value_string or range_string values.
                if (hfinfo->strings && ! ((hfinfo->display & FIELD_DISPLAY_E_MASK) == BASE_CUSTOM)) {
                    if (hfinfo->display & BASE_RANGE_STRING) {
                        fillEnumRangeValues((const range_string *)hfinfo->strings);
                    } else if (hfinfo->display & BASE_VAL64_STRING) {
                        const val64_string *vals = (const val64_string *)hfinfo->strings;
                        fillEnumInt64Values(vals, base);
                    } else { // Plain old value_string / VALS
                        const value_string *vals = (const value_string *)hfinfo->strings;
                        if (hfinfo->display & BASE_EXT_STRING)
                            vals = VALUE_STRING_EXT_VS_P((const value_string_ext *)vals);
                        fillEnumIntValues(vals, base);
                    }
                }
                break;
            }
            default:
                break;
            }
        }
    }
    if (ui->enumListWidget->count() > 0) {
        ui->enumListWidget->setCurrentRow(0);
    }

    bool all_show = field_ != NULL;
    for (int i = 0; i < ui->relationListWidget->count(); i++) {
        QListWidgetItem *li = ui->relationListWidget->item(i);
        switch (li->type()) {
        case any_eq_op_:
        case all_eq_op_:
        case any_ne_op_:
        case all_ne_op_:
            li->setHidden(!ftype_can_eq(ftype_) && !(ftype_can_slice(ftype_) && ftype_can_eq(FT_BYTES)));
            break;
        case gt_op_:
        case lt_op_:
        case ge_op_:
        case le_op_:
        case in_op_:
            li->setHidden(!ftype_can_cmp(ftype_) && !(ftype_can_slice(ftype_) && ftype_can_cmp(FT_BYTES)));
            break;
        case contains_op_:
            li->setHidden(!ftype_can_contains(ftype_) && !(ftype_can_slice(ftype_) && ftype_can_contains(FT_BYTES)));
            break;
        case matches_op_:
            li->setHidden(!ftype_can_matches(ftype_) && !(ftype_can_slice(ftype_) && ftype_can_matches(FT_BYTES)));
            break;
        default:
            li->setHidden(!all_show);
            break;
        }
    }
    if (all_show) {
        // Select "==" if it's present and we have a value, "is present" otherwise
        int row = ui->relationListWidget->count() > 1 && ui->enumListWidget->count() > 0 ? 1 : 0;
        ui->relationListWidget->setCurrentRow(row);
    }

    if (ftype_ != FT_NONE) {
        ui->valueLabel->setText(QStringLiteral("%1 (%2)")
                                .arg(value_label_pfx_)
                                .arg(ftype_pretty_name(ftype_)));
    } else {
        ui->valueLabel->setText(value_label_pfx_);
    }

    updateWidgets();
}

void DisplayFilterExpressionDialog::on_relationListWidget_itemSelectionChanged()
{
    updateWidgets();
}

void DisplayFilterExpressionDialog::on_enumListWidget_itemSelectionChanged()
{
    QStringList values;
    QList<QListWidgetItem *> items = ui->enumListWidget->selectedItems();
    QList<QListWidgetItem *>::const_iterator it = items.constBegin();
    while (it != items.constEnd())
    {
        values << (*it)->data(Qt::UserRole).toString();
        ++it;
    }

    ui->valueLineEdit->setText(values.join(" "));

    updateWidgets();
}

void DisplayFilterExpressionDialog::on_searchLineEdit_textChanged(const QString &search_re)
{
    ui->fieldTreeWidget->setUpdatesEnabled(false);
    QTreeWidgetItemIterator it(ui->fieldTreeWidget);
    QRegularExpression regex(search_re, QRegularExpression::CaseInsensitiveOption);
    if (! regex.isValid())
        return;

    while (*it) {
        bool hidden = true;
        if (search_re.isEmpty() || (*it)->text(0).contains(regex)) {
            hidden = false;
            if ((*it)->type() == field_type_) {
                (*it)->parent()->setHidden(false);
            }
        }
        (*it)->setHidden(hidden);
        ++it;
    }
    ui->fieldTreeWidget->setUpdatesEnabled(true);
}

void DisplayFilterExpressionDialog::on_buttonBox_accepted()
{
    emit insertDisplayFilter(ui->displayFilterLineEdit->text());
}

void DisplayFilterExpressionDialog::on_buttonBox_helpRequested()
{
    mainApp->helpTopicAction(HELP_FILTER_EXPRESSION_DIALOG);
}
