// SPDX-License-Identifier: GPL-2.0-only
/*
 * Digital Voice Modem - Common Library
 * GPLv2 Open Source. Use is subject to license terms.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 *  Copyright (C) 2022,2024,2026 Bryan Biedenkapp, N2PLL
 *
 */
#include "lookups/AffiliationLookup.h"
#include "Log.h"

using namespace lookups;

#include <cassert>

// ---------------------------------------------------------------------------
//  Constants
// ---------------------------------------------------------------------------

const uint32_t UNIT_REG_TIMEOUT = 43200U; // 12 hours
const uint32_t GRP_AFF_TIMEOUT = 43200U; // 12 hours

// ---------------------------------------------------------------------------
//  Public Class Members
// ---------------------------------------------------------------------------

/* Initializes a new instance of the AffiliationLookup class. */

AffiliationLookup::AffiliationLookup(const std::string name, ChannelLookup* channelLookup, bool verbose) :
    m_rfGrantChCnt(0U),
    m_unitRegTable(),
    m_unitRegTimers(),
    m_grpAffTable(),
    m_grpAffTimers(),
    m_grantChTable(),
    m_grantSrcIdTable(),
    m_uuGrantedTable(),
    m_netGrantedTable(),
    m_grantTimers(),
    m_grantCallTimers(),
    m_releaseGrant(nullptr),
    m_unitDereg(nullptr),
    m_name(),
    m_chLookup(channelLookup),
    m_disableUnitRegTimeout(false),
    m_disableGrpAffTimeout(false),
    m_verbose(verbose)
{
    m_name = name;

    m_unitRegTable.clear();
    m_unitRegTimers.clear();
    m_grpAffTable.clear();
    m_grpAffTimers.clear();

    m_grantChTable.clear();
    m_grantSrcIdTable.clear();
    m_grantTimers.clear();
}

/* Finalizes a instance of the AffiliationLookup class. */

AffiliationLookup::~AffiliationLookup() = default;

/* Helper to group affiliate a source ID. */

void AffiliationLookup::unitReg(uint32_t srcId)
{
    if (isUnitReg(srcId)) {
        return;
    }

    __lock();

    m_unitRegTable.push_back(srcId);

    m_unitRegTimers[srcId] = Timer(1000U, UNIT_REG_TIMEOUT);
    m_unitRegTimers[srcId].start();

    if (m_verbose) {
        LogInfoEx(LOG_HOST, "%s, unit registration, srcId = %u",
            m_name.c_str(), srcId);
    }

    __unlock();
}

/* Helper to group unaffiliate a source ID. */

bool AffiliationLookup::unitDereg(uint32_t srcId, bool automatic)
{
    bool ret = false;

    if (!isUnitReg(srcId)) {
        return false;
    }

    groupUnaff(srcId);

    __lock();

    if (m_verbose) {
        LogInfoEx(LOG_HOST, "%s, unit deregistration, srcId = %u",
            m_name.c_str(), srcId);
    }

    m_unitRegTimers[srcId].stop();

    // remove dynamic unit registration table entry
    m_unitRegTable.lock(false);
    if (std::find(m_unitRegTable.begin(), m_unitRegTable.end(), srcId) != m_unitRegTable.end()) {
        auto it = std::find(m_unitRegTable.begin(), m_unitRegTable.end(), srcId);
        m_unitRegTable.unlock();
        m_unitRegTable.erase(it);
        ret = true;
    } else {
        m_unitRegTable.unlock();
    }

    if (ret) {
        if (m_unitDereg != nullptr) {
            m_unitDereg(srcId, automatic);
        }
    }

    __unlock();

    return ret;
}

/* Helper to start the source ID registration timer. */

void AffiliationLookup::touchUnitReg(uint32_t srcId)
{
    if (srcId == 0U) {
        return;
    }

    __spinlock();

    // restart the unit registration timer
    if (isUnitReg(srcId)) {
        m_unitRegTimers[srcId].start();
    }

    // restart the group affiliation timer if the source ID is group affiliated
    if (isSrcIdGrpAff(srcId)) {
        m_grpAffTimers[srcId].start();
    }
}

/* Gets the current timer timeout for this unit registration. */

