// -------------------------------------------------------------------------------------------
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// This file is part of the AWS CDI-SDK, licensed under the BSD 2-Clause "Simplified" License.
// License details at: https://github.com/aws/aws-cdi-sdk/blob/mainline/LICENSE
// -------------------------------------------------------------------------------------------

/*
 * @file
 * @brief
 * This file contains definitions and implementation for setting and managing timeouts.
 */

#include "timeout.h"

#include "cdi_logger_api.h"
#include "internal.h"
#include "internal_log.h"

//*********************************************************************************************************************
//***************************************** START OF DEFINITIONS AND TYPES ********************************************
//*********************************************************************************************************************

/**
 * @brief A structure for reading and writing FIFO entries which contains callback data and callback function pointer
 */
typedef struct TimeoutCbFifoData {
    CdiTimeoutCbData cb_data; ///< Return data for timeout callback.
    CdiTimeoutCallback cb_ptr; ///< Pointer to timeout callback function.
} TimeoutCbFifoData;

//*********************************************************************************************************************
//******************************************* START OF STATIC FUNCTIONS ***********************************************
//*********************************************************************************************************************

/**
 *  This is an optimized version of CdiTimeoutRemove for dealing with expired timers.
 *  If a timer expiration occurs the active timer is always the one removed, the stop
 *  signal does not need to be set, and memory is not freed until after the callback
 *  function executes.
 *
 *  @param handle_instance is the handle for TimeoutInstanceState state info
 */
static void ExpiredTimeoutRemove(CdiTimeoutInstanceHandle instance_handle)
{
    CdiOsCritSectionReserve(instance_handle->critical_section);
    if (instance_handle != NULL) {
        CdiListPop(&instance_handle->timeout_list);
        if (CdiListIsEmpty(&instance_handle->timeout_list)) {
            CdiOsSignalClear(instance_handle->go_signal);
        }
    }
    CdiOsCritSectionRelease(instance_handle->critical_section);
}

/**
 *  This function packages up the data for the callback FIFO to use and then removes the expired timeout from
 *  the list of timeouts.
 *
 *  @param instance_handle is the TimeoutInstanceState handle with timeout process information
 *  @param expired_handle is the TimeoutDataState handle for the timeout that has expired
 *
 *  @return true if successful, false if failed to write to callback fifo
 */
static bool ServiceExpiredTimeout(CdiTimeoutInstanceHandle instance_handle, TimeoutHandle expired_handle) {
    bool ret = true;
    TimeoutCbFifoData fifo_data;
    // Package data for sending into the callback fifo
    fifo_data.cb_ptr = expired_handle->cb_ptr;
    fifo_data.cb_data.handle = expired_handle;
    fifo_data.cb_data.user_data_ptr = expired_handle->user_data_ptr;
    // Remove the expired timeout at head of timeout list.
    ExpiredTimeoutRemove(instance_handle);
    // Send the expired timeout data to the callback thread for servicing
    if (!CdiFifoWrite(instance_handle->cb_fifo_handle, 1, NULL, &fifo_data)) {
        ret = false;
        CDI_LOG_THREAD(kLogError, "Timeout callback FIFO write failed");
    }

    return ret;
}

/* @brief This thread waits for data to be sent to the callback FIFO or for a shutdown signal.
 * When callback FIFO data is received the callback pointer is pulled from the structure and
 * exectues the callback pointer with the callback data from the structure used as the sole
 * parameter for the callback function. Callbacks occur after a timeout has expired so the expired
 * timeout TimeoutDataState structure is not sent back to the memory pool until after the callback
 * function has completed.
 */
static CDI_THREAD TimeoutCbThread(void* ptr)
{
    TimeoutInstanceState* state_ptr = (TimeoutInstanceState*)ptr;

    // Set this thread to use the desired log. Can now use CDI_LOG_THREAD() for logging within this thread.
    CdiLoggerThreadLogSet(state_ptr->log_handle);

    CDI_LOG_THREAD(kLogInfo, "Timeout Callback Thread established");

    // loop until shutdown signal received
    while (!CdiOsSignalGet(state_ptr->shutdown_signal)) {
        // wait on read data or shutdown signal
        TimeoutCbFifoData fifo_data;
        if (CdiFifoRead(state_ptr->cb_fifo_handle, CDI_INFINITE, state_ptr->shutdown_signal, &fifo_data)) {
            CDI_LOG_THREAD(kLogDebug, "Timeout expired, executing callback function");
            (fifo_data.cb_ptr)(&fifo_data.cb_data); // execute callback function
            CdiPoolPut(state_ptr->mem_pool_handle, fifo_data.cb_data.handle);
        }
    }

    CDI_LOG_THREAD(kLogInfo, "Timeout Callback Thread exiting");

    return 0;
}

