/*!
 * \file      LoRaMacCommands.c
 *
 * \brief     LoRa MAC commands
 *
 * \copyright Revised BSD License, see section \ref LICENSE.
 *
 * \code
 *                ______                              _
 *               / _____)             _              | |
 *              ( (____  _____ ____ _| |_ _____  ____| |__
 *               \____ \| ___ |    (_   _) ___ |/ ___)  _ \
 *               _____) ) ____| | | || |_| ____( (___| | | |
 *              (______/|_____)_|_|_| \__)_____)\____)_| |_|
 *              (C)2013 Semtech
 *
 *               ___ _____ _   ___ _  _____ ___  ___  ___ ___
 *              / __|_   _/_\ / __| |/ / __/ _ \| _ \/ __| __|
 *              \__ \ | |/ _ \ (__| ' <| _| (_) |   / (__| _|
 *              |___/ |_/_/ \_\___|_|\_\_| \___/|_|_\\___|___|
 *              embedded.connectivity.solutions===============
 *
 * \endcode
 *
 * \author    Miguel Luis ( Semtech )
 *
 * \author    Daniel Jaeckle ( STACKFORCE )
 *
 * \author    Johannes Bruder ( STACKFORCE )
 */
#include "utilities.h"
#include "LoRaMacCommands.h"
#include "LoRaMacConfirmQueue.h"
#include "LoRaMacVersion.h"

#ifndef NUM_OF_MAC_COMMANDS
/*!
 * Number of MAC Command slots
 */
#if (defined( LORAMAC_VERSION ) && ( LORAMAC_VERSION == 0x01000300 ))
#define NUM_OF_MAC_COMMANDS 15
#elif (defined( LORAMAC_VERSION ) && (( LORAMAC_VERSION == 0x01000400 ) || ( LORAMAC_VERSION == 0x01010100 )))
#define NUM_OF_MAC_COMMANDS 32
#endif /* LORAMAC_VERSION */
#endif

/*!
 * Size of the CID field of MAC commands
 */
#define CID_FIELD_SIZE 1

/*!
 *  Mac Commands list structure
 */
typedef struct sMacCommandsList
{
    /*
     * First element of MAC command list.
     */
    MacCommand_t* First;
    /*
     * Last element of MAC command list.
     */
    MacCommand_t* Last;
} MacCommandsList_t;

/*!
 * LoRaMac Commands Context structure
 */
typedef struct sLoRaMacCommandsCtx
{
    /*
     * List of MAC command elements
     */
    MacCommandsList_t MacCommandList;
    /*
     * Buffer to store MAC command elements
     */
    MacCommand_t MacCommandSlots[NUM_OF_MAC_COMMANDS];
    /*
     * Size of all MAC commands serialized as buffer
     */
    size_t SerializedCmdsSize;
} LoRaMacCommandsCtx_t;

/*!
 * Non-volatile module context.
 */
static LoRaMacCommandsCtx_t CommandsCtx;

/* Memory management functions */

/*!
 * \brief Determines if a MAC command slot is free
 *
 * \param [in]    slot           - Slot to check
 * \retval                       - Status of the operation
 */
static bool IsSlotFree( const MacCommand_t* slot )
{
    uint8_t* mem = ( uint8_t* )slot;

    for( uint16_t size = 0; size < sizeof( MacCommand_t ); size++ )
    {
        if( mem[size] != 0x00 )
        {
            return false;
        }
    }
    return true;
}

/*!
 * \brief Allocates a new MAC command memory slot
 *
 * \retval                       - Pointer to slot
 */
static MacCommand_t* MallocNewMacCommandSlot( void )
{
    uint8_t itr = 0;

    while( IsSlotFree( ( const MacCommand_t* )&CommandsCtx.MacCommandSlots[itr] ) == false )
    {
        itr++;
        if( itr == NUM_OF_MAC_COMMANDS )
        {
            return NULL;
        }
    }

    return &CommandsCtx.MacCommandSlots[itr];
}

/*!
 * \brief Free memory slot
 *
 * \param [in]    slot           - Slot to free
 *
 * \retval                       - Status of the operation
 */
static bool FreeMacCommandSlot( MacCommand_t* slot )
{
    if( slot == NULL )
    {
        return false;
    }

    memset1( ( uint8_t* )slot, 0x00, sizeof( MacCommand_t ) );

    return true;
}

/* Linked list functions */

/*!
 * \brief Initialize list
 *
 * \param [in]    list           - List that shall be initialized
 * \retval                       - Status of the operation
 */
static bool LinkedListInit( MacCommandsList_t* list )
{
    if( list == NULL )
    {
        return false;
    }

    list->First = NULL;
    list->Last = NULL;

    return true;
}

