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

// warning C4267: 'argument' : conversion from 'size_t' to 'int', possible loss of data
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4267)
#endif

#define WS_LOG_DOMAIN  LOG_DOMAIN_MAIN

#include "main_application.h"

#include <algorithm>
#include <errno.h>

#include "wsutil/filesystem.h"

#include "epan/addr_resolv.h"
#include "epan/column-utils.h"
#include "epan/disabled_protos.h"
#include "epan/ftypes/ftypes.h"
#include "epan/prefs.h"
#include "epan/proto.h"
#include "epan/tap.h"
#include "epan/timestamp.h"
#include "epan/decode_as.h"
#include "epan/dfilter/dfilter-macro.h"

#include "ui/commandline.h"
#include "ui/decode_as_utils.h"
#include "ui/preference_utils.h"
#include "ui/iface_lists.h"
#include "ui/language.h"
#include "ui/recent.h"
#include "ui/simple_dialog.h"
#include "ui/util.h"

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

#include "epan/color_filters.h"
#include "recent_file_status.h"

#include "extcap.h"
#ifdef HAVE_LIBPCAP
#include <capture/iface_monitor.h>
#endif

#include "wsutil/filter_files.h"
#include "ui/capture_globals.h"
#include "ui/software_update.h"
#include "ui/file_dialog.h"
#include "ui/recent_utils.h"

#ifdef HAVE_LIBPCAP
#include "ui/capture.h"
#endif

#include "wsutil/utf8_entities.h"

#ifdef _WIN32
#  include "wsutil/file_util.h"
#  include <QMessageBox>
#  include <QSettings>
#endif /* _WIN32 */

#include <ui/qt/capture_file.h>

#include <ui/qt/main_window.h>
#include <ui/qt/main_status_bar.h>

#include <QAction>
#include <QApplication>
#include <QColorDialog>
#include <QDesktopServices>
#include <QDir>
#include <QEvent>
#include <QFileOpenEvent>
#include <QFontInfo>
#include <QFontMetrics>
#include <QLibraryInfo>
#include <QLocale>
#include <QMainWindow>
#include <QMutableListIterator>
#include <QSocketNotifier>
#include <QThreadPool>
#include <QUrl>
#include <qmath.h>

#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
#include <QFontDatabase>
#endif
#include <QMimeDatabase>

#include <QStyleHints>

#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) && defined(Q_OS_WIN)
#include <QStyleFactory>
#endif

#ifdef _MSC_VER
#pragma warning(pop)
#endif

MainApplication *mainApp;

// XXX - Copied from ui/gtk/file_dlg.c

static QList<recent_item_status *> recent_captures_;
static QHash<int, QList<QAction *> > dynamic_menu_groups_;
static QHash<int, QList<QAction *> > added_menu_groups_;
static QHash<int, QList<QAction *> > removed_menu_groups_;

QString MainApplication::window_title_separator_ = QString::fromUtf8(" " UTF8_MIDDLE_DOT " ");

// QMimeDatabase parses a large-ish XML file and can be slow to initialize.
// Do so in a worker thread as early as possible.
// https://github.com/lxde/pcmanfm-qt/issues/415
class MimeDatabaseInitThread : public QRunnable
{
private:
    void run()
    {
        QMimeDatabase mime_db;
        mime_db.mimeTypeForData(QByteArray());
    }
};

#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
// Populating the font database can be slow as well.
class FontDatabaseInitThread : public QRunnable
{
private:
    void run()
    {
        QFontDatabase font_db;
    }
};
#endif

void
topic_action(topic_action_e action)
{
    if (mainApp) mainApp->helpTopicAction(action);
}

/*
 * Add the capture filename to the application-wide "Recent Files" list.
 * Contrary to the name this isn't limited to the "recent" menu.
 */
/*
 * XXX - We might want to call SHAddToRecentDocs under Windows 7:
 * https://stackoverflow.com/questions/437212/how-do-you-register-a-most-recently-used-list-with-windows-in-preparation-for-win
 */
extern "C" void
add_menu_recent_capture_file(const char *cf_name, bool force) {
    QString normalized_cf_name = QString::fromUtf8(cf_name);
    QDir cf_path;

    cf_path.setPath(normalized_cf_name);
    normalized_cf_name = cf_path.absolutePath();
    normalized_cf_name = QDir::cleanPath(normalized_cf_name);
    normalized_cf_name = QDir::toNativeSeparators(normalized_cf_name);

    /* Iterate through the recent items list, removing duplicate entries and every
     * item above count_max
     */
    unsigned int cnt = 1;
    QMutableListIterator<recent_item_status *> rii(recent_captures_);
    while (rii.hasNext()) {
        recent_item_status *ri = rii.next();
        /* if this element string is one of our special items (separator, ...) or
         * already in the list or
         * this element is above maximum count (too old), remove it
         */
        if (ri->filename.length() < 1 ||
#ifdef _WIN32
            /* do a case insensitive compare on win32 */
            ri->filename.compare(normalized_cf_name, Qt::CaseInsensitive) == 0 ||
#else   /* _WIN32 */
            /*
             * Do a case sensitive compare on UN*Xes.
             *
             * XXX - on UN*Xes such as macOS, where you can use pathconf()
             * to check whether a given file system is case-sensitive or
             * not, we should check whether this particular file system
             * is case-sensitive and do the appropriate comparison.
             */
            ri->filename.compare(normalized_cf_name) == 0 ||
#endif
            (!force && cnt >= prefs.gui_recent_files_count_max)) {
            rii.remove();
            delete(ri);
            cnt--;
        }
        cnt++;
    }
    mainApp->addRecentItem(normalized_cf_name, 0, false);
}

