/*!
 * \file      LmhpClockSync.c
 *
 * \brief     Implements the LoRa-Alliance clock synchronization package
 *            Specification V1.0.0: https://resources.lora-alliance.org/technical-specifications/lorawan-application-layer-clock-synchronization-specification-v1-0-0
 *            Specification V2.0.0: https://resources.lora-alliance.org/technical-specifications/ts003-2-0-0-application-layer-clock-synchronization
 *
 * \copyright Revised BSD License, see section \ref LICENSE.
 *
 * \code
 *                ______                              _
 *               / _____)             _              | |
 *              ( (____  _____ ____ _| |_ _____  ____| |__
 *               \____ \| ___ |    (_   _) ___ |/ ___)  _ \
 *               _____) ) ____| | | || |_| ____( (___| | | |
 *              (______/|_____)_|_|_| \__)_____)\____)_| |_|
 *              (C)2013-2018 Semtech
 *
 * \endcode
 *
 * \author    Miguel Luis ( Semtech )
 */
/**
  ******************************************************************************
  *
  *          Portions COPYRIGHT 2020 STMicroelectronics
  *
  * @file    LmhpClockSync.c
  * @author  MCD Application Team
  * @brief   Clock Synchronisation Package definition
  ******************************************************************************
  */
#include "LoRaMac.h"
#include "LmHandler.h"
#include "LmhpClockSync.h"
#include "utilities.h"

/*!
 * LoRaWAN Application Layer Clock Synchronization Specification
 */
#define CLOCK_SYNC_PORT                             202

#define CLOCK_SYNC_ID                               1

#if (LORAWAN_PACKAGES_VERSION == 1)
#define CLOCK_SYNC_VERSION                          1
#elif (LORAWAN_PACKAGES_VERSION == 2)
#define CLOCK_SYNC_VERSION                          2
#endif /* LORAWAN_PACKAGES_VERSION */

/*!
 * Package current context
 */
typedef struct LmhpClockSyncState_s
{
    bool Initialized;
    bool IsTxPending;
    uint8_t DataBufferMaxSize;
    uint8_t *DataBuffer;
    union
    {
        uint8_t Value;
        struct
        {
            uint8_t TokenReq:    4;
            uint8_t AnsRequired: 1;
            uint8_t RFU:         3;
        } Fields;
    } TimeReqParam;
    bool AppTimeReqPending;
#if ( CLOCK_SYNC_VERSION == 2 )
    bool SysTimeNotSync;
#endif /* CLOCK_SYNC_VERSION */
    bool AdrEnabledPrev;
    uint8_t NbTransPrev;
    uint8_t DataratePrev;
    uint8_t NbTransmissions;
} LmhpClockSyncState_t;

typedef enum LmhpClockSyncMoteCmd_e
{
    CLOCK_SYNC_PKG_VERSION_ANS       = 0x00,
    CLOCK_SYNC_APP_TIME_REQ          = 0x01,
    CLOCK_SYNC_APP_TIME_PERIOD_ANS   = 0x02,
    CLOCK_SYNC_FORCE_RESYNC_ANS      = 0x03,
} LmhpClockSyncMoteCmd_t;

typedef enum LmhpClockSyncSrvCmd_e
{
    CLOCK_SYNC_PKG_VERSION_REQ       = 0x00,
    CLOCK_SYNC_APP_TIME_ANS          = 0x01,
    CLOCK_SYNC_APP_TIME_PERIOD_REQ   = 0x02,
    CLOCK_SYNC_FORCE_RESYNC_REQ      = 0x03,
} LmhpClockSyncSrvCmd_t;

/*!
 * Initializes the package with provided parameters
 *
 * \param [in] params            Pointer to the package parameters
 * \param [in] dataBuffer        Pointer to main application buffer
 * \param [in] dataBufferMaxSize Main application buffer maximum size
 */
static void LmhpClockSyncInit( void *params, uint8_t *dataBuffer, uint8_t dataBufferMaxSize );

/*!
 * Returns the current package initialization status.
 *
 * \retval status Package initialization status
 *                [true: Initialized, false: Not initialized]
 */
static bool LmhpClockSyncIsInitialized( void );

/*!
 * Returns if a package transmission is pending or not.
 *
 * \retval status Package transmission status
 *                [true: pending, false: Not pending]
 */
static bool LmhpClockSyncIsTxPending( void );

/*!
 * Processes the internal package events.
 */
static void LmhpClockSyncProcess( void );

/*!
 * Processes the MCSP Confirm
 *
 * \param [in] mcpsConfirm MCPS confirmation primitive data
 */
static void LmhpClockSyncOnMcpsConfirm( McpsConfirm_t *mcpsConfirm );

