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

/*
 * @file Tap parameter dialog class
 *
 * Base class for statistics dialogs. Subclasses must implement:
 * - fillTree. Called when the dialog is first displayed and when a display
 *   filter is applied. In most cases the subclass should clear the tree and
 *   retap packets here.
 * - filterExpression. If the subclass supports filtering context menu items
 *   ("Apply As Filter", etc.) it should fill in ctx_menu_ and implement
 *   filterExpression.
 * - getTreeAsString or treeItemData. Used for "Copy" and "Save As...".
 * -
 */

#include "tap_parameter_dialog.h"
#include <ui_tap_parameter_dialog.h>

#include <errno.h>

#include "epan/stat_tap_ui.h"

#ifdef Q_OS_WIN
#include <windows.h>
#include "ui/packet_range.h"
#include "ui/win32/file_dlg_win32.h"
#endif // Q_OS_WIN

#include "ui/last_open_dir.h"
#include <wsutil/utf8_entities.h>

#include "wsutil/file_util.h"

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

#include <QClipboard>
#include <QContextMenuEvent>
#include <QMessageBox>
#include <QFileDialog>

// The GTK+ counterpart uses tap_param_dlg, which we don't use. If we
// need tap parameters we should probably create a TapParameterDialog
// class based on WiresharkDialog and subclass it here.

// To do:
// - Add tap parameters? SCSI SRT uses PARAM_ENUM. Everything appears to use
//   PARAM_FILTER. Nothing uses _UINT, _STRING, or _UUID.
// - Update to match bug 9452 / r53657.
// - Create a TapParameterTreeWidgetItem class?
// - Better / more usable XML output.

const int expand_all_threshold_ = 100; // Arbitrary

static QHash<const QString, tpdCreator> cfg_str_to_creator_;
const QString TapParameterDialog::action_name_ = "TapParameterAction";

TapParameterDialog::TapParameterDialog(QWidget &parent, CaptureFile &cf, int help_topic) :
    WiresharkDialog(parent, cf),
    ui(new Ui::TapParameterDialog),
    help_topic_(help_topic)
{
    ui->setupUi(this);

    // Only show a hint label if a subclass provides a hint.
    ui->hintLabel->hide();

    ctx_menu_.addAction(ui->actionCopyToClipboard);
    ctx_menu_.addAction(ui->actionSaveAs);

    QPushButton *button;
    button = ui->buttonBox->addButton(tr("Copy"), QDialogButtonBox::ActionRole);
    connect(button, SIGNAL(clicked()), this, SLOT(on_actionCopyToClipboard_triggered()));

    button = ui->buttonBox->addButton(tr("Save as" UTF8_HORIZONTAL_ELLIPSIS), QDialogButtonBox::ActionRole);
    connect(button, SIGNAL(clicked()), this, SLOT(on_actionSaveAs_triggered()));

    connect(ui->displayFilterLineEdit, SIGNAL(textChanged(QString)),
            this, SLOT(updateWidgets()));

    ProgressFrame::addToButtonBox(ui->buttonBox, &parent);

    if (help_topic_ < 1) {
        ui->buttonBox->button(QDialogButtonBox::Help)->hide();
    }

    if (!ui->displayFilterLineEdit->text().isEmpty()) {
        QString filter = ui->displayFilterLineEdit->text();
        emit updateFilter(filter);
    }
    show_timer_ = new QTimer(this);
    setRetapOnShow(true);
}

TapParameterDialog::~TapParameterDialog()
{
    delete ui;
    show_timer_->stop();
    delete show_timer_;
}