/* write all capture filenames of the menu to the user's recent file */
extern "C" void menu_recent_file_write_all(FILE *rf) {

    /* we have to iterate backwards through the children's list,
     * so we get the latest item last in the file.
     */
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    int i = qMin(recent_captures_.size(), (int)(prefs.gui_recent_files_count_max)) - 1;
#else
    qsizetype i = qMin(recent_captures_.size(), (qsizetype)prefs.gui_recent_files_count_max) - 1;
#endif
    for (; i >= 0; i--) {
        recent_item_status *ri = recent_captures_.at(i);
        /* get capture filename from the menu item label */
        QString cf_name = ri->filename;
        if (!cf_name.isNull()) {
            fprintf (rf, RECENT_KEY_CAPTURE_FILE ": %s\n", qUtf8Printable(cf_name));
        }
    }
}

#if defined(HAVE_SOFTWARE_UPDATE) && defined(Q_OS_WIN)
/** Check to see if Wireshark can shut down safely (e.g. offer to save the
 *  current capture).
 */
extern "C" int software_update_can_shutdown_callback(void) {
    return mainApp->softwareUpdateCanShutdown();
}

/** Shut down Wireshark in preparation for an upgrade.
 */
extern "C" void software_update_shutdown_request_callback(void) {
    mainApp->softwareUpdateShutdownRequest();
}
#endif // HAVE_SOFTWARE_UPDATE && Q_OS_WIN

// Check each recent item in a separate thread so that we don't hang while
// calling stat(). This is called periodically because files and entire
// volumes can disappear and reappear at any time.
void MainApplication::refreshRecentCaptures() {
    recent_item_status *ri;
    RecentFileStatus *rf_status;

    // We're in the middle of a capture. Don't create traffic.
    if (active_captures_ > 0) return;

    foreach (ri, recent_captures_) {
        if (ri->in_thread) {
            continue;
        }
        rf_status = new RecentFileStatus(ri->filename, this);
        QThreadPool::globalInstance()->start(rf_status);
    }
}

void MainApplication::refreshPacketData()
{
    if (host_name_lookup_process()) {
        emit addressResolutionChanged();
    } else if (col_data_changed()) {
        emit columnDataChanged();
    }
}

#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) && defined(Q_OS_WIN)
void MainApplication::colorSchemeChanged() {
    if (ColorUtils::themeIsDark()) {
        setStyle(QStyleFactory::create("fusion"));
    } else {
        setStyle(QStyleFactory::create("windowsvista"));
    }
}
#endif

void MainApplication::updateTaps()
{
    draw_tap_listeners(false);
}

QDir MainApplication::openDialogInitialDir() {
    return QDir(get_open_dialog_initial_dir());
}

void MainApplication::setLastOpenDirFromFilename(const QString file_name)
{
    /* XXX - Use canonicalPath() instead of absolutePath()? */
    QString directory = QDir::toNativeSeparators(QFileInfo(file_name).absolutePath());
    /* XXX - printable? */
    set_last_open_dir(qUtf8Printable(directory));
}

void MainApplication::helpTopicAction(topic_action_e action)
{
    QString url = gchar_free_to_qstring(topic_action_url(action));

    if (!url.isEmpty()) {
        QDesktopServices::openUrl(QUrl(QDir::fromNativeSeparators(url)));
    }
}

const QFont MainApplication::monospaceFont(bool zoomed) const
{
    if (zoomed) {
        return zoomed_font_;
    }
    return mono_font_;
}

void MainApplication::setMonospaceFont(const char *font_string) {

    if (font_string && strlen(font_string) > 0) {
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
        // Qt 6's QFont::toString returns a value with 16 or 17 fields, e.g.
        // Consolas,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
        // Corbel,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1,Regular
        // Qt 5's QFont::fromString expects a value with 10 or 11 fields, e.g.
        // Consolas,10,-1,5,50,0,0,0,0,0
        // Corbel,10,-1,5,50,0,0,0,0,0,Regular
        // It looks like Qt6's QFont::fromString can read both forms:
        // https://github.com/qt/qtbase/blob/6.0/src/gui/text/qfont.cpp#L2146
        // but Qt5's cannot:
        // https://github.com/qt/qtbase/blob/5.15/src/gui/text/qfont.cpp#L2151
        const char *fs_ptr = font_string;
        int field_count = 1;
        while ((fs_ptr = strchr(fs_ptr, ',')) != NULL) {
            fs_ptr++;
            field_count++;
        }
        if (field_count <= 11) {
#endif
            mono_font_.fromString(font_string);

            // Only accept the font name if it actually exists.
            if (mono_font_.family() == QFontInfo(mono_font_).family()) {
                return;
            } else {
                ws_warning("Monospace font family %s differs from its fontinfo: %s",
                    qUtf8Printable(mono_font_.family()), qUtf8Printable(QFontInfo(mono_font_).family()));
            }
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
        } else {
            ws_warning("Monospace font %s appears to be from Qt6 and we're running Qt5.", font_string);
        }
#endif
    }

    // https://en.wikipedia.org/wiki/Category:Monospaced_typefaces
    const char *win_default_font = "Consolas";
    const char *win_alt_font = "Lucida Console";
    // SF Mono might be a system font someday. Right now (Oct 2016) it appears
    // to be limited to Xcode and Terminal.
    // http://www.openradar.me/26790072
    // http://www.openradar.me/26862220
    const char *osx_default_font = "SF Mono";
    const QStringList osx_alt_fonts = QStringList() << "Menlo" << "Monaco";
    // XXX Detect Ubuntu systems (e.g. via /etc/os-release and/or
    // /etc/lsb_release) and add "Ubuntu Mono Regular" there.
    // https://design.ubuntu.com/font/
    const char *x11_default_font = "Liberation Mono";
    const QStringList x11_alt_fonts = QStringList() << "DejaVu Sans Mono" << "Bitstream Vera Sans Mono";
    const QStringList fallback_fonts = QStringList() << "Lucida Sans Typewriter" << "Inconsolata" << "Droid Sans Mono" << "Andale Mono" << "Courier New" << "monospace";
    QStringList substitutes;
    int font_size_adjust = 0;

    // Try to pick the latest, shiniest fixed-width font for our OS.
#if defined(Q_OS_WIN)
    const char *default_font = win_default_font;
    substitutes << win_alt_font << osx_default_font << osx_alt_fonts << x11_default_font << x11_alt_fonts << fallback_fonts;
# if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
    font_size_adjust = 1;
# else // QT_VERSION
    font_size_adjust = 2;
# endif // QT_VERSION
#elif defined(Q_OS_MAC)
    const char *default_font = osx_default_font;
    substitutes << osx_alt_fonts << win_default_font << win_alt_font << x11_default_font << x11_alt_fonts << fallback_fonts;
#else // Q_OS
    const char *default_font = x11_default_font;
    substitutes << x11_alt_fonts << win_default_font << win_alt_font << osx_default_font << osx_alt_fonts << fallback_fonts;
#endif // Q_OS

    mono_font_ = QFont(default_font, mainApp->font().pointSize() + font_size_adjust);
    mono_font_.insertSubstitutions(default_font, substitutes);
    mono_font_.setBold(false);

    // Retrieve the effective font and apply it.
    mono_font_.setFamily(QFontInfo(mono_font_).family());

    g_free(prefs.gui_font_name);
    prefs.gui_font_name = qstring_strdup(mono_font_.toString());
}