uint32_t AffiliationLookup::unitRegTimeout(uint32_t srcId)
{
    if (srcId == 0U) {
        return 0U;
    }

    __spinlock();

    if (isUnitReg(srcId)) {
        return m_unitRegTimers[srcId].getTimeout();
    }

    return 0U;
}

/* Gets the current timer value for this unit registration. */

uint32_t AffiliationLookup::unitRegTimer(uint32_t srcId)
{
    if (srcId == 0U) {
        return 0U;
    }

    __spinlock();

    if (isUnitReg(srcId)) {
        return m_unitRegTimers[srcId].getTimer();
    }

    return 0U;
}

/* Helper to determine if the source ID has unit registered. */

bool AffiliationLookup::isUnitReg(uint32_t srcId) const
{
    __spinlock();

    // lookup dynamic unit registration table entry
    m_unitRegTable.lock(false);
    if (std::find(m_unitRegTable.begin(), m_unitRegTable.end(), srcId) != m_unitRegTable.end()) {
        m_unitRegTable.unlock();
        return true;
    }
    else {
        m_unitRegTable.unlock();
        return false;
    }
}

/* Helper to release unit registrations. */

void AffiliationLookup::clearUnitReg()
{
    __lock();
    LogWarning(LOG_HOST, "%s, releasing all unit registrations", m_name.c_str());
    m_unitRegTable.clear();
    __unlock();
}

/* Helper to group affiliate a source ID. */

void AffiliationLookup::groupAff(uint32_t srcId, uint32_t dstId)
{
    if (!isGroupAff(srcId, dstId)) {
        __lock();

        // update dynamic affiliation table
        m_grpAffTable[srcId] = dstId;

        m_grpAffTimers[srcId] = Timer(1000U, GRP_AFF_TIMEOUT);
        m_grpAffTimers[srcId].start();

        if (m_verbose) {
            LogInfoEx(LOG_HOST, "%s, group affiliation, srcId = %u, dstId = %u",
                m_name.c_str(), srcId, dstId);
        }

        __unlock();
    }
}

/* Helper to group unaffiliate a source ID. */

bool AffiliationLookup::groupUnaff(uint32_t srcId)
{
    __lock();

    // lookup dynamic affiliation table entry
    auto it = m_grpAffTable.find(srcId);
    if (it != m_grpAffTable.end()) {
        if (m_verbose) {
            LogInfoEx(LOG_HOST, "%s, group unaffiliation, srcId = %u, dstId = %u",
                m_name.c_str(), srcId, it->second);
        }

        m_grpAffTable.erase(srcId);

        m_grpAffTimers[srcId].stop();

        __unlock();
        return true;
    } 

    __unlock();
    return false;
}

/* Helper to determine if the group destination ID has any affiations. */

bool AffiliationLookup::hasGroupAff(uint32_t dstId) const
{
    __spinlock();

    // lookup dynamic affiliation table entry
    m_grpAffTable.lock(false);
    for (auto entry : m_grpAffTable) {
        if (entry.second == dstId) {
            m_grpAffTable.unlock();
            return true;
        }
    }
    m_grpAffTable.unlock();

    return false;
}

/* Helper to determine if the source ID has affiliated to the group destination ID. */

bool AffiliationLookup::isGroupAff(uint32_t srcId, uint32_t dstId) const
{
    __spinlock();

    // lookup dynamic affiliation table entry
    m_grpAffTable.lock(false);
    auto it = m_grpAffTable.find(srcId);
    if (it != m_grpAffTable.end()) {
        if (it->second == dstId) {
            m_grpAffTable.unlock();
            return true;
        }
    }
    m_grpAffTable.unlock();

    return false;
}

/* Helper to release group affiliations. */

std::vector<uint32_t> AffiliationLookup::clearGroupAff(uint32_t dstId, bool releaseAll)
{
    std::vector<uint32_t> srcToRel = std::vector<uint32_t>();
    if (dstId == 0U && !releaseAll) {
        return srcToRel;
    }

    if (dstId == 0U && releaseAll) {
        LogWarning(LOG_HOST, "%s, releasing all group affiliations", m_name.c_str());
        for (auto entry : m_grpAffTable) {
            uint32_t srcId = entry.first;
            srcToRel.push_back(srcId);
        }
    }
    else {
        LogWarning(LOG_HOST, "%s, releasing group affiliations, dstId = %u", m_name.c_str(), dstId);
        for (auto entry : m_grpAffTable) {
            uint32_t srcId = entry.first;
            uint32_t grpId = entry.second;
            if (grpId == dstId) {
                srcToRel.push_back(srcId);
            }
        }
    }

    __lock();

    for (auto srcId : srcToRel) {
        m_grpAffTable.erase(srcId);
    }

    __unlock();

    return srcToRel;
}

