// SPDX-License-Identifier: GPL-2.0-only
/*
 * Digital Voice Modem - FNE System View
 * GPLv2 Open Source. Use is subject to license terms.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 *  Copyright (C) 2024-2026 Bryan Biedenkapp, N2PLL
 *
 */
#include "Defines.h"
#include "common/Clock.h"
#include "common/analog/AnalogDefines.h"
#include "common/dmr/DMRDefines.h"
#include "common/dmr/lc/csbk/CSBKFactory.h"
#include "common/dmr/lc/LC.h"
#include "common/dmr/lc/FullLC.h"
#include "common/dmr/SlotType.h"
#include "common/dmr/Sync.h"
#include "common/p25/P25Defines.h"
#include "common/p25/lc/tdulc/TDULCFactory.h"
#include "common/p25/lc/tsbk/TSBKFactory.h"
#include "common/nxdn/NXDNDefines.h"
#include "common/nxdn/channel/LICH.h"
#include "common/nxdn/lc/RCCH.h"
#include "common/nxdn/lc/rcch/RCCHFactory.h"
#include "common/nxdn/lc/RTCH.h"
#include "common/nxdn/NXDNUtils.h"
#include "common/yaml/Yaml.h"
#include "common/Log.h"
#include "common/StopWatch.h"
#include "common/Thread.h"
#include "p25/tsbk/OSP_GRP_AFF.h"
#include "network/PeerNetwork.h"
#include "SysViewMain.h"
#include "SysViewApplication.h"
#include "SysViewMainWnd.h"
#if !defined(NO_WEBSOCKETS)
#include "HostWS.h"
#endif // !defined(NO_WEBSOCKETS)

using namespace system_clock;
using namespace network;
using namespace lookups;

#include <cstdio>
#include <cstdarg>
#include <cstring>
#include <vector>

// ---------------------------------------------------------------------------
//	Macros
// ---------------------------------------------------------------------------

#define IS(s) (::strcmp(argv[i], s) == 0)

// ---------------------------------------------------------------------------
//  Global Variables
// ---------------------------------------------------------------------------

int g_signal = 0;

std::string g_progExe = std::string(__EXE_NAME__);
std::string g_iniFile = std::string(DEFAULT_CONF_FILE);
yaml::Node g_conf;
bool g_debug = false;

bool g_foreground = false;
bool g_killed = false;
bool g_hideLoggingWnd = false;

bool g_webSocketMode = false;

lookups::RadioIdLookup* g_ridLookup = nullptr;
lookups::TalkgroupRulesLookup* g_tidLookup = nullptr;
lookups::IdenTableLookup* g_idenTable = nullptr;

std::unordered_map<uint32_t, std::string> g_peerIdentityNameMap = std::unordered_map<uint32_t, std::string>();

std::function<void(json::object)> g_netDataEvent = nullptr;

SysViewApplication* g_app = nullptr;

network::PeerNetwork* g_network = nullptr;

/**
 * @brief Represents the receive status of a call.
 */
class RxStatus {
public:
    ::system_clock::hrc::hrc_t callStartTime;
    ::system_clock::hrc::hrc_t lastPacket;
    uint32_t srcId = 0U;
    uint32_t dstId = 0U;
    uint8_t slotNo = 0U;
    uint32_t streamId = 0U;
};
typedef std::pair<const uint32_t, RxStatus> StatusMapPair;
std::unordered_map<uint32_t, RxStatus> g_dmrStatus;
std::unordered_map<uint32_t, RxStatus> g_p25Status;
std::unordered_map<uint32_t, RxStatus> g_nxdnStatus;
std::unordered_map<uint32_t, RxStatus> g_analogStatus;

// ---------------------------------------------------------------------------
//	Global Functions
// ---------------------------------------------------------------------------

/* Internal signal handler. */

static void sigHandler(int signum)
{
    g_signal = signum;
    g_killed = true;
}

/* Helper to print a fatal error message and exit. */

void fatal(const char* msg, ...)
{
    char buffer[400U];
    ::memset(buffer, 0x20U, 400U);

    va_list vl;
    va_start(vl, msg);

    ::vsprintf(buffer, msg, vl);

    va_end(vl);

    ::fprintf(stderr, "%s: FATAL PANIC; %s\n", g_progExe.c_str(), buffer);
    exit(EXIT_FAILURE);
}

/* Helper to set the network data event callback. */

void setNetDataEventCallback(std::function<void(json::object)>&& callback) 
{ 
    g_netDataEvent = callback; 
}

/* Helper to resolve a TGID to a textual name. */

std::string resolveRID(uint32_t id)
{
    switch (id) {
    case DMRDEF::WUID_REGI:
        return std::string("DMR REG SVCS");
    case DMRDEF::WUID_STUNI:
        return std::string("DMR MS STUN");
    case DMRDEF::WUID_AUTHI:
        return std::string("DMR AUTH SVCS");
    case DMRDEF::WUID_KILLI:
        return std::string("DMR MS KILL");
    case DMRDEF::WUID_ALLL:
        return std::string("ALL CALL SW");

    case P25DEF::WUID_REG:
        return std::string("REG SVCS");
    case P25DEF::WUID_FNE:
        return std::string("SYS/FNE");
    case P25DEF::WUID_ALL:
        return std::string("ALL CALL");

    case 0:
        return std::string("EXTERNAL/PATCH");
    }

    auto entry = g_ridLookup->find(id);
    if (!entry.radioDefault()) {
        return entry.radioAlias();
    }

    return std::string("UNK");
}

/* Helper to resolve a TGID to a textual name. */

std::string resolveTGID(uint32_t id)
{
    auto entry = g_tidLookup->find(id);
    if (!entry.isInvalid()) {
        return entry.name();
    }

    return std::string("UNK");
}

/* Helper to emit a call start or end event. */

void emitCallStartEndEvent(uint32_t srcId, uint32_t dstId, uint8_t slotNo, uint32_t streamId, const std::string& mode,
    bool callEnd = false, uint64_t duration = 0U)
{
    if (g_netDataEvent == nullptr)
        return;

    json::object event;
    std::string type = "call_start";
    if (callEnd) {
        type = "call_end";
    }
    event["type"].set<std::string>(type);

    event["mode"].set<std::string>(mode);

    event["slot"].set<uint8_t>(slotNo);

    std::string resolvedSrc = resolveRID(srcId);
    std::string resolvedDst = resolveTGID(dstId);
    event["srcId"].set<uint32_t>(srcId);
    event["srcStr"].set<std::string>(resolvedSrc);
    event["dstId"].set<uint32_t>(dstId);
    event["dstStr"].set<std::string>(resolvedDst);

    event["streamId"].set<uint32_t>(streamId);

    if (callEnd) {
        event["duration"].set<uint64_t>(duration);
    }

    g_netDataEvent(event);
}

/* Initializes peer network connectivity. */

