/*
 * FreeRTOS-Cellular-Interface v1.3.0
 * Copyright (C) 2020 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * https://www.FreeRTOS.org
 * https://github.com/FreeRTOS
 */

#include "cellular_config.h"
#include "cellular_config_defaults.h"

#include <stdint.h>

#include "cellular_platform.h"
#include "cellular_common.h"
#include "cellular_common_portable.h"
#include "cellular_r4.h"

/*-----------------------------------------------------------*/

#define ENBABLE_MODULE_UE_RETRY_COUNT         ( 3U )
#define ENBABLE_MODULE_UE_RETRY_TIMEOUT       ( 5000U )
#define ENBABLE_MODULE_UE_REBOOT_POLL_TIME    ( 2000U )
#define ENBABLE_MODULE_UE_REBOOT_MAX_TIME     ( 25000U )

/*-----------------------------------------------------------*/

static CellularError_t sendAtCommandWithRetryTimeout( CellularContext_t * pContext,
                                                      const CellularAtReq_t * pAtReq );

/*-----------------------------------------------------------*/

static cellularModuleContext_t cellularHl7802Context = { 0 };

/* FreeRTOS Cellular Common Library porting interface. */
/* coverity[misra_c_2012_rule_8_7_violation] */
const char * CellularSrcTokenErrorTable[] =
{ "ERROR", "BUSY", "NO CARRIER", "NO ANSWER", "NO DIALTONE", "ABORTED", "+CMS ERROR", "+CME ERROR", "SEND FAIL" };
/* FreeRTOS Cellular Common Library porting interface. */
/* coverity[misra_c_2012_rule_8_7_violation] */
uint32_t CellularSrcTokenErrorTableSize = sizeof( CellularSrcTokenErrorTable ) / sizeof( char * );

/* FreeRTOS Cellular Common Library porting interface. */
/* coverity[misra_c_2012_rule_8_7_violation] */
const char * CellularSrcTokenSuccessTable[] =
{ "OK", "@" };
/* FreeRTOS Cellular Common Library porting interface. */
/* coverity[misra_c_2012_rule_8_7_violation] */
uint32_t CellularSrcTokenSuccessTableSize = sizeof( CellularSrcTokenSuccessTable ) / sizeof( char * );

/* FreeRTOS Cellular Common Library porting interface. */
/* coverity[misra_c_2012_rule_8_7_violation] */
const char * CellularUrcTokenWoPrefixTable[] =
{ "RDY" };
/* FreeRTOS Cellular Common Library porting interface. */
/* coverity[misra_c_2012_rule_8_7_violation] */
uint32_t CellularUrcTokenWoPrefixTableSize = sizeof( CellularUrcTokenWoPrefixTable ) / sizeof( char * );

/*-----------------------------------------------------------*/

static CellularError_t sendAtCommandWithRetryTimeout( CellularContext_t * pContext,
                                                      const CellularAtReq_t * pAtReq )
{
    CellularError_t cellularStatus = CELLULAR_SUCCESS;
    CellularPktStatus_t pktStatus = CELLULAR_PKT_STATUS_OK;
    uint8_t tryCount = 0;

    if( pAtReq == NULL )
    {
        cellularStatus = CELLULAR_BAD_PARAMETER;
    }
    else
    {
        for( ; tryCount < ENBABLE_MODULE_UE_RETRY_COUNT; tryCount++ )
        {
            pktStatus = _Cellular_TimeoutAtcmdRequestWithCallback( pContext, *pAtReq, ENBABLE_MODULE_UE_RETRY_TIMEOUT );
            cellularStatus = _Cellular_TranslatePktStatus( pktStatus );

            if( cellularStatus == CELLULAR_SUCCESS )
            {
                break;
            }
        }
    }

    return cellularStatus;
}

/*-----------------------------------------------------------*/

/* FreeRTOS Cellular Common Library porting interface. */
/* coverity[misra_c_2012_rule_8_7_violation] */
CellularError_t Cellular_ModuleInit( const CellularContext_t * pContext,
                                     void ** ppModuleContext )
{
    CellularError_t cellularStatus = CELLULAR_SUCCESS;
    uint32_t i = 0;

    if( pContext == NULL )
    {
        cellularStatus = CELLULAR_INVALID_HANDLE;
    }
    else if( ppModuleContext == NULL )
    {
        cellularStatus = CELLULAR_BAD_PARAMETER;
    }
    else
    {
        /* Initialize the module context. */
        ( void ) memset( &cellularHl7802Context, 0, sizeof( cellularModuleContext_t ) );

        for( i = 0; i < TCP_SESSION_TABLE_LEGNTH; i++ )
        {
            cellularHl7802Context.pSessionMap[ i ] = INVALID_SOCKET_INDEX;
        }

        *ppModuleContext = ( void * ) &cellularHl7802Context;
    }

    return cellularStatus;
}

