/*
 * Amazon FreeRTOS POSIX V1.1.0
 * Copyright (C) 2018 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.
 *
 * http://aws.amazon.com/freertos
 * http://www.FreeRTOS.org
 */

/**
 * @file FreeRTOS_POSIX_timer.c
 * @brief Implementation of timer functions in time.h
 */

/* C standard library includes. */
#include <stddef.h>

/* FreeRTOS+POSIX includes. */
#include "FreeRTOS_POSIX.h"
#include "FreeRTOS_POSIX/errno.h"
#include "FreeRTOS_POSIX/pthread.h"
#include "FreeRTOS_POSIX/signal.h"
#include "FreeRTOS_POSIX/time.h"
#include "FreeRTOS_POSIX/utils.h"

/* FreeRTOS timer include. */
#include "timers.h"

/* Timespec zero check macros. */
#define TIMESPEC_IS_ZERO( xTimespec )        ( xTimespec.tv_sec == 0 && xTimespec.tv_nsec == 0 ) /**< Check for 0. */
#define TIMESPEC_IS_NOT_ZERO( xTimespec )    ( !( TIMESPEC_IS_ZERO( xTimespec ) ) )              /**< Check for not 0. */

/**
 * @brief Internal timer structure.
 */
typedef struct timer_internal
{
    StaticTimer_t xTimerBuffer;  /**< Memory that holds the FreeRTOS timer. */
    struct sigevent xTimerEvent; /**< What to do when this timer expires. */
    TickType_t xTimerPeriod;     /**< Period of this timer. */
} timer_internal_t;

/*-----------------------------------------------------------*/

void prvTimerCallback( TimerHandle_t xTimerHandle )
{
    timer_internal_t * pxTimer = ( timer_internal_t * ) pvTimerGetTimerID( xTimerHandle );
    pthread_t xTimerNotificationThread;

    /* The value of the timer ID, set in timer_create, should not be NULL. */
    configASSERT( pxTimer != NULL );

    /* A value of SIGEV_SIGNAL isn't supported and should not have been successfully
     * set. */
    configASSERT( pxTimer->xTimerEvent.sigev_notify != SIGEV_SIGNAL );

    /* Update the timer period, which may need to be set to an it_interval
     * argument. This call should not block. */
    if( pxTimer->xTimerPeriod > 0 )
    {
        xTimerChangePeriod( xTimerHandle, pxTimer->xTimerPeriod, 0 );
    }

    /* Create the timer notification thread if requested. */
    if( pxTimer->xTimerEvent.sigev_notify == SIGEV_THREAD )
    {
        /* if the user has provided thread attributes, create a thread
         * with the provided attributes. Otherwise dispatch callback directly */
        if( pxTimer->xTimerEvent.sigev_notify_attributes == NULL )
        {
            ( *pxTimer->xTimerEvent.sigev_notify_function )( pxTimer->xTimerEvent.sigev_value );
        }
        else
        {
            ( void ) pthread_create( &xTimerNotificationThread,
                                     pxTimer->xTimerEvent.sigev_notify_attributes,
                                     ( void * ( * )( void * ) )pxTimer->xTimerEvent.sigev_notify_function,
                                     pxTimer->xTimerEvent.sigev_value.sival_ptr );
        }
    }
}

/*-----------------------------------------------------------*/

