/*!
 * \file      RegionCommon.c
 *
 * \brief     LoRa MAC common region implementation
 *
 * \copyright Revised BSD License, see section \ref LICENSE.
 *
 * \code
 *                ______                              _
 *               / _____)             _              | |
 *              ( (____  _____ ____ _| |_ _____  ____| |__
 *               \____ \| ___ |    (_   _) ___ |/ ___)  _ \
 *               _____) ) ____| | | || |_| ____( (___| | | |
 *              (______/|_____)_|_|_| \__)_____)\____)_| |_|
 *              (C)2013-2017 Semtech
 *
 *               ___ _____ _   ___ _  _____ ___  ___  ___ ___
 *              / __|_   _/_\ / __| |/ / __/ _ \| _ \/ __| __|
 *              \__ \ | |/ _ \ (__| ' <| _| (_) |   / (__| _|
 *              |___/ |_/_/ \_\___|_|\_\_| \___/|_|_\\___|___|
 *              embedded.connectivity.solutions===============
 *
 * \endcode
 *
 * \author    Miguel Luis ( Semtech )
 *
 * \author    Gregory Cristian ( Semtech )
 *
 * \author    Daniel Jaeckle ( STACKFORCE )
 */
/**
  ******************************************************************************
  *
  *          Portions COPYRIGHT 2020 STMicroelectronics
  *
  * @file    RegionCommon.c
  * @author  MCD Application Team
  * @brief   LoRa MAC common region implementation
  ******************************************************************************
  */
#include <math.h>
#include "radio.h"
#include "utilities.h"
#include "RegionCommon.h"
#include "systime.h"
#include "mw_log_conf.h"

#define BACKOFF_DC_1_HOUR                   100
#define BACKOFF_DC_10_HOURS                 1000
#define BACKOFF_DC_24_HOURS                 10000

#define BACKOFF_DUTY_CYCLE_1_HOUR_IN_S      3600
#define BACKOFF_DUTY_CYCLE_10_HOURS_IN_S    ( BACKOFF_DUTY_CYCLE_1_HOUR_IN_S + ( BACKOFF_DUTY_CYCLE_1_HOUR_IN_S * 10 ) )
#define BACKOFF_DUTY_CYCLE_24_HOURS_IN_S    ( BACKOFF_DUTY_CYCLE_10_HOURS_IN_S + ( BACKOFF_DUTY_CYCLE_1_HOUR_IN_S * 24 ) )
#define BACKOFF_24_HOURS_IN_S               ( BACKOFF_DUTY_CYCLE_1_HOUR_IN_S * 24 )

#ifndef DUTY_CYCLE_TIME_PERIOD
/*!
 * Default duty cycle observation time period
 *
 * \remark The ETSI observation time period is 1 hour (3600000 ms) but, the implemented algorithm may violate the
 *         defined duty-cycle restrictions. In order to ensure that these restrictions never get violated we changed the
 *         default duty cycle observation time period to 1/2 hour (1800000 ms).
 */
#if (defined( REGION_VERSION ) && ( REGION_VERSION == 0x02010003 )) 
#define DUTY_CYCLE_TIME_PERIOD              3600000
#else
#define DUTY_CYCLE_TIME_PERIOD              1800000
#endif
#endif

#ifndef DUTY_CYCLE_TIME_PERIOD_JOIN_BACKOFF_24H
/*!
 * Time credits for the join backoff algorithm for the 24H period.
 */
#define DUTY_CYCLE_TIME_PERIOD_JOIN_BACKOFF_24H 870000
#endif

/*!
 * \brief Returns `N / D` rounded to the smallest integer value greater than or equal to `N / D`
 *
 * \warning when `D == 0`, the result is undefined
 *
 * \remark `N` and `D` can be signed or unsigned
 *
 * \param [in] N the numerator, which can have any sign
 * \param [in] D the denominator, which can have any sign
 * \retval N / D with any fractional part rounded to the smallest integer value greater than or equal to `N / D`
 */
#define DIV_CEIL( N, D )                                                       \
    (                                                                          \
        ( N > 0 ) ?                                                            \
        ( ( ( N ) + ( D ) - 1 ) / ( D ) ) :                                    \
        ( ( N ) / ( D ) )                                                      \
    )