/*-----------------------------------------------------------*/

/* FreeRTOS Cellular Common Library porting interface. */
/* coverity[misra_c_2012_rule_8_7_violation] */
CellularError_t Cellular_ModuleCleanUp( const CellularContext_t * pContext )
{
    CellularError_t cellularStatus = CELLULAR_SUCCESS;

    if( pContext == NULL )
    {
        cellularStatus = CELLULAR_INVALID_HANDLE;
    }

    return cellularStatus;
}

/*-----------------------------------------------------------*/

/* Parse AT response for current MNO profile */
static CellularPktStatus_t _Cellular_RecvFuncGetCurrentMNOProfile( CellularContext_t * pContext,
                                                                   const CellularATCommandResponse_t * pAtResp,
                                                                   void * pData,
                                                                   uint16_t dataLen )
{
    char * pInputLine = NULL;
    CellularPktStatus_t pktStatus = CELLULAR_PKT_STATUS_OK;
    CellularATError_t atCoreStatus = CELLULAR_AT_SUCCESS;
    MNOProfileType_t * pCurrentMNOProfile = ( MNOProfileType_t * ) pData;
    char * pToken = NULL;
    int32_t tempValue = 0;

    if( pContext == NULL )
    {
        LogError( ( "_Cellular_RecvFuncGetCurrentMNOProfile: Invalid handle" ) );
        pktStatus = CELLULAR_PKT_STATUS_INVALID_HANDLE;
    }
    else if( ( pData == NULL ) || ( dataLen != sizeof( MNOProfileType_t ) ) )
    {
        LogError( ( "_Cellular_RecvFuncGetCurrentMNOProfile: Invalid param" ) );
        pktStatus = CELLULAR_PKT_STATUS_BAD_PARAM;
    }
    else if( ( pAtResp == NULL ) || ( pAtResp->pItm == NULL ) || ( pAtResp->pItm->pLine == NULL ) )
    {
        LogError( ( "_Cellular_RecvFuncGetCurrentMNOProfile: Input Line passed is NULL" ) );
        pktStatus = CELLULAR_PKT_STATUS_FAILURE;
    }
    else
    {
        pInputLine = pAtResp->pItm->pLine;

        /* Remove prefix. */
        atCoreStatus = Cellular_ATRemovePrefix( &pInputLine );

        /* Remove leading space. */
        if( atCoreStatus == CELLULAR_AT_SUCCESS )
        {
            atCoreStatus = Cellular_ATRemoveLeadingWhiteSpaces( &pInputLine );
        }

        if( atCoreStatus == CELLULAR_AT_SUCCESS )
        {
            atCoreStatus = Cellular_ATGetNextTok( &pInputLine, &pToken );

            if( atCoreStatus == CELLULAR_AT_SUCCESS )
            {
                atCoreStatus = Cellular_ATStrtoi( pToken, 10, &tempValue );

                if( atCoreStatus == CELLULAR_AT_SUCCESS )
                {
                    if( ( tempValue >= MNO_PROFILE_SW_DEFAULT ) && ( tempValue <= MNO_PROFILE_STANDARD_EUROPE ) )
                    {
                        *pCurrentMNOProfile = tempValue;
                        LogInfo( ( "_Cellular_RecvFuncGetCurrentMNOProfile: pCurrentMNOProfile [%d]", *pCurrentMNOProfile ) );
                    }
                }
                else
                {
                    atCoreStatus = CELLULAR_AT_ERROR;
                }
            }
        }

        pktStatus = _Cellular_TranslateAtCoreStatus( atCoreStatus );
    }

    return pktStatus;
}