int MainApplication::monospaceTextSize(const char *str)
{
    return QFontMetrics(mono_font_).horizontalAdvance(str);
}

void MainApplication::setConfigurationProfile(const char *profile_name, bool write_recent_file)
{
    char  *rf_path;
    int    rf_open_errno;
    char *err_msg = NULL;

    bool prev_capture_no_interface_load;
    bool prev_capture_no_extcap;

    /* First check if profile exists */
    if (!profile_exists(profile_name, false)) {
        if (profile_exists(profile_name, true)) {
            char  *pf_dir_path, *pf_dir_path2, *pf_filename;
            /* Copy from global profile */
            if (create_persconffile_profile(profile_name, &pf_dir_path) == -1) {
                simple_dialog(ESD_TYPE_ERROR, ESD_BTN_OK,
                    "Can't create directory\n\"%s\":\n%s.",
                    pf_dir_path, g_strerror(errno));

                g_free(pf_dir_path);
            }

            if (copy_persconffile_profile(profile_name, profile_name, true, &pf_filename,
                    &pf_dir_path, &pf_dir_path2) == -1) {
                simple_dialog(ESD_TYPE_ERROR, ESD_BTN_OK,
                    "Can't copy file \"%s\" in directory\n\"%s\" to\n\"%s\":\n%s.",
                    pf_filename, pf_dir_path2, pf_dir_path, g_strerror(errno));

                g_free(pf_filename);
                g_free(pf_dir_path);
                g_free(pf_dir_path2);
            }
        } else {
            /* No personal and no global profile exists */
            return;
        }
    }

    /* Then check if changing to another profile */
    if (profile_name && strcmp (profile_name, get_profile_name()) == 0) {
        return;
    }

    prev_capture_no_interface_load = prefs.capture_no_interface_load;
    prev_capture_no_extcap = prefs.capture_no_extcap;

    /* Get the current geometry, before writing it to disk */
    emit profileChanging();

    if (write_recent_file && profile_exists(get_profile_name(), false))
    {
        /* Write recent file for profile we are leaving, if it still exists */
        write_profile_recent();
    }

    // Freeze the packet list early to avoid updating column data before doing a
    // full redissection. The packet list will be thawed when redissection is done.
    emit freezePacketList(true);

    /* Set profile name and update the status bar */
    set_profile_name (profile_name);
    emit profileNameChanged(profile_name);

    /* Apply new preferences */
    readConfigurationFiles(true);

    /* Apply command-line preferences */
    commandline_options_reapply();
    extcap_register_preferences();

    /* Switching profile requires reloading the macro list. */
    reloadDisplayFilterMacros();

    if (!recent_read_profile_static(&rf_path, &rf_open_errno)) {
        simple_dialog(ESD_TYPE_WARN, ESD_BTN_OK,
            "Could not open common recent file\n\"%s\": %s.",
            rf_path, g_strerror(rf_open_errno));
        g_free(rf_path);
    }
    if (recent.gui_fileopen_remembered_dir &&
        test_for_directory(recent.gui_fileopen_remembered_dir) == EISDIR) {
        set_last_open_dir(recent.gui_fileopen_remembered_dir);
    }
    timestamp_set_type(recent.gui_time_format);
    timestamp_set_precision(recent.gui_time_precision);
    timestamp_set_seconds_type (recent.gui_seconds_format);

    prefs_to_capture_opts();
    prefs_apply_all();
#ifdef HAVE_LIBPCAP
    update_local_interfaces();
#endif

    emit columnsChanged();
    emit colorsChanged();
    emit preferencesChanged();
    emit recentPreferencesRead();
    emit filterExpressionsChanged();
    emit checkDisplayFilter();
    emit captureFilterListChanged();
    emit displayFilterListChanged();

    /* Reload color filters */
    if (!color_filters_reload(&err_msg, color_filter_add_cb)) {
        simple_dialog(ESD_TYPE_ERROR, ESD_BTN_OK, "%s", err_msg);
        g_free(err_msg);
    }

    /* Load interfaces if settings have changed */
    if (!prefs.capture_no_interface_load &&
        ((prefs.capture_no_interface_load != prev_capture_no_interface_load) ||
         (prefs.capture_no_extcap != prev_capture_no_extcap))) {
        refreshLocalInterfaces();
    }

    emit localInterfaceListChanged();
    emit packetDissectionChanged();

    /* Write recent_common file to ensure last used profile setting is stored. */
    write_recent();
}

void MainApplication::reloadLuaPluginsDelayed()
{
    QTimer::singleShot(0, this, &MainApplication::reloadLuaPlugins);
}

const QIcon &MainApplication::normalIcon()
{
    if (normal_icon_.isNull()) {
        initializeIcons();
    }
    return normal_icon_;
}

const QIcon &MainApplication::captureIcon()
{
    if (capture_icon_.isNull()) {
        initializeIcons();
    }
    return capture_icon_;
}