#ifdef MW_LOG_ENABLED
static const char *EventRXSlotStrings[] = { "1", "2", "C", "Multi_C", "P", "Multi_P" };
#endif

static uint16_t GetDutyCycle( Band_t* band, bool joined, SysTime_t elapsedTimeSinceStartup )
{
    uint16_t dutyCycle = band->DCycle;

    if( joined == false )
    {
#if (defined( REGION_VERSION ) && ( REGION_VERSION == 0x02010003 ))  
        uint16_t joinDutyCycle = BACKOFF_DC_1_HOUR;
#else
        uint16_t joinDutyCycle = BACKOFF_DC_24_HOURS;

        if( elapsedTimeSinceStartup.Seconds < BACKOFF_DUTY_CYCLE_1_HOUR_IN_S )
        {
            joinDutyCycle = BACKOFF_DC_1_HOUR;
        }
        else if( elapsedTimeSinceStartup.Seconds < BACKOFF_DUTY_CYCLE_10_HOURS_IN_S )
        {
            joinDutyCycle = BACKOFF_DC_10_HOURS;
        }
        else
        {
            joinDutyCycle = BACKOFF_DC_24_HOURS;
        }
#endif
        // Take the most restrictive duty cycle
        dutyCycle = MAX( dutyCycle, joinDutyCycle );
    }

    // Prevent value of 0
    if( dutyCycle == 0 )
    {
        dutyCycle = 1;
    }

    return dutyCycle;
}

static uint16_t SetMaxTimeCredits( Band_t* band, bool joined, SysTime_t elapsedTimeSinceStartup,
                                   bool dutyCycleEnabled, bool lastTxIsJoinRequest )
{
    uint16_t dutyCycle = band->DCycle;
    TimerTime_t maxCredits = DUTY_CYCLE_TIME_PERIOD;

    // Get the band duty cycle. If not joined, the function either returns the join duty cycle
    // or the band duty cycle, whichever is more restrictive.
    dutyCycle = GetDutyCycle( band, joined, elapsedTimeSinceStartup );

    if( joined == false )
    {
#if (defined( REGION_VERSION ) && ( REGION_VERSION == 0x02010003 ))
		if( elapsedTimeSinceStartup.Seconds < BACKOFF_DUTY_CYCLE_1_HOUR_IN_S )
        {
            maxCredits = DUTY_CYCLE_TIME_PERIOD;
        }
        else if( elapsedTimeSinceStartup.Seconds < BACKOFF_DUTY_CYCLE_10_HOURS_IN_S )
        {
            maxCredits = DUTY_CYCLE_TIME_PERIOD;
        }
        else
        {
            maxCredits = DUTY_CYCLE_TIME_PERIOD_JOIN_BACKOFF_24H;
        }
#else
    	TimerTime_t elapsedTime = SysTimeToMs( elapsedTimeSinceStartup );
    	SysTime_t timeDiff = { 0 };
        if( dutyCycle == BACKOFF_DC_1_HOUR )
        {
            maxCredits = DUTY_CYCLE_TIME_PERIOD;
            band->LastMaxCreditAssignTime = elapsedTime;
        }
        else if( dutyCycle == BACKOFF_DC_10_HOURS )
        {
            maxCredits = DUTY_CYCLE_TIME_PERIOD * 10;
            band->LastMaxCreditAssignTime = elapsedTime;
        }
        else
        {
            maxCredits = DUTY_CYCLE_TIME_PERIOD * 24;
        }

        timeDiff = SysTimeSub( elapsedTimeSinceStartup, SysTimeFromMs( band->LastMaxCreditAssignTime ) );

        // Verify if we have to assign the maximum credits in cases
        // of the preconditions have changed.
        if( ( ( dutyCycleEnabled == false ) && ( lastTxIsJoinRequest == false ) ) ||
            ( band->MaxTimeCredits != maxCredits ) ||
            ( timeDiff.Seconds >= BACKOFF_24_HOURS_IN_S ) )
        {
            band->TimeCredits = maxCredits;

            if( elapsedTimeSinceStartup.Seconds >= BACKOFF_DUTY_CYCLE_24_HOURS_IN_S )
            {
                timeDiff.Seconds = ( elapsedTimeSinceStartup.Seconds - BACKOFF_DUTY_CYCLE_24_HOURS_IN_S ) / BACKOFF_24_HOURS_IN_S;
                timeDiff.Seconds *= BACKOFF_24_HOURS_IN_S;
                timeDiff.Seconds += BACKOFF_DUTY_CYCLE_24_HOURS_IN_S;
                timeDiff.SubSeconds = 0;
                band->LastMaxCreditAssignTime = SysTimeToMs( timeDiff );
            }
        }
#endif

    }
    else
    {
        if( dutyCycleEnabled == false )
        {
            // Assign max credits when the duty cycle is disabled.
            band->TimeCredits = maxCredits;
        }
    }

#if (defined( REGION_VERSION ) && (( REGION_VERSION == 0x01010003 ) || ( REGION_VERSION == 0x02010001 )))
    // Assign the max credits if its the first time
    if( band->LastBandUpdateTime == 0 )
    {
        band->TimeCredits = maxCredits;
    }
#endif 

    // Setup the maximum allowed credits. We can assign them
    // safely all the time.
    band->MaxTimeCredits = maxCredits;

    return dutyCycle;
}