void TapParameterDialog::registerDialog(const QString title, const char *cfg_abbr, register_stat_group_t group, stat_tap_init_cb tap_init_cb, tpdCreator creator)
{
    stat_tap_ui ui_info;

    ui_info.group = group;
    ui_info.title = title.toUtf8().constData();
    ui_info.cli_string = cfg_abbr;
    ui_info.tap_init_cb = tap_init_cb;
    ui_info.nparams = 0; // We'll need this for SCSI SRT
    ui_info.params = NULL;
    register_stat_tap_ui(&ui_info, NULL);

    QString cfg_str = cfg_abbr;
    cfg_str_to_creator_[cfg_str] = creator;

    QAction *tpd_action = new QAction(title, wsApp);
    tpd_action->setObjectName(action_name_);
    tpd_action->setData(cfg_str);
    wsApp->addDynamicMenuGroupItem(group, tpd_action);
}

TapParameterDialog *TapParameterDialog::showTapParameterStatistics(QWidget &parent, CaptureFile &cf, const QString cfg_str, const QString arg, void *)
{
    if (cfg_str_to_creator_.contains(cfg_str)) {
        TapParameterDialog *tpd = cfg_str_to_creator_[cfg_str](parent, cfg_str, arg, cf);
        return tpd;
    }
    return NULL;
}

QTreeWidget *TapParameterDialog::statsTreeWidget()
{
    return ui->statsTreeWidget;
}

QLineEdit *TapParameterDialog::displayFilterLineEdit()
{
    return ui->displayFilterLineEdit;
}

QPushButton *TapParameterDialog::applyFilterButton()
{
    return ui->applyFilterButton;
}

QVBoxLayout *TapParameterDialog::verticalLayout()
{
    return ui->verticalLayout;
}

QHBoxLayout *TapParameterDialog::filterLayout()
{
    return ui->filterLayout;
}

QString TapParameterDialog::displayFilter()
{
    return ui->displayFilterLineEdit->text();
}

// This assumes that we're called before signals are connected or show()
// is called.
void TapParameterDialog::setDisplayFilter(const QString &filter)
{
    ui->displayFilterLineEdit->setText(filter);
}

void TapParameterDialog::setHint(const QString &hint)
{
    ui->hintLabel->setText(hint);
    ui->hintLabel->show();
}

void TapParameterDialog::setRetapOnShow(bool retap)
{
    show_timer_->stop();
    if (retap) {
        show_timer_->singleShot(0, this, SLOT(on_applyFilterButton_clicked()));
    }
}

void TapParameterDialog::filterActionTriggered()
{
    FilterAction *fa = qobject_cast<FilterAction *>(QObject::sender());
    QString filter_expr = filterExpression();

    if (!fa || filter_expr.isEmpty()) {
        return;
    }

    emit filterAction(filter_expr, fa->action(), fa->actionType());
}

QString TapParameterDialog::itemDataToPlain(QVariant var, int width)
{
    QString plain_str;
    int align_mul = 1;

    switch (var.type()) {
    case QVariant::String:
        align_mul = -1;
        // Fall through
    case QVariant::Int:
    case QVariant::UInt:
        plain_str = var.toString();
        break;
    case QVariant::Double:
        plain_str = QString::number(var.toDouble(), 'f', 6);
        break;
    default:
        break;
    }

    if (plain_str.length() < width) {
        plain_str = QString("%1").arg(plain_str, width * align_mul);
    }
    return plain_str;
}

QList<QVariant> TapParameterDialog::treeItemData(QTreeWidgetItem *) const
{
    return QList<QVariant>();
}