const QString MainApplication::windowTitleString(QStringList title_parts)
{
    QMutableStringListIterator tii(title_parts);
    while (tii.hasNext()) {
        QString ti = tii.next();
        if (ti.isEmpty()) tii.remove();
    }
    title_parts.prepend(applicationName());
    return title_parts.join(window_title_separator_);
}

void MainApplication::applyCustomColorsFromRecent()
{
    int i = 0;
    bool ok;
    for (GList *custom_color = recent.custom_colors; custom_color; custom_color = custom_color->next) {
        QRgb rgb = QString((const char *)custom_color->data).toUInt(&ok, 16);
        if (ok) {
            QColorDialog::setCustomColor(i++, QColor(rgb));
        }
    }
}

// Return the first top-level MainWindow.
MainWindow *MainApplication::mainWindow()
{
    foreach (QWidget *tlw, topLevelWidgets()) {
        MainWindow *tlmw = qobject_cast<MainWindow *>(tlw);
        if (tlmw && tlmw->isVisible()) {
            return tlmw;
        }
    }
    return nullptr;
}

void MainApplication::storeCustomColorsInRecent()
{
    if (QColorDialog::customCount()) {
        prefs_clear_string_list(recent.custom_colors);
        recent.custom_colors = NULL;
        for (int i = 0; i < QColorDialog::customCount(); i++) {
            QRgb rgb = QColorDialog::customColor(i).rgb();
            recent.custom_colors = g_list_append(recent.custom_colors, ws_strdup_printf("%08x", rgb));
        }
    }
}

bool MainApplication::event(QEvent *event)
{
    QString display_filter = NULL;
    if (event->type() == QEvent::FileOpen) {
        QFileOpenEvent *foe = static_cast<QFileOpenEvent *>(event);
        if (foe && foe->file().length() > 0) {
            QString cf_path(foe->file());
            if (initialized_) {
                emit openCaptureFile(cf_path, display_filter, WTAP_TYPE_AUTO);
            } else {
                pending_open_files_.append(cf_path);
            }
        }
        return true;
    }
    return QApplication::event(event);
}

void MainApplication::clearRecentCaptures() {
    qDeleteAll(recent_captures_);
    recent_captures_.clear();
    emit updateRecentCaptureStatus(NULL, 0, false);
}

void MainApplication::cleanup()
{
    software_update_cleanup();
    storeCustomColorsInRecent();
    // Write the user's recent file(s) to disk.
    write_profile_recent();
    write_recent();

    qDeleteAll(recent_captures_);
    recent_captures_.clear();
    // We might end up here via exit_application.
    QThreadPool::globalInstance()->waitForDone();
}

void MainApplication::itemStatusFinished(const QString filename, qint64 size, bool accessible) {
    recent_item_status *ri;

    foreach (ri, recent_captures_) {
        if (filename == ri->filename && (size != ri->size || accessible != ri->accessible)) {
            ri->size = size;
            ri->accessible = accessible;
            ri->in_thread = false;

            emit updateRecentCaptureStatus(filename, size, accessible);
        }
    }
}

MainApplication::MainApplication(int &argc,  char **argv) :
    QApplication(argc, argv),
    initialized_(false),
    is_reloading_lua_(false),
    if_notifier_(NULL),
    active_captures_(0),
    refresh_interfaces_pending_(false)
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
    , normal_icon_(windowIcon())
#endif
#ifdef HAVE_LIBPCAP
    , cached_if_list_(NULL)