#if (defined( REGION_VERSION ) && ( REGION_VERSION == 0x02010003 ))  
static uint16_t UpdateTimeCredits( Band_t* band, bool joined, bool dutyCycleEnabled,
                                   bool lastTxIsJoinRequest, SysTime_t elapsedTimeSinceStartup,
                                   TimerTime_t currentTime, TimerTime_t lastBandUpdateTime )
{
    uint16_t dutyCycle = SetMaxTimeCredits( band, joined, elapsedTimeSinceStartup,
                                            dutyCycleEnabled, lastTxIsJoinRequest );
    TimerTime_t observation = DUTY_CYCLE_TIME_PERIOD;

    if( joined == false )
    {
        if( elapsedTimeSinceStartup.Seconds < BACKOFF_DUTY_CYCLE_1_HOUR_IN_S )
        {
            observation = BACKOFF_DUTY_CYCLE_1_HOUR_IN_S * 1000;
        }
        else if( elapsedTimeSinceStartup.Seconds < BACKOFF_DUTY_CYCLE_10_HOURS_IN_S )
        {
            observation = ( BACKOFF_DUTY_CYCLE_10_HOURS_IN_S * 1000 );
        }
        else
        {
            observation = ( BACKOFF_DUTY_CYCLE_24_HOURS_IN_S * 1000 );
        }
    }

    // Apply new credits only if the observation period has been elapsed.
    if( ( observation <= lastBandUpdateTime ) ||
        ( band->LastMaxCreditAssignTime != observation ) ||
        ( band->LastBandUpdateTime == 0 ) )
    {
        band->TimeCredits = band->MaxTimeCredits;
        band->LastBandUpdateTime = currentTime;
        band->LastMaxCreditAssignTime = observation;
    }
    return dutyCycle;
}
#else
static uint16_t UpdateTimeCredits( Band_t* band, bool joined, bool dutyCycleEnabled,
                                   bool lastTxIsJoinRequest, SysTime_t elapsedTimeSinceStartup,
                                   TimerTime_t currentTime )
{
    uint16_t dutyCycle = SetMaxTimeCredits( band, joined, elapsedTimeSinceStartup,
                                            dutyCycleEnabled, lastTxIsJoinRequest );

    if( joined == true )
    {
        // Apply a sliding window for the duty cycle with collection and speding
        // credits.
        band->TimeCredits += TimerGetElapsedTime( band->LastBandUpdateTime );
    }

    // Limit band credits to maximum
    if( band->TimeCredits > band->MaxTimeCredits )
    {
        band->TimeCredits = band->MaxTimeCredits;
    }

    // Synchronize update time
    band->LastBandUpdateTime = currentTime;

    return dutyCycle;
}
#endif

static uint8_t CountChannels( uint16_t mask, uint8_t nbBits )
{
    uint8_t nbActiveBits = 0;

    for( uint8_t j = 0; j < nbBits; j++ )
    {
        if( ( mask & ( 1 << j ) ) == ( 1 << j ) )
        {
            nbActiveBits++;
        }
    }
    return nbActiveBits;
}