/*-----------------------------------------------------------*/
/* Get modem's current MNO profile */
static CellularError_t _Cellular_GetCurrentMNOProfile( CellularContext_t * pContext,
                                                       MNOProfileType_t * pCurrentMNOProfile )
{
    CellularError_t cellularStatus = CELLULAR_SUCCESS;
    CellularPktStatus_t pktStatus = CELLULAR_PKT_STATUS_OK;
    CellularAtReq_t atReqGetCurrentMNOProfile =
    {
        "AT+UMNOPROF?",
        CELLULAR_AT_WITH_PREFIX,
        "+UMNOPROF",
        _Cellular_RecvFuncGetCurrentMNOProfile,
        NULL,
        sizeof( MNOProfileType_t ),
    };

    atReqGetCurrentMNOProfile.pData = pCurrentMNOProfile;

    /* Internal function. Callee check parameters. */
    pktStatus = _Cellular_AtcmdRequestWithCallback( pContext, atReqGetCurrentMNOProfile );
    cellularStatus = _Cellular_TranslatePktStatus( pktStatus );

    return cellularStatus;
}

/*-----------------------------------------------------------*/

/* Reboot modem and wait for ready state. */

CellularError_t rebootCellularModem( CellularContext_t * pContext,
                                     bool disablePsm,
                                     bool disableEidrx )
{
    CellularError_t cellularStatus = CELLULAR_SUCCESS;
    CellularPktStatus_t pktStatus = CELLULAR_PKT_STATUS_OK;
    uint32_t count = 0;
    CellularAtReq_t atReqGetWoPrefix =
    {
        NULL,
        CELLULAR_AT_WO_PREFIX,
        NULL,
        NULL,
        NULL,
        0
    };
    CellularAtReq_t atReqGetNoResult =
    {
        "AT+CFUN=15",
        CELLULAR_AT_NO_RESULT,
        NULL,
        NULL,
        NULL,
        0
    };

    LogInfo( ( "rebootCellularModem: Rebooting Modem." ) );
    cellularStatus = sendAtCommandWithRetryTimeout( pContext, &atReqGetNoResult );
    Platform_Delay( ENBABLE_MODULE_UE_REBOOT_POLL_TIME );
    count = count + ENBABLE_MODULE_UE_REBOOT_POLL_TIME;

    /* wait for modem after reboot*/
    while( count < ENBABLE_MODULE_UE_REBOOT_MAX_TIME )
    {
        LogInfo( ( "rebootCellularModem: Use ATE0 command to test modem status." ) );

        atReqGetWoPrefix.pAtCmd = "ATE0";

        pktStatus = _Cellular_TimeoutAtcmdRequestWithCallback( pContext, atReqGetWoPrefix, ENBABLE_MODULE_UE_REBOOT_POLL_TIME );
        cellularStatus = _Cellular_TranslatePktStatus( pktStatus );

        if( cellularStatus == CELLULAR_SUCCESS )
        {
            LogInfo( ( "rebootCellularModem: Modem is now available." ) );

            Platform_Delay( ENBABLE_MODULE_UE_REBOOT_POLL_TIME * 3 );

            /* Query current PSM settings. */
            atReqGetNoResult.pAtCmd = "AT+CPSMS?";
            cellularStatus = sendAtCommandWithRetryTimeout( pContext, &atReqGetNoResult );

            if( disablePsm && ( cellularStatus == CELLULAR_SUCCESS ) )
            {
                LogInfo( ( "rebootCellularModem: Disable +CPSMS setting." ) );
                atReqGetNoResult.pAtCmd = "AT+CPSMS=0";
                cellularStatus = sendAtCommandWithRetryTimeout( pContext, &atReqGetNoResult );
            }

            if( disableEidrx && ( cellularStatus == CELLULAR_SUCCESS ) )
            {
                LogInfo( ( "rebootCellularModem: Disable +CEDRXS setting." ) );
                atReqGetNoResult.pAtCmd = "AT+CEDRXS=0,4";
                cellularStatus = sendAtCommandWithRetryTimeout( pContext, &atReqGetNoResult );
            }

            break;
        }
        else
        {
            LogWarn( ( "rebootCellularModem: Modem is not ready. Retry sending ATE0." ) );
        }

        count = count + ENBABLE_MODULE_UE_REBOOT_POLL_TIME;
    }

    return cellularStatus;
}

/*-----------------------------------------------------------*/