#endif
{
    mainApp = this;

    MimeDatabaseInitThread *mime_db_init_thread = new(MimeDatabaseInitThread);
    QThreadPool::globalInstance()->start(mime_db_init_thread);
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
    FontDatabaseInitThread *font_db_init_thread = new (FontDatabaseInitThread);
    QThreadPool::globalInstance()->start(font_db_init_thread);
#endif

    Q_INIT_RESOURCE(about);
    Q_INIT_RESOURCE(i18n);
    Q_INIT_RESOURCE(layout);
    Q_INIT_RESOURCE(stock_icons);
    Q_INIT_RESOURCE(languages);

#ifdef Q_OS_WIN
    /* RichEd20.DLL is needed for native file dialog filter entries. */
    ws_load_library("riched20.dll");
#endif // Q_OS_WIN

#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
    setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    setAttribute(Qt::AA_DisableWindowContextHelpButton);
#endif

    // We use a lot of style sheets that base their colors on the main
    // application palette, so this works better.
    setAttribute(Qt::AA_UseStyleSheetPropagationInWidgetStyles, true);

    // Throw various settings at the wall with the hope that one of them will
    // enable context menu shortcuts QTBUG-69452, QTBUG-109590
    setAttribute(Qt::AA_DontShowShortcutsInContextMenus, false);
    styleHints()->setShowShortcutsInContextMenus(true);

    //
    // XXX - this means we try to check for the existence of all files
    // in the recent list every 2 seconds; that causes noticeable network
    // traffic if any of them are stored on file servers.
    //
    // QFileSystemWatcher should allow us to watch for files being
    // removed or renamed.  It uses kqueues and EVFILT_VNODE on FreeBSD,
    // NetBSD, FSEvents on macOS, inotify on Linux if available, and
    // FindFirstChangeNotification() on Windows.  On all other platforms,
    // it just periodically polls, as we're doing now.
    //
    // For unmounts:
    //
    // macOS and FreeBSD deliver NOTE_REVOKE notes for EVFILT_VNODE, and
    // QFileSystemWatcher delivers signals for them, just as it does for
    // NOTE_DELETE and NOTE_RENAME.
    //
    // On Linux, inotify:
    //
    //    http://man7.org/linux/man-pages/man7/inotify.7.html
    //
    // appears to deliver "filesystem containing watched object was
    // unmounted" events.  It looks as if Qt turns them into "changed"
    // events.
    //
    // On Windows, it's not clearly documented what happens on a handle
    // opened with FindFirstChangeNotification() if the volume on which
    // the path handed to FindFirstChangeNotification() is removed, or
    // ejected, or whatever the Windowsese is for "unmounted".  The
    // handle obviously isn't valid any more, but whether it just hangs
    // around and never delivers any notifications or delivers an
    // event that turns into an error indication doesn't seem to be
    // documented.  If it just hangs around, I think our main loop will
    // receive a WM_DEVICECHANGE Windows message with DBT_DEVICEREMOVECOMPLETE
    // if an unmount occurs - even for network devices.  If we need to watch
    // for those, we can use the winEvent method of the QWidget for the
    // top-level window to get Windows messages.
    //
    // Note also that remote file systems might not report file
    // removal or renames if they're done on the server or done by
    // another client.  At least on macOS, they *will* get reported
    // if they're done on the machine running the program doing the
    // kqueue stuff, and, at least in newer versions, should get
    // reported on SMB-mounted (and AFP-mounted?) file systems
    // even if done on the server or another client.
    //
    // But, when push comes to shove, the file manager(s) on the
    // OSes in question probably use the same mechanisms to
    // monitor folders in folder windows or open/save dialogs or...,
    // so my inclination is just to use QFileSystemWatcher.
    //
    // However, that wouldn't catch files that become *re*-accessible
    // by virtue of a file system being re-mounted.  The only way to
    // catch *that* would be to watch for mounts and re-check all
    // marked-as-inaccessible files.
    //
    // macOS and FreeBSD also support EVFILT_FS events, which notify you
    // of file system mounts and unmounts.  We'd need to add our own
    // kqueue for that, if we can check those with QSocketNotifier.
    //
    // On Linux, at least as of 2006, you're supposed to poll /proc/mounts:
    //
    //    https://lkml.org/lkml/2006/2/22/169
    //
    // to discover mounts.
    //
    // On Windows, you'd probably have to watch for WM_DEVICECHANGE events.
    //
    // Then again, with an automounter, a file system containing a
    // recent capture might get unmounted automatically if you haven't
    // referred to anything on that file system for a while, and get
    // treated as inaccessible.  However, if you try to access it,
    // the automounter will attempt to re-mount it, so the access *will*
    // succeed if the automounter can remount the file.
    //
    // (Speaking of automounters, repeatedly polling recent files will
    // keep the file system from being unmounted, for what that's worth.)
    //
    // At least on macOS, you can determine whether a file is on an
    // automounted file system by calling statfs() on its path and
    // checking whether MNT_AUTOMOUNTED is set in f_flags.  FreeBSD
    // appears to support that flag as well, but no other *BSD appears
    // to.
    //
    // I'm not sure what can be done on Linux.
    //
    recent_timer_.setParent(this);
    connect(&recent_timer_, &QTimer::timeout, this, &MainApplication::refreshRecentCaptures);
    recent_timer_.start(2000);

    packet_data_timer_.setParent(this);
    connect(&packet_data_timer_, &QTimer::timeout, this, &MainApplication::refreshPacketData);
    packet_data_timer_.start(1000);

    tap_update_timer_.setParent(this);
    // tap_update_timer interval is set when preferences are set before init
    connect(this, &MainApplication::appInitialized, &tap_update_timer_, [&]() { tap_update_timer_.start(); });
    connect(&tap_update_timer_, &QTimer::timeout, this, &MainApplication::updateTaps);


    // Application-wide style sheet
    QString app_style_sheet = qApp->styleSheet();
    app_style_sheet += QStringLiteral(
        "QMessageBox { "
        "  messagebox-text-interaction-flags: %1;"
        "}"
        ).arg(Qt::TextSelectableByMouse);
    qApp->setStyleSheet(app_style_sheet);

    // If our window text is lighter than the window background, assume the theme is dark.
    prefs_set_gui_theme_is_dark(ColorUtils::themeIsDark());

#if defined(HAVE_SOFTWARE_UPDATE) && defined(Q_OS_WIN)
    connect(this, &MainApplication::softwareUpdateQuit, this, &MainApplication::quit, Qt::QueuedConnection);
#endif

#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) && defined(Q_OS_WIN)
    colorSchemeChanged();
    connect(styleHints(), &QStyleHints::colorSchemeChanged, this, &MainApplication::colorSchemeChanged);
#endif

    connect(qApp, &QApplication::aboutToQuit, this, &MainApplication::cleanup);
}

MainApplication::~MainApplication()
{
    mainApp = NULL;
#ifdef HAVE_LIBPCAP
    free_interface_list(cached_if_list_);
#endif
    clearDynamicMenuGroupItems();
}

void MainApplication::registerUpdate(register_action_e action, const char *message)
{
    emit splashUpdate(action, message);
}

void MainApplication::emitAppSignal(AppSignal signal)
{
    switch (signal) {
    case ColumnsChanged:
        emit columnsChanged();
        break;
    case CaptureFilterListChanged:
        emit captureFilterListChanged();
        break;
    case DisplayFilterListChanged:
        emit displayFilterListChanged();
        break;
    case FilterExpressionsChanged:
        emit filterExpressionsChanged();
        break;
    case LocalInterfacesChanged:
        emit localInterfaceListChanged();
        break;
    case NameResolutionChanged:
        emit addressResolutionChanged();
        break;
    case PreferencesChanged:
        tap_update_timer_.setInterval(prefs.tap_update_interval);
        setMonospaceFont(prefs.gui_font_name);
        emit preferencesChanged();
        break;
    case PacketDissectionChanged:
        emit packetDissectionChanged();
        break;
    case ProfileChanging:
        emit profileChanging();
        break;
    case RecentCapturesChanged:
        emit updateRecentCaptureStatus(NULL, 0, false);
        break;
    case RecentPreferencesRead:
        emit recentPreferencesRead();
        break;
    case FieldsChanged:
        emit fieldsChanged();
        break;
    case ColorsChanged:
        ColorUtils::setScheme(prefs.gui_color_scheme);
        emit colorsChanged();
        break;
    case FreezePacketList:
        emit freezePacketList(false);
        break;
    default:
        break;
    }
}