bool RegionCommonChanVerifyDr( uint8_t nbChannels, uint16_t* channelsMask, int8_t dr, int8_t minDr, int8_t maxDr, ChannelParams_t* channels )
{
    if( RegionCommonValueInRange( dr, minDr, maxDr ) == 0 )
    {
        return false;
    }

    for( uint8_t i = 0, k = 0; i < nbChannels; i += 16, k++ )
    {
        for( uint8_t j = 0; j < 16; j++ )
        {
            if( ( ( channelsMask[k] & ( 1 << j ) ) != 0 ) )
            {// Check datarate validity for enabled channels
                if( RegionCommonValueInRange( dr, ( channels[i + j].DrRange.Fields.Min & 0x0F ),
                                                  ( channels[i + j].DrRange.Fields.Max & 0x0F ) ) == 1 )
                {
                    // At least 1 channel has been found we can return OK.
                    return true;
                }
            }
        }
    }
    return false;
}

uint8_t RegionCommonValueInRange( int8_t value, int8_t min, int8_t max )
{
    if( ( value >= min ) && ( value <= max ) )
    {
        return 1;
    }
    return 0;
}

bool RegionCommonChanDisable( uint16_t* channelsMask, uint8_t id, uint8_t maxChannels )
{
    uint8_t index = id / 16;

    if( ( index > ( maxChannels / 16 ) ) || ( id >= maxChannels ) )
    {
        return false;
    }

    // Deactivate channel
    channelsMask[index] &= ~( 1 << ( id % 16 ) );

    return true;
}

uint8_t RegionCommonCountChannels( uint16_t* channelsMask, uint8_t startIdx, uint8_t stopIdx )
{
    uint8_t nbChannels = 0;

    if( channelsMask == NULL )
    {
        return 0;
    }

    for( uint8_t i = startIdx; i < stopIdx; i++ )
    {
        nbChannels += CountChannels( channelsMask[i], 16 );
    }

    return nbChannels;
}

void RegionCommonChanMaskCopy( uint16_t* channelsMaskDest, uint16_t* channelsMaskSrc, uint8_t len )
{
    if( ( channelsMaskDest != NULL ) && ( channelsMaskSrc != NULL ) )
    {
        for( uint8_t i = 0; i < len; i++ )
        {
            channelsMaskDest[i] = channelsMaskSrc[i];
        }
    }
}

void RegionCommonSetBandTxDone( Band_t* band, TimerTime_t lastTxAirTime, bool joined, SysTime_t elapsedTimeSinceStartup )
{
    // Get the band duty cycle. If not joined, the function either returns the join duty cycle
    // or the band duty cycle, whichever is more restrictive.
    uint16_t dutyCycle = GetDutyCycle( band, joined, elapsedTimeSinceStartup );

    // Reduce with transmission time
    if( band->TimeCredits > ( lastTxAirTime * dutyCycle ) )
    {
        // Reduce time credits by the time of air
        band->TimeCredits -= ( lastTxAirTime * dutyCycle );
    }
    else
    {
        band->TimeCredits = 0;
    }
}