/*!
 * \brief Add an element to the list
 *
 * \param [in]    list           - List where the element shall be added.
 * \param [in]    element        - Element to add
 * \retval                       - Status of the operation
 */
static bool LinkedListAdd( MacCommandsList_t* list, MacCommand_t* element )
{
    if( ( list == NULL ) || ( element == NULL ) )
    {
        return false;
    }

    // Check if this is the first entry to enter the list.
    if( list->First == NULL )
    {
        list->First = element;
    }

    // Check if the last entry exists and update its next point.
    if( list->Last )
    {
        list->Last->Next = element;
    }

    // Update the next point of this entry.
    element->Next = NULL;

    // Update the last entry of the list.
    list->Last = element;

    return true;
}

/*!
 * \brief Return the previous element in the list.
 *
 * \param [in]    list           - List
 * \param [in]    element        - Element where the previous element shall be searched
 * \retval                       - Status of the operation
 */
static MacCommand_t* LinkedListGetPrevious( MacCommandsList_t* list, MacCommand_t* element )
{
    if( ( list == NULL ) || ( element == NULL ) )
    {
        return NULL;
    }

    MacCommand_t* curElement;

    // Start at the head of the list
    curElement = list->First;

    // When current element is the first of the list, there's no previous element so we can return NULL immediately.
    if( element != curElement )
    {
        // Loop through all elements until the end is reached or the next of current is the current element.
        while( ( curElement != NULL ) && ( curElement->Next != element ) )
        {
            curElement = curElement->Next;
        }
    }
    else
    {
        curElement = NULL;
    }

    return curElement;
}

/*!
 * \brief Remove an element from the list
 *
 * \param [in]    list           - List where the element shall be removed from.
 * \param [in]    element        - Element to remove
 * \retval                       - Status of the operation
 */
static bool LinkedListRemove( MacCommandsList_t* list, MacCommand_t* element )
{
    if( ( list == NULL ) || ( element == NULL ) )
    {
        return false;
    }

    MacCommand_t* PrevElement = LinkedListGetPrevious( list, element );

    if( list->First == element )
    {
        list->First = element->Next;
    }

    if( list->Last == element )
    {
        list->Last = PrevElement;
    }

    if( PrevElement != NULL )
    {
        PrevElement->Next = element->Next;
    }

    element->Next = NULL;

    return true;
}

/*
 * \brief Determines if a MAC command is sticky or not
 *
 * \param[IN]   cid            - MAC command identifier
 *
 * \retval                     - Status of the operation
 */
static bool IsSticky( uint8_t cid )
{
    switch( cid )
    {
#if (defined( LORAMAC_VERSION ) && ( LORAMAC_VERSION == 0x01010100 ))
        case MOTE_MAC_RESET_IND:
        case MOTE_MAC_REKEY_IND:
        case MOTE_MAC_DEVICE_MODE_IND:
#endif /* LORAMAC_VERSION */
        case MOTE_MAC_DL_CHANNEL_ANS:
        case MOTE_MAC_RX_PARAM_SETUP_ANS:
        case MOTE_MAC_RX_TIMING_SETUP_ANS:
        case MOTE_MAC_TX_PARAM_SETUP_ANS:
#if (defined( LORAMAC_VERSION ) && (( LORAMAC_VERSION == 0x01000400 ) || ( LORAMAC_VERSION == 0x01010100 )))
        case MOTE_MAC_PING_SLOT_CHANNEL_ANS:
#endif /* LORAMAC_VERSION */
            return true;
        default:
            return false;
    }
}

/*
 * \brief Determines if a MAC command requires an explicit confirmation
 *
 * \param[IN]   cid            - MAC command identifier
 *
 * \retval                     - Status of the operation
 */
static bool IsConfirmationRequired( uint8_t cid )
{
    switch( cid )
    {
#if (defined( LORAMAC_VERSION ) && ( LORAMAC_VERSION == 0x01010100 ))
        case MOTE_MAC_RESET_IND:
        case MOTE_MAC_REKEY_IND:
        case MOTE_MAC_DEVICE_MODE_IND:
            return true;
#endif /* LORAMAC_VERSION */
        default:
            return false;
    }
}

LoRaMacCommandStatus_t LoRaMacCommandsInit( void )
{
    // Initialize with default
    memset1( ( uint8_t* )&CommandsCtx, 0, sizeof( CommandsCtx ) );

    LinkedListInit( &CommandsCtx.MacCommandList );

    return LORAMAC_COMMANDS_SUCCESS;
}