/*!
 * Processes the MCPS Indication
 *
 * \param [in] mcpsIndication     MCPS indication primitive data
 */
static void LmhpClockSyncOnMcpsIndication( McpsIndication_t *mcpsIndication );

static void OnPeriodicTimeStartTimer( void *context );

static LmhpClockSyncState_t LmhpClockSyncState =
{
    .Initialized = false,
    .IsTxPending = false,
    .TimeReqParam.Value = 0,
    .AppTimeReqPending = false,
#if ( CLOCK_SYNC_VERSION == 2 )
    .SysTimeNotSync = false,
#endif /* CLOCK_SYNC_VERSION */
    .AdrEnabledPrev = false,
    .NbTransPrev = 0,
    .NbTransmissions = 0,
};

static LmhPackage_t LmhpClockSyncPackage =
{
    .Port = CLOCK_SYNC_PORT,
    .Init = LmhpClockSyncInit,
    .IsInitialized = LmhpClockSyncIsInitialized,
    .IsTxPending = LmhpClockSyncIsTxPending,
    .Process = LmhpClockSyncProcess,
    .OnMcpsConfirmProcess = LmhpClockSyncOnMcpsConfirm,
    .OnMcpsIndicationProcess = LmhpClockSyncOnMcpsIndication,
    .OnMlmeConfirmProcess = NULL,                              /* Not used in this package */
    .OnMlmeIndicationProcess = NULL,                           /* Not used in this package */
    .OnJoinRequest = NULL,                                     /* To be initialized by LmHandler */
    .OnDeviceTimeRequest = NULL,                               /* To be initialized by LmHandler */
    .OnSysTimeUpdate = NULL,                                   /* To be initialized by LmHandler */
    .OnPackageProcessEvent = NULL,                             /* To be initialized by LmHandler */
};

/*!
 * Periodic Time start timer
 */
static TimerEvent_t PeriodicTimeStartTimer;

LmhPackage_t *LmhpClockSyncPackageFactory( void )
{
    return &LmhpClockSyncPackage;
}

static void LmhpClockSyncInit( void *params, uint8_t *dataBuffer, uint8_t dataBufferMaxSize )
{
    if( dataBuffer != NULL )
    {
        LmhpClockSyncState.DataBuffer = dataBuffer;
        LmhpClockSyncState.DataBufferMaxSize = dataBufferMaxSize;
        LmhpClockSyncState.Initialized = true;
        TimerInit( &PeriodicTimeStartTimer, OnPeriodicTimeStartTimer );
    }
    else
    {
        LmhpClockSyncState.Initialized = false;
    }
    LmhpClockSyncState.IsTxPending = false;
}

static bool LmhpClockSyncIsInitialized( void )
{
    return LmhpClockSyncState.Initialized;
}

static bool LmhpClockSyncIsTxPending( void )
{
    return LmhpClockSyncState.IsTxPending;
}

static void LmhpClockSyncProcess( void )
{
    if( LmhpClockSyncState.NbTransmissions > 0 )
    {
        if( LmhpClockSyncAppTimeReq( ) == LORAMAC_HANDLER_SUCCESS )
        {
            LmhpClockSyncState.NbTransmissions--;
        }
    }
}

static void LmhpClockSyncOnMcpsConfirm( McpsConfirm_t *mcpsConfirm )
{
    MibRequestConfirm_t mibReq;

    if( LmhpClockSyncState.AppTimeReqPending == true )
    {
        /* Revert ADR setting */
        mibReq.Type = MIB_ADR;
        mibReq.Param.AdrEnable = LmhpClockSyncState.AdrEnabledPrev;
        LoRaMacMibSetRequestConfirm( &mibReq );

        /* Revert NbTrans setting */
        mibReq.Type = MIB_CHANNELS_NB_TRANS;
        mibReq.Param.ChannelsNbTrans = LmhpClockSyncState.NbTransPrev;
        LoRaMacMibSetRequestConfirm( &mibReq );

        /* Revert data rate setting */
        mibReq.Type = MIB_CHANNELS_DATARATE;
        mibReq.Param.ChannelsDatarate = LmhpClockSyncState.DataratePrev;
        LoRaMacMibSetRequestConfirm( &mibReq );

        LmhpClockSyncState.AppTimeReqPending = false;
    }
}