TimerTime_t RegionCommonUpdateBandTimeOff( bool joined, Band_t* bands,
                                           uint8_t nbBands, bool dutyCycleEnabled,
                                           bool lastTxIsJoinRequest, SysTime_t elapsedTimeSinceStartup,
                                           TimerTime_t expectedTimeOnAir )
{
    TimerTime_t minTimeToWait = TIMERTIME_T_MAX;
    TimerTime_t currentTime = TimerGetCurrentTime( );
    TimerTime_t creditCosts = 0;
    uint16_t dutyCycle = 1;
    uint8_t validBands = 0;

    for( uint8_t i = 0; i < nbBands; i++ )
    {
#if (defined( REGION_VERSION ) && ( REGION_VERSION == 0x02010003 ))
        TimerTime_t elapsedTime = TimerGetElapsedTime( bands[i].LastBandUpdateTime );

        // Synchronization of bands and credits
        dutyCycle = UpdateTimeCredits( &bands[i], joined, dutyCycleEnabled,
                                       lastTxIsJoinRequest, elapsedTimeSinceStartup,
                                       currentTime, elapsedTime );
#else
        // Synchronization of bands and credits
        dutyCycle = UpdateTimeCredits( &bands[i], joined, dutyCycleEnabled,
                                       lastTxIsJoinRequest, elapsedTimeSinceStartup,
                                       currentTime );
#endif

        // Calculate the credit costs for the next transmission
        // with the duty cycle and the expected time on air
        creditCosts = expectedTimeOnAir * dutyCycle;

        // Check if the band is ready for transmission. Its ready,
        // when the duty cycle is off, or the TimeCredits of the band
        // is higher than the credit costs for the transmission.
        if( ( bands[i].TimeCredits > creditCosts ) ||
            ( ( dutyCycleEnabled == false ) && ( joined == true ) ) )
        {
            bands[i].ReadyForTransmission = true;
            // This band is a potential candidate for an
            // upcoming transmission, so increase the counter.
            validBands++;
        }
        else
        {
            // In this case, the band has not enough credits
            // for the next transmission.
            bands[i].ReadyForTransmission = false;

            if( bands[i].MaxTimeCredits > creditCosts )
            {
                // The band can only be taken into account, if the maximum credits
                // of the band are higher than the credit costs.
                // We calculate the minTimeToWait among the bands which are not
                // ready for transmission and which are potentially available
                // for a transmission in the future.
#if (defined( REGION_VERSION ) && ( REGION_VERSION == 0x02010003 ))  
                TimerTime_t observationTimeDiff = 0;
                if( bands[i].LastMaxCreditAssignTime >= elapsedTime )
                {
                    observationTimeDiff = bands[i].LastMaxCreditAssignTime - elapsedTime;
                }
                minTimeToWait = MIN( minTimeToWait, observationTimeDiff );
#else
                minTimeToWait = MIN( minTimeToWait, ( creditCosts - bands[i].TimeCredits ) );
#endif

                // This band is a potential candidate for an
                // upcoming transmission (even if its time credits are not enough
                // at the moment), so increase the counter.
                validBands++;
            }

#if (defined( REGION_VERSION ) && (( REGION_VERSION == 0x01010003 ) || ( REGION_VERSION == 0x02010001 )))
            // Apply a special calculation if the device is not joined.
            if( joined == false )
            {
                SysTime_t backoffTimeRange = {
                    .Seconds    = 0,
                    .SubSeconds = 0,
                };
                // Get the backoff time range based on the duty cycle definition
                if( dutyCycle == BACKOFF_DC_1_HOUR )
                {
                    backoffTimeRange.Seconds = BACKOFF_DUTY_CYCLE_1_HOUR_IN_S;
                }
                else if( dutyCycle == BACKOFF_DC_10_HOURS )
                {
                    backoffTimeRange.Seconds = BACKOFF_DUTY_CYCLE_10_HOURS_IN_S;
                }
                else
                {
                    backoffTimeRange.Seconds = BACKOFF_DUTY_CYCLE_24_HOURS_IN_S;
                }
                // Calculate the time to wait.
                if( elapsedTimeSinceStartup.Seconds > BACKOFF_DUTY_CYCLE_24_HOURS_IN_S )
                {
                    backoffTimeRange.Seconds += BACKOFF_24_HOURS_IN_S * ( ( ( elapsedTimeSinceStartup.Seconds - BACKOFF_DUTY_CYCLE_24_HOURS_IN_S ) / BACKOFF_24_HOURS_IN_S ) + 1 );
                }
                // Calculate the time difference between now and the next range
                backoffTimeRange  = SysTimeSub( backoffTimeRange, elapsedTimeSinceStartup );
                minTimeToWait = SysTimeToMs( backoffTimeRange );
            }
#endif

        }
    }

    if( validBands == 0 )
    {
        // There is no valid band available to handle a transmission
        // in the given DUTY_CYCLE_TIME_PERIOD.
        return TIMERTIME_T_MAX;
    }
    return minTimeToWait;
}