/* @brief This thread checks for timer signals Go, Stop, and Shutdown and sets new timers when timers are available.
 * If there are active timers this thread sleeps until the first timer to expire goes off,
 * is cleared, or shutdown is received. If the timer is expired it is sent to a separate FIFO thread to execute the
 * user callback function. This separates the execution time of the callback function from the time of managing the
 * timers themselves.
 */
static CDI_THREAD TimeoutMainThread(void* ptr)
{
    TimeoutInstanceState* state_ptr = (TimeoutInstanceState*)ptr;

    // Set this thread to use the desired log. Can now use CDI_LOG_THREAD() for logging within this thread.
    CdiLoggerThreadLogSet(state_ptr->log_handle);

    bool thread_exit = false;
    CdiSignalType outer_signal_array[2];
    CdiSignalType inner_signal_array[2];
    outer_signal_array[0] = state_ptr->shutdown_signal;
    outer_signal_array[1] = state_ptr->go_signal;
    inner_signal_array[0] = state_ptr->shutdown_signal;
    inner_signal_array[1] = state_ptr->stop_signal;

    // Loop to check whether there are active timers exit on shutdown.
    while (!thread_exit) {
        unsigned int signal_index;
        // have thread go to sleep until shutdown_signal or go_signal is received
        CdiOsSignalsWait(outer_signal_array, 2, false, CDI_INFINITE, &signal_index);
        CdiOsCritSectionReserve(state_ptr->critical_section);
        if (signal_index == 0) {
            // Shutdown received.
            CdiOsCritSectionRelease(state_ptr->critical_section);
            thread_exit = true;
            CDI_LOG_THREAD(kLogInfo, "Timeout thread shutdown received");

        // Do a NULL check in case last timer got cleared after the go signal was seen.
        } else if (!CdiListIsEmpty(&state_ptr->timeout_list)) {
            //
            // Timers are available to set, so get the current time to calculate when the head timer will expire so
            // the next timer can be set.
            //
            uint64_t current_time = CdiOsGetMicroseconds();
            TimeoutDataState* timeout_head_ptr = CONTAINER_OF(CdiListPeek(&state_ptr->timeout_list), TimeoutDataState, list_entry);
            if (timeout_head_ptr->deadline_us > current_time) {

                // Get time difference in ms
                const unsigned int new_timeout = (timeout_head_ptr->deadline_us - current_time + 500) / 1000;

                CdiOsCritSectionRelease(state_ptr->critical_section);

                // Set a wait for the length of timeout_head remaining deadline time in ms break from wait if
                // stop_signal or shutdown_signal is received.
                CdiOsSignalsWait(inner_signal_array, 2, false, new_timeout, &signal_index);
                if (0 == signal_index) {
                    // Shutdown signal sent.
                    thread_exit = true;
                    CDI_LOG_THREAD(kLogInfo, "Cancelled timer without logging. Shutdown received");
                } else if (CDI_OS_SIG_TIMEOUT == signal_index) {
                    // Timeout occurred.
                    if (!ServiceExpiredTimeout(state_ptr, timeout_head_ptr)) {
                        CDI_LOG_THREAD(kLogError, "Failed to service expired timeout");
                    }
                } else {
                    // Stop_signal received so restart loop and grab next timeout_head if available.
                    CdiOsSignalClear(state_ptr->stop_signal);
                }
            } else {
                 // Timeout has occurred before wait could be set.
                if (!ServiceExpiredTimeout(state_ptr, timeout_head_ptr)) {
                    CDI_LOG_THREAD(kLogError, "Failed to service expired timeout");
                }
            }
        }
        // Timeout_list is empty.
    }

    CDI_LOG_THREAD(kLogInfo, "Timeout main thread exiting");
    return 0;
}

//*********************************************************************************************************************
//******************************************* START OF PUBLIC FUNCTIONS ***********************************************
//*********************************************************************************************************************