static void LmhpClockSyncOnMcpsIndication( McpsIndication_t *mcpsIndication )
{
    uint8_t cmdIndex = 0;
    uint8_t dataBufferIndex = 0;

    if( mcpsIndication->Port != CLOCK_SYNC_PORT )
    {
        return;
    }

    while( cmdIndex < mcpsIndication->BufferSize )
    {
        switch( mcpsIndication->Buffer[cmdIndex++] )
        {
            case CLOCK_SYNC_PKG_VERSION_REQ:
                {
                    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = CLOCK_SYNC_PKG_VERSION_ANS;
                    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = CLOCK_SYNC_ID;
                    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = CLOCK_SYNC_VERSION;
                    break;
                }
            case CLOCK_SYNC_APP_TIME_ANS:
                {
                    LmhpClockSyncState.NbTransmissions = 0;

                    /* Check if a more precise time correction has been received. */
                    /* If yes then don't process and ignore this answer. */
                    if( mcpsIndication->DeviceTimeAnsReceived == true )
                    {
                        cmdIndex += 5;
                        break;
                    }
                    int32_t timeCorrection = 0;
                    timeCorrection  = ( mcpsIndication->Buffer[cmdIndex++] << 0 ) & 0x000000FF;
                    timeCorrection += ( mcpsIndication->Buffer[cmdIndex++] << 8 ) & 0x0000FF00;
                    timeCorrection += ( mcpsIndication->Buffer[cmdIndex++] << 16 ) & 0x00FF0000;
                    timeCorrection += ( mcpsIndication->Buffer[cmdIndex++] << 24 ) & 0xFF000000;
                    if( ( mcpsIndication->Buffer[cmdIndex++] & 0x0F ) == LmhpClockSyncState.TimeReqParam.Fields.TokenReq )
                    {
                        SysTime_t curTime = { .Seconds = 0, .SubSeconds = 0 };
                        curTime = SysTimeGet( );
#if ( CLOCK_SYNC_VERSION == 1 )
                        curTime.Seconds += timeCorrection;
                        SysTimeSet( curTime );
                        LmhpClockSyncState.TimeReqParam.Fields.TokenReq = ( LmhpClockSyncState.TimeReqParam.Fields.TokenReq + 1 ) & 0x0F;
                        if( LmhpClockSyncPackage.OnSysTimeUpdate != NULL )
                        {
                            if( ( timeCorrection >= -1 ) && ( timeCorrection <= 1 ) )
                            {
                                LmhpClockSyncPackage.OnSysTimeUpdate( );
                            }
                        }
#elif ( CLOCK_SYNC_VERSION == 2 )
                        if( LmhpClockSyncState.SysTimeNotSync == true )
                        {
                            curTime.Seconds += UNIX_GPS_EPOCH_OFFSET;
                        }

                        curTime.Seconds += timeCorrection;
                        SysTimeSet( curTime );
                        LmhpClockSyncState.TimeReqParam.Fields.TokenReq = ( LmhpClockSyncState.TimeReqParam.Fields.TokenReq + 1 ) & 0x0F;

                        if( timeCorrection == ( int32_t )0x7FFFFFFF )
                        {
                            LmhpClockSyncState.NbTransmissions = 1;
                        }
                        else if( LmhpClockSyncPackage.OnSysTimeUpdate != NULL )
                        {
                            LmhpClockSyncPackage.OnSysTimeUpdate( );
                        }
#endif /* CLOCK_SYNC_VERSION */
                    }
                    break;
                }
            case CLOCK_SYNC_APP_TIME_PERIOD_REQ:
                {
                    /* Increment index */
                    cmdIndex++;

                    uint32_t periodTime = mcpsIndication->Buffer[cmdIndex++] & 0x0F;
                    periodTime = ( 128 << periodTime ) + randr( 0, 30 );

                    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = CLOCK_SYNC_APP_TIME_PERIOD_ANS;
                    /* Answer status supported. */
                    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = 0x00;

                    SysTime_t curTime = SysTimeGet( );
                    /* Subtract Unix to Gps epoch offset. The system time is based on Unix time. */
                    if( curTime.Seconds > UNIX_GPS_EPOCH_OFFSET )
                    {
                        curTime.Seconds -= UNIX_GPS_EPOCH_OFFSET;
                    }
                    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = ( curTime.Seconds >> 0 ) & 0xFF;
                    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = ( curTime.Seconds >> 8 ) & 0xFF;
                    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = ( curTime.Seconds >> 16 ) & 0xFF;
                    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = ( curTime.Seconds >> 24 ) & 0xFF;

                    /* Start Periodic timer */
                    TimerSetValue( &PeriodicTimeStartTimer, periodTime * 1000 );
                    TimerStart( &PeriodicTimeStartTimer );

                    break;
                }
            case CLOCK_SYNC_FORCE_RESYNC_REQ:
                {
                    LmhpClockSyncState.NbTransmissions = mcpsIndication->Buffer[cmdIndex++] & 0X07;
                    break;
                }
        }
    }

    if( dataBufferIndex != 0 )
    {
        /* Answer commands */
        LmHandlerAppData_t appData =
        {
            .Buffer = LmhpClockSyncState.DataBuffer,
            .BufferSize = dataBufferIndex,
            .Port = CLOCK_SYNC_PORT
        };

        bool current_dutycycle;
        LmHandlerGetDutyCycleEnable( &current_dutycycle );

        /* force Duty Cycle OFF to this Send */
        LmHandlerSetDutyCycleEnable( false );
        LmHandlerSend( &appData, LORAMAC_HANDLER_UNCONFIRMED_MSG, true );

        /* restore initial Duty Cycle */
        LmHandlerSetDutyCycleEnable( current_dutycycle );
    }
}