bool createPeerNetwork()
{
    yaml::Node fne = g_conf["fne"];

    std::string password = fne["password"].as<std::string>();

    std::string address = fne["masterAddress"].as<std::string>();
    uint16_t port = (uint16_t)fne["masterPort"].as<uint32_t>();
    uint32_t id = fne["peerId"].as<uint32_t>();

    bool encrypted = fne["encrypted"].as<bool>(false);
    std::string key = fne["presharedKey"].as<std::string>();
    uint8_t presharedKey[AES_WRAPPED_PCKT_KEY_LEN];
    if (!key.empty()) {
        if (key.size() == 32) {
            // bryanb: shhhhhhh....dirty nasty hacks
            key = key.append(key); // since the key is 32 characters (16 hex pairs), double it on itself for 64 characters (32 hex pairs)
            LogWarning(LOG_HOST, "Half-length network preshared encryption key detected, doubling key on itself.");
        }

        if (key.size() == 64) {
            if ((key.find_first_not_of("0123456789abcdefABCDEF", 2) == std::string::npos)) {
                const char* keyPtr = key.c_str();
                ::memset(presharedKey, 0x00U, AES_WRAPPED_PCKT_KEY_LEN);

                for (uint8_t i = 0; i < AES_WRAPPED_PCKT_KEY_LEN; i++) {
                    char t[4] = {keyPtr[0], keyPtr[1], 0};
                    presharedKey[i] = (uint8_t)::strtoul(t, NULL, 16);
                    keyPtr += 2 * sizeof(char);
                }
            }
            else {
                LogWarning(LOG_HOST, "Invalid characters in the network preshared encryption key. Encryption disabled.");
                encrypted = false;
            }
        }
        else {
            LogWarning(LOG_HOST, "Invalid  network preshared encryption key length, key should be 32 hex pairs, or 64 characters. Encryption disabled.");
            encrypted = false;
        }
    }

    std::string identity = fne["identity"].as<std::string>();

    LogInfo("Network Parameters");
    LogInfo("    Peer ID: %u", id);
    LogInfo("    Address: %s", address.c_str());
    LogInfo("    Port: %u", port);

    LogInfo("    Encrypted: %s", encrypted ? "yes" : "no");

    if (id > 999999999U) {
        ::LogError(LOG_HOST, "Network Peer ID cannot be greater then 999999999.");
        return false;
    }

    // initialize networking
    g_network = new PeerNetwork(address, port, 0U, id, password, true, g_debug, true, false, true, false);
    g_network->setMetadata(identity, 0U, 0U, 0.0F, 0.0F, 0, 0, 0, 0.0F, 0.0F, 0, "");
    g_network->setLookups(g_ridLookup, g_tidLookup);

    ::LogSetNetwork(g_network);

    if (encrypted) {
        g_network->setPresharedKey(presharedKey);
    }

    g_network->enable(true);
    bool ret = g_network->open();
    if (!ret) {
        delete g_network;
        g_network = nullptr;
        LogError(LOG_HOST, "failed to initialize traffic networking for PEER %u", id);
        return false;
    }

    ::LogSetNetwork(g_network);

    return true;
}

/* Gets the current network instance. */

network::PeerNetwork* getNetwork()
{
    return g_network;
}

/* Entry point to network pump update thread. */