LoRaMacCommandStatus_t LoRaMacCommandsAddCmd( uint8_t cid, uint8_t* payload, size_t payloadSize )
{
    if( payload == NULL )
    {
        return LORAMAC_COMMANDS_ERROR_NPE;
    }
    MacCommand_t* newCmd;

    // Allocate a memory slot
    newCmd = MallocNewMacCommandSlot( );

    if( newCmd == NULL )
    {
        return LORAMAC_COMMANDS_ERROR_MEMORY;
    }

    // Add it to the list of Mac commands
    if( LinkedListAdd( &CommandsCtx.MacCommandList, newCmd ) == false )
    {
        return LORAMAC_COMMANDS_ERROR;
    }

    // Set Values
    newCmd->CID = cid;
    newCmd->PayloadSize = payloadSize;
    memcpy1( ( uint8_t* )newCmd->Payload, payload, payloadSize );
    newCmd->IsSticky = IsSticky( cid );
    newCmd->IsConfirmationRequired = IsConfirmationRequired( cid );

    CommandsCtx.SerializedCmdsSize += ( CID_FIELD_SIZE + payloadSize );

    return LORAMAC_COMMANDS_SUCCESS;
}

LoRaMacCommandStatus_t LoRaMacCommandsRemoveCmd( MacCommand_t* macCmd )
{
    if( macCmd == NULL )
    {
        return LORAMAC_COMMANDS_ERROR_NPE;
    }

    // Remove the Mac command element from MacCommandList
    if( LinkedListRemove( &CommandsCtx.MacCommandList, macCmd ) == false )
    {
        return LORAMAC_COMMANDS_ERROR_CMD_NOT_FOUND;
    }

    CommandsCtx.SerializedCmdsSize -= ( CID_FIELD_SIZE + macCmd->PayloadSize );

    // Free the MacCommand Slot
    if( FreeMacCommandSlot( macCmd ) == false )
    {
        return LORAMAC_COMMANDS_ERROR;
    }

    return LORAMAC_COMMANDS_SUCCESS;
}

LoRaMacCommandStatus_t LoRaMacCommandsGetCmd( uint8_t cid, MacCommand_t** macCmd )
{
    MacCommand_t* curElement;

    // Start at the head of the list
    curElement = CommandsCtx.MacCommandList.First;

    // Loop through all elements until we find the element with the given CID
    while( ( curElement != NULL ) && ( curElement->CID != cid ) )
    {
        curElement = curElement->Next;
    }

    // Update the pointer anyway
    *macCmd = curElement;

    // Handle error in case if we reached the end without finding it.
    if( curElement == NULL )
    {
        return LORAMAC_COMMANDS_ERROR_CMD_NOT_FOUND;
    }
    return LORAMAC_COMMANDS_SUCCESS;
}

LoRaMacCommandStatus_t LoRaMacCommandsRemoveNoneStickyCmds( void )
{
    MacCommand_t* curElement;
    MacCommand_t* nexElement;

    // Start at the head of the list
    curElement = CommandsCtx.MacCommandList.First;

    // Loop through all elements
    while( curElement != NULL )
    {
        if( curElement->IsSticky == false )
        {
            nexElement = curElement->Next;
            LoRaMacCommandsRemoveCmd( curElement );
            curElement = nexElement;
        }
        else
        {
            curElement = curElement->Next;
        }
    }

    return LORAMAC_COMMANDS_SUCCESS;
}

LoRaMacCommandStatus_t LoRaMacCommandsRemoveStickyAnsCmds( void )
{
    MacCommand_t* curElement;
    MacCommand_t* nexElement;

    // Start at the head of the list
    curElement = CommandsCtx.MacCommandList.First;

    // Loop through all elements
    while( curElement != NULL )
    {
        nexElement = curElement->Next;
        if( ( IsSticky( curElement->CID ) == true ) &&
            ( IsConfirmationRequired( curElement->CID ) == false ) )
        {
            LoRaMacCommandsRemoveCmd( curElement );
        }
        curElement = nexElement;
    }

    return LORAMAC_COMMANDS_SUCCESS;
}

LoRaMacCommandStatus_t LoRaMacCommandsGetSizeSerializedCmds( size_t* size )
{
    if( size == NULL )
    {
        return LORAMAC_COMMANDS_ERROR_NPE;
    }
    *size = CommandsCtx.SerializedCmdsSize;
    return LORAMAC_COMMANDS_SUCCESS;
}