const QString plain_sep_ = "  ";
QByteArray TapParameterDialog::getTreeAsString(st_format_type format)
{
    QByteArray ba;
    QTreeWidgetItemIterator it(ui->statsTreeWidget, QTreeWidgetItemIterator::NotHidden);

    QList<int> col_widths;
    QByteArray footer;

    // Title + header
    switch (format) {
    case ST_FORMAT_PLAIN:
    {
        // Iterating over trees.
        QTreeWidgetItemIterator width_it(it);
        QString plain_header;
        while (*width_it) {
            // Iterating over items within this tree.
            for (int col=0; col < ui->statsTreeWidget->columnCount(); col++) {
                if (col_widths.size() <= col) {
                    col_widths.append(ui->statsTreeWidget->headerItem()->text(col).length());
                }
                QVariant var = ui->statsTreeWidget->headerItem()->data(col, Qt::DisplayRole);
                if (var.type() == QVariant::String) {
                    col_widths[col] = qMax(col_widths[col], itemDataToPlain(var).length());
                }
            }
            ++width_it;
        }
        QStringList ph_parts;
        for (int col = 0; col < ui->statsTreeWidget->columnCount() && col < col_widths.length(); col++) {
            ph_parts << ui->statsTreeWidget->headerItem()->text(col);
        }
        plain_header = ph_parts.join(plain_sep_);

        QByteArray top_separator;
        top_separator.fill('=', plain_header.length());
        top_separator.append('\n');
        QString file_header = QString("%1 - %2:\n").arg(windowSubtitle(), cap_file_.fileDisplayName());
        footer.fill('-', plain_header.length());
        footer.append('\n');
        plain_header.append('\n');

        ba.append(top_separator);
        ba.append(file_header);
        ba.append(plain_header);
        ba.append(footer);
        break;
    }
    case ST_FORMAT_CSV:
    {
        QString csv_header;
        QStringList ch_parts;
        for (int col = 0; col < ui->statsTreeWidget->columnCount(); col++) {
            ch_parts << QString("\"%1\"").arg(ui->statsTreeWidget->headerItem()->text(col));
        }
        csv_header = ch_parts.join(",");
        csv_header.append('\n');
        ba.append(csv_header.toUtf8().constData());
        break;
    }
    case ST_FORMAT_XML:
    {
        // XXX What's a useful format? This mostly conforms to DocBook.
        ba.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
        QString title = html_escape(windowSubtitle());
        QString xml_header = QString("<table>\n<title>%1</title>\n").arg(title);
        ba.append(xml_header.toUtf8());
        ba.append("<thead>\n<row>\n");
        for (int col = 0; col < ui->statsTreeWidget->columnCount(); col++) {
            title = html_escape(ui->statsTreeWidget->headerItem()->text(col));
            title = QString("  <entry>%1</entry>\n").arg(title);
            ba.append(title.toUtf8());
        }
        ba.append("</row>\n</thead>\n");
        ba.append("<tbody>\n");
        footer = "</tbody>\n</table>\n";
        break;
    }
    case ST_FORMAT_YAML:
    {
        QString yaml_header;
        ba.append("---\n");
        yaml_header = QString("Description: \"%1\"\nFile: \"%2\"\nItems:\n").arg(windowSubtitle()).arg(cap_file_.fileDisplayName());
        ba.append(yaml_header.toUtf8());
        break;
    }
    default:
        break;
    }

    // Data
    while (*it) {
        QList<QVariant> tid = treeItemData((*it));
        if (tid.length() < 1) {
            ++it;
            continue;
        }

        if (tid.length() < ui->statsTreeWidget->columnCount()) {
            // Assume we have a header
        }

        // Assume var length == columnCount
        QString line;
        QStringList parts;

        switch (format) {
        case ST_FORMAT_PLAIN:
        {
            int i = 0;
            foreach (QVariant var, tid) {
                parts << itemDataToPlain(var, col_widths[i]);
                i++;
            }
            line = parts.join(plain_sep_);
            line.append('\n');
            break;
        }
        case ST_FORMAT_CSV:
            foreach (QVariant var, tid) {
                if (var.type() == QVariant::String) {
                    parts << QString("\"%1\"").arg(var.toString());
                } else {
                    parts << var.toString();
                }
            }
            line = parts.join(",");
            line.append('\n');
            break;
        case ST_FORMAT_XML:
        {
            line = "<row>\n";
            foreach (QVariant var, tid) {
                QString entry = html_escape(var.toString());
                line.append(QString("  <entry>%1</entry>\n").arg(entry));
            }
            line.append("</row>\n");
            break;
        }
        case ST_FORMAT_YAML:
        {
            int col = 0;
            QString indent = "-";
            foreach (QVariant var, tid) {
                QString entry;
                if (var.type() == QVariant::String) {
                    entry = QString("\"%1\"").arg(var.toString());
                } else {
                    entry = var.toString();
                }
                line.append(QString("  %1 %2: %3\n").arg(indent).arg(ui->statsTreeWidget->headerItem()->text(col), entry));
                indent = " ";
                col++;
            }
            break;
        }
        default:
            break;
        }

        ba.append(line.toUtf8());
        ++it;
    }

    // Footer
    ba.append(footer); // plain only?
    return ba;
}

