/* conversation_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 "conversation_dialog.h"

#include <epan/prefs.h>
#include <epan/dissectors/packet-tcp.h>

#include "ui/recent.h"
#include "ui/tap-tcp-stream.h"
#include "ui/traffic_table_ui.h"

#include "wsutil/str_util.h"

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

#include <QCheckBox>
#include <QDateTime>
#include <QDialogButtonBox>
#include <QPushButton>

// To do:
// - https://gitlab.com/wireshark/wireshark/-/issues/6727
//   - Wide last column?
//   + No arrows on unsorted columns
//   - Add follow stream to context menu
//   + Change "A <- B" to "B -> A"
// - Improper wildcard handling https://gitlab.com/wireshark/wireshark/-/issues/8010
// - TShark consolidation https://gitlab.com/wireshark/wireshark/-/issues/6310
// - Display filter entry?
// - Add follow, copy & graph actions to context menu.

// Bugs:
// - Slow for large numbers of items.
// - Name resolution doesn't do anything if its preference is disabled.
// - Columns don't resize correctly.
// - Closing the capture file clears conversation data.

// Fixed bugs:
// - Friendly unit displays https://gitlab.com/wireshark/wireshark/-/issues/9231
// - Misleading bps calculation https://gitlab.com/wireshark/wireshark/-/issues/8703
// - Show Absolute time in conversation tables https://gitlab.com/wireshark/wireshark/-/issues/11618
// - The value of 'Rel start' and 'Duration' in "Conversations" no need too precise https://gitlab.com/wireshark/wireshark/-/issues/12803


static const QString table_name_ = QObject::tr("Conversation");
ConversationDialog::ConversationDialog(QWidget &parent, CaptureFile &cf, int cli_proto_id, const char *filter) :
    TrafficTableDialog(parent, cf, filter, table_name_),
    tcp_graph_requested_(false)
{
    follow_bt_ = buttonBox()->addButton(tr("Follow Stream…"), QDialogButtonBox::ActionRole);
    follow_bt_->setToolTip(tr("Follow a TCP or UDP stream."));
    connect(follow_bt_, SIGNAL(clicked()), this, SLOT(followStream()));

    graph_bt_ = buttonBox()->addButton(tr("Graph…"), QDialogButtonBox::ActionRole);
    graph_bt_->setToolTip(tr("Graph a TCP conversation."));
    connect(graph_bt_, SIGNAL(clicked()), this, SLOT(graphTcp()));

    connect(wsApp->mainWindow(), SIGNAL(displayFilterSuccess(bool)),
            this, SLOT(displayFilterSuccess(bool)));

    absoluteTimeCheckBox()->show();

    addProgressFrame(&parent);

    QList<int> conv_protos;
    for (GList *conv_tab = recent.conversation_tabs; conv_tab; conv_tab = conv_tab->next) {
        int proto_id = proto_get_id_by_short_name((const char *)conv_tab->data);
        if (proto_id > -1 && !conv_protos.contains(proto_id)) {
            conv_protos.append(proto_id);
        }
    }

    if (conv_protos.isEmpty()) {
        conv_protos = defaultProtos();
    }

    // Bring the command-line specified type to the front.
    if ((cli_proto_id > 0) && (get_conversation_by_proto_id(cli_proto_id))) {
        conv_protos.removeAll(cli_proto_id);
        conv_protos.prepend(cli_proto_id);
    }

    // QTabWidget selects the first item by default.
    foreach (int conv_proto, conv_protos) {
        addTrafficTable(get_conversation_by_proto_id(conv_proto));
    }

    fillTypeMenu(conv_protos);

    QPushButton *close_bt = buttonBox()->button(QDialogButtonBox::Close);
    if (close_bt) {
        close_bt->setDefault(true);
    }

    updateWidgets();
//    currentTabChanged();

    cap_file_.delayedRetapPackets();
}

ConversationDialog::~ConversationDialog()
{
    prefs_clear_string_list(recent.conversation_tabs);
    recent.conversation_tabs = NULL;

    ConversationTreeWidget *cur_tree = qobject_cast<ConversationTreeWidget *>(trafficTableTabWidget()->currentWidget());
    foreach (QAction *ca, traffic_type_menu_.actions()) {
        int proto_id = ca->data().value<int>();
        if (proto_id_to_tree_.contains(proto_id) && ca->isChecked()) {
            char *title = g_strdup(proto_get_protocol_short_name(find_protocol_by_id(proto_id)));
            if (proto_id_to_tree_[proto_id] == cur_tree) {
                recent.conversation_tabs = g_list_prepend(recent.conversation_tabs, title);
            } else {
                recent.conversation_tabs = g_list_append(recent.conversation_tabs, title);
            }
        }
    }
}

void ConversationDialog::captureFileClosing()
{
    // Keep the dialog around but disable any controls that depend
    // on a live capture file.
    for (int i = 0; i < trafficTableTabWidget()->count(); i++) {
        ConversationTreeWidget *cur_tree = qobject_cast<ConversationTreeWidget *>(trafficTableTabWidget()->widget(i));
        disconnect(cur_tree, SIGNAL(filterAction(QString,FilterAction::Action,FilterAction::ActionType)),
                   this, SIGNAL(filterAction(QString,FilterAction::Action,FilterAction::ActionType)));
    }
    displayFilterCheckBox()->setEnabled(false);
    enabledTypesPushButton()->setEnabled(false);
    follow_bt_->setEnabled(false);
    graph_bt_->setEnabled(false);
    TrafficTableDialog::captureFileClosing();
}

bool ConversationDialog::addTrafficTable(register_ct_t* table)
{
    int proto_id = get_conversation_proto_id(table);

    if (!table || proto_id_to_tree_.contains(proto_id)) {
        return false;
    }

    ConversationTreeWidget *conv_tree = new ConversationTreeWidget(this, table);

    proto_id_to_tree_[proto_id] = conv_tree;
    const char* table_name = proto_get_protocol_short_name(find_protocol_by_id(proto_id));

    trafficTableTabWidget()->addTab(conv_tree, table_name);

    conv_tree->setItemDelegateForColumn(CONV_COLUMN_START, new TimelineDelegate(conv_tree));
    conv_tree->setItemDelegateForColumn(CONV_COLUMN_DURATION, new TimelineDelegate(conv_tree));

    connect(conv_tree, SIGNAL(itemSelectionChanged()),
            this, SLOT(conversationSelectionChanged()));
    connect(conv_tree, SIGNAL(titleChanged(QWidget*,QString)),
            this, SLOT(setTabText(QWidget*,QString)));
    connect(conv_tree, SIGNAL(filterAction(QString,FilterAction::Action,FilterAction::ActionType)),
            this, SIGNAL(filterAction(QString,FilterAction::Action,FilterAction::ActionType)));
    connect(nameResolutionCheckBox(), SIGNAL(toggled(bool)),
            conv_tree, SLOT(setNameResolutionEnabled(bool)));
    connect(absoluteTimeCheckBox(), SIGNAL(toggled(bool)),
            conv_tree, SLOT(updateStartTime(bool)));


    // XXX Move to ConversationTreeWidget ctor?
    QByteArray filter_utf8;
    const char *filter = NULL;
    if (displayFilterCheckBox()->isChecked()) {
        filter = cap_file_.capFile()->dfilter;
    } else if (!filter_.isEmpty()) {
        filter_utf8 = filter_.toUtf8();
        filter = filter_utf8.constData();
    }

    conv_tree->trafficTreeHash()->user_data = conv_tree;

    registerTapListener(proto_get_protocol_filter_name(proto_id), conv_tree->trafficTreeHash(), filter, 0,
                        ConversationTreeWidget::tapReset,
                        get_conversation_packet_func(table),
                        ConversationTreeWidget::tapDraw);

    return true;
}

void ConversationDialog::followStream()
{
    if (file_closed_) {
        return;
    }

    conv_item_t *conv_item = currentConversation();
    if (!conv_item) {
        return;
    }

    follow_type_t ftype;
    switch (conv_item->etype) {
    case ENDPOINT_TCP:
        ftype = FOLLOW_TCP;
        break;
    case ENDPOINT_UDP:
        ftype = FOLLOW_UDP;
        break;
    default:
        return;
    }

    // Will set the display filter too.
    // TCP and UDP do not have a "sub-stream", so set a dummy value.
    emit openFollowStreamDialog(ftype, conv_item->conv_id, 0);
}

void ConversationDialog::graphTcp()
{
    if (file_closed_) {
        return;
    }

    conv_item_t *conv_item = currentConversation();
    if (!conv_item) {
        return;
    }

    // XXX The GTK+ code opens the TCP Stream dialog. We might want
    // to open the I/O Graphs dialog instead.
    QString filter;
    if (conv_item->etype == ENDPOINT_TCP) {
        filter = QString("tcp.stream eq %1").arg(conv_item->conv_id);
    } else {
        return;
    }

    tcp_graph_requested_ = true;
    // Apply the filter for this conversation. When the filter is active, we
    // can draw the TCP graph.
    emit filterAction(filter, FilterAction::ActionApply, FilterAction::ActionTypePlain);
}

void ConversationDialog::currentTabChanged()
{
    bool copy_enable = trafficTableTabWidget()->currentWidget() ? true : false;

    copy_bt_->setEnabled(copy_enable);

    conversationSelectionChanged();
    TrafficTableDialog::currentTabChanged();
}

void ConversationDialog::conversationSelectionChanged()
{
    bool follow_enable = false, graph_enable = false;
    conv_item_t *conv_item = currentConversation();

    if (!file_closed_ && conv_item) {
        switch (conv_item->etype) {
        case ENDPOINT_TCP:
            graph_enable = true;
            // Fall through
        case ENDPOINT_UDP:
            follow_enable = true;
            break;
        default:
            break;
        }
    }
    follow_bt_->setEnabled(follow_enable);
    graph_bt_->setEnabled(graph_enable);
}

void ConversationDialog::on_displayFilterCheckBox_toggled(bool checked)
{
    if (file_closed_) {
        return;
    }

    QByteArray filter_utf8;
    const char *filter = NULL;
    if (checked) {
        filter = cap_file_.capFile()->dfilter;
    } else if (!filter_.isEmpty()) {
        filter_utf8 = filter_.toUtf8();
        filter = filter_utf8.constData();
    }

    for (int i = 0; i < trafficTableTabWidget()->count(); i++) {
        set_tap_dfilter(trafficTableTabWidget()->widget(i), filter);
    }

    cap_file_.retapPackets();
}

void ConversationDialog::on_buttonBox_helpRequested()
{
    wsApp->helpTopicAction(HELP_STATS_CONVERSATIONS_DIALOG);
}

void ConversationDialog::displayFilterSuccess(bool success)
{
    if (tcp_graph_requested_) {
        if (success) {
            // The display filter was applied successfully, i.e. the current
            // packet is now part of our selected tcp conversation.
            openTcpStreamGraph(GRAPH_TSEQ_TCPTRACE);
        }
        tcp_graph_requested_ = false;
    }
}

void init_conversation_table(struct register_ct* ct, const char *filter)
{
    wsApp->emitStatCommandSignal("Conversations", filter, GINT_TO_POINTER(get_conversation_proto_id(ct)));
}


// ConversationTreeWidgetItem
// TrafficTableTreeWidgetItem / QTreeWidgetItem subclass that allows sorting

// Minimum bandwidth calculation duration
// https://gitlab.com/wireshark/wireshark/-/issues/8703
static const double min_bw_calc_duration_ = 5 / 1000.0; // seconds
static const char *bps_na_ = UTF8_EM_DASH;

class ConversationTreeWidgetItem : public TrafficTableTreeWidgetItem
{
public:
    ConversationTreeWidgetItem(GArray *conv_array, guint conv_idx, bool *resolve_names_ptr) :
        TrafficTableTreeWidgetItem(NULL),
        conv_array_(conv_array),
        conv_idx_(conv_idx),
        resolve_names_ptr_(resolve_names_ptr)
    {
        QString timeline_tt = QObject::tr("Bars show the relative timeline for each conversation.");
        setToolTip(CONV_COLUMN_START, timeline_tt);
        setToolTip(CONV_COLUMN_DURATION, timeline_tt);
    }

    conv_item_t *convItem() {
        return &g_array_index(conv_array_, conv_item_t, conv_idx_);
    }

    virtual QVariant data(int column, int role) const {
        switch (role) {
        case Qt::DisplayRole:
        {
            // Column text cooked representation.
            conv_item_t *conv_item = &g_array_index(conv_array_, conv_item_t, conv_idx_);

            bool resolve_names = false;
            if (resolve_names_ptr_ && *resolve_names_ptr_) resolve_names = true;
            double duration = nstime_to_sec(&conv_item->stop_time) - nstime_to_sec(&conv_item->start_time);
            QString bps_ab = bps_na_, bps_ba = bps_na_;

            switch (column) {
            case CONV_COLUMN_PACKETS:
                return QString("%L1").arg(conv_item->tx_frames + conv_item->rx_frames);
            case CONV_COLUMN_BYTES:
                return gchar_free_to_qstring(format_size(conv_item->tx_bytes + conv_item->rx_bytes, format_size_unit_none|format_size_prefix_si));
            case CONV_COLUMN_PKT_AB:
                return QString("%L1").arg(conv_item->tx_frames);
            case CONV_COLUMN_BYTES_AB:
                return gchar_free_to_qstring(format_size(conv_item->tx_bytes, format_size_unit_none|format_size_prefix_si));
            case CONV_COLUMN_PKT_BA:
                return QString("%L1").arg(conv_item->rx_frames);
            case CONV_COLUMN_BYTES_BA:
                return gchar_free_to_qstring(format_size(conv_item->rx_bytes, format_size_unit_none|format_size_prefix_si));
            case CONV_COLUMN_START:
            {
                bool use_ns = treeWidget()->window()->property("nanosecond_precision").toBool();
                int width = use_ns ? 9 : 6;

                if (treeWidget()->window()->property("absolute_start_time").toBool()) {
                    nstime_t *abs_time = &conv_item->start_abs_time;
                    QDateTime abs_dt = QDateTime::fromMSecsSinceEpoch(nstime_to_msec(abs_time));
                    return QString("%1.%2")
                            // Mimic column-utils:set_abs_time as best we can
                            .arg(abs_dt.toString("hh:mm:ss"))
                            .arg(use_ns ? abs_time->nsecs : abs_time->nsecs / 1000, width, 10, QChar('0'));
                }

                return QString::number(nstime_to_sec(&conv_item->start_time), 'f', width);
            }
            case CONV_COLUMN_DURATION:
            {
                // The GTK+ UI uses 9 digit precision for the start time and 4 for the duration.
                // Do the same here and above for non-nanosecond precision and add a couple
                // of digits for nanosecond precision.
                bool use_ns = treeWidget()->window()->property("nanosecond_precision").toBool();
                int width = use_ns ? 6 : 4;

                return QString::number(duration, 'f', width);
            }
            case CONV_COLUMN_BPS_AB:
                if (duration > min_bw_calc_duration_) {
                    bps_ab = gchar_free_to_qstring(format_size((gint64) conv_item->tx_bytes * 8 / duration, format_size_unit_none|format_size_prefix_si));
                }
                return bps_ab;
            case CONV_COLUMN_BPS_BA:
                if (duration > min_bw_calc_duration_) {
                    bps_ba = gchar_free_to_qstring(format_size((gint64) conv_item->rx_bytes * 8 / duration, format_size_unit_none|format_size_prefix_si));
                }
                return bps_ba;
            default:
                return colData(column, resolve_names).toString();
            }
            break;
        }
        case Qt::UserRole:
        {
            if (column != CONV_COLUMN_START && column != CONV_COLUMN_DURATION) break;

            ConversationTreeWidget *ctw = qobject_cast<ConversationTreeWidget *>(treeWidget());
            if (!ctw) break;

            conv_item_t *conv_item = &g_array_index(conv_array_, conv_item_t, conv_idx_);
            double start_time = nstime_to_sec(&conv_item->start_time);
            double stop_time = nstime_to_sec(&conv_item->stop_time);

            double span_s = ctw->maxRelStopTime() - ctw->minRelStartTime();
            if (span_s <= 0) break;
            int start_px = ctw->columnWidth(CONV_COLUMN_START);
            int column_px = start_px + ctw->columnWidth(CONV_COLUMN_DURATION);

            struct timeline_span span_px;
            span_px.start = ((start_time - ctw->minRelStartTime()) * column_px) / span_s;
            span_px.width = ((stop_time - start_time) * column_px) / span_s;

            if (column == CONV_COLUMN_DURATION) {
                span_px.start -= start_px;
            }
            return QVariant::fromValue(span_px);

            break;
        }
        default:
            break;
        }
        return QTreeWidgetItem::data(column, role);
    }

    // Column text raw representation.
    // Return a QString, qulonglong, double, or invalid QVariant representing the raw column data.
    QVariant colData(int col, bool resolve_names) const {
        conv_item_t *conv_item = &g_array_index(conv_array_, conv_item_t, conv_idx_);

        double duration = nstime_to_sec(&conv_item->stop_time) - nstime_to_sec(&conv_item->start_time);
        double bps_ab = 0, bps_ba = 0;
        if (duration > min_bw_calc_duration_) {
            bps_ab = conv_item->tx_bytes * 8 / duration;
            bps_ba = conv_item->rx_bytes * 8 / duration;
        }

        switch (col) {
        case CONV_COLUMN_SRC_ADDR:
            {
            char* addr_str = get_conversation_address(NULL, &conv_item->src_address, resolve_names);
            QString q_addr_str(addr_str);
            wmem_free(NULL, addr_str);
            return q_addr_str;
            }
        case CONV_COLUMN_SRC_PORT:
            if (resolve_names) {
                char* port_str = get_conversation_port(NULL, conv_item->src_port, conv_item->etype, resolve_names);
                QString q_port_str(port_str);
                wmem_free(NULL, port_str);
                return q_port_str;
            } else {
                return quint32(conv_item->src_port);
            }
        case CONV_COLUMN_DST_ADDR:
            {
            char* addr_str = get_conversation_address(NULL, &conv_item->dst_address, resolve_names);
            QString q_addr_str(addr_str);
            wmem_free(NULL, addr_str);
            return q_addr_str;
            }
        case CONV_COLUMN_DST_PORT:
            if (resolve_names) {
                char* port_str = get_conversation_port(NULL, conv_item->dst_port, conv_item->etype, resolve_names);
                QString q_port_str(port_str);
                wmem_free(NULL, port_str);
                return q_port_str;
            } else {
                return quint32(conv_item->dst_port);
            }
        case CONV_COLUMN_PACKETS:
            return quint64(conv_item->tx_frames + conv_item->rx_frames);
        case CONV_COLUMN_BYTES:
            return quint64(conv_item->tx_bytes + conv_item->rx_bytes);
        case CONV_COLUMN_PKT_AB:
            return quint64(conv_item->tx_frames);
        case CONV_COLUMN_BYTES_AB:
            return quint64(conv_item->tx_bytes);
        case CONV_COLUMN_PKT_BA:
            return quint64(conv_item->rx_frames);
        case CONV_COLUMN_BYTES_BA:
            return quint64(conv_item->rx_bytes);
        case CONV_COLUMN_START:
            return nstime_to_sec(&conv_item->start_time);
        case CONV_COLUMN_DURATION:
            return duration;
        case CONV_COLUMN_BPS_AB:
            return bps_ab;
        case CONV_COLUMN_BPS_BA:
            return bps_ba;
        default:
            return QVariant();
        }
    }

    bool operator< (const QTreeWidgetItem &other) const
    {
        const ConversationTreeWidgetItem *other_row = static_cast<const ConversationTreeWidgetItem *>(&other);
        conv_item_t *conv_item = &g_array_index(conv_array_, conv_item_t, conv_idx_);
        conv_item_t *other_item = &g_array_index(other_row->conv_array_, conv_item_t, other_row->conv_idx_);

        int sort_col = treeWidget()->sortColumn();
        double conv_duration = nstime_to_sec(&conv_item->stop_time) - nstime_to_sec(&conv_item->start_time);
        double other_duration = nstime_to_sec(&other_item->stop_time) - nstime_to_sec(&other_item->start_time);

        switch(sort_col) {
        case CONV_COLUMN_SRC_ADDR:
            return cmp_address(&conv_item->src_address, &other_item->src_address) < 0 ? true : false;
        case CONV_COLUMN_SRC_PORT:
            return conv_item->src_port < other_item->src_port;
        case CONV_COLUMN_DST_ADDR:
            return cmp_address(&conv_item->dst_address, &other_item->dst_address) < 0 ? true : false;
        case CONV_COLUMN_DST_PORT:
            return conv_item->dst_port < other_item->dst_port;
        case CONV_COLUMN_PACKETS:
            return (conv_item->tx_frames + conv_item->rx_frames) < (other_item->tx_frames + other_item->rx_frames);
        case CONV_COLUMN_BYTES:
            return (conv_item->tx_bytes + conv_item->rx_bytes) < (other_item->tx_bytes + other_item->rx_bytes);
        case CONV_COLUMN_PKT_AB:
            return conv_item->tx_frames < other_item->tx_frames;
        case CONV_COLUMN_BYTES_AB:
            return conv_item->tx_bytes < other_item->tx_bytes;
        case CONV_COLUMN_PKT_BA:
            return conv_item->rx_frames < other_item->rx_frames;
        case CONV_COLUMN_BYTES_BA:
            return conv_item->rx_bytes < other_item->rx_bytes;
        case CONV_COLUMN_START:
            return nstime_to_sec(&conv_item->start_time) < nstime_to_sec(&other_item->start_time);
        case CONV_COLUMN_DURATION:
            return conv_duration < other_duration;
        case CONV_COLUMN_BPS_AB:
            return conv_item->tx_bytes / conv_duration < other_item->tx_bytes / other_duration;
        case CONV_COLUMN_BPS_BA:
            return conv_item->rx_bytes / conv_duration < other_item->rx_bytes / other_duration;
        default:
            return false;
        }
    }
private:
    GArray *conv_array_;
    guint conv_idx_;
    bool *resolve_names_ptr_;
};

conv_item_t *ConversationDialog::currentConversation()
{
    ConversationTreeWidget *cur_tree = qobject_cast<ConversationTreeWidget *>(trafficTableTabWidget()->currentWidget());

    if (!cur_tree || cur_tree->selectedItems().count() < 1) {
        return NULL;
    }

    ConversationTreeWidgetItem *ctwi = dynamic_cast<ConversationTreeWidgetItem *>(cur_tree->selectedItems()[0]);
    return (ctwi ? ctwi->convItem() : NULL);
}

// ConversationTreeWidget
// TrafficTableTreeWidget / QTreeWidget subclass that allows tapping

ConversationTreeWidget::ConversationTreeWidget(QWidget *parent, register_ct_t* table) :
    TrafficTableTreeWidget(parent, table),
    min_rel_start_time_(0),
    max_rel_stop_time_(0)
{
    setColumnCount(CONV_NUM_COLUMNS);
    setUniformRowHeights(true);

    for (int i = 0; i < CONV_NUM_COLUMNS; i++) {
        headerItem()->setText(i, conv_column_titles[i]);
    }

    if (get_conversation_hide_ports(table_)) {
        hideColumn(CONV_COLUMN_SRC_PORT);
        hideColumn(CONV_COLUMN_DST_PORT);
    } else if (!strcmp(proto_get_protocol_filter_name(get_conversation_proto_id(table_)), "ncp")) {
        headerItem()->setText(CONV_COLUMN_SRC_PORT, conv_conn_a_title);
        headerItem()->setText(CONV_COLUMN_DST_PORT, conv_conn_b_title);
    }

    int one_en = fontMetrics().height() / 2;
    for (int i = 0; i < CONV_NUM_COLUMNS; i++) {
        switch (i) {
        case CONV_COLUMN_SRC_ADDR:
        case CONV_COLUMN_DST_ADDR:
            setColumnWidth(i, one_en * (int) strlen("000.000.000.000"));
            break;
        case CONV_COLUMN_SRC_PORT:
        case CONV_COLUMN_DST_PORT:
            setColumnWidth(i, one_en * (int) strlen("000000"));
            break;
        case CONV_COLUMN_PACKETS:
        case CONV_COLUMN_PKT_AB:
        case CONV_COLUMN_PKT_BA:
            setColumnWidth(i, one_en * (int) strlen("00,000"));
            break;
        case CONV_COLUMN_BYTES:
        case CONV_COLUMN_BYTES_AB:
        case CONV_COLUMN_BYTES_BA:
            setColumnWidth(i, one_en * (int) strlen("000,000"));
            break;
        case CONV_COLUMN_START:
            setColumnWidth(i, one_en * (int) strlen("00.000"));
            break;
        case CONV_COLUMN_DURATION:
            setColumnWidth(i, one_en * (int) strlen("00.000000"));
            break;
        case CONV_COLUMN_BPS_AB:
        case CONV_COLUMN_BPS_BA:
            setColumnWidth(i, one_en * (int) strlen("000 k"));
            break;
        default:
            setColumnWidth(i, one_en * 5);
        }
    }

    QMenu *submenu;

    initDirectionMap();

    FilterAction::Action cur_action = FilterAction::ActionApply;
    submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
    foreach (FilterAction::ActionType at, FilterAction::actionTypes()) {
        QMenu *subsubmenu = submenu->addMenu(FilterAction::actionTypeName(at));
        foreach (FilterAction::ActionDirection ad, FilterAction::actionDirections()) {
            FilterAction *fa = new FilterAction(subsubmenu, cur_action, at, ad);
            subsubmenu->addAction(fa);
            connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
        }
    }

    cur_action = FilterAction::ActionPrepare;
    submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
    foreach (FilterAction::ActionType at, FilterAction::actionTypes()) {
        QMenu *subsubmenu = submenu->addMenu(FilterAction::actionTypeName(at));
        foreach (FilterAction::ActionDirection ad, FilterAction::actionDirections()) {
            FilterAction *fa = new FilterAction(subsubmenu, cur_action, at, ad);
            subsubmenu->addAction(fa);
            connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
        }
    }

    cur_action = FilterAction::ActionFind;
    submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
    foreach (FilterAction::ActionDirection ad, FilterAction::actionDirections()) {
        FilterAction *fa = new FilterAction(submenu, cur_action, FilterAction::ActionTypePlain, ad);
        submenu->addAction(fa);
        connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
    }

    cur_action = FilterAction::ActionColorize;
    submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
    foreach (FilterAction::ActionDirection ad, FilterAction::actionDirections()) {
        FilterAction *fa = new FilterAction(submenu, cur_action, FilterAction::ActionTypePlain, ad);
        submenu->addAction(fa);
        connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
    }

    updateItems();
}

ConversationTreeWidget::~ConversationTreeWidget() {
    reset_conversation_table_data(&hash_);
}

// Callbacks for register_tap_listener
void ConversationTreeWidget::tapReset(void *conv_hash_ptr)
{
    conv_hash_t *hash = (conv_hash_t*)conv_hash_ptr;
    ConversationTreeWidget *conv_tree = qobject_cast<ConversationTreeWidget *>((ConversationTreeWidget *)hash->user_data);
    if (!conv_tree) return;

    conv_tree->clear();
    reset_conversation_table_data(&conv_tree->hash_);
    conv_tree->min_rel_start_time_ = 0;
    conv_tree->max_rel_stop_time_ = 0;
}

void ConversationTreeWidget::tapDraw(void *conv_hash_ptr)
{
    conv_hash_t *hash = (conv_hash_t*)conv_hash_ptr;
    ConversationTreeWidget *conv_tree = qobject_cast<ConversationTreeWidget *>((ConversationTreeWidget *)hash->user_data);
    if (!conv_tree) return;

    conv_tree->updateItems();
}

void ConversationTreeWidget::updateStartTime(bool absolute)
{
    headerItem()->setText(CONV_COLUMN_START, absolute
                          ? conv_abs_start_title
                          : conv_column_titles[CONV_COLUMN_START]);

    dataChanged(QModelIndex(), QModelIndex());

    if (topLevelItemCount() > 0) {
        resizeColumnToContents(CONV_COLUMN_START);
    }
}

QMap<FilterAction::ActionDirection, conv_direction_e> fad_to_cd_;

void ConversationTreeWidget::initDirectionMap()
{
    if (fad_to_cd_.size() > 0) {
        return;
    }

    fad_to_cd_[FilterAction::ActionDirectionAToFromB] = CONV_DIR_A_TO_FROM_B;
    fad_to_cd_[FilterAction::ActionDirectionAToB] = CONV_DIR_A_TO_B;
    fad_to_cd_[FilterAction::ActionDirectionAFromB] = CONV_DIR_A_FROM_B;
    fad_to_cd_[FilterAction::ActionDirectionAToFromAny] = CONV_DIR_A_TO_FROM_ANY;
    fad_to_cd_[FilterAction::ActionDirectionAToAny] = CONV_DIR_A_TO_ANY;
    fad_to_cd_[FilterAction::ActionDirectionAFromAny] = CONV_DIR_A_FROM_ANY;
    fad_to_cd_[FilterAction::ActionDirectionAnyToFromB] = CONV_DIR_ANY_TO_FROM_B;
    fad_to_cd_[FilterAction::ActionDirectionAnyToB] = CONV_DIR_ANY_TO_B;
    fad_to_cd_[FilterAction::ActionDirectionAnyFromB] = CONV_DIR_ANY_FROM_B;
}

void ConversationTreeWidget::updateItems() {
    bool resize = topLevelItemCount() < resizeThreshold();
    title_ = proto_get_protocol_short_name(find_protocol_by_id(get_conversation_proto_id(table_)));

    if (hash_.conv_array && hash_.conv_array->len > 0) {
        title_.append(QString(" %1 %2").arg(UTF8_MIDDLE_DOT).arg(hash_.conv_array->len));
    }
    emit titleChanged(this, title_);

    if (!hash_.conv_array) {
        return;
    }

    setSortingEnabled(false);

    QList<QTreeWidgetItem *>new_items;
    for (int i = topLevelItemCount(); i < (int) hash_.conv_array->len; i++) {
        ConversationTreeWidgetItem *ctwi = new ConversationTreeWidgetItem(hash_.conv_array, i, &resolve_names_);
        new_items << ctwi;

        if (i == 0) {
            min_rel_start_time_ = nstime_to_sec(&(ctwi->convItem()->start_time));
            max_rel_stop_time_ = nstime_to_sec(&(ctwi->convItem()->stop_time));
        }

        for (int col = 0; col < columnCount(); col++) {
            switch (col) {
            case CONV_COLUMN_SRC_ADDR:
            case CONV_COLUMN_DST_ADDR:
            break;
            default:
                ctwi->setTextAlignment(col, Qt::AlignRight);
                break;
            }
        }
    }
    addTopLevelItems(new_items);

    for (int i = 0; i < topLevelItemCount(); i++) {
        ConversationTreeWidgetItem *ctwi = dynamic_cast<ConversationTreeWidgetItem *>(topLevelItem(i));

        if (ctwi) {
            double item_rel_start = nstime_to_sec(&(ctwi->convItem()->start_time));
            if (item_rel_start < min_rel_start_time_) {
                min_rel_start_time_ = item_rel_start;
            }

            double item_rel_stop = nstime_to_sec(&(ctwi->convItem()->stop_time));
            if (item_rel_stop > max_rel_stop_time_) {
                max_rel_stop_time_ = item_rel_stop;
            }
        }
    }

    setSortingEnabled(true);

    if (resize) {
        for (int col = 0; col < columnCount(); col++) {
            resizeColumnToContents(col);
        }
    }
}

void ConversationTreeWidget::filterActionTriggered()
{
    ConversationTreeWidgetItem *ctwi = static_cast<ConversationTreeWidgetItem *>(currentItem());
    FilterAction *fa = qobject_cast<FilterAction *>(QObject::sender());

    if (!fa || !ctwi) {
        return;
    }

    conv_item_t *conv_item = ctwi->convItem();
    if (!conv_item) {
        return;
    }

    char* tmp_str = get_conversation_filter(conv_item, fad_to_cd_[fa->actionDirection()]);
    QString filter(tmp_str);

    g_free(tmp_str);
    emit filterAction(filter, fa->action(), fa->actionType());
}

/*
 * 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:
 */