void* threadNetworkPump(void* arg)
{
    thread_t* th = (thread_t*)arg;
    if (th != nullptr) {
#if defined(_WIN32)
        ::CloseHandle(th->thread);
#else
        ::pthread_detach(th->thread);
#endif // defined(_WIN32)

        std::string threadName("sysview:net-pump");
        if (g_killed) {
            delete th;
            return nullptr;
        }

        LogInfoEx(LOG_HOST, "[ OK ] %s", threadName.c_str());
#ifdef _GNU_SOURCE
        ::pthread_setname_np(th->thread, threadName.c_str());
#endif // _GNU_SOURCE

        Timer networkPeerStatusNotify(1000U, 5U);
        networkPeerStatusNotify.start();

        StopWatch stopWatch;
        stopWatch.start();

        while (!g_killed) {
            uint32_t ms = stopWatch.elapsed();
            stopWatch.start();

            // ------------------------------------------------------
            //  -- Network Clocking                               --
            // ------------------------------------------------------

            if (g_network != nullptr) {
                g_network->clock(ms);

                // clock peer status timers and remove expired entries from the peer status map
                g_network->lockPeerStatus();
                std::vector<uint32_t> expiredPeerIds;
                for (auto it = g_network->peerStatusTimers.begin(); it != g_network->peerStatusTimers.end();) {
                    it->second.clock(ms);
                    if (it->second.isRunning() && it->second.hasExpired()) {
                        uint32_t peerId = it->first;
                        it->second.stop();
                        expiredPeerIds.push_back(peerId);
                    }
                    else {
                        ++it;
                    }
                }

                // remove expired entries from the peer status map
                for (uint32_t peerId : expiredPeerIds) {
                    g_network->peerStatus.erase(peerId);
                    g_network->peerStatusTimers.erase(peerId);
                }
                g_network->unlockPeerStatus();

                hrc::hrc_t pktTime = hrc::now();

                uint32_t length = 0U;
                bool netReadRet = false;
                UInt8Array dmrBuffer = g_network->readDMR(netReadRet, length);
                if (netReadRet) {
                    using namespace dmr;

                    uint8_t seqNo = dmrBuffer[4U];

                    uint32_t srcId = GET_UINT24(dmrBuffer, 5U);
                    uint32_t dstId = GET_UINT24(dmrBuffer, 8U);

                    DMRDEF::FLCO::E flco = (dmrBuffer[15U] & 0x40U) == 0x40U ? DMRDEF::FLCO::PRIVATE : DMRDEF::FLCO::GROUP;

                    uint32_t slotNo = (dmrBuffer[15U] & 0x80U) == 0x80U ? 2U : 1U;

                    uint32_t streamId = g_network->getRxDMRStreamId(slotNo);

                    DMRDEF::DataType::E dataType = (DMRDEF::DataType::E)(dmrBuffer[15U] & 0x0FU);

                    data::NetData dmrData;
                    dmrData.setSeqNo(seqNo);
                    dmrData.setSlotNo(slotNo);
                    dmrData.setSrcId(srcId);
                    dmrData.setDstId(dstId);
                    dmrData.setFLCO(flco);

                    bool dataSync = (dmrBuffer[15U] & 0x20U) == 0x20U;
                    bool voiceSync = (dmrBuffer[15U] & 0x10U) == 0x10U;

                    if (dataSync) {
                        dmrData.setData(dmrBuffer.get() + 20U);
                        dmrData.setDataType(dataType);
                        dmrData.setN(0U);
                    }
                    else if (voiceSync) {
                        dmrData.setData(dmrBuffer.get() + 20U);
                        dmrData.setDataType(DMRDEF::DataType::VOICE_SYNC);
                        dmrData.setN(0U);
                    }
                    else {
                        uint8_t n = dmrBuffer[15U] & 0x0FU;
                        dmrData.setData(dmrBuffer.get() + 20U);
                        dmrData.setDataType(DMRDEF::DataType::VOICE);
                        dmrData.setN(n);
                    }

                    uint8_t controlByte = dmrData.getControl();

                    // is this the end of the call stream?
                    if (dataSync && (dataType == DMRDEF::DataType::TERMINATOR_WITH_LC)) {
                        if (srcId == 0U && dstId == 0U) {
                            LogWarning(LOG_NET, "DMR, invalid TERMINATOR, srcId = %u (%s), dstId = %u (%s)", 
                                srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                        }

                        RxStatus status;
                        auto it = std::find_if(g_dmrStatus.begin(), g_dmrStatus.end(), [&](StatusMapPair& x) { return (x.second.dstId == dstId && x.second.slotNo == slotNo); });
                        if (it == g_dmrStatus.end()) {
                            LogError(LOG_NET, "DMR, tried to end call for non-existent call in progress?, srcId = %u (%s), dstId = %u (%s)",
                                srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                        }
                        else {
                            status = it->second;
                        }

                        uint64_t duration = hrc::diff(pktTime, status.callStartTime);

                        if (std::find_if(g_dmrStatus.begin(), g_dmrStatus.end(), [&](StatusMapPair& x) { return (x.second.dstId == dstId && x.second.slotNo == slotNo); }) != g_dmrStatus.end()) {
                            g_dmrStatus.erase(dstId);

                            LogInfoEx(LOG_NET, "DMR, Call End, srcId = %u (%s), dstId = %u (%s), duration = %u",
                                        srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str(), duration / 1000);
                            emitCallStartEndEvent(srcId, dstId, slotNo, status.streamId, "dmr", true, duration / 1000);
                        }
                    }

                    // is this a new call stream?
                    if (dataSync && (dataType == DMRDEF::DataType::VOICE_LC_HEADER)) {
                        if (srcId == 0U && dstId == 0U) {
                            LogWarning(LOG_NET, "DMR, invalid call, srcId = %u (%s), dstId = %u (%s)", 
                                srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                        }

                        auto it = std::find_if(g_dmrStatus.begin(), g_dmrStatus.end(), [&](StatusMapPair& x) { return (x.second.dstId == dstId && x.second.slotNo == slotNo); });
                        if (it == g_dmrStatus.end()) {
                            // this is a new call stream
                            RxStatus status = RxStatus();
                            status.callStartTime = pktTime;
                            status.srcId = srcId;
                            status.dstId = dstId;
                            status.slotNo = slotNo;
                            status.streamId = streamId;
                            g_dmrStatus[dstId] = status; // this *could* be an issue if a dstId appears on both slots somehow...

                            LogInfoEx(LOG_NET, "DMR, Call Start, srcId = %u (%s), dstId = %u (%s)", 
                                srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                            emitCallStartEndEvent(srcId, dstId, slotNo, status.streamId, "dmr");
                        }
                    }

                    // are we receiving a CSBK?
                    if (dmrData.getDataType() == DMRDEF::DataType::CSBK) {
                        uint8_t data[DMRDEF::DMR_FRAME_LENGTH_BYTES + 2U];
                        dmrData.getData(data + 2U);

                        std::unique_ptr<lc::CSBK> csbk = lc::csbk::CSBKFactory::createCSBK(data + 2U, DMRDEF::DataType::CSBK);
                        if (csbk != nullptr) {
                            json::object netEvent = json::object();
                            std::string type = "csbk";
                            netEvent["type"].set<std::string>(type);

                            uint8_t slot = dmrData.getSlotNo();
                            netEvent["slot"].set<uint8_t>(slot);
                            uint8_t csbko = csbk->getCSBKO();
                            netEvent["opcode"].set<uint8_t>(csbko);
                            std::string desc = csbk->toString();
                            netEvent["desc"].set<std::string>(desc);

                            // report raw CSBK bytes in hex format
                            uint8_t* csbkRaw = csbk.get()->getDecodedRaw();
                            std::stringstream ss;
                            ss << std::hex << std::setfill('0');
                            for (uint8_t i = 0; i < DMRDEF::DMR_CSBK_LENGTH_BYTES; i++) {
                                ss << std::setw(2) << (int)csbkRaw[i];
                            }
                            netEvent["data"].set<std::string>(ss.str());

                            switch (csbk->getCSBKO()) {
                            case DMRDEF::CSBKO::BROADCAST:
                                {
                                    lc::csbk::CSBK_BROADCAST* osp = static_cast<lc::csbk::CSBK_BROADCAST*>(csbk.get());
                                    if (osp->getAnncType() == DMRDEF::BroadcastAnncType::ANN_WD_TSCC) {
                                        LogInfoEx(LOG_NET, "DMR Slot %u, DT_CSBK, %s, sysId = $%03X, chNo = %u", dmrData.getSlotNo(), csbk->toString().c_str(),
                                            osp->getSystemId(), osp->getLogicalCh1());

                                        // generate a net event for this
                                        if (g_netDataEvent != nullptr) {
                                            uint32_t sysId = osp->getSystemId();
                                            uint16_t logicalCh1 = osp->getLogicalCh1();
                                            netEvent["sysId"].set<uint32_t>(sysId);
                                            netEvent["chNo"].set<uint16_t>(logicalCh1);

                                            g_netDataEvent(netEvent);
                                        }
                                    }
                                }
                                break;
                            default:
                                {
                                    LogInfoEx(LOG_NET, "DMR Slot %u, DT_CSBK, %s, srcId = %u (%s), dstId = %u (%s)", dmrData.getSlotNo(), csbk->toString().c_str(), 
                                        srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());

                                    // generate a net event for this
                                    if (g_netDataEvent != nullptr) {
                                        std::string resolvedSrc = resolveRID(srcId);
                                        std::string resolvedDst = resolveTGID(dstId);
                                        netEvent["srcId"].set<uint32_t>(srcId);
                                        netEvent["srcStr"].set<std::string>(resolvedSrc);
                                        netEvent["dstId"].set<uint32_t>(dstId);
                                        netEvent["dstStr"].set<std::string>(resolvedDst);

                                        g_netDataEvent(netEvent);
                                    }
                                }
                                break;
                            }
                        }
                    }

                    if (g_debug)
                        LogInfoEx(LOG_NET, "DMR, slotNo = %u, seqNo = %u, flco = $%02X, srcId = %u, dstId = %u, len = %u", slotNo, seqNo, flco, srcId, dstId, length);
                }

                UInt8Array p25Buffer = g_network->readP25(netReadRet, length);
                if (netReadRet) {
                    using namespace p25;

                    uint32_t streamId = g_network->getRxP25StreamId();

                    uint8_t duid = p25Buffer[22U];
                    uint8_t MFId = p25Buffer[15U];

                    // process raw P25 data bytes
                    UInt8Array data;
                    uint8_t frameLength = p25Buffer[23U];
                    if (duid == P25DEF::DUID::PDU) {
                        frameLength = length;
                        data = std::unique_ptr<uint8_t[]>(new uint8_t[length]);
                        ::memset(data.get(), 0x00U, length);
                        ::memcpy(data.get(), p25Buffer.get(), length);
                    }
                    else {
                        if (frameLength <= 24) {
                            data = std::unique_ptr<uint8_t[]>(new uint8_t[frameLength]);
                            ::memset(data.get(), 0x00U, frameLength);
                        }
                        else {
                            data = std::unique_ptr<uint8_t[]>(new uint8_t[frameLength]);
                            ::memset(data.get(), 0x00U, frameLength);
                            ::memcpy(data.get(), p25Buffer.get() + 24U, frameLength);
                        }
                    }

                    uint8_t lco = p25Buffer[4U];

                    uint32_t srcId = GET_UINT24(p25Buffer, 5U);
                    uint32_t dstId = GET_UINT24(p25Buffer, 8U);

                    uint32_t sysId = (p25Buffer[11U] << 8) | (p25Buffer[12U] << 0);
                    uint32_t netId = GET_UINT24(p25Buffer, 16U);

                    uint8_t controlByte = p25Buffer[14U];

                    // log call status
                    if (duid != P25DEF::DUID::TSDU && duid != P25DEF::DUID::PDU) {
                        // is this the end of the call stream?
                        if ((duid == P25DEF::DUID::TDU) || (duid == P25DEF::DUID::TDULC)) {
                            if (srcId == 0U && dstId == 0U) {
                                LogWarning(LOG_NET, "P25, invalid TDU, srcId = %u (%s), dstId = %u (%s)", 
                                    srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                            }

                            RxStatus status = g_p25Status[dstId];
                            uint64_t duration = hrc::diff(pktTime, status.callStartTime);

                            if (std::find_if(g_p25Status.begin(), g_p25Status.end(), [&](StatusMapPair& x) { return x.second.dstId == dstId; }) != g_p25Status.end()) {
                                g_p25Status.erase(dstId);

                                LogInfoEx(LOG_NET, "P25, Call End, srcId = %u (%s), dstId = %u (%s), sysId = $%03X, netId = $%05X, duration = %u",
                                    srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str(), sysId, netId, duration / 1000);
                                emitCallStartEndEvent(srcId, dstId, 0U, status.streamId, "p25", true, duration / 1000);
                            }
                        }

                        // is this a new call stream?
                        if ((duid != P25DEF::DUID::TDU) && (duid != P25DEF::DUID::TDULC)) {
                            if (srcId == 0U && dstId == 0U) {
                                LogWarning(LOG_NET, "P25, invalid call, srcId = %u (%s), dstId = %u (%s)", 
                                    srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                            }

                            auto it = std::find_if(g_p25Status.begin(), g_p25Status.end(), [&](StatusMapPair& x) { return x.second.dstId == dstId; });
                            if (it == g_p25Status.end()) {
                                // this is a new call stream
                                RxStatus status = RxStatus();
                                status.callStartTime = pktTime;
                                status.srcId = srcId;
                                status.dstId = dstId;
                                status.streamId = streamId;
                                g_p25Status[dstId] = status;

                                LogInfoEx(LOG_NET, "P25, Call Start, srcId = %u (%s), dstId = %u (%s), sysId = $%03X, netId = $%05X", 
                                    srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str(), sysId, netId);
                                emitCallStartEndEvent(srcId, dstId, 0U, status.streamId, "p25");
                            }
                        }
                    }

                    switch (duid) {
                    case P25DEF::DUID::TDU:
                    case P25DEF::DUID::TDULC:
                        if (duid == P25DEF::DUID::TDU) {
                            LogInfoEx(LOG_NET, P25_TDU_STR ", srcId = %u (%s), dstId = %u (%s)", 
                                    srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());

                            // if this is a grant demand TDU, generate a GRP_VCH TSDU event to indicate that the call
                            // is trying to start
                            if (((controlByte & network::NET_CTRL_GRANT_DEMAND) == network::NET_CTRL_GRANT_DEMAND) &&
                                g_netDataEvent != nullptr) {
                                bool unitToUnit = (controlByte & network::NET_CTRL_U2U) == network::NET_CTRL_U2U;

                                json::object netEvent = json::object();
                                std::string type = "tsbk";
                                netEvent["type"].set<std::string>(type);

                                if (!unitToUnit) {
                                    std::unique_ptr<lc::tsbk::IOSP_GRP_VCH> tsbk = std::make_unique<lc::tsbk::IOSP_GRP_VCH>();

                                    uint8_t lco = tsbk->getLCO();
                                    netEvent["opcode"].set<uint8_t>(lco);
                                    std::string desc = tsbk->toString();
                                    desc.append(" Demand");
                                    netEvent["desc"].set<std::string>(desc);

                                    tsbk->setSrcId(srcId);
                                    tsbk->setDstId(dstId);
                                    tsbk->setGrpVchId(0U);
                                    tsbk->setGrpVchNo(0U);
                                    tsbk->setPriority(4U);

                                    bool emerg = tsbk->getEmergency();
                                    netEvent["emerg"].set<bool>(emerg);
                                    bool encry = tsbk->getEncrypted();
                                    netEvent["encry"].set<bool>(encry);
                                    uint8_t prio = tsbk->getPriority();
                                    netEvent["prio"].set<uint8_t>(prio);
                                    uint32_t chId = tsbk->getGrpVchId();
                                    netEvent["chId"].set<uint32_t>(chId);
                                    uint32_t chNo = tsbk->getGrpVchNo();
                                    netEvent["chNo"].set<uint32_t>(chNo);

                                    std::string resolvedSrc = resolveRID(srcId);
                                    std::string resolvedDst = resolveTGID(dstId);
                                    netEvent["srcId"].set<uint32_t>(srcId);
                                    netEvent["srcStr"].set<std::string>(resolvedSrc);
                                    netEvent["dstId"].set<uint32_t>(dstId);
                                    netEvent["dstStr"].set<std::string>(resolvedDst);

                                    // report raw TSBK bytes in hex format
                                    DECLARE_UINT8_ARRAY(tsbkRaw, P25DEF::P25_TSBK_LENGTH_BYTES);
                                    tsbk->encode(tsbkRaw, true, true);
                                    std::stringstream ss;
                                    ss << std::hex << std::setfill('0');
                                    for (uint8_t i = 0; i < P25DEF::P25_TSBK_LENGTH_BYTES; i++) {
                                        ss << std::setw(2) << (int)tsbkRaw[i];
                                    }
                                    netEvent["data"].set<std::string>(ss.str());
                                }
                                else {
                                    std::unique_ptr<lc::tsbk::IOSP_UU_VCH> tsbk = std::make_unique<lc::tsbk::IOSP_UU_VCH>();

                                    uint8_t lco = tsbk->getLCO();
                                    netEvent["opcode"].set<uint8_t>(lco);
                                    std::string desc = tsbk->toString();
                                    desc.append(" Demand");
                                    netEvent["desc"].set<std::string>(desc);

                                    tsbk->setSrcId(srcId);
                                    tsbk->setDstId(dstId);
                                    tsbk->setGrpVchId(0U);
                                    tsbk->setGrpVchNo(0U);
                                    tsbk->setPriority(4U);

                                    bool emerg = tsbk->getEmergency();
                                    netEvent["emerg"].set<bool>(emerg);
                                    bool encry = tsbk->getEncrypted();
                                    netEvent["encry"].set<bool>(encry);
                                    uint8_t prio = tsbk->getPriority();
                                    netEvent["prio"].set<uint8_t>(prio);
                                    uint32_t chId = tsbk->getGrpVchId();
                                    netEvent["chId"].set<uint32_t>(chId);
                                    uint32_t chNo = tsbk->getGrpVchNo();
                                    netEvent["chNo"].set<uint32_t>(chNo);

                                    std::string resolvedSrc = resolveRID(srcId);
                                    std::string resolvedDst = resolveTGID(dstId);
                                    netEvent["srcId"].set<uint32_t>(srcId);
                                    netEvent["srcStr"].set<std::string>(resolvedSrc);
                                    netEvent["dstId"].set<uint32_t>(dstId);
                                    netEvent["dstStr"].set<std::string>(resolvedDst);

                                    // report raw TSBK bytes in hex format
                                    DECLARE_UINT8_ARRAY(tsbkRaw, P25DEF::P25_TSBK_LENGTH_BYTES);
                                    tsbk->encode(tsbkRaw, true, true);
                                    std::stringstream ss;
                                    ss << std::hex << std::setfill('0');
                                    for (uint8_t i = 0; i < P25DEF::P25_TSBK_LENGTH_BYTES; i++) {
                                        ss << std::setw(2) << (int)tsbkRaw[i];
                                    }
                                    netEvent["data"].set<std::string>(ss.str());
                                }

                                g_netDataEvent(netEvent);
                            }
                        }
                        else {
                            std::unique_ptr<lc::TDULC> tdulc = lc::tdulc::TDULCFactory::createTDULC(data.get());
                            if (tdulc == nullptr) {
                                LogWarning(LOG_NET, P25_TDULC_STR ", undecodable TDULC");
                            }
                            else {
                                LogInfoEx(LOG_NET, P25_TDULC_STR ", srcId = %u (%s), dstId = %u (%s)", 
                                    srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                            }
                        }
                        break;

                    case P25DEF::DUID::TSDU:
                        std::unique_ptr<lc::TSBK> tsbk = lc::tsbk::TSBKFactory::createTSBK(data.get());
                        if (tsbk == nullptr) {
                            LogWarning(LOG_NET, P25_TSDU_STR ", undecodable TSBK");
                        }
                        else {
                            json::object netEvent = json::object();
                            std::string type = "tsbk";
                            netEvent["type"].set<std::string>(type);

                            uint8_t lco = tsbk->getLCO();
                            netEvent["opcode"].set<uint8_t>(lco);
                            std::string desc = tsbk->toString();
                            netEvent["desc"].set<std::string>(desc);

                            // report raw TSBK bytes in hex format
                            uint8_t* tsbkRaw = tsbk.get()->getDecodedRaw();
                            std::stringstream ss;
                            ss << std::hex << std::setfill('0');
                            for (uint8_t i = 0; i < P25DEF::P25_TSBK_LENGTH_BYTES; i++) {
                                ss << std::setw(2) << (int)tsbkRaw[i];
                            }
                            netEvent["data"].set<std::string>(ss.str());

                            switch (tsbk->getLCO()) {
                                case P25DEF::TSBKO::IOSP_GRP_VCH:
                                case P25DEF::TSBKO::IOSP_UU_VCH:
                                {
                                    LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, emerg = %u, encrypt = %u, prio = %u, chNo = %u-%u, srcId = %u (%s), dstId = %u (%s), sysId = $%03X, netId = $%05X",
                                        tsbk->toString(true).c_str(), tsbk->getEmergency(), tsbk->getEncrypted(), tsbk->getPriority(), tsbk->getGrpVchId(), tsbk->getGrpVchNo(), 
                                        srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str(), sysId, netId);

                                    // generate a net event for this
                                    if (g_netDataEvent != nullptr) {
                                        bool emerg = tsbk->getEmergency();
                                        netEvent["emerg"].set<bool>(emerg);
                                        bool encry = tsbk->getEncrypted();
                                        netEvent["encry"].set<bool>(encry);
                                        uint8_t prio = tsbk->getPriority();
                                        netEvent["prio"].set<uint8_t>(prio);
                                        uint32_t chId = tsbk->getGrpVchId();
                                        netEvent["chId"].set<uint32_t>(chId);
                                        uint32_t chNo = tsbk->getGrpVchNo();
                                        netEvent["chNo"].set<uint32_t>(chNo);

                                        std::string resolvedSrc = resolveRID(srcId);
                                        std::string resolvedDst = resolveTGID(dstId);
                                        netEvent["srcId"].set<uint32_t>(srcId);
                                        netEvent["srcStr"].set<std::string>(resolvedSrc);
                                        netEvent["dstId"].set<uint32_t>(dstId);
                                        netEvent["dstStr"].set<std::string>(resolvedDst);

                                        g_netDataEvent(netEvent);
                                    }
                                }
                                break;
                                case P25DEF::TSBKO::IOSP_UU_ANS:
                                {
                                    lc::tsbk::IOSP_UU_ANS* iosp = static_cast<lc::tsbk::IOSP_UU_ANS*>(tsbk.get());
                                    if (iosp->getResponse() > 0U) {
                                        LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, response = $%02X, srcId = %u (%s), dstId = %u (%s)",
                                            tsbk->toString(true).c_str(), iosp->getResponse(),
                                            srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());

                                        // generate a net event for this
                                        if (g_netDataEvent != nullptr) {
                                            uint8_t response = iosp->getResponse();
                                            netEvent["response"].set<uint8_t>(response);

                                            std::string resolvedSrc = resolveRID(srcId);
                                            std::string resolvedDst = resolveTGID(dstId);
                                            netEvent["srcId"].set<uint32_t>(srcId);
                                            netEvent["srcStr"].set<std::string>(resolvedSrc);
                                            netEvent["dstId"].set<uint32_t>(dstId);
                                            netEvent["dstStr"].set<std::string>(resolvedDst);

                                            g_netDataEvent(netEvent);
                                        }
                                    }
                                }
                                break;
                                case P25DEF::TSBKO::IOSP_STS_UPDT:
                                {
                                    lc::tsbk::IOSP_STS_UPDT* iosp = static_cast<lc::tsbk::IOSP_STS_UPDT*>(tsbk.get());
                                    LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, status = $%02X, srcId = %u (%s)",
                                        tsbk->toString(true).c_str(), iosp->getStatus(), srcId, resolveRID(srcId).c_str());

                                    // generate a net event for this
                                    if (g_netDataEvent != nullptr) {
                                        uint8_t status = iosp->getStatus();
                                        netEvent["status"].set<uint8_t>(status);

                                        std::string resolvedSrc = resolveRID(srcId);
                                        std::string resolvedDst = resolveTGID(dstId);
                                        netEvent["srcId"].set<uint32_t>(srcId);
                                        netEvent["srcStr"].set<std::string>(resolvedSrc);
                                        netEvent["dstId"].set<uint32_t>(dstId);
                                        netEvent["dstStr"].set<std::string>(resolvedDst);

                                        g_netDataEvent(netEvent);
                                    }
                                }
                                break;
                                case P25DEF::TSBKO::IOSP_MSG_UPDT:
                                {
                                    lc::tsbk::IOSP_MSG_UPDT* iosp = static_cast<lc::tsbk::IOSP_MSG_UPDT*>(tsbk.get());
                                    LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, message = $%02X, srcId = %u (%s), dstId = %u (%s)",
                                        tsbk->toString(true).c_str(), iosp->getMessage(), 
                                        srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());

                                    // generate a net event for this
                                    if (g_netDataEvent != nullptr) {
                                        uint8_t message = iosp->getMessage();
                                        netEvent["message"].set<uint8_t>(message);

                                        std::string resolvedSrc = resolveRID(srcId);
                                        std::string resolvedDst = resolveTGID(dstId);
                                        netEvent["srcId"].set<uint32_t>(srcId);
                                        netEvent["srcStr"].set<std::string>(resolvedSrc);
                                        netEvent["dstId"].set<uint32_t>(dstId);
                                        netEvent["dstStr"].set<std::string>(resolvedDst);

                                        g_netDataEvent(netEvent);
                                    }
                                }
                                break;
                                case P25DEF::TSBKO::IOSP_RAD_MON:
                                {
                                    //lc::tsbk::IOSP_RAD_MON* iosp = static_cast<lc::tsbk::IOSP_RAD_MON*>(tsbk.get());
                                    LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, srcId = %u (%s), dstId = %u (%s)", tsbk->toString(true).c_str(), 
                                        srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());

                                    // generate a net event for this
                                    if (g_netDataEvent != nullptr) {
                                        std::string resolvedSrc = resolveRID(srcId);
                                        std::string resolvedDst = resolveTGID(dstId);
                                        netEvent["srcId"].set<uint32_t>(srcId);
                                        netEvent["srcStr"].set<std::string>(resolvedSrc);
                                        netEvent["dstId"].set<uint32_t>(dstId);
                                        netEvent["dstStr"].set<std::string>(resolvedDst);

                                        g_netDataEvent(netEvent);
                                    }
                                }
                                break;
                                case P25DEF::TSBKO::IOSP_CALL_ALRT:
                                {
                                    LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, srcId = %u (%s), dstId = %u (%s)", tsbk->toString(true).c_str(), 
                                        srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                                
                                    // generate a net event for this
                                    if (g_netDataEvent != nullptr) {
                                        std::string resolvedSrc = resolveRID(srcId);
                                        std::string resolvedDst = resolveTGID(dstId);
                                        netEvent["srcId"].set<uint32_t>(srcId);
                                        netEvent["srcStr"].set<std::string>(resolvedSrc);
                                        netEvent["dstId"].set<uint32_t>(dstId);
                                        netEvent["dstStr"].set<std::string>(resolvedDst);

                                        g_netDataEvent(netEvent);
                                    }
                                }
                                break;
                                case P25DEF::TSBKO::IOSP_ACK_RSP:
                                {
                                    lc::tsbk::IOSP_ACK_RSP* iosp = static_cast<lc::tsbk::IOSP_ACK_RSP*>(tsbk.get());
                                    LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, AIV = %u, serviceType = $%02X, srcId = %u (%s), dstId = %u (%s)",
                                        tsbk->toString(true).c_str(), iosp->getAIV(), iosp->getService(), 
                                        srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());

                                    // generate a net event for this
                                    if (g_netDataEvent != nullptr) {
                                        uint8_t service = iosp->getService();
                                        netEvent["service"].set<uint8_t>(service);

                                        std::string resolvedSrc = resolveRID(srcId);
                                        std::string resolvedDst = resolveTGID(dstId);
                                        netEvent["srcId"].set<uint32_t>(srcId);
                                        netEvent["srcStr"].set<std::string>(resolvedSrc);
                                        netEvent["dstId"].set<uint32_t>(dstId);
                                        netEvent["dstStr"].set<std::string>(resolvedDst);

                                        g_netDataEvent(netEvent);
                                    }
                                }
                                break;
                                case P25DEF::TSBKO::IOSP_EXT_FNCT:
                                {
                                    lc::tsbk::IOSP_EXT_FNCT* iosp = static_cast<lc::tsbk::IOSP_EXT_FNCT*>(tsbk.get());
                                    LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, serviceType = $%02X, arg = %u, tgt = %u",
                                        tsbk->toString(true).c_str(), iosp->getService(), srcId, dstId);

                                    // generate a net event for this
                                    if (g_netDataEvent != nullptr) {
                                        uint8_t service = iosp->getService();
                                        netEvent["service"].set<uint8_t>(service);

                                        netEvent["arg"].set<uint32_t>(srcId);

                                        std::string resolvedDst = resolveRID(dstId);
                                        netEvent["dstId"].set<uint32_t>(dstId);
                                        netEvent["dstStr"].set<std::string>(resolvedDst);

                                        g_netDataEvent(netEvent);
                                    }
                                }
                                break;
                                case P25DEF::TSBKO::OSP_DENY_RSP:
                                {
                                    // non-emergency mode is a TSBKO::OSP_DENY_RSP
                                    if (!tsbk->getEmergency()) {
                                        // bryanb: because our TSBKFactory will emit ISP_EMERG_ALRM_REQ for emergency alarm requests, we decoding OSP_DENY_RSP here
                                        lc::tsbk::OSP_DENY_RSP* osp = new lc::tsbk::OSP_DENY_RSP();
                                        osp->decode(data.get());

                                        LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, AIV = %u, reason = $%02X (%s), srcId = %u (%s), dstId = %u (%s)",
                                            osp->toString().c_str(), osp->getAIV(), osp->getResponse(), P25Utils::denyRsnToString(osp->getResponse()).c_str(),
                                            osp->getSrcId(), resolveRID(osp->getSrcId()).c_str(), osp->getDstId(), resolveTGID(osp->getDstId()).c_str());

                                        // generate a net event for this
                                        if (g_netDataEvent != nullptr) {
                                            uint8_t reason = osp->getResponse();
                                            netEvent["reason"].set<uint8_t>(reason);

                                            std::string resolvedSrc = resolveRID(srcId);
                                            std::string resolvedDst = resolveTGID(dstId);
                                            netEvent["srcId"].set<uint32_t>(srcId);
                                            netEvent["srcStr"].set<std::string>(resolvedSrc);
                                            netEvent["dstId"].set<uint32_t>(dstId);
                                            netEvent["dstStr"].set<std::string>(resolvedDst);

                                            g_netDataEvent(netEvent);
                                        }

                                        delete osp;
                                    } else {
                                        LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, srcId = %u (%s), dstId = %u (%s)", tsbk->toString().c_str(), 
                                            srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());

                                        // generate a net event for this
                                        if (g_netDataEvent != nullptr) {
                                            std::string resolvedSrc = resolveRID(srcId);
                                            std::string resolvedDst = resolveTGID(dstId);
                                            netEvent["srcId"].set<uint32_t>(srcId);
                                            netEvent["srcStr"].set<std::string>(resolvedSrc);
                                            netEvent["dstId"].set<uint32_t>(dstId);
                                            netEvent["dstStr"].set<std::string>(resolvedDst);

                                            g_netDataEvent(netEvent);
                                        }
                                    }
                                }
                                break;
                                case P25DEF::TSBKO::IOSP_GRP_AFF:
                                {
                                    lc::tsbk::OSP_GRP_AFF* iosp = static_cast<lc::tsbk::OSP_GRP_AFF*>(tsbk.get());
                                    LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, anncId = %u (%s), srcId = %u (%s), dstId = %u (%s), response = $%02X", tsbk->toString().c_str(),
                                            iosp->getAnnounceGroup(), resolveTGID(iosp->getAnnounceGroup()).c_str(),
                                            srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str(),
                                            iosp->getResponse());

                                    // generate a net event for this
                                    if (g_netDataEvent != nullptr) {
                                        uint32_t anncId = iosp->getAnnounceGroup();
                                        netEvent["anncId"].set<uint32_t>(anncId);
                                        uint8_t resp = iosp->getResponse();
                                        netEvent["response"].set<uint8_t>(resp);

                                        std::string resolvedSrc = resolveRID(srcId);
                                        std::string resolvedDst = resolveTGID(dstId);
                                        netEvent["srcId"].set<uint32_t>(srcId);
                                        netEvent["srcStr"].set<std::string>(resolvedSrc);
                                        netEvent["dstId"].set<uint32_t>(dstId);
                                        netEvent["dstStr"].set<std::string>(resolvedDst);

                                        g_netDataEvent(netEvent);
                                    }
                                }
                                break;
                                case P25DEF::TSBKO::OSP_U_DEREG_ACK:
                                {
                                    //lc::tsbk::OSP_U_DEREG_ACK* iosp = static_cast<lc::tsbk::OSP_U_DEREG_ACK*>(tsbk.get());
                                    LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, srcId = %u (%s)",
                                        tsbk->toString(true).c_str(), srcId, resolveRID(srcId).c_str());

                                    // generate a net event for this
                                    if (g_netDataEvent != nullptr) {
                                        std::string resolvedSrc = resolveRID(srcId);
                                        netEvent["srcId"].set<uint32_t>(srcId);
                                        netEvent["srcStr"].set<std::string>(resolvedSrc);

                                        g_netDataEvent(netEvent);
                                    }
                                }
                                break;
                                case P25DEF::TSBKO::OSP_LOC_REG_RSP:
                                {
                                    lc::tsbk::OSP_LOC_REG_RSP* osp = static_cast<lc::tsbk::OSP_LOC_REG_RSP*>(tsbk.get());
                                    LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, srcId = %u (%s), dstId = %u (%s)", osp->toString().c_str(), 
                                        srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());

                                    // generate a net event for this
                                    if (g_netDataEvent != nullptr) {
                                        std::string resolvedSrc = resolveRID(srcId);
                                        std::string resolvedDst = resolveTGID(dstId);
                                        netEvent["srcId"].set<uint32_t>(srcId);
                                        netEvent["srcStr"].set<std::string>(resolvedSrc);
                                        netEvent["dstId"].set<uint32_t>(dstId);
                                        netEvent["dstStr"].set<std::string>(resolvedDst);

                                        g_netDataEvent(netEvent);
                                    }
                                }
                                break;
                                case P25DEF::TSBKO::OSP_ADJ_STS_BCAST:
                                {
                                    lc::tsbk::OSP_ADJ_STS_BCAST* osp = static_cast<lc::tsbk::OSP_ADJ_STS_BCAST*>(tsbk.get());
                                    LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, sysId = $%03X, rfss = $%02X, site = $%02X, chNo = %u-%u, svcClass = $%02X", tsbk->toString().c_str(),
                                        osp->getAdjSiteSysId(), osp->getAdjSiteRFSSId(), osp->getAdjSiteId(), osp->getAdjSiteChnId(), osp->getAdjSiteChnNo(), osp->getAdjSiteSvcClass());

                                    // generate a net event for this
                                    if (g_netDataEvent != nullptr) {
                                        uint32_t sysId = osp->getAdjSiteSysId();
                                        netEvent["sysId"].set<uint32_t>(sysId);
                                        uint8_t rfssId = osp->getAdjSiteRFSSId();
                                        netEvent["rfssId"].set<uint8_t>(rfssId);
                                        uint8_t siteId = osp->getAdjSiteId();
                                        netEvent["siteId"].set<uint8_t>(siteId);
                                        uint8_t chId = osp->getAdjSiteChnId();
                                        netEvent["chId"].set<uint8_t>(chId);
                                        uint32_t chNo = osp->getAdjSiteChnNo();
                                        netEvent["chNo"].set<uint32_t>(chNo);
                                        uint8_t svcClass = osp->getAdjSiteSvcClass();
                                        netEvent["svcClass"].set<uint8_t>(svcClass);

                                        g_netDataEvent(netEvent);
                                    }
                                }
                                break;
                                default:
                                    {
                                        LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, srcId = %u (%s), dstId = %u (%s)", tsbk->toString().c_str(), 
                                            srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());

                                        // generate a net event for this
                                        if (g_netDataEvent != nullptr) {
                                            std::string resolvedSrc = resolveRID(srcId);
                                            std::string resolvedDst = resolveTGID(dstId);
                                            netEvent["srcId"].set<uint32_t>(srcId);
                                            netEvent["srcStr"].set<std::string>(resolvedSrc);
                                            netEvent["dstId"].set<uint32_t>(dstId);
                                            netEvent["dstStr"].set<std::string>(resolvedDst);

                                            g_netDataEvent(netEvent);
                                        }
                                    }
                                    break;
                            }
                        }
                        break;
                    }

                    if (g_debug)
                        LogInfoEx(LOG_NET, "P25, duid = $%02X, lco = $%02X, MFId = $%02X, srcId = %u, dstId = %u, len = %u", duid, lco, MFId, srcId, dstId, length);
                }

                UInt8Array nxdnBuffer = g_network->readNXDN(netReadRet, length);
                if (netReadRet) {
                    using namespace nxdn;

                    uint32_t streamId = g_network->getRxNXDNStreamId();

                    uint8_t messageType = nxdnBuffer[4U];

                    uint32_t srcId = GET_UINT24(nxdnBuffer, 5U);
                    uint32_t dstId = GET_UINT24(nxdnBuffer, 8U);

                    lc::RTCH lc;

                    lc.setMessageType(messageType);
                    lc.setSrcId((uint16_t)srcId & 0xFFFFU);
                    lc.setDstId((uint16_t)dstId & 0xFFFFU);

                    bool group = (nxdnBuffer[15U] & 0x40U) == 0x40U ? false : true;
                    lc.setGroup(group);

                    // process raw NXDN data bytes
                    UInt8Array frame;
                    uint8_t frameLength = nxdnBuffer[23U];
                    if (frameLength <= 24) {
                        frame = std::unique_ptr<uint8_t[]>(new uint8_t[frameLength]);
                        ::memset(frame.get(), 0x00U, frameLength);
                    }
                    else {
                        frame = std::unique_ptr<uint8_t[]>(new uint8_t[frameLength]);
                        ::memset(frame.get(), 0x00U, frameLength);
                        ::memcpy(frame.get(), nxdnBuffer.get() + 24U, frameLength);
                    }

                    NXDNUtils::scrambler(frame.get() + 2U);

                    channel::LICH lich;
                    bool valid = lich.decode(frame.get() + 2U);

                    // specifically only check the following logic for end of call, voice or data frames
                    if ((messageType == NXDDEF::MessageType::RTCH_TX_REL || messageType == NXDDEF::MessageType::RTCH_TX_REL_EX) ||
                        (messageType == NXDDEF::MessageType::RTCH_VCALL || messageType == NXDDEF::MessageType::RTCH_DCALL_HDR ||
                        messageType == NXDDEF::MessageType::RTCH_DCALL_DATA)) {
                        // is this the end of the call stream?
                        if (messageType == NXDDEF::MessageType::RTCH_TX_REL || messageType == NXDDEF::MessageType::RTCH_TX_REL_EX) {
                            if (srcId == 0U && dstId == 0U) {
                                LogWarning(LOG_NET, "NXDN, invalid TX_REL, srcId = %u (%s), dstId = %u (%s)", 
                                    srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                            }

                            RxStatus status = g_nxdnStatus[dstId];
                            uint64_t duration = hrc::diff(pktTime, status.callStartTime);

                            if (std::find_if(g_nxdnStatus.begin(), g_nxdnStatus.end(), [&](StatusMapPair& x) { return x.second.dstId == dstId; }) != g_nxdnStatus.end()) {
                                g_nxdnStatus.erase(dstId);

                                LogInfoEx(LOG_NET, "NXDN, Call End, srcId = %u (%s), dstId = %u (%s), duration = %u",
                                    srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str(), duration / 1000);
                                emitCallStartEndEvent(srcId, dstId, 0U, status.streamId, "nxdn", true, duration / 1000);
                            }
                        }

                        // is this a new call stream?
                        if ((messageType != NXDDEF::MessageType::RTCH_TX_REL && messageType != NXDDEF::MessageType::RTCH_TX_REL_EX)) {
                            if (srcId == 0U && dstId == 0U) {
                                LogWarning(LOG_NET, "NXDN, invalid call, srcId = %u (%s), dstId = %u (%s)", 
                                    srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                            }

                            auto it = std::find_if(g_nxdnStatus.begin(), g_nxdnStatus.end(), [&](StatusMapPair& x) { return x.second.dstId == dstId; });
                            if (it == g_nxdnStatus.end()) {
                                // this is a new call stream
                                RxStatus status = RxStatus();
                                status.callStartTime = pktTime;
                                status.srcId = srcId;
                                status.dstId = dstId;
                                status.streamId = streamId;
                                g_nxdnStatus[dstId] = status;

                                LogInfoEx(LOG_NET, "NXDN, Call Start, srcId = %u (%s), dstId = %u (%s)", 
                                    srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                                emitCallStartEndEvent(srcId, dstId, 0U, status.streamId, "nxdn");
                            }
                        }
                    }

                    std::unique_ptr<lc::RCCH> rcch;
                    if (valid) {
                        NXDDEF::RFChannelType::E rfct = lich.getRFCT();
                        NXDDEF::FuncChannelType::E fct = lich.getFCT();
                        NXDDEF::ChOption::E option = lich.getOption();

                        if (rfct == NXDDEF::RFChannelType::RCCH) {
                            rcch = lc::rcch::RCCHFactory::createRCCH(frame.get(), frameLength);

                            json::object netEvent = json::object();
                            std::string type = "rcch";
                            netEvent["type"].set<std::string>(type);

                            uint8_t lco = rcch->getMessageType();
                            netEvent["opcode"].set<uint8_t>(lco);
                            std::string desc = rcch->toString();
                            netEvent["desc"].set<std::string>(desc);

                            // report raw RCCH bytes in hex format
                            uint8_t* rcchRaw = rcch.get()->getDecodedRaw();
                            std::stringstream ss;
                            ss << std::hex << std::setfill('0');
                            for (uint8_t i = 0; i < NXDDEF::NXDN_RCCH_LC_LENGTH_BYTES; i++) {
                                ss << std::setw(2) << (int)rcchRaw[i];
                            }
                            netEvent["data"].set<std::string>(ss.str());

                            // generate a net event for this
                            if (g_netDataEvent != nullptr) {
                                std::string resolvedSrc = resolveRID(srcId);
                                std::string resolvedDst = resolveTGID(dstId);
                                netEvent["srcId"].set<uint32_t>(srcId);
                                netEvent["srcStr"].set<std::string>(resolvedSrc);
                                netEvent["dstId"].set<uint32_t>(dstId);
                                netEvent["dstStr"].set<std::string>(resolvedDst);

                                g_netDataEvent(netEvent);
                            }
                        }
                    }

                    if (g_debug)
                        LogInfoEx(LOG_NET, "NXDN, messageType = $%02X, srcId = %u, dstId = %u, len = %u", messageType, srcId, dstId, length);
                }

                UInt8Array analogBuffer = g_network->readAnalog(netReadRet, length);
                if (netReadRet) {
                    using namespace analog;

                    uint32_t streamId = g_network->getRxAnalogStreamId();

                    uint32_t srcId = GET_UINT24(analogBuffer, 5U);
                    uint32_t dstId = GET_UINT24(analogBuffer, 8U);

                    ANODEF::AudioFrameType::E frameType = (ANODEF::AudioFrameType::E)(analogBuffer[15U] & 0x0FU);

                    // specifically only check the following logic for end of call, voice or data frames
                    if (frameType == ANODEF::AudioFrameType::TERMINATOR) {
                        if (srcId == 0U && dstId == 0U) {
                            LogWarning(LOG_NET, "Analog, invalid TX_REL, srcId = %u (%s), dstId = %u (%s)", 
                                srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                        }

                        RxStatus status = g_analogStatus[dstId];
                        uint64_t duration = hrc::diff(pktTime, status.callStartTime);

                        if (std::find_if(g_analogStatus.begin(), g_analogStatus.end(), [&](StatusMapPair& x) { return x.second.dstId == dstId; }) != g_analogStatus.end()) {
                            g_analogStatus.erase(dstId);

                            LogInfoEx(LOG_NET, "Analog, Call End, srcId = %u (%s), dstId = %u (%s), duration = %u",
                                srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str(), duration / 1000);
                            emitCallStartEndEvent(srcId, dstId, 0U, status.streamId, "analog", true, duration / 1000);
                        }
                    }

                    // is this a new call stream?
                    if (frameType == ANODEF::AudioFrameType::VOICE_START) {
                        if (srcId == 0U && dstId == 0U) {
                            LogWarning(LOG_NET, "Analog, invalid call, srcId = %u (%s), dstId = %u (%s)", 
                                srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                        }

                        auto it = std::find_if(g_analogStatus.begin(), g_analogStatus.end(), [&](StatusMapPair& x) { return x.second.dstId == dstId; });
                        if (it == g_analogStatus.end()) {
                            // this is a new call stream
                            RxStatus status = RxStatus();
                            status.callStartTime = pktTime;
                            status.srcId = srcId;
                            status.dstId = dstId;
                            status.streamId = streamId;
                            g_analogStatus[dstId] = status;

                            LogInfoEx(LOG_NET, "Analog, Call Start, srcId = %u (%s), dstId = %u (%s)", 
                                srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str());
                            emitCallStartEndEvent(srcId, dstId, 0U, status.streamId, "analog");
                        }
                    }

                    if (g_debug)
                        LogInfoEx(LOG_NET, "Analog, frameType = $%02X, srcId = %u, dstId = %u, len = %u", frameType, srcId, dstId, length);
                }
            }

            if (ms < 2U)
                Thread::sleep(1U);
        }

        LogInfoEx(LOG_HOST, "[STOP] %s", threadName.c_str());
        delete th;
    }

    return nullptr;
}

/* Starts the network pump worker thread. */

bool startNetworkPumpThread()
{
    return Thread::runAsThread(nullptr, threadNetworkPump);
}

/* Helper to pring usage the command line arguments. (And optionally an error.) */

void usage(const char* message, const char* arg)
{
    ::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__);
    ::fprintf(stdout, "Copyright (c) 2017-2026 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\n");
    ::fprintf(stdout, "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\n\n");
    if (message != nullptr) {
        ::fprintf(stderr, "%s: ", g_progExe.c_str());
        ::fprintf(stderr, message, arg);
        ::fprintf(stderr, "\n\n");
    }

    ::fprintf(stdout, 
        "usage: %s [-dvh]"
        "[--hide-log]"
        "[-c <configuration file>]"
#if !defined(NO_WEBSOCKETS)
        "[-f][-ws]"
#endif // !defined(NO_WEBSOCKETS)
        "\n\n"
        "  -d                          enable debug\n"
        "  -v                          show version information\n"
        "  -h                          show this screen\n"
        "\n"
        "  --hide-log                  hide interactive logging window on startup\n"
        "\n"
        "  -c <file>                   specifies the system view configuration file to use\n"
        "\n"
#if !defined(NO_WEBSOCKETS)
        "  -f                          foreground mode\n"
        "  -ws                         websocket mode\n"
        "\n"
#endif // !defined(NO_WEBSOCKETS)
        "  --                          stop handling options\n",
        g_progExe.c_str());

    exit(EXIT_FAILURE);
}