LmHandlerErrorStatus_t LmhpClockSyncAppTimeReq( void )
{
    if( LmHandlerIsBusy( ) == true )
    {
        return LORAMAC_HANDLER_ERROR;
    }

    if( LmhpClockSyncState.AppTimeReqPending == false )
    {
        MibRequestConfirm_t mibReq;

        /* Disable ADR */
        mibReq.Type = MIB_ADR;
        LoRaMacMibGetRequestConfirm( &mibReq );
        LmhpClockSyncState.AdrEnabledPrev = mibReq.Param.AdrEnable;
        mibReq.Param.AdrEnable = false;
        LoRaMacMibSetRequestConfirm( &mibReq );

        /* Set NbTrans = 1 */
        mibReq.Type = MIB_CHANNELS_NB_TRANS;
        LoRaMacMibGetRequestConfirm( &mibReq );
        LmhpClockSyncState.NbTransPrev = mibReq.Param.ChannelsNbTrans;
        mibReq.Param.ChannelsNbTrans = 1;
        LoRaMacMibSetRequestConfirm( &mibReq );

        /* Store data rate */
        mibReq.Type = MIB_CHANNELS_DATARATE;
        LoRaMacMibGetRequestConfirm( &mibReq );
        LmhpClockSyncState.DataratePrev = mibReq.Param.ChannelsDatarate;

#if ( CLOCK_SYNC_VERSION == 1 )
        /* Add DeviceTimeReq MAC command. */
        /* In case the network server supports this more precise command */
        /* this package will use DeviceTimeAns answer as clock synchronization */
        /* mechanism. */
        if( LmhpClockSyncPackage.OnDeviceTimeRequest != NULL )
        {
            LmhpClockSyncPackage.OnDeviceTimeRequest( );
        }
#endif /* CLOCK_SYNC_VERSION */
    }

    SysTime_t curTime = SysTimeGet( );
    uint8_t dataBufferIndex = 0;

    /* Subtract Unix to Gps epoch offset. The system time is based on Unix time. */
    curTime.Seconds -= UNIX_GPS_EPOCH_OFFSET;
    if( curTime.Seconds > UNIX_GPS_EPOCH_OFFSET )
    {
        curTime.Seconds -= UNIX_GPS_EPOCH_OFFSET;
    }
#if ( CLOCK_SYNC_VERSION == 2 )
    else
    {
        LmhpClockSyncState.SysTimeNotSync = true;
    }
#endif /* CLOCK_SYNC_VERSION */
    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = CLOCK_SYNC_APP_TIME_REQ;
    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = ( curTime.Seconds >> 0 ) & 0xFF;
    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = ( curTime.Seconds >> 8 ) & 0xFF;
    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = ( curTime.Seconds >> 16 ) & 0xFF;
    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = ( curTime.Seconds >> 24 ) & 0xFF;
    LmhpClockSyncState.TimeReqParam.Fields.AnsRequired = 0;
    LmhpClockSyncState.DataBuffer[dataBufferIndex++] = LmhpClockSyncState.TimeReqParam.Value;

    LmHandlerAppData_t appData =
    {
        .Buffer = LmhpClockSyncState.DataBuffer,
        .BufferSize = dataBufferIndex,
        .Port = CLOCK_SYNC_PORT
    };
    LmhpClockSyncState.AppTimeReqPending = true;

    bool current_dutycycle;
    LmHandlerGetDutyCycleEnable( &current_dutycycle );

    /* force Duty Cycle OFF to this Send */
    LmHandlerSetDutyCycleEnable( false );
    LmHandlerErrorStatus_t status = LmHandlerSend( &appData, LORAMAC_HANDLER_UNCONFIRMED_MSG, true );

    /* restore initial Duty Cycle */
    LmHandlerSetDutyCycleEnable( current_dutycycle );

    return status;
}

static void OnPeriodicTimeStartTimer( void *context )
{
    LmhpClockSyncState.NbTransmissions = 1;
    TimerStart( &PeriodicTimeStartTimer );
    if( LmhpClockSyncPackage.OnPackageProcessEvent != NULL )
    {
        LmhpClockSyncPackage.OnPackageProcessEvent();
    }
}