CdiReturnStatus CdiTimeoutCreate(CdiLogHandle log_handle, CdiTimeoutInstanceHandle* ret_handle_ptr)
{
    CdiReturnStatus ret = kCdiStatusOk;

    TimeoutInstanceState* state_ptr = (TimeoutInstanceState*)CdiOsMemAllocZero(sizeof(TimeoutInstanceState));
    if (state_ptr == NULL) {
        // NOTE: Must use CDI_LOG_HANDLE() to direct the log message to the desired log.
        CDI_LOG_HANDLE(log_handle, kLogError, "Insufficient memory for TimeoutInstanceState allocation");
        ret = kCdiStatusNotEnoughMemory;
    }

    state_ptr->log_handle = log_handle;

    if (ret == kCdiStatusOk) {
        if (!CdiPoolCreate("Timeout TimeoutDataState Pool", MAX_TIMERS, MAX_TIMERS_GROW, MAX_POOL_GROW_COUNT,
                            sizeof(TimeoutDataState), true, // true= Make thread-safe
                            &state_ptr->mem_pool_handle)) {
                            CDI_LOG_HANDLE(log_handle, kLogError, "ERROR: Failed to create memory pool");
                            ret = kCdiStatusNotEnoughMemory;
        }
    }

    if (ret == kCdiStatusOk) {
       if (!CdiOsCritSectionCreate(&state_ptr->critical_section)) {
            CDI_LOG_HANDLE(log_handle, kLogError, "Failed to create critical section for Timeout Instance State");
            ret = kCdiStatusNotEnoughMemory;
        }
    }

    if (ret == kCdiStatusOk) {
        if (!CdiOsSignalCreate(&state_ptr->shutdown_signal)) {
            CDI_LOG_HANDLE(log_handle, kLogError, "Failed to create signal for Timeout Shutdown");
            ret = kCdiStatusNotEnoughMemory;
        }
    }

    if (ret == kCdiStatusOk) {
        if (!CdiOsSignalCreate(&state_ptr->stop_signal)) {
            CDI_LOG_HANDLE(log_handle, kLogError, "Failed to create signal for Timeout Timer Stop");
            ret = kCdiStatusNotEnoughMemory;
        }
    }

    if (ret == kCdiStatusOk) {
        if (!CdiOsSignalCreate(&state_ptr->go_signal)) {
            CDI_LOG_HANDLE(log_handle, kLogError, "Failed to create signal for Timeout Go");
            ret = kCdiStatusNotEnoughMemory;
        }
    }

    if (ret == kCdiStatusOk) {
        if (!CdiOsThreadCreate(TimeoutMainThread, &state_ptr->main_thread_id, "TimeoutMain", (void*)state_ptr, NULL)) {
            CDI_LOG_HANDLE(log_handle, kLogError, "Timeout main thread creation failed");
            ret = kCdiStatusFatal;
        }
    }

    if (ret == kCdiStatusOk) {
        if (!CdiFifoCreate("Timeout CB FIFO", MAX_TIMERS, sizeof(TimeoutCbFifoData), NULL, NULL,
                           &state_ptr->cb_fifo_handle)) {
            CDI_LOG_HANDLE(log_handle, kLogError, "Callback FIFO creation failed");
            ret = kCdiStatusNotEnoughMemory;
        }
    }

    if (ret == kCdiStatusOk) {
        if (!CdiOsThreadCreate(TimeoutCbThread, &state_ptr->cb_thread_id, "TimeoutCb", (void*)state_ptr, NULL)) {
            CDI_LOG_HANDLE(log_handle, kLogError, "Timeout callback thread creation failed");
            ret = kCdiStatusFatal;
        }
    }

    if (ret == kCdiStatusOk) {
        CdiListInit(&state_ptr->timeout_list);
    }

    // If the timeout creation process fails a NULL handle is returned and the partially created timeout is destroyed,
    if (ret == kCdiStatusOk) {
        *ret_handle_ptr = (CdiTimeoutInstanceHandle)state_ptr;
    } else {
        *ret_handle_ptr = NULL;
        CdiTimeoutDestroy(state_ptr);
    }

    return ret;
}