/* Helper to validate the command line arguments. */

int checkArgs(int argc, char* argv[])
{
    int i, p = 0;

    // iterate through arguments
    for (i = 1; i <= argc; i++)
    {
        if (argv[i] == nullptr) {
            break;
        }

        if (*argv[i] != '-') {
            continue;
        }
        else if (IS("--")) {
            ++p;
            break;
        }
        else if (IS("-c")) {
            if (argc-- <= 0)
                usage("error: %s", "must specify the monitor configuration file to use");
            g_iniFile = std::string(argv[++i]);

            if (g_iniFile.empty())
                usage("error: %s", "monitor configuration file cannot be blank!");

            p += 2;
        }
        else if (IS("--hide-log")) {
            ++p;
            g_hideLoggingWnd = true;
        }
#if !defined(NO_WEBSOCKETS)
        else if (IS("-f")) {
            ++p;
            g_foreground = true;
        }
        else if (IS("-ws")) {
            ++p;
            g_webSocketMode = true;
        }
#endif // !defined(NO_WEBSOCKETS)
        else if (IS("-d")) {
            ++p;
            g_debug = true;
        }
        else if (IS("-v")) {
            ::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__);
            ::fprintf(stdout, "Copyright (c) 2017-2026 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\n");
            ::fprintf(stdout, "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\n\n");
            if (argc == 2)
                exit(EXIT_SUCCESS);
        }
        else if (IS("-h")) {
            usage(nullptr, nullptr);
            if (argc == 2)
                exit(EXIT_SUCCESS);
        }
        else {
            usage("unrecognized option `%s'", argv[i]);
        }
    }

    if (p < 0 || p > argc) {
        p = 0;
    }

    return ++p;
}