// Flush any collected app signals.
//
// On macOS emitting PacketDissectionChanged from a dialog can
// render the application unusable:
// https://gitlab.com/wireshark/wireshark/-/issues/11361
// https://gitlab.com/wireshark/wireshark/-/issues/11448
// Work around the problem by queueing up app signals and emitting them
// after the dialog is closed.
//
// The following bugs might be related although they don't describe the
// exact behavior we're working around here:
// https://bugreports.qt.io/browse/QTBUG-38512
// https://bugreports.qt.io/browse/QTBUG-38600
void MainApplication::flushAppSignals()
{
    while (!app_signals_.isEmpty()) {
        mainApp->emitAppSignal(app_signals_.takeFirst());
    }
}

void MainApplication::emitStatCommandSignal(const QString &menu_path, const char *arg, void *userdata)
{
    emit openStatCommandDialog(menu_path, arg, userdata);
}

void MainApplication::emitTapParameterSignal(const QString cfg_abbr, const QString arg, void *userdata)
{
    emit openTapParameterDialog(cfg_abbr, arg, userdata);
}

// XXX Combine statistics and funnel routines into addGroupItem + groupItems?
void MainApplication::addDynamicMenuGroupItem(int group, QAction *sg_action)
{
    if (!dynamic_menu_groups_.contains(group)) {
        dynamic_menu_groups_[group] = QList<QAction *>();
    }
    dynamic_menu_groups_[group] << sg_action;
}

void MainApplication::appendDynamicMenuGroupItem(int group, QAction *sg_action)
{
    if (!added_menu_groups_.contains(group)) {
        added_menu_groups_[group] = QList<QAction *>();
    }
    added_menu_groups_[group] << sg_action;
    addDynamicMenuGroupItem(group, sg_action);
}

void MainApplication::removeDynamicMenuGroupItem(int group, QAction *sg_action)
{
    if (!removed_menu_groups_.contains(group)) {
        removed_menu_groups_[group] = QList<QAction *>();
    }
    removed_menu_groups_[group] << sg_action;
    dynamic_menu_groups_[group].removeAll(sg_action);
}

void MainApplication::clearDynamicMenuGroupItems()
{
    foreach (int group, dynamic_menu_groups_.keys()) {
        dynamic_menu_groups_[group].clear();
    }
}

QList<QAction *> MainApplication::dynamicMenuGroupItems(int group)
{
    if (!dynamic_menu_groups_.contains(group)) {
        return QList<QAction *>();
    }

    QList<QAction *> sgi_list = dynamic_menu_groups_[group];
    std::sort(sgi_list.begin(), sgi_list.end(), qActionLessThan);
    return sgi_list;
}

QList<QAction *> MainApplication::addedMenuGroupItems(int group)
{
    if (!added_menu_groups_.contains(group)) {
        return QList<QAction *>();
    }

    QList<QAction *> sgi_list = added_menu_groups_[group];
    std::sort(sgi_list.begin(), sgi_list.end(), qActionLessThan);
    return sgi_list;
}

QList<QAction *> MainApplication::removedMenuGroupItems(int group)
{
    if (!removed_menu_groups_.contains(group)) {
        return QList<QAction *>();
    }

    QList<QAction *> sgi_list = removed_menu_groups_[group];
    std::sort(sgi_list.begin(), sgi_list.end(), qActionLessThan);
    return sgi_list;
}

void MainApplication::clearAddedMenuGroupItems()
{
    foreach (int group, added_menu_groups_.keys()) {
        added_menu_groups_[group].clear();
    }
}

void MainApplication::clearRemovedMenuGroupItems()
{
    foreach (int group, removed_menu_groups_.keys()) {
        foreach (QAction *action, removed_menu_groups_[group]) {
            delete action;
        }
        removed_menu_groups_[group].clear();
    }
}

#ifdef HAVE_LIBPCAP

static void
iface_mon_event_cb(const char *iface, int added, int up)
{
    int present = 0;
    unsigned ifs, j;
    interface_t *device;
    interface_options *interface_opts;

    for (ifs = 0; ifs < global_capture_opts.all_ifaces->len; ifs++) {
        device = &g_array_index(global_capture_opts.all_ifaces, interface_t, ifs);
        if (strcmp(device->name, iface) == 0) {
            present = 1;
            if (!up) {
                /*
                 * Interface went down or disappeared; remove all instances
                 * of it from the current list of interfaces selected
                 * for capturing.
                 */
                for (j = 0; j < global_capture_opts.ifaces->len; j++) {
                    interface_opts = &g_array_index(global_capture_opts.ifaces, interface_options, j);
                    if (strcmp(interface_opts->name, device->name) == 0) {
                        capture_opts_del_iface(&global_capture_opts, j);
                }
             }
          }
        }
    }

    mainApp->emitLocalInterfaceEvent(iface, added, up);
    if (present != up) {
        /*
         * We've been told that there's a new interface or that an old
         * interface is gone; reload the local interface list.
         *
         * XXX: We also want to reload the local interface list if [what
         * we can retrieve about] the capabilities of the device have changed.
         * Ideally we'd update the capabilities of just the one device in
         * the cache and signal that the list has been updated, instead of
         * freeing the entire cache and scanning again - but some extcaps
         * depend on other interfaces being up; e.g. by default androiddump
         * tries to connect to the loopback interface to look for adb running,
         * so if the loopback interface changes so does the status of
         * androiddump.
         *
         * On Linux, at least, you can't get the capabilities from a down
         * interface, but it's still present in all_ifaces - dumpcap returns
         * it in the list, and we show it so the user can get a status / error
         * message when trying to capture on it instead of it vanishing.
         * So if both present and up are true, then we still want to refresh
         * to update the capabilities and restart the stats.
         *
         * We also store the address in all_ifaces and show them to the user,
         * so we probably should monitor those events as well and update
         * the interface list appropriately when those change.
         */
        mainApp->refreshLocalInterfaces();
    }
}

#endif

void MainApplication::ifChangeEventsAvailable()
{
#ifdef HAVE_LIBPCAP
    /*
     * Something's readable from the descriptor for interface
     * monitoring.
     *
     * Have the interface-monitoring code Read whatever interface-change
     * events are available, and call the callback for them.
     */
    iface_mon_event();
#endif
}