uint8_t RegionCommonParseLinkAdrReq( uint8_t* payload, RegionCommonLinkAdrParams_t* linkAdrParams )
{
    uint8_t retIndex = 0;

    if( payload[0] == SRV_MAC_LINK_ADR_REQ )
    {
        // Parse datarate and tx power
        linkAdrParams->Datarate = payload[1];
        linkAdrParams->TxPower = linkAdrParams->Datarate & 0x0F;
        linkAdrParams->Datarate = ( linkAdrParams->Datarate >> 4 ) & 0x0F;
        // Parse ChMask
        linkAdrParams->ChMask = ( uint16_t )payload[2];
        linkAdrParams->ChMask |= ( uint16_t )payload[3] << 8;
        // Parse ChMaskCtrl and nbRep
        linkAdrParams->NbRep = payload[4];
        linkAdrParams->ChMaskCtrl = ( linkAdrParams->NbRep >> 4 ) & 0x07;
        linkAdrParams->NbRep &= 0x0F;

        // LinkAdrReq has 4 bytes length + 1 byte CMD
        retIndex = 5;
    }
    return retIndex;
}

uint8_t RegionCommonLinkAdrReqVerifyParams( RegionCommonLinkAdrReqVerifyParams_t* verifyParams, int8_t* dr, int8_t* txPow, uint8_t* nbRep )
{
    uint8_t status = verifyParams->Status;
    int8_t datarate = verifyParams->Datarate;
    int8_t txPower = verifyParams->TxPower;
    int8_t nbRepetitions = verifyParams->NbRep;

    // Handle the case when ADR is off.
    if( verifyParams->AdrEnabled == false )
    {
        // When ADR is off, we are allowed to change the channels mask
        nbRepetitions = verifyParams->CurrentNbRep;
        datarate =  verifyParams->CurrentDatarate;
        txPower =  verifyParams->CurrentTxPower;
    }

    if( status != 0 )
    {
        // Verify datarate. The variable phyParam. Value contains the minimum allowed datarate.
        if( datarate == 0x0F )
        { // 0xF means that the device MUST ignore that field, and keep the current parameter value.
            datarate =  verifyParams->CurrentDatarate;
        }
        else if( RegionCommonChanVerifyDr( verifyParams->NbChannels, verifyParams->ChannelsMask, datarate,
                                      verifyParams->MinDatarate, verifyParams->MaxDatarate, verifyParams->Channels  ) == false )
        {
            status &= 0xFD; // Datarate KO
        }

        // Verify tx power
        if( txPower == 0x0F )
        { // 0xF means that the device MUST ignore that field, and keep the current parameter value.
            txPower =  verifyParams->CurrentTxPower;
        }
        else if( RegionCommonValueInRange( txPower, verifyParams->MaxTxPower, verifyParams->MinTxPower ) == 0 )
        {
            // Verify if the maximum TX power is exceeded
            if( verifyParams->MaxTxPower > txPower )
            { // Apply maximum TX power. Accept TX power.
                txPower = verifyParams->MaxTxPower;
            }
            else
            {
                status &= 0xFB; // TxPower KO
            }
        }
    }

    // If the status is ok, verify the NbRep
    if( status == 0x07 )
    {
        if( nbRepetitions == 0 )
        { // Set nbRep to the default value of 1.
            nbRepetitions = 1;
        }
    }

    // Apply changes
    *dr = datarate;
    *txPow = txPower;
    *nbRep = nbRepetitions;

    return status;
}

uint32_t RegionCommonComputeSymbolTimeLoRa( uint8_t phyDr, uint32_t bandwidthInHz )
{
    return ( 1 << phyDr ) * 1000000 / bandwidthInHz;
}

uint32_t RegionCommonComputeSymbolTimeFsk( uint8_t phyDrInKbps )
{
    return 8000 / ( uint32_t )phyDrInKbps; // 1 symbol equals 1 byte
}