/* Helper to grant a channel. */

bool AffiliationLookup::grantCh(uint32_t dstId, uint32_t srcId, uint32_t grantTimeout, bool grp, bool netGranted)
{
    if (dstId == 0U) {
        return false;
    }

    if (m_chLookup == nullptr) {
        return false;
    }

    uint32_t chNo = 0U;
    if (!m_chLookup->takeFirstRFChannel(chNo)) {
        return false;
    }

    __lock();

    m_grantChTable[dstId] = chNo;
    m_grantSrcIdTable[dstId] = srcId;
    m_rfGrantChCnt++;

    m_uuGrantedTable[dstId] = !grp;
    m_netGrantedTable[dstId] = netGranted;

    m_grantTimers[dstId] = Timer(1000U, grantTimeout);
    m_grantTimers[dstId].start();
    m_grantCallTimers[dstId] = StopWatch();
    m_grantCallTimers[dstId].start();

    if (m_verbose) {
        LogInfoEx(LOG_HOST, "%s, granting channel, chNo = %u, dstId = %u, srcId = %u, group = %u",
            m_name.c_str(), chNo, dstId, srcId, grp);
    }

    __unlock();

    return true;
}

/* Helper to start the destination ID grant timer. */

void AffiliationLookup::touchGrant(uint32_t dstId)
{
    if (dstId == 0U) {
        return;
    }

    /*
    ** bryanb: this doesn't __spinlock(), this is dangerous but necessary
    **  otherwise an affiliations lookup lock can cause audio cuts if this is
    **  used in an audio processing chain
    */

    m_grantTimers.lock(false);
    if (isGranted(dstId)) {
        m_grantTimers[dstId].start();
    }
    m_grantTimers.unlock();
}

/* Helper to release the channel grant for the destination ID. */

bool AffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll)
{
    if (dstId == 0U && !releaseAll) {
        return false;
    }

    // are we trying to release all grants?
    if (dstId == 0U && releaseAll) {
        LogWarning(LOG_HOST, "%s, force releasing all channel grants", m_name.c_str());

        m_grantChTable.lock(false);
        std::vector<uint32_t> gntsToRel = std::vector<uint32_t>();
        for (auto entry : m_grantChTable) {
            uint32_t dstId = entry.first;
            gntsToRel.push_back(dstId);
        }
        m_grantChTable.unlock();

        // release grants
        for (uint32_t dstId : gntsToRel) {
            releaseGrant(dstId, false);
        }

        return true;
    }

    if (isGranted(dstId)) {
        uint32_t chNo = 0U;
        m_grantChTable.lock(false);
        for (auto entry : m_grantChTable) {
            if (entry.first == dstId) {
                chNo = entry.second;
                break;
            }
        }
        m_grantChTable.unlock();

        uint32_t srcId = getGrantedSrcId(dstId);

        if (m_verbose) {
            LogInfoEx(LOG_HOST, "%s, releasing channel grant, chNo = %u, dstId = %u",
                m_name.c_str(), chNo, dstId);
        }

        if (m_releaseGrant != nullptr) {
            m_releaseGrant(chNo, srcId, dstId, 0U);
        }

        __lock();

        m_grantChTable.erase(dstId);
        m_grantSrcIdTable.erase(dstId);
        m_uuGrantedTable.erase(dstId);
        m_netGrantedTable.erase(dstId);

        m_grantCallTimers.erase(dstId);

        if (m_chLookup != nullptr) {
            m_chLookup->freeRFCh(chNo);
        }

        if (m_rfGrantChCnt > 0U) {
            m_rfGrantChCnt--;
        }
        else {
            m_rfGrantChCnt = 0U;
        }

        m_grantTimers[dstId].stop();

        __unlock();

        return true;
    }

    return false;
}