int timer_create( clockid_t clockid,
                  struct sigevent * evp,
                  timer_t * timerid )
{
    int iStatus = 0;
    timer_internal_t * pxTimer = NULL;

    /* Silence warnings about unused parameters. */
    ( void ) clockid;

    /* POSIX specifies that when evp is NULL, the behavior shall be as is
     * sigev_notify is SIGEV_SIGNAL. SIGEV_SIGNAL is currently not supported. */
    if( ( evp == NULL ) || ( evp->sigev_notify == SIGEV_SIGNAL ) )
    {
        errno = ENOTSUP;
        iStatus = -1;
    }

    /* Allocate memory for a new timer object. */
    if( iStatus == 0 )
    {
        pxTimer = pvPortMalloc( sizeof( timer_internal_t ) );

        if( pxTimer == NULL )
        {
            errno = EAGAIN;
            iStatus = -1;
        }
    }

    if( iStatus == 0 )
    {
        /* Copy the event notification structure and set the current timer period. */
        pxTimer->xTimerEvent = *evp;
        pxTimer->xTimerPeriod = 0;

        /* Create a new FreeRTOS timer. This call will not fail because the
         * memory for it has already been allocated, so the output parameter is
         * also set. */
        *timerid = ( timer_t ) xTimerCreateStatic( posixconfigTIMER_NAME,    /* Timer name. */
                                                   portMAX_DELAY,            /* Initial timer period. Timers are created disarmed. */
                                                   pdFALSE,                  /* Don't auto-reload timer. */
                                                   ( void * ) pxTimer,       /* Timer id. */
                                                   prvTimerCallback,         /* Timer expiration callback. */
                                                   &pxTimer->xTimerBuffer ); /* Pre-allocated memory for timer. */
    }

    return iStatus;
}

/*-----------------------------------------------------------*/

int timer_delete( timer_t timerid )
{
    TimerHandle_t xTimerHandle = timerid;
    timer_internal_t * pxTimer = ( timer_internal_t * ) pvTimerGetTimerID( xTimerHandle );

    /* The value of the timer ID, set in timer_create, should not be NULL. */
    configASSERT( pxTimer != NULL );

    /* Stop the FreeRTOS timer. Because the timer is statically allocated, no call
     * to xTimerDelete is necessary. The timer is stopped so that it's not referenced
     * anywhere. xTimerStop will not fail when it has unlimited block time. */
    ( void ) xTimerStop( xTimerHandle, portMAX_DELAY );

    /* Wait until the timer stop command is processed. */
    while( xTimerIsTimerActive( xTimerHandle ) == pdTRUE )
    {
        vTaskDelay( 1 );
    }

    /* Free the memory in use by the timer. */
    vPortFree( pxTimer );

    return 0;
}

/*-----------------------------------------------------------*/

int timer_getoverrun( timer_t timerid )
{
    /* Silence warnings about unused parameters. */
    ( void ) timerid;

    return 0;
}

/*-----------------------------------------------------------*/

int timer_settime( timer_t timerid,
                   int flags,
                   const struct itimerspec * value,
                   struct itimerspec * ovalue )
{
    int iStatus = 0;
    TimerHandle_t xTimerHandle = timerid;
    timer_internal_t * pxTimer = ( timer_internal_t * ) pvTimerGetTimerID( xTimerHandle );
    TickType_t xNextTimerExpiration = 0, xTimerExpirationPeriod = 0;
    BaseType_t xTimerCommandSent = pdFAIL;

    /* Validate the value argument, but only if the timer isn't being disarmed. */
    if( TIMESPEC_IS_NOT_ZERO( value->it_value ) )
    {
        if( ( UTILS_ValidateTimespec( &value->it_interval ) == false ) ||
            ( UTILS_ValidateTimespec( &value->it_value ) == false ) )
        {
            errno = EINVAL;
            iStatus = -1;
        }
    }

    /* Set ovalue, if given. */
    if( ovalue != NULL )
    {
        ( void ) timer_gettime( timerid, ovalue );
    }

    /* Stop the timer if it's currently active. */
    if( ( iStatus == 0 ) && xTimerIsTimerActive( xTimerHandle ) )
    {
        ( void ) xTimerStop( xTimerHandle, portMAX_DELAY );
    }