void RegionCommonComputeRxWindowParameters( uint32_t tSymbolInUs, uint8_t minRxSymbols, uint32_t rxErrorInMs, uint32_t wakeUpTimeInMs, uint32_t* windowTimeoutInSymbols, int32_t* windowOffsetInMs )
{
    *windowTimeoutInSymbols = MAX( DIV_CEIL( ( ( 2 * minRxSymbols - 8 ) * tSymbolInUs + 2 * ( rxErrorInMs * 1000 ) ),  tSymbolInUs ), minRxSymbols ); // Computed number of symbols
    *windowOffsetInMs = ( int32_t )DIV_CEIL( ( int32_t )( 4 * tSymbolInUs ) -
                                               ( int32_t )DIV_CEIL( ( *windowTimeoutInSymbols * tSymbolInUs ), 2 ) -
                                               ( int32_t )( wakeUpTimeInMs * 1000 ), 1000 );
}

int8_t RegionCommonComputeTxPower( int8_t txPowerIndex, float maxEirp, float antennaGain )
{
    int8_t phyTxPower = 0;

    phyTxPower = ( int8_t )floor( ( maxEirp - ( txPowerIndex * 2U ) ) - antennaGain );

    return phyTxPower;
}

void RegionCommonRxBeaconSetup( RegionCommonRxBeaconSetupParams_t* rxBeaconSetupParams )
{
    bool rxContinuous = true;
    uint8_t datarate;

    // Set the radio into sleep mode
    Radio.Sleep( );

    // Setup frequency and payload length
    Radio.SetChannel( rxBeaconSetupParams->Frequency );
    Radio.SetMaxPayloadLength( MODEM_LORA, rxBeaconSetupParams->BeaconSize );

    // Check the RX continuous mode
    if( rxBeaconSetupParams->RxTime != 0 )
    {
        rxContinuous = false;
    }

    // Get region specific datarate
    datarate = rxBeaconSetupParams->Datarates[rxBeaconSetupParams->BeaconDatarate];

    // Setup radio
    Radio.SetRxConfig( MODEM_LORA, rxBeaconSetupParams->BeaconChannelBW, datarate,
                       1, 0, 10, rxBeaconSetupParams->SymbolTimeout, true, rxBeaconSetupParams->BeaconSize, false, 0, 0, false, rxContinuous );

    Radio.Rx( rxBeaconSetupParams->RxTime );
    MW_LOG(TS_ON, VLEVEL_M, "RX_BC on freq %d Hz at DR %d\r\n", rxBeaconSetupParams->Frequency, rxBeaconSetupParams->BeaconDatarate );
}

void RegionCommonCountNbOfEnabledChannels( RegionCommonCountNbOfEnabledChannelsParams_t* countNbOfEnabledChannelsParams,
                                           uint8_t* enabledChannels, uint8_t* nbEnabledChannels, uint8_t* nbRestrictedChannels )
{
    uint8_t nbChannelCount = 0;
    uint8_t nbRestrictedChannelsCount = 0;

    for( uint8_t i = 0, k = 0; i < countNbOfEnabledChannelsParams->MaxNbChannels; i += 16, k++ )
    {
        for( uint8_t j = 0; j < 16; j++ )
        {
            if( ( countNbOfEnabledChannelsParams->ChannelsMask[k] & ( 1 << j ) ) != 0 )
            {
                if( countNbOfEnabledChannelsParams->Channels[i + j].Frequency == 0 )
                { // Check if the channel is enabled
                    continue;
                }
                if( ( countNbOfEnabledChannelsParams->Joined == false ) &&
                    ( countNbOfEnabledChannelsParams->JoinChannels != NULL ) )
                {
                    if( ( countNbOfEnabledChannelsParams->JoinChannels[k] & ( 1 << j ) ) == 0 )
                    {
                        continue;
                    }
                }
                if( RegionCommonValueInRange( countNbOfEnabledChannelsParams->Datarate,
                                              countNbOfEnabledChannelsParams->Channels[i + j].DrRange.Fields.Min,
                                              countNbOfEnabledChannelsParams->Channels[i + j].DrRange.Fields.Max ) == false )
                { // Check if the current channel selection supports the given datarate
                    continue;
                }
                if( countNbOfEnabledChannelsParams->Bands[countNbOfEnabledChannelsParams->Channels[i + j].Band].ReadyForTransmission == false )
                { // Check if the band is available for transmission
                    nbRestrictedChannelsCount++;
                    continue;
                }
                enabledChannels[nbChannelCount++] = i + j;
            }
        }
    }
    *nbEnabledChannels = nbChannelCount;
    *nbRestrictedChannels = nbRestrictedChannelsCount;
}