// ---------------------------------------------------------------------------
//  Program Entry Point
// ---------------------------------------------------------------------------

int main(int argc, char** argv)
{
    if (argv[0] != nullptr && *argv[0] != 0)
        g_progExe = std::string(argv[0]);

    if (argc > 1) {
        // check arguments
        int i = checkArgs(argc, argv);
        if (i < argc) {
            argc -= i;
            argv += i;
        }
        else {
            argc--;
            argv++;
        }
    }

    // initialize system logging
    bool ret = ::LogInitialise("", "", 0U, 1U);
    if (!ret) {
        ::fprintf(stderr, "unable to open the log file\n");
        return 1;
    }

    ::LogInfo(__PROG_NAME__ " " __VER__ " (built " __BUILD__ ")\r\n" \
        "Copyright (c) 2017-2026 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\r\n" \
        "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\r\n" \
        ">> FNE System View\r\n");

    try {
        ret = yaml::Parse(g_conf, g_iniFile.c_str());
        if (!ret) {
            ::fatal("cannot read the configuration file, %s\n", g_iniFile.c_str());
        }
    }
    catch (yaml::OperationException const& e) {
        ::fatal("cannot read the configuration file - %s (%s)", g_iniFile.c_str(), e.message());
    }

    bool wsDaemonMode = false;
#if !defined(NO_WEBSOCKETS)
    if (g_webSocketMode) {
        bool daemon = g_conf["daemon"].as<bool>(false);
        wsDaemonMode = daemon && !g_foreground;
    }
#endif // !defined(NO_WEBSOCKETS)

    // In websocket daemon mode HostWS forks, so the child must start the network pump.
    if (!wsDaemonMode) {
        if (!startNetworkPumpThread())
            return EXIT_FAILURE;
    }

    finalcut::FApplication::setColorTheme<dvmColorTheme>();

    // setup the finalcut tui
    SysViewApplication* app = nullptr;
    SysViewMainWnd* wnd = nullptr;
    if (!g_webSocketMode) {
        app = new SysViewApplication(argc, argv);
        g_app = app;

        wnd = new SysViewMainWnd(app);
        finalcut::FWidget::setMainWidget(wnd);
    } else {
        // in WebSocket mode install signal handlers
        ::signal(SIGINT, sigHandler);
        ::signal(SIGTERM, sigHandler);
        ::signal(SIGHUP, sigHandler);
    }

    // try to load bandplan identity table
    std::string idenLookupFile = g_conf["iden_table"]["file"].as<std::string>();
    uint32_t idenReloadTime = g_conf["iden_table"]["time"].as<uint32_t>(0U);

    if (idenLookupFile.length() <= 0U) {
        ::LogError(LOG_HOST, "No bandplan identity table? This must be defined!");
        return 1;
    }

    if (!g_webSocketMode)
        g_logDisplayLevel = 0U;

    // try to load radio IDs table
    std::string ridLookupFile = g_conf["radio_id"]["file"].as<std::string>();
    uint32_t ridReloadTime = g_conf["radio_id"]["time"].as<uint32_t>(0U);

    LogInfo("Radio Id Lookups");
    LogInfo("    File: %s", ridLookupFile.length() > 0U ? ridLookupFile.c_str() : "None");
    if (ridReloadTime > 0U)
        LogInfo("    Reload: %u mins", ridReloadTime);

    g_ridLookup = new RadioIdLookup(ridLookupFile, ridReloadTime, false);
    g_ridLookup->read();

    // try to load talkgroup IDs table
    std::string tidLookupFile = g_conf["talkgroup_rules"]["file"].as<std::string>();
    uint32_t tidReloadTime = g_conf["talkgroup_rules"]["time"].as<uint32_t>(0U);

    LogInfo("Talkgroup Rule Lookups");
    LogInfo("    File: %s", tidLookupFile.length() > 0U ? tidLookupFile.c_str() : "None");
    if (tidReloadTime > 0U)
        LogInfo("    Reload: %u mins", tidReloadTime);

    g_tidLookup = new TalkgroupRulesLookup(tidLookupFile, tidReloadTime, false);
    g_tidLookup->read();

    LogInfo("Iden Table Lookups");
    LogInfo("    File: %s", idenLookupFile.length() > 0U ? idenLookupFile.c_str() : "None");
    if (idenReloadTime > 0U)
        LogInfo("    Reload: %u mins", idenReloadTime);

    g_idenTable = new IdenTableLookup(idenLookupFile, idenReloadTime);
    g_idenTable->read();

    int _errno = 0U;
    if (!g_webSocketMode) {
        // show and start the application
        wnd->show();

        app->resetColors();
        app->redraw();
        
        _errno = app->exec();

        if (wnd != nullptr)
            delete wnd;
        if (app != nullptr)
            delete app;
    } else {
#if !defined(NO_WEBSOCKETS)
        ::LogFinalise(); // HostWS will reinitialize logging after this point...

        HostWS* host = new HostWS(g_iniFile);
        _errno = host->run();
        delete host;
#endif // !defined(NO_WEBSOCKETS)
    }

    g_logDisplayLevel = 1U;

    g_killed = true;
    if (g_network != nullptr) {
        g_network->close();
        delete g_network;
    }

    ::LogFinalise();
    return _errno;
}