void MainApplication::emitLocalInterfaceEvent(const char *ifname, int added, int up)
{
    emit localInterfaceEvent(ifname, added, up);
}

void MainApplication::refreshLocalInterfaces()
{
    if (active_captures_ > 0) {
        refresh_interfaces_pending_ = true;
        return;
    }

    refresh_interfaces_pending_ = false;
    extcap_clear_interfaces();

#ifdef HAVE_LIBPCAP
    emit scanLocalInterfaces(nullptr);
#endif
}

#ifdef HAVE_LIBPCAP
GList* MainApplication::getInterfaceList() const
{
    return interface_list_copy(cached_if_list_);
}

void MainApplication::setInterfaceList(GList *if_list)
{
    free_interface_list(cached_if_list_);
    cached_if_list_ = interface_list_copy(if_list);
}
#endif

void MainApplication::allSystemsGo()
{
    QString display_filter = NULL;
    initialized_ = true;
    emit appInitialized();
    while (pending_open_files_.length() > 0) {
        emit openCaptureFile(pending_open_files_.front(), display_filter, WTAP_TYPE_AUTO);
        pending_open_files_.pop_front();
    }
    software_update_init();

#ifdef HAVE_LIBPCAP
    int err;
    err = iface_mon_start(&iface_mon_event_cb);
    if (err == 0) {
        if_notifier_ = new QSocketNotifier(iface_mon_get_sock(),
                                           QSocketNotifier::Read, this);
        connect(if_notifier_, &QSocketNotifier::activated, this, &MainApplication::ifChangeEventsAvailable);
    }
#endif
}

_e_prefs *MainApplication::readConfigurationFiles(bool reset)
{
    e_prefs             *prefs_p;

    if (reset) {
        //
        // Reset current preferences and enabled/disabled protocols and
        // heuristic dissectors before reading.
        // (Needed except when this is called at startup.)
        //
        prefs_reset();
        proto_reenable_all();
    }

    /* Load libwireshark settings from the current profile. */
    prefs_p = epan_load_settings();

    return prefs_p;
}

QList<recent_item_status *> MainApplication::recentItems() const {
    return recent_captures_;
}

void MainApplication::addRecentItem(const QString filename, qint64 size, bool accessible) {
    recent_item_status *ri = new(recent_item_status);

    ri->filename = filename;
    ri->size = size;
    ri->accessible = accessible;
    ri->in_thread = false;
    recent_captures_.prepend(ri);

    itemStatusFinished(filename, size, accessible);
}