LoRaMacCommandStatus_t LoRaMacCommandsSerializeCmds( size_t availableSize, size_t* effectiveSize, uint8_t* buffer )
{
    MacCommand_t* curElement = CommandsCtx.MacCommandList.First;
    MacCommand_t* nextElement;
    uint8_t itr = 0;

    if( ( buffer == NULL ) || ( effectiveSize == NULL ) )
    {
        return LORAMAC_COMMANDS_ERROR_NPE;
    }

    // Loop through all elements which fits into the buffer
    while( curElement != NULL )
    {
        // If the next MAC command still fits into the buffer, add it.
        if( ( availableSize - itr ) >= ( CID_FIELD_SIZE + curElement->PayloadSize ) )
        {
            buffer[itr++] = curElement->CID;
            memcpy1( &buffer[itr], curElement->Payload, curElement->PayloadSize );
            itr += curElement->PayloadSize;
        }
        else
        {
            break;
        }
        curElement = curElement->Next;
    }

    // Remove all commands which do not fit into the buffer
    while( curElement != NULL )
    {
        // Store the next element before removing the current one
        nextElement = curElement->Next;
        LoRaMacCommandsRemoveCmd( curElement );
        curElement = nextElement;
    }

    // Fetch the effective size of the mac commands
    LoRaMacCommandsGetSizeSerializedCmds( effectiveSize );

    return LORAMAC_COMMANDS_SUCCESS;
}

uint8_t LoRaMacCommandsGetCmdSize( uint8_t cid )
{
    uint8_t cidSize = 0;

    // Decode Frame MAC commands
    switch( cid )
    {
#if (defined( LORAMAC_VERSION ) && ( LORAMAC_VERSION == 0x01010100 ))
        case SRV_MAC_RESET_CONF:
        {
            // cid + Serv_LoRaWAN_version
            cidSize = 2;
            break;
        }
#endif /* LORAMAC_VERSION */
        case SRV_MAC_LINK_CHECK_ANS:
        {
            // cid + Margin + GwCnt
            cidSize = 3;
            break;
        }
        case SRV_MAC_LINK_ADR_REQ:
        {
            // cid + DataRate_TXPower + ChMask (2) + Redundancy
            cidSize = 5;
            break;
        }
        case SRV_MAC_DUTY_CYCLE_REQ:
        {
            // cid + DutyCyclePL
            cidSize = 2;
            break;
        }
        case SRV_MAC_RX_PARAM_SETUP_REQ:
        {
            // cid + DLsettings + Frequency (3)
            cidSize = 5;
            break;
        }
        case SRV_MAC_DEV_STATUS_REQ:
        {
            // cid
            cidSize = 1;
            break;
        }
        case SRV_MAC_NEW_CHANNEL_REQ:
        {
            // cid + ChIndex + Frequency (3) + DrRange
            cidSize = 6;
            break;
        }
        case SRV_MAC_RX_TIMING_SETUP_REQ:
        {
            // cid + Settings
            cidSize = 2;
            break;
        }
        case SRV_MAC_TX_PARAM_SETUP_REQ:
        {
            // cid + EIRP_DwellTime
            cidSize = 2;
            break;
        }
        case SRV_MAC_DL_CHANNEL_REQ:
        {
            // cid + ChIndex + Frequency (3)
            cidSize = 5;
            break;
        }
#if (defined( LORAMAC_VERSION ) && ( LORAMAC_VERSION == 0x01010100 ))
        case SRV_MAC_REKEY_CONF:
        {
            // cid + Serv_LoRaWAN_version
            cidSize = 2;
            break;
        }
        case SRV_MAC_ADR_PARAM_SETUP_REQ:
        {
            // cid + ADRparam
            cidSize = 2;
            break;
        }
        case SRV_MAC_FORCE_REJOIN_REQ:
        {
            // cid + Payload (2)
            cidSize = 3;
            break;
        }
        case SRV_MAC_REJOIN_PARAM_REQ:
        {
            // cid + Payload (1)
            cidSize = 2;
            break;
        }
        case SRV_MAC_DEVICE_MODE_CONF:
        {
            // cid + Class
            cidSize = 2;
            break;
        }
#endif /* LORAMAC_VERSION */
        case SRV_MAC_DEVICE_TIME_ANS:
        {
            // cid + Seconds (4) + Fractional seconds (1)
            cidSize = 6;
            break;
        }
        case SRV_MAC_PING_SLOT_INFO_ANS:
        {
            // cid
            cidSize = 1;
            break;
        }
        case SRV_MAC_PING_SLOT_CHANNEL_REQ:
        {
            // cid + Frequency (3) + DR
            cidSize = 5;
            break;
        }
        case SRV_MAC_BEACON_TIMING_ANS:
        {
            // cid + TimingDelay (2) + Channel
            cidSize = 4;
            break;
        }
        case SRV_MAC_BEACON_FREQ_REQ:
        {
            // cid + Frequency (3)
            cidSize = 4;
            break;
        }
        default:
        {
            // Unknown command. ABORT MAC commands processing
            break;
        }
    }
    return cidSize;
}