    /* Only restart the timer if it_value is not zero. */
    if( ( iStatus == 0 ) && TIMESPEC_IS_NOT_ZERO( value->it_value ) )
    {
        /* Convert it_interval to ticks, but only if it_interval is not 0. If
         * it_interval is 0, then the timer is not periodic. */
        if( TIMESPEC_IS_NOT_ZERO( value->it_interval ) )
        {
            ( void ) UTILS_TimespecToTicks( &value->it_interval, &xTimerExpirationPeriod );
        }

        /* Set the new timer period. A non-periodic timer will have its period set
         * to portMAX_DELAY. */
        pxTimer->xTimerPeriod = xTimerExpirationPeriod;

        /* Convert it_value to ticks, but only if it_value is not 0. If it_value
         * is 0, then the timer will remain disarmed. */
        if( TIMESPEC_IS_NOT_ZERO( value->it_value ) )
        {
            /* Absolute timeout. */
            if( ( flags & TIMER_ABSTIME ) == TIMER_ABSTIME )
            {
                struct timespec xCurrentTime = { 0 };

                /* Get current time */
                if( clock_gettime( CLOCK_REALTIME, &xCurrentTime ) != 0 )
                {
                    iStatus = EINVAL;
                }
                else
                {
                    iStatus = UTILS_AbsoluteTimespecToDeltaTicks( &value->it_value, &xCurrentTime, &xNextTimerExpiration );
                }

                /* Make sure xNextTimerExpiration is zero in case we got negative time difference */
                if( iStatus != 0 )
                {
                    xNextTimerExpiration = 0;

                    if( iStatus == ETIMEDOUT )
                    {
                        /* Set Status to 0 as absolute time is past is treated as expiry but not an error */
                        iStatus = 0;
                    }
                }
            }
            /* Relative timeout. */
            else
            {
                ( void ) UTILS_TimespecToTicks( &value->it_value, &xNextTimerExpiration );
            }
        }

        /* If xNextTimerExpiration is still 0, that means that it_value specified
         * an absolute timeout in the past. Per POSIX spec, a notification should be
         * triggered immediately. */
        if( xNextTimerExpiration == 0 )
        {
            prvTimerCallback( xTimerHandle );
        }
        else
        {
            /* Set the timer to expire at the it_value, then start it. */
            ( void ) xTimerChangePeriod( xTimerHandle, xNextTimerExpiration, portMAX_DELAY );
            xTimerCommandSent = xTimerStart( xTimerHandle, xNextTimerExpiration );

            /* Wait until the timer start command is processed. */
            while( ( xTimerCommandSent != pdFAIL ) && ( xTimerIsTimerActive( xTimerHandle ) == pdFALSE ) )
            {
                vTaskDelay( 1 );
            }
        }
    }

    return iStatus;
}

/*-----------------------------------------------------------*/

int timer_gettime( timer_t timerid,
                   struct itimerspec * value )
{
    TimerHandle_t xTimerHandle = timerid;
    timer_internal_t * pxTimer = ( timer_internal_t * ) pvTimerGetTimerID( xTimerHandle );
    TickType_t xNextExpirationTime = xTimerGetExpiryTime( xTimerHandle ) - xTaskGetTickCount(),
               xTimerExpirationPeriod = pxTimer->xTimerPeriod;

    /* Set it_value only if the timer is armed. Otherwise, set it to 0. */
    if( xTimerIsTimerActive( xTimerHandle ) != pdFALSE )
    {
        value->it_value.tv_sec = ( time_t ) ( xNextExpirationTime / configTICK_RATE_HZ );
        value->it_value.tv_nsec = ( long ) ( ( xNextExpirationTime % configTICK_RATE_HZ ) * NANOSECONDS_PER_TICK );
    }
    else
    {
        value->it_value.tv_sec = 0;
        value->it_value.tv_nsec = 0;
    }

    /* Set it_interval only if the timer is periodic. Otherwise, set it to 0. */
    if( xTimerExpirationPeriod != portMAX_DELAY )
    {
        value->it_interval.tv_sec = ( time_t ) ( xTimerExpirationPeriod / configTICK_RATE_HZ );
        value->it_interval.tv_nsec = ( long ) ( ( xTimerExpirationPeriod % configTICK_RATE_HZ ) * NANOSECONDS_PER_TICK );
    }
    else
    {
        value->it_interval.tv_sec = 0;
        value->it_interval.tv_nsec = 0;
    }

    return 0;
}

/*-----------------------------------------------------------*/