void TapParameterDialog::drawTreeItems()
{
    if (ui->statsTreeWidget->model()->rowCount() < expand_all_threshold_) {
        ui->statsTreeWidget->expandAll();
    }

    for (int col = 0; col < ui->statsTreeWidget->columnCount(); col++) {
        ui->statsTreeWidget->resizeColumnToContents(col);
    }
}

void TapParameterDialog::contextMenuEvent(QContextMenuEvent *event)
{
    bool enable = filterExpression().length() > 0 ? true : false;

    foreach (QAction *fa, filter_actions_) {
        fa->setEnabled(enable);
    }

    ctx_menu_.exec(event->globalPos());
}

void TapParameterDialog::addFilterActions()
{
    QMenu *submenu;
    QAction *insert_action = ctx_menu_.actions().first();

    FilterAction::Action cur_action = FilterAction::ActionApply;
    submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
    foreach (FilterAction::ActionType at, FilterAction::actionTypes()) {
        FilterAction *fa = new FilterAction(submenu, cur_action, at);
        submenu->addAction(fa);
        connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
        filter_actions_ << fa;
    }
    ctx_menu_.insertMenu(insert_action, submenu);

    cur_action = FilterAction::ActionPrepare;
    submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
    foreach (FilterAction::ActionType at, FilterAction::actionTypes()) {
        FilterAction *fa = new FilterAction(submenu, cur_action, at);
        submenu->addAction(fa);
        connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
        filter_actions_ << fa;
    }
    ctx_menu_.insertMenu(insert_action, submenu);

    cur_action = FilterAction::ActionFind;
    submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
    foreach (FilterAction::ActionType at, FilterAction::actionTypes(cur_action)) {
        FilterAction *fa = new FilterAction(submenu, cur_action, at);
        submenu->addAction(fa);
        connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
        filter_actions_ << fa;
    }
    ctx_menu_.insertMenu(insert_action, submenu);

    cur_action = FilterAction::ActionColorize;
    submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
    foreach (FilterAction::ActionType at, FilterAction::actionTypes(cur_action)) {
        FilterAction *fa = new FilterAction(submenu, cur_action, at);
        submenu->addAction(fa);
        connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
        filter_actions_ << fa;
    }
    ctx_menu_.insertMenu(insert_action, submenu);
    ctx_menu_.insertSeparator(insert_action);
}

void TapParameterDialog::updateWidgets()
{
    bool edit_enable = true;
    bool apply_enable = true;

    if (file_closed_) {
        edit_enable = false;
        apply_enable = false;
    } else if (!ui->displayFilterLineEdit->checkFilter()) {
        // XXX Tell the user why the filter is invalid.
        apply_enable = false;
    }
    ui->displayFilterLineEdit->setEnabled(edit_enable);
    ui->applyFilterButton->setEnabled(apply_enable);

    WiresharkDialog::updateWidgets();
}