void MainApplication::removeRecentItem(const QString &filename)
{
    QMutableListIterator<recent_item_status *> rii(recent_captures_);

    while (rii.hasNext()) {
        recent_item_status *ri = rii.next();
#ifdef _WIN32
        /* Do a case insensitive compare on win32 */
        if (ri->filename.compare(filename, Qt::CaseInsensitive) == 0) {
#else
        /* Do a case sensitive compare on UN*Xes.
         *
         * XXX - on UN*Xes such as macOS, where you can use pathconf()
         * to check whether a given file system is case-sensitive or
         * not, we should check whether this particular file system
         * is case-sensitive and do the appropriate comparison.
         */
        if (ri->filename.compare(filename) == 0) {
#endif
            rii.remove();
            delete(ri);
        }
    }

    emit updateRecentCaptureStatus(NULL, 0, false);
}

static void switchTranslator(QTranslator& myTranslator, const QString& filename,
    const QString& searchPath)
{
    mainApp->removeTranslator(&myTranslator);

    if (myTranslator.load(filename, searchPath))
        mainApp->installTranslator(&myTranslator);
}

void MainApplication::loadLanguage(const QString newLanguage)
{
    QLocale locale;
    QString localeLanguage;

    if (newLanguage.isEmpty() || newLanguage == USE_SYSTEM_LANGUAGE) {
        locale = QLocale::system();
        localeLanguage = locale.name();
    } else {
        localeLanguage = newLanguage;
        locale = QLocale(localeLanguage);
    }

    QLocale::setDefault(locale);
    switchTranslator(mainApp->translator,
            QStringLiteral("wireshark_%1.qm").arg(localeLanguage), QStringLiteral(":/i18n/"));
    if (QFile::exists(QStringLiteral("%1/%2/wireshark_%3.qm")
            .arg(get_datafile_dir()).arg("languages").arg(localeLanguage)))
        switchTranslator(mainApp->translator,
                QStringLiteral("wireshark_%1.qm").arg(localeLanguage), QStringLiteral("%1/languages").arg(get_datafile_dir()));
    if (QFile::exists(QStringLiteral("%1/wireshark_%3.qm")
            .arg(gchar_free_to_qstring(get_persconffile_path("languages", false))).arg(localeLanguage)))
        switchTranslator(mainApp->translator,
                QStringLiteral("wireshark_%1.qm").arg(localeLanguage), gchar_free_to_qstring(get_persconffile_path("languages", false)));
    if (QFile::exists(QStringLiteral("%1/qt_%2.qm")
            .arg(get_datafile_dir()).arg(localeLanguage))) {
        switchTranslator(mainApp->translatorQt,
                QStringLiteral("qt_%1.qm").arg(localeLanguage), QString(get_datafile_dir()));
    } else if (QFile::exists(QStringLiteral("%1/qt_%2.qm")
            .arg(get_datafile_dir()).arg(localeLanguage.left(localeLanguage.lastIndexOf('_'))))) {
        switchTranslator(mainApp->translatorQt,
                QStringLiteral("qt_%1.qm").arg(localeLanguage.left(localeLanguage.lastIndexOf('_'))), QString(get_datafile_dir()));
    } else {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
        QString translationPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
#else
        QString translationPath = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
#endif
        switchTranslator(mainApp->translatorQt, QStringLiteral("qt_%1.qm").arg(localeLanguage), translationPath);
    }
}

void MainApplication::doTriggerMenuItem(MainMenuItem menuItem)
{
    switch (menuItem)
    {
    case FileOpenDialog:
        emit openCaptureFile(QString(), QString(), WTAP_TYPE_AUTO);
        break;
    case CaptureOptionsDialog:
        emit openCaptureOptions();
        break;
    }
}

void MainApplication::zoomTextFont(int zoomLevel)
{
    // Scale by 10%, rounding to nearest half point, minimum 1 point.
    // XXX Small sizes repeat. It might just be easier to create a map of multipliers.
    qreal zoom_size = mono_font_.pointSize() * 2 * qPow(qreal(1.1), zoomLevel);
    zoom_size = qRound(zoom_size) / qreal(2.0);
    zoom_size = qMax(zoom_size, qreal(1.0));

    zoomed_font_ = mono_font_;
    zoomed_font_.setPointSizeF(zoom_size);
    emit zoomMonospaceFont(zoomed_font_);

    QFont zoomed_application_font = font();
    zoomed_application_font.setPointSizeF(zoom_size);
    emit zoomRegularFont(zoomed_application_font);
}

#if defined(HAVE_SOFTWARE_UPDATE) && defined(Q_OS_WIN)
bool MainApplication::softwareUpdateCanShutdown() {
    software_update_ok_ = true;
    // At this point the update is ready to install, but WinSparkle has
    // not yet run the installer. We need to close our "Wireshark is
    // running" mutexes since the IsWiresharkRunning NSIS macro checks
    // for them.
    //
    // We must not exit the Qt main event loop here, which means we must
    // not close the main window.

    // Step 1: See if we have any open files.
    emit softwareUpdateRequested();
    if (software_update_ok_ == true) {

        // Step 2: Close the "running" mutexes.
        close_app_running_mutex();
    }
    return software_update_ok_;
}

void MainApplication::softwareUpdateShutdownRequest() {
    // At this point the installer has been launched. Neither Wireshark nor
    // its children should have any "Wireshark is running" mutexes open.
    // The main window should still be open as noted above in
    // softwareUpdateCanShutdown and it's safe to exit the Qt main
    // event loop.

    // Step 3: Quit.
    emit softwareUpdateQuit();
}
#endif

void MainApplication::captureEventHandler(CaptureEvent ev)
{
    switch(ev.captureContext())
    {
#ifdef HAVE_LIBPCAP
    case CaptureEvent::Update:
    case CaptureEvent::Fixed:
        switch (ev.eventType())
        {
        case CaptureEvent::Prepared:
            iface_mon_enable(true);
            break;
        case CaptureEvent::Started:
            active_captures_++;
            emit captureActive(active_captures_);
            break;
        case CaptureEvent::Finished:
            active_captures_--;
            emit captureActive(active_captures_);
            if (refresh_interfaces_pending_ && !global_capture_opts.restart) {
                refreshLocalInterfaces();
            }
            break;
        default:
            break;
        }
        break;
#endif
    case CaptureEvent::File:
    case CaptureEvent::Reload:
    case CaptureEvent::Rescan:
        switch (ev.eventType())
        {
        case CaptureEvent::Started:
            QTimer::singleShot(TAP_UPDATE_DEFAULT_INTERVAL / 5, this, SLOT(updateTaps()));
            QTimer::singleShot(TAP_UPDATE_DEFAULT_INTERVAL / 2, this, SLOT(updateTaps()));
            break;
        case CaptureEvent::Finished:
            updateTaps();
            break;
        default:
            break;
        }
        break;
    default:
        break;
    }
}

void MainApplication::pushStatus(StatusInfo status, const QString &message, const QString &messagetip)
{
    MainWindow * mw = mainWindow();
    if (! mw) {
        return;
    }

    MainStatusBar * bar = mw->statusBar();
    if (! bar) {
        return;
    }

    switch(status)
    {
        case FilterSyntax:
            bar->pushGenericStatus(MainStatusBar::STATUS_CTX_FILTER, message);
            break;
        case FieldStatus:
            bar->pushGenericStatus(MainStatusBar::STATUS_CTX_FIELD, message);
            break;
        case FileStatus:
            bar->pushGenericStatus(MainStatusBar::STATUS_CTX_FILE, message, messagetip);
            break;
        case ByteStatus:
            bar->pushGenericStatus(MainStatusBar::STATUS_CTX_BYTE, message);
            break;
        case BusyStatus:
            bar->pushGenericStatus(MainStatusBar::STATUS_CTX_PROGRESS, message, messagetip);
            break;
        case TemporaryStatus:
            bar->pushGenericStatus(MainStatusBar::STATUS_CTX_TEMPORARY, message);
            break;
    }
}

void MainApplication::popStatus(StatusInfo status)
{
    MainWindow * mw = mainWindow();
    if (! mw) {
        return;
    }

    MainStatusBar * bar = mw->statusBar();
    if (! bar) {
        return;
    }

    switch(status)
    {
        case FilterSyntax:
            bar->popGenericStatus(MainStatusBar::STATUS_CTX_FILTER);
            break;
        case FieldStatus:
            bar->popGenericStatus(MainStatusBar::STATUS_CTX_FIELD);
            break;
        case FileStatus:
            bar->popGenericStatus(MainStatusBar::STATUS_CTX_FILE);
            break;
        case ByteStatus:
            bar->popGenericStatus(MainStatusBar::STATUS_CTX_BYTE);
            break;
        case BusyStatus:
            bar->popGenericStatus(MainStatusBar::STATUS_CTX_PROGRESS);
            break;
        case TemporaryStatus:
            bar->popGenericStatus(MainStatusBar::STATUS_CTX_TEMPORARY);
            break;
    }
}

void MainApplication::gotoFrame(int frame)
{
    MainWindow * mw = mainWindow();
    if (! mw) {
        return;
    }

    mw->gotoFrame(frame);
}

void MainApplication::reloadDisplayFilterMacros()
{
    dfilter_macro_reload();
    // The signal is needed when the display filter grammar changes for
    // any reason (not just "fields".)
    mainApp->emitAppSignal(MainApplication::FieldsChanged);
}