/* Helper to determine if the channel number is busy. */

bool AffiliationLookup::isChBusy(uint32_t chNo) const
{
    if (chNo == 0U) {
        return false;
    }

    __spinlock();

    // lookup dynamic channel grant table entry
    m_grantChTable.lock(false);
    for (auto entry : m_grantChTable) {
        if (entry.second == chNo) {
            m_grantChTable.unlock();
            return true;
        }
    }
    m_grantChTable.unlock();

    return false;
}

/* Helper to determine if the destination ID is already granted. */

bool AffiliationLookup::isGranted(uint32_t dstId) const
{
    if (dstId == 0U) {
        return false;
    }

    /*
    ** bryanb: this doesn't __spinlock(), this is dangerous but necessary
    **  otherwise an affiliations lookup lock can cause audio cuts if this is
    **  used in an audio processing chain
    */

    // lookup dynamic channel grant table entry
    m_grantChTable.lock(false);
    for (auto entry : m_grantChTable) {
        uint32_t gntDstId = entry.first;
        uint32_t chNo = entry.second;

        if (gntDstId == dstId && chNo != 0U) {
            m_grantChTable.unlock();
            return true;
        }
    }
    m_grantChTable.unlock();

    return false;
}

/* Helper to determine if the destination ID is network granted. */

bool AffiliationLookup::isGroup(uint32_t dstId) const
{
    if (dstId == 0U) {
        return true;
    }

    __spinlock();

    // lookup U-U grant flag table entry
    m_uuGrantedTable.lock(false);
    for (auto entry : m_uuGrantedTable) {
        uint32_t gntDstId = entry.first;
        bool uu = entry.second;

        if (gntDstId == dstId) {
            m_uuGrantedTable.unlock();
            return !uu;
        }
    }
    m_uuGrantedTable.unlock();

    return true;
}

/* Helper to determine if the destination ID is network granted. */

bool AffiliationLookup::isNetGranted(uint32_t dstId) const
{
    if (dstId == 0U) {
        return false;
    }

    __spinlock();

    // lookup net granted flag table entry
    m_netGrantedTable.lock(false);
    for (auto entry : m_netGrantedTable) {
        uint32_t gntDstId = entry.first;
        bool net = entry.second;

        if (gntDstId == dstId) {
            m_netGrantedTable.unlock();
            return net;
        }
    }
    m_netGrantedTable.unlock();

    return false;
}

/* Helper to get the channel granted for the given destination ID. */

uint32_t AffiliationLookup::getGrantedCh(uint32_t dstId)
{
    if (dstId == 0U) {
        return 0U;
    }

    __spinlock();

    if (isGranted(dstId)) {
        // lookup dynamic channel grant table entry
        m_grantChTable.lock(false);
        auto it = m_grantChTable.find(dstId);
        if (it != m_grantChTable.end()) {
            uint32_t chNo = it->second;
            m_grantChTable.unlock();
            return chNo;
        }
        m_grantChTable.unlock();
    }

    return 0U;
}

/* Helper to get the destination ID for the given channel. */

uint32_t AffiliationLookup::getGrantedDstByCh(uint32_t chNo)
{
    __spinlock();

    // lookup dynamic channel grant table entry
    m_grantChTable.lock(false);
    for (auto entry : m_grantChTable) {
        if (entry.second == chNo) {
            m_grantChTable.unlock();
            return entry.first;
        }
    }
    m_grantChTable.unlock();

    return 0U;
}

/* Helper to get the destination ID granted to the given source ID. */

uint32_t AffiliationLookup::getGrantedBySrcId(uint32_t srcId)
{
    if (srcId == 0U) {
        return 0U;
    }

    __spinlock();

    // lookup dynamic channel grant source table entry
    m_grantSrcIdTable.lock(false);
    for (auto entry : m_grantSrcIdTable) {
        if (entry.second == srcId) {
            m_grantSrcIdTable.unlock();
            return entry.first;
        }
    }
    m_grantSrcIdTable.unlock();

    return 0U;
}

/* Helper to get the source ID granted for the given destination ID. */