void CdiTimeoutDestroy(CdiTimeoutInstanceHandle handle)
{
    // Check for valid handle before doing anything
    if (NULL != handle) {
        // Clean-up thread resources. We will wait for it to exit using thread join.
        SdkThreadJoin(handle->main_thread_id, handle->shutdown_signal);
        handle->main_thread_id = NULL;
        SdkThreadJoin(handle->cb_thread_id, handle->shutdown_signal);
        handle->cb_thread_id = NULL;

        // Not setting any of these to NULL, since the memory is freed directly below.
        CdiFifoDestroy(handle->cb_fifo_handle);
        CdiOsSignalDelete(handle->shutdown_signal);
        CdiOsSignalDelete(handle->stop_signal);
        CdiOsSignalDelete(handle->go_signal);
        CdiOsCritSectionDelete(handle->critical_section);
        CdiPoolDestroy(handle->mem_pool_handle);

        CdiOsMemFree(handle);
    }
}

bool CdiTimeoutAdd(CdiTimeoutInstanceHandle instance_handle, CdiTimeoutCallback cb_ptr, int timeout_us, void* user_data_ptr, TimeoutHandle* ret_handle_ptr)
{
    bool ret = true;

    TimeoutDataState* new_timeout_ptr = NULL;

    ret = CdiPoolGet(instance_handle->mem_pool_handle, (void**)&new_timeout_ptr);

    // Initialize newly allocated timeout that will be added to timeout list.
    if (ret) {
        new_timeout_ptr->cb_ptr = cb_ptr;
        new_timeout_ptr->user_data_ptr = user_data_ptr;
        new_timeout_ptr->deadline_us = CdiOsGetMicroseconds() + timeout_us;
    }

    if (ret) {
        CdiOsCritSectionReserve(instance_handle->critical_section);
        if(CdiListIsEmpty(&instance_handle->timeout_list)) { // no active timeouts so setting the new one
            CdiListAddHead(&instance_handle->timeout_list, &new_timeout_ptr->list_entry);
            CdiOsCritSectionRelease(instance_handle->critical_section);
            if (!CdiOsSignalSet(instance_handle->go_signal)) {
                CDI_LOG_THREAD(kLogError,"Unable to set timer GO signal");
                ret = false;
            }
        } else {
            // Find where the new timeout belongs within the list.
            TimeoutDataState* compare_ptr = CONTAINER_OF(CdiListPeek(&instance_handle->timeout_list), TimeoutDataState, list_entry);
            while ((compare_ptr->deadline_us <= new_timeout_ptr->deadline_us)
                    && (CONTAINER_OF(CdiListPeekTail(&instance_handle->timeout_list), TimeoutDataState, list_entry)  != compare_ptr)) {
                compare_ptr = CONTAINER_OF(compare_ptr->list_entry.next_ptr, TimeoutDataState, list_entry);
            }

            // if smaller insert new entry in list before compare_ptr else insert at end of list
            if (compare_ptr->deadline_us >= new_timeout_ptr->deadline_us) {
                // check if inserting new head
                if (compare_ptr ==
                        CONTAINER_OF(CdiListPeek(&instance_handle->timeout_list), TimeoutDataState, list_entry)) {
                    CdiListAddHead(&instance_handle->timeout_list, &new_timeout_ptr->list_entry);
                    if (!CdiOsSignalSet(instance_handle->stop_signal)) {
                        CDI_LOG_THREAD(kLogError, "Unable to set stop on setting new head timer");
                        ret = false;
                    }
                } else {
                    // New timeout is somewhere in the middle of the list.
                    CdiListAddAfter(&instance_handle->timeout_list, &new_timeout_ptr->list_entry, compare_ptr->list_entry.prev_ptr);
                }
            } else {
                // New timeout is the new tail of the list.
                CdiListAddTail(&instance_handle->timeout_list, &new_timeout_ptr->list_entry);
            }
            CdiOsCritSectionRelease(instance_handle->critical_section);
        }
    }

    *ret_handle_ptr = new_timeout_ptr;

    return ret;
}

bool CdiTimeoutRemove(TimeoutHandle handle, CdiTimeoutInstanceHandle instance_handle)
{
    bool ret = handle != NULL;

    if (ret) {
        CdiOsCritSectionReserve(instance_handle->critical_section);
        if (&handle->list_entry == CdiListPeek(&instance_handle->timeout_list)) {
            CdiOsSignalSet(instance_handle->stop_signal);
        }
        if (instance_handle->timeout_list.count == 1) {
            CdiOsSignalClear(instance_handle->go_signal);
        }
        CdiListRemove(&instance_handle->timeout_list, &handle->list_entry);

        CdiOsCritSectionRelease(instance_handle->critical_section);
        CdiPoolPut(instance_handle->mem_pool_handle, handle);
    }

    return ret;
}