/* FreeRTOS Cellular Common Library porting interface. */
/* coverity[misra_c_2012_rule_8_7_violation] */
CellularError_t Cellular_ModuleEnableUE( CellularContext_t * pContext )
{
    CellularError_t cellularStatus = CELLULAR_SUCCESS;
    CellularAtReq_t atReqGetNoResult =
    {
        NULL,
        CELLULAR_AT_NO_RESULT,
        NULL,
        NULL,
        NULL,
        0
    };
    CellularAtReq_t atReqGetWithResult =
    {
        NULL,
        CELLULAR_AT_WO_PREFIX,
        NULL,
        NULL,
        NULL,
        0
    };
    char pAtCmdBuf[ CELLULAR_AT_CMD_MAX_SIZE ] = { 0 };

    if( pContext != NULL )
    {
        /* Disable echo. */
        atReqGetWithResult.pAtCmd = "ATE0";
        cellularStatus = sendAtCommandWithRetryTimeout( pContext, &atReqGetWithResult );

        if( cellularStatus == CELLULAR_SUCCESS )
        {
            /* Disable DTR function. */
            atReqGetNoResult.pAtCmd = "AT&D0";
            cellularStatus = sendAtCommandWithRetryTimeout( pContext, &atReqGetNoResult );
        }

        #ifndef CELLULAR_CONFIG_DISABLE_FLOW_CONTROL
            if( cellularStatus == CELLULAR_SUCCESS )
            {
                /* Enable RTS/CTS hardware flow control. */
                atReqGetNoResult.pAtCmd = "AT+IFC=2,2";
                cellularStatus = sendAtCommandWithRetryTimeout( pContext, &atReqGetNoResult );
            }
        #endif

        if( cellularStatus == CELLULAR_SUCCESS )
        {
            /* Report verbose mobile termination error. */
            atReqGetNoResult.pAtCmd = "AT+CMEE=2";
            cellularStatus = sendAtCommandWithRetryTimeout( pContext, &atReqGetNoResult );
        }

        if( cellularStatus == CELLULAR_SUCCESS )
        {
            /* Setup mobile network operator profiles. */
            /* Setting +UMNOPROF profile will automatically select suitable values for +URAT and +UBANDMASK. */

            /* Check current MNO profile first to avoid unneccessary modem reboot. */
            MNOProfileType_t currentMNOProfile = MNO_PROFILE_NOT_SET;
            cellularStatus = _Cellular_GetCurrentMNOProfile( pContext, &currentMNOProfile );

            LogInfo( ( "Cellular_ModuleEnableUE: currentMNOProfile = [%d], desiredProfile = [%d]", currentMNOProfile, CELLULAR_CONFIG_SARA_R4_SET_MNO_PROFILE ) );

            if( cellularStatus == CELLULAR_SUCCESS )
            {
                /* Set MNO profile if not set already */
                if( ( currentMNOProfile != CELLULAR_CONFIG_SARA_R4_SET_MNO_PROFILE ) && ( CELLULAR_CONFIG_SARA_R4_SET_MNO_PROFILE != MNO_PROFILE_NOT_SET ) )
                {
                    atReqGetNoResult.pAtCmd = pAtCmdBuf;
                    ( void ) snprintf( ( char * ) atReqGetNoResult.pAtCmd, CELLULAR_AT_CMD_MAX_SIZE, "%s%d", "AT+COPS=2;+UMNOPROF=", CELLULAR_CONFIG_SARA_R4_SET_MNO_PROFILE );
                    cellularStatus = sendAtCommandWithRetryTimeout( pContext, &atReqGetNoResult );

                    if( cellularStatus == CELLULAR_SUCCESS )
                    {
                        cellularStatus = rebootCellularModem( pContext, true, true );
                    }
                }

                #ifdef CELLULAR_CONFIG_SARA_R4_REBOOT_ON_INIT
                    else
                    {
                        cellularStatus = rebootCellularModem( pContext, true, true );
                    }
                #endif
            }
        }

        if( cellularStatus == CELLULAR_SUCCESS )
        {
            atReqGetNoResult.pAtCmd = "AT+COPS=0";
            cellularStatus = sendAtCommandWithRetryTimeout( pContext, &atReqGetNoResult );
        }

        if( cellularStatus == CELLULAR_SUCCESS )
        {
            atReqGetNoResult.pAtCmd = "AT+CFUN=1";
            cellularStatus = sendAtCommandWithRetryTimeout( pContext, &atReqGetNoResult );
        }
    }

    return cellularStatus;
}

/*-----------------------------------------------------------*/