LoRaMacStatus_t RegionCommonIdentifyChannels( RegionCommonIdentifyChannelsParam_t* identifyChannelsParam,
                                              TimerTime_t* aggregatedTimeOff, uint8_t* enabledChannels,
                                              uint8_t* nbEnabledChannels, uint8_t* nbRestrictedChannels,
                                              TimerTime_t* nextTxDelay )
{
    TimerTime_t elapsed = TimerGetElapsedTime( identifyChannelsParam->LastAggrTx );
    *nextTxDelay = identifyChannelsParam->AggrTimeOff - elapsed;
    *nbRestrictedChannels = 1;
    *nbEnabledChannels = 0;

    if( ( identifyChannelsParam->LastAggrTx == 0 ) ||
        ( identifyChannelsParam->AggrTimeOff <= elapsed ) )
    {
        // Reset Aggregated time off
        *aggregatedTimeOff = 0;

        // Update bands Time OFF
        *nextTxDelay = RegionCommonUpdateBandTimeOff( identifyChannelsParam->CountNbOfEnabledChannelsParam->Joined,
                                                      identifyChannelsParam->CountNbOfEnabledChannelsParam->Bands,
                                                      identifyChannelsParam->MaxBands,
                                                      identifyChannelsParam->DutyCycleEnabled,
                                                      identifyChannelsParam->LastTxIsJoinRequest,
                                                      identifyChannelsParam->ElapsedTimeSinceStartUp,
                                                      identifyChannelsParam->ExpectedTimeOnAir );

        RegionCommonCountNbOfEnabledChannels( identifyChannelsParam->CountNbOfEnabledChannelsParam, enabledChannels,
                                              nbEnabledChannels, nbRestrictedChannels );
    }

    if( *nbEnabledChannels > 0 )
    {
        *nextTxDelay = 0;
        return LORAMAC_STATUS_OK;
    }
    else if( *nbRestrictedChannels > 0 )
    {
        return LORAMAC_STATUS_DUTYCYCLE_RESTRICTED;
    }
    else
    {
        return LORAMAC_STATUS_NO_CHANNEL_FOUND;
    }
}

int8_t RegionCommonGetNextLowerTxDr( RegionCommonGetNextLowerTxDrParams_t *params )
{
    int8_t drLocal = params->CurrentDr;

    if( params->CurrentDr == params->MinDr )
    {
        return params->MinDr;
    }
    else
    {
        do
        {
            drLocal = ( drLocal - 1 );
        } while( ( drLocal != params->MinDr ) &&
                 ( RegionCommonChanVerifyDr( params->NbChannels, params->ChannelsMask, drLocal, params->MinDr, params->MaxDr, params->Channels  ) == false ) );

        return drLocal;
    }
}

int8_t RegionCommonLimitTxPower( int8_t txPower, int8_t maxBandTxPower )
{
    // Limit tx power to the band max
    return MAX( txPower, maxBandTxPower );
}

uint32_t RegionCommonGetBandwidth( uint32_t drIndex, const uint32_t* bandwidths )
{
    switch( bandwidths[drIndex] )
    {
        default:
        case 125000:
            return 0;
        case 250000:
            return 1;
        case 500000:
            return 2;
    }
}

void RegionCommonRxConfigPrint(LoRaMacRxSlot_t rxSlot, uint32_t frequency, int8_t dr)
{
    if ( rxSlot < RX_SLOT_NONE )
    {
        MW_LOG(TS_ON, VLEVEL_M,  "RX_%s on freq %d Hz at DR %d\r\n", EventRXSlotStrings[rxSlot], frequency, dr );
    }
    else
    {
        MW_LOG(TS_ON, VLEVEL_M,  "RX on freq %d Hz at DR %d\r\n", frequency, dr );
    }
}

void RegionCommonTxConfigPrint(uint32_t frequency, int8_t dr)
{
    MW_LOG(TS_ON, VLEVEL_M,  "TX on freq %d Hz at DR %d\r\n", frequency, dr );
}