uint32_t AffiliationLookup::getGrantedSrcId(uint32_t dstId)
{
    if (dstId == 0U) {
        return 0U;
    }

    __spinlock();

    if (isGranted(dstId)) {
        // lookup dynamic channel grant source table entry
        m_grantSrcIdTable.lock(false);
        auto it = m_grantSrcIdTable.find(dstId);
        if (it != m_grantSrcIdTable.end()) {
            uint32_t srcId = it->second;
            m_grantSrcIdTable.unlock();
            return srcId;
        }
        m_grantSrcIdTable.unlock();
    }

    return 0U;
}

/* Helper to get the current elapsed time for the given destination ID. */

uint32_t AffiliationLookup::getGrantCallElapsed(uint32_t dstId)
{
    if (dstId == 0U) {
        return 0U;
    }

    __spinlock();

    if (isGranted(dstId)) {
        // lookup dynamic channel grant timer table entry
        m_grantCallTimers.lock(false);
        auto it = m_grantCallTimers.find(dstId);
        if (it != m_grantCallTimers.end()) {
            uint32_t time = it->second.elapsed();

            m_grantCallTimers.unlock();
            return time;
        }
        m_grantCallTimers.unlock();
    }

    return 0U;
}

/* Updates the processor by the passed number of milliseconds. */

void AffiliationLookup::clock(uint32_t ms)
{
    m_grantChTable.spinlock();

    // clock all the grant timers
    m_grantChTable.lock(false);
    std::vector<uint32_t> gntsToRel = std::vector<uint32_t>();
    for (auto entry : m_grantChTable) {
        uint32_t dstId = entry.first;

        auto it = m_grantTimers.find(dstId);
        if (it != m_grantTimers.end()) {
            it->second.clock(ms);
            if (it->second.isRunning() && it->second.hasExpired()) {
                gntsToRel.push_back(dstId);
            }
        }
    }
    m_grantChTable.unlock();

    // release grants that have timed out
    for (uint32_t dstId : gntsToRel) {
        releaseGrant(dstId, false);
    }

    if (!m_disableGrpAffTimeout) {
        m_grpAffTable.spinlock();

        // clock all the group affiliation timers
        m_grpAffTable.lock(false);
        std::vector<uint32_t> affsToRel = std::vector<uint32_t>();
        for (auto entry : m_grpAffTable) {
            uint32_t srcId = entry.first;
            auto it = m_grpAffTimers.find(srcId);
            if (it != m_grpAffTimers.end()) {
                it->second.clock(ms);
                if (it->second.isRunning() && it->second.hasExpired()) {
                    affsToRel.push_back(srcId);
                }
            }
        }
        m_grpAffTable.unlock();

        // release group affiliations that have timed out
        for (uint32_t srcId : affsToRel) {
            LogWarning(LOG_HOST, "%s, clearing stale group affiliation, srcId = %u", m_name.c_str(), srcId);
            groupUnaff(srcId);
        }
    }

    if (!m_disableUnitRegTimeout) {
        m_unitRegTable.spinlock();

        // clock all the unit registration timers
        m_unitRegTable.lock(false);
        std::vector<uint32_t> unitsToDereg = std::vector<uint32_t>();
        for (uint32_t srcId : m_unitRegTable) {
            auto it = m_unitRegTimers.find(srcId);
            if (it != m_unitRegTimers.end()) {
                it->second.clock(ms);
                if (it->second.isRunning() && it->second.hasExpired()) {
                    unitsToDereg.push_back(srcId);
                }
            }
        }
        m_unitRegTable.unlock();

        // release units registrations that have timed out
        for (uint32_t srcId : unitsToDereg) {
            LogWarning(LOG_HOST, "%s, clearing stale unit deregistration, srcId = %u", m_name.c_str(), srcId);
            unitDereg(srcId, true);
        }
    }
}

// ---------------------------------------------------------------------------
//  Protected Class Members
// ---------------------------------------------------------------------------

/* Helper to determine if the source ID has group affiliations. */

bool AffiliationLookup::isSrcIdGrpAff(uint32_t srcId) const
{
    __spinlock();

    // lookup dynamic affiliation table entry
    m_grpAffTable.lock(false);
    auto it = m_grpAffTable.find(srcId);
    if (it != m_grpAffTable.end()) {
        m_grpAffTable.unlock();
        return true;
    }
    m_grpAffTable.unlock();

    return false;
}