/* FreeRTOS Cellular Common Library porting interface. */
/* coverity[misra_c_2012_rule_8_7_violation] */
CellularError_t Cellular_ModuleEnableUrc( CellularContext_t * pContext )
{
    CellularError_t cellularStatus = CELLULAR_SUCCESS;
    CellularAtReq_t atReqGetNoResult =
    {
        NULL,
        CELLULAR_AT_NO_RESULT,
        NULL,
        NULL,
        NULL,
        0
    };

    atReqGetNoResult.pAtCmd = "AT+COPS=3,2";
    ( void ) _Cellular_AtcmdRequestWithCallback( pContext, atReqGetNoResult );

    atReqGetNoResult.pAtCmd = "AT+CREG=2";
    ( void ) _Cellular_AtcmdRequestWithCallback( pContext, atReqGetNoResult );

    atReqGetNoResult.pAtCmd = "AT+CGREG=2";
    ( void ) _Cellular_AtcmdRequestWithCallback( pContext, atReqGetNoResult );

    atReqGetNoResult.pAtCmd = "AT+CEREG=2";
    ( void ) _Cellular_AtcmdRequestWithCallback( pContext, atReqGetNoResult );

    atReqGetNoResult.pAtCmd = "AT+CTZR=1";
    ( void ) _Cellular_AtcmdRequestWithCallback( pContext, atReqGetNoResult );

    /* TODO: +CGEV URC enable. */
    /* atReqGetNoResult.pAtCmd = "AT+CGEREP=2,0"; */
    /* (void)_Cellular_AtcmdRequestWithCallback(pContext, atReqGetNoResult); */

    /* Power saving mode URC enable. */
    atReqGetNoResult.pAtCmd = "AT+UPSMR=1";
    ( void ) _Cellular_AtcmdRequestWithCallback( pContext, atReqGetNoResult );

    /* Mobile termination event reporting +CIEV URC enable. */
    atReqGetNoResult.pAtCmd = "AT+CMER=1,0,0,2,1";
    ( void ) _Cellular_AtcmdRequestWithCallback( pContext, atReqGetNoResult );

    /* Enable signal level change indication via +CIEV URC. (To enable all indications, set to 4095) */
    atReqGetNoResult.pAtCmd = "AT+UCIND=2";
    ( void ) _Cellular_AtcmdRequestWithCallback( pContext, atReqGetNoResult );

    /* Enable greeting message "RDY" on modem bootup. */
    atReqGetNoResult.pAtCmd = "AT+CSGT=1,\"RDY\"";
    ( void ) _Cellular_AtcmdRequestWithCallback( pContext, atReqGetNoResult );

    return cellularStatus;
}

/*-----------------------------------------------------------*/

uint32_t _Cellular_GetSocketId( CellularContext_t * pContext,
                                uint8_t sessionId )
{
    cellularModuleContext_t * pModuleContext = NULL;
    uint32_t socketIndex = INVALID_SOCKET_INDEX;
    CellularError_t cellularStatus = CELLULAR_SUCCESS;

    if( pContext != NULL )
    {
        cellularStatus = _Cellular_GetModuleContext( pContext, ( void ** ) &pModuleContext );
    }
    else
    {
        cellularStatus = CELLULAR_BAD_PARAMETER;
    }

    if( ( cellularStatus == CELLULAR_SUCCESS ) && ( sessionId <= ( ( uint8_t ) MAX_TCP_SESSION_ID ) ) )
    {
        socketIndex = pModuleContext->pSessionMap[ sessionId ];
    }

    return socketIndex;
}

/*-----------------------------------------------------------*/

uint32_t _Cellular_GetSessionId( CellularContext_t * pContext,
                                 uint32_t socketIndex )
{
    cellularModuleContext_t * pModuleContext = NULL;
    CellularError_t cellularStatus = CELLULAR_SUCCESS;
    uint32_t sessionId = INVALID_SESSION_ID;

    if( pContext == NULL )
    {
        LogError( ( "_Cellular_GetSessionId invalid cellular context" ) );
        cellularStatus = CELLULAR_BAD_PARAMETER;
    }
    else if( socketIndex == INVALID_SOCKET_INDEX )
    {
        LogError( ( "_Cellular_GetSessionId invalid socketIndex" ) );
        cellularStatus = CELLULAR_BAD_PARAMETER;
    }
    else
    {
        cellularStatus = _Cellular_GetModuleContext( pContext, ( void ** ) &pModuleContext );
    }

    if( cellularStatus == CELLULAR_SUCCESS )
    {
        for( sessionId = 0; sessionId < TCP_SESSION_TABLE_LEGNTH; sessionId++ )
        {
            if( pModuleContext->pSessionMap[ sessionId ] == socketIndex )
            {
                break;
            }
        }

        /* Mapping is not found in the session mapping table. */
        if( sessionId == TCP_SESSION_TABLE_LEGNTH )
        {
            sessionId = INVALID_SESSION_ID;
        }
    }
    else
    {
        sessionId = INVALID_SESSION_ID;
    }

    return sessionId;
}