void TapParameterDialog::on_applyFilterButton_clicked()
{
    beginRetapPackets();
    if (!ui->displayFilterLineEdit->checkFilter())
        return;

    QString filter = ui->displayFilterLineEdit->text();
    emit updateFilter(filter);
    // If we wanted to be fancy we could add an isRetapping function to
    // either WiresharkDialog or CaptureFile and use it in updateWidgets
    // to enable and disable the apply button as needed.
    // For now we use more simple but less useful logic.
    bool df_enabled = ui->displayFilterLineEdit->isEnabled();
    bool af_enabled = ui->applyFilterButton->isEnabled();
    ui->displayFilterLineEdit->setEnabled(false);
    ui->applyFilterButton->setEnabled(false);
    fillTree();
    ui->applyFilterButton->setEnabled(af_enabled);
    ui->displayFilterLineEdit->setEnabled(df_enabled);
    endRetapPackets();
}

void TapParameterDialog::on_actionCopyToClipboard_triggered()
{
    wsApp->clipboard()->setText(getTreeAsString(ST_FORMAT_PLAIN));
}

void TapParameterDialog::on_actionSaveAs_triggered()
{
    QString selectedFilter;
    st_format_type format;
    const char *file_ext;
    FILE *f;
    bool success = false;
    int last_errno;

#ifdef Q_OS_WIN
    HANDLE da_ctx = set_thread_per_monitor_v2_awareness();
#endif
    QFileDialog SaveAsDialog(this, wsApp->windowTitleString(tr("Save Statistics As" UTF8_HORIZONTAL_ELLIPSIS)),
                                                            get_last_open_dir());
    SaveAsDialog.setNameFilter(tr("Plain text file (*.txt);;"
                                    "Comma separated values (*.csv);;"
                                    "XML document (*.xml);;"
                                    "YAML document (*.yaml)"));
    SaveAsDialog.selectNameFilter(tr("Plain text file (*.txt)"));
    SaveAsDialog.setAcceptMode(QFileDialog::AcceptSave);
    int result = SaveAsDialog.exec();
#ifdef Q_OS_WIN
    revert_thread_per_monitor_v2_awareness(da_ctx);
#endif
    if (!result) {
        return;
    }
    selectedFilter= SaveAsDialog.selectedNameFilter();
    if (selectedFilter.contains("*.yaml", Qt::CaseInsensitive)) {
        format = ST_FORMAT_YAML;
        file_ext = ".yaml";
    }
    else if (selectedFilter.contains("*.xml", Qt::CaseInsensitive)) {
        format = ST_FORMAT_XML;
        file_ext = ".xml";
    }
    else if (selectedFilter.contains("*.csv", Qt::CaseInsensitive)) {
        format = ST_FORMAT_CSV;
        file_ext = ".csv";
    }
    else {
        format = ST_FORMAT_PLAIN;
        file_ext = ".txt";
    }

    // Get selected filename and add extension of necessary
    QString file_name = SaveAsDialog.selectedFiles()[0];
    if (!file_name.endsWith(file_ext, Qt::CaseInsensitive)) {
        file_name.append(file_ext);
    }

    QByteArray tree_as_ba = getTreeAsString(format);

    // actually save the file
    f = ws_fopen (file_name.toUtf8().constData(), "w");
    last_errno = errno;
    if (f) {
        if (fputs(tree_as_ba.data(), f) != EOF) {
            success = true;
        }
        last_errno = errno;
        fclose(f);
    }
    if (!success) {
        QMessageBox::warning(this, tr("Error saving file %1").arg(file_name),
                             g_strerror (last_errno));
    }
}

void TapParameterDialog::on_buttonBox_helpRequested()
{
    if (help_topic_ > 0) {
        wsApp->helpTopicAction((topic_action_e) help_topic_);
    }
}

/*
 * Editor modelines
 *
 * Local Variables:
 * c-basic-offset: 4
 * tab-width: 8
 * indent-tabs-mode: nil
 * End:
 *
 * ex: set shiftwidth=4 tabstop=8 expandtab:
 * :indentSize=4:tabSize=8:noTabs=true:
 */
