/*
 * FreeRTOS V202212.00
 * 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
 *
 */

/**
 * This file exercises the event mechanism whereby more than one task is
 * blocked waiting for the same event.
 *
 * The demo creates five tasks - four 'event' tasks, and a controlling task.
 * The event tasks have various different priorities and all block on reading
 * the same queue.  The controlling task writes data to the queue, then checks
 * to see which of the event tasks read the data from the queue.  The
 * controlling task has the lowest priority of all the tasks so is guaranteed
 * to always get preempted immediately upon writing to the queue.
 *
 * By selectively suspending and resuming the event tasks the controlling task
 * can check that the highest priority task that is blocked on the queue is the
 * task that reads the posted data from the queue.
 *
 * Two of the event tasks share the same priority.  When neither of these tasks
 * are suspended they should alternate - one reading one message from the queue,
 * the other the next message, etc.
 */

/* Standard includes. */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/* Scheduler include files. */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

/* Demo program include files. */
#include "mevents.h"
#include "print.h"

/* Demo specific constants. */
#define evtSTACK_SIZE                  ( ( unsigned portBASE_TYPE ) configMINIMAL_STACK_SIZE )
#define evtNUM_TASKS                   ( 4 )
#define evtQUEUE_LENGTH                ( ( unsigned portBASE_TYPE ) 3 )
#define evtNO_DELAY                    0

/* Just indexes used to uniquely identify the tasks.  Note that two tasks are
 * 'highest' priority. */
#define evtHIGHEST_PRIORITY_INDEX_2    3
#define evtHIGHEST_PRIORITY_INDEX_1    2
#define evtMEDIUM_PRIORITY_INDEX       1
#define evtLOWEST_PRIORITY_INDEX       0

/* Each event task increments one of these counters each time it reads data
 * from the queue. */
static volatile portBASE_TYPE xTaskCounters[ evtNUM_TASKS ] = { 0, 0, 0, 0 };

/* Each time the controlling task posts onto the queue it increments the
 * expected count of the task that it expected to read the data from the queue
 * (i.e. the task with the highest priority that should be blocked on the queue).
 *
 * xExpectedTaskCounters are incremented from the controlling task, and
 * xTaskCounters are incremented from the individual event tasks - therefore
 * comparing xTaskCounters to xExpectedTaskCounters shows whether or not the
 * correct task was unblocked by the post. */
static portBASE_TYPE xExpectedTaskCounters[ evtNUM_TASKS ] = { 0, 0, 0, 0 };

/* Handles to the four event tasks.  These are required to suspend and resume
 * the tasks. */
static TaskHandle_t xCreatedTasks[ evtNUM_TASKS ];

/* The single queue onto which the controlling task posts, and the four event
 * tasks block. */
static QueueHandle_t xQueue;

/* Flag used to indicate whether or not an error has occurred at any time.
 * An error is either the queue being full when not expected, or an unexpected
 * task reading data from the queue. */
static portBASE_TYPE xHealthStatus = pdPASS;

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

/* Function that implements the event task.  This is created four times. */
static void prvMultiEventTask( void * pvParameters );

/* Function that implements the controlling task. */
static void prvEventControllerTask( void * pvParameters );

/* This is a utility function that posts data to the queue, then compares
 * xExpectedTaskCounters with xTaskCounters to ensure everything worked as
 * expected.
 *
 * The event tasks all have higher priorities the controlling task.  Therefore
 * the controlling task will always get preempted between writhing to the queue
 * and checking the task counters.
 *
 * @param xExpectedTask  The index to the task that the controlling task thinks
 *                    should be the highest priority task waiting for data, and
 *                    therefore the task that will unblock.
 *
 * @param	xIncrement    The number of items that should be written to the queue.
 */
static void prvCheckTaskCounters( portBASE_TYPE xExpectedTask,
                                  portBASE_TYPE xIncrement );

/* This is just incremented each cycle of the controlling tasks function so
 * the main application can ensure the test is still running. */
static portBASE_TYPE xCheckVariable = 0;

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

void vStartMultiEventTasks( void )
{
    /* Create the queue to be used for all the communications. */
    xQueue = xQueueCreate( evtQUEUE_LENGTH, ( unsigned portBASE_TYPE ) sizeof( unsigned portBASE_TYPE ) );

    /* Start the controlling task.  This has the idle priority to ensure it is
     * always preempted by the event tasks. */
    xTaskCreate( prvEventControllerTask, "EvntCTRL", evtSTACK_SIZE, NULL, tskIDLE_PRIORITY, NULL );

    /* Start the four event tasks.  Note that two have priority 3, one
     * priority 2 and the other priority 1. */
    xTaskCreate( prvMultiEventTask, "Event0", evtSTACK_SIZE, ( void * ) &( xTaskCounters[ 0 ] ), 1, &( xCreatedTasks[ evtLOWEST_PRIORITY_INDEX ] ) );
    xTaskCreate( prvMultiEventTask, "Event1", evtSTACK_SIZE, ( void * ) &( xTaskCounters[ 1 ] ), 2, &( xCreatedTasks[ evtMEDIUM_PRIORITY_INDEX ] ) );
    xTaskCreate( prvMultiEventTask, "Event2", evtSTACK_SIZE, ( void * ) &( xTaskCounters[ 2 ] ), 3, &( xCreatedTasks[ evtHIGHEST_PRIORITY_INDEX_1 ] ) );
    xTaskCreate( prvMultiEventTask, "Event3", evtSTACK_SIZE, ( void * ) &( xTaskCounters[ 3 ] ), 3, &( xCreatedTasks[ evtHIGHEST_PRIORITY_INDEX_2 ] ) );
}
/*-----------------------------------------------------------*/

static void prvMultiEventTask( void * pvParameters )
{
    portBASE_TYPE * pxCounter;
    unsigned portBASE_TYPE uxDummy;
    const char * const pcTaskStartMsg = "Multi event task started.\r\n";

    /* The variable this task will increment is passed in as a parameter. */
    pxCounter = ( portBASE_TYPE * ) pvParameters;

    vPrintDisplayMessage( &pcTaskStartMsg );

    for( ; ; )
    {
        /* Block on the queue. */
        if( xQueueReceive( xQueue, &uxDummy, portMAX_DELAY ) )
        {
            /* We unblocked by reading the queue - so simply increment
             * the counter specific to this task instance. */
            ( *pxCounter )++;
        }
        else
        {
            xHealthStatus = pdFAIL;
        }
    }
}
/*-----------------------------------------------------------*/

static void prvEventControllerTask( void * pvParameters )
{
    const char * const pcTaskStartMsg = "Multi event controller task started.\r\n";
    portBASE_TYPE xDummy = 0;

    /* Just to stop warnings. */
    ( void ) pvParameters;

    vPrintDisplayMessage( &pcTaskStartMsg );

    for( ; ; )
    {
        /* All tasks are blocked on the queue.  When a message is posted one of
         * the two tasks that share the highest priority should unblock to read
         * the queue.  The next message written should unblock the other task with
         * the same high priority, and so on in order.   No other task should
         * unblock to read data as they have lower priorities. */

        prvCheckTaskCounters( evtHIGHEST_PRIORITY_INDEX_1, 1 );
        prvCheckTaskCounters( evtHIGHEST_PRIORITY_INDEX_2, 1 );
        prvCheckTaskCounters( evtHIGHEST_PRIORITY_INDEX_1, 1 );
        prvCheckTaskCounters( evtHIGHEST_PRIORITY_INDEX_2, 1 );
        prvCheckTaskCounters( evtHIGHEST_PRIORITY_INDEX_1, 1 );

        /* For the rest of these tests we don't need the second 'highest'
         * priority task - so it is suspended. */
        vTaskSuspend( xCreatedTasks[ evtHIGHEST_PRIORITY_INDEX_2 ] );



        /* Now suspend the other highest priority task.  The medium priority
         * task will then be the task with the highest priority that remains
         * blocked on the queue. */
        vTaskSuspend( xCreatedTasks[ evtHIGHEST_PRIORITY_INDEX_1 ] );

        /* This time, when we post onto the queue we will expect the medium
         * priority task to unblock and preempt us. */
        prvCheckTaskCounters( evtMEDIUM_PRIORITY_INDEX, 1 );

        /* Now try resuming the highest priority task while the scheduler is
         * suspended.  The task should start executing as soon as the scheduler
         * is resumed - therefore when we post to the queue again, the highest
         * priority task should again preempt us. */
        vTaskSuspendAll();
        vTaskResume( xCreatedTasks[ evtHIGHEST_PRIORITY_INDEX_1 ] );
        xTaskResumeAll();
        prvCheckTaskCounters( evtHIGHEST_PRIORITY_INDEX_1, 1 );

        /* Now we are going to suspend the high and medium priority tasks.  The
         * low priority task should then preempt us.  Again the task suspension is
         * done with the whole scheduler suspended just for test purposes. */
        vTaskSuspendAll();
        vTaskSuspend( xCreatedTasks[ evtHIGHEST_PRIORITY_INDEX_1 ] );
        vTaskSuspend( xCreatedTasks[ evtMEDIUM_PRIORITY_INDEX ] );
        xTaskResumeAll();
        prvCheckTaskCounters( evtLOWEST_PRIORITY_INDEX, 1 );

        /* Do the same basic test another few times - selectively suspending
         * and resuming tasks and each time calling prvCheckTaskCounters() passing
         * to the function the number of the task we expected to be unblocked by
         * the	post. */

        vTaskResume( xCreatedTasks[ evtHIGHEST_PRIORITY_INDEX_1 ] );
        prvCheckTaskCounters( evtHIGHEST_PRIORITY_INDEX_1, 1 );

        vTaskSuspendAll(); /* Just for test. */
        vTaskSuspendAll(); /* Just for test. */
        vTaskSuspendAll(); /* Just for even more test. */
        vTaskSuspend( xCreatedTasks[ evtHIGHEST_PRIORITY_INDEX_1 ] );
        xTaskResumeAll();
        xTaskResumeAll();
        xTaskResumeAll();
        prvCheckTaskCounters( evtLOWEST_PRIORITY_INDEX, 1 );

        vTaskResume( xCreatedTasks[ evtMEDIUM_PRIORITY_INDEX ] );
        prvCheckTaskCounters( evtMEDIUM_PRIORITY_INDEX, 1 );

        vTaskResume( xCreatedTasks[ evtHIGHEST_PRIORITY_INDEX_1 ] );
        prvCheckTaskCounters( evtHIGHEST_PRIORITY_INDEX_1, 1 );

        /* Now a slight change, first suspend all tasks. */
        vTaskSuspend( xCreatedTasks[ evtHIGHEST_PRIORITY_INDEX_1 ] );
        vTaskSuspend( xCreatedTasks[ evtMEDIUM_PRIORITY_INDEX ] );
        vTaskSuspend( xCreatedTasks[ evtLOWEST_PRIORITY_INDEX ] );

        /* Now when we resume the low priority task and write to the queue 3
         * times.  We expect the low priority task to service the queue three
         * times. */
        vTaskResume( xCreatedTasks[ evtLOWEST_PRIORITY_INDEX ] );
        prvCheckTaskCounters( evtLOWEST_PRIORITY_INDEX, evtQUEUE_LENGTH );

        /* Again suspend all tasks (only the low priority task is not suspended
         * already). */
        vTaskSuspend( xCreatedTasks[ evtLOWEST_PRIORITY_INDEX ] );

        /* This time we are going to suspend the scheduler, resume the low
         *  priority task, then resume the high priority task.  In this state we
         *  will write to the queue three times.  When the scheduler is resumed
         *  we expect the high priority task to service all three messages. */
        vTaskSuspendAll();
        {
            vTaskResume( xCreatedTasks[ evtLOWEST_PRIORITY_INDEX ] );
            vTaskResume( xCreatedTasks[ evtHIGHEST_PRIORITY_INDEX_1 ] );

            for( xDummy = 0; xDummy < evtQUEUE_LENGTH; xDummy++ )
            {
                if( xQueueSend( xQueue, &xDummy, evtNO_DELAY ) != pdTRUE )
                {
                    xHealthStatus = pdFAIL;
                }
            }

            /* The queue should not have been serviced yet!.  The scheduler
             * is still suspended. */
            if( memcmp( ( void * ) xExpectedTaskCounters, ( void * ) xTaskCounters, sizeof( xExpectedTaskCounters ) ) )
            {
                xHealthStatus = pdFAIL;
            }
        }
        xTaskResumeAll();

        /* We should have been preempted by resuming the scheduler - so by the
         * time we are running again we expect the high priority task to have
         * removed three items from the queue. */
        xExpectedTaskCounters[ evtHIGHEST_PRIORITY_INDEX_1 ] += evtQUEUE_LENGTH;

        if( memcmp( ( void * ) xExpectedTaskCounters, ( void * ) xTaskCounters, sizeof( xExpectedTaskCounters ) ) )
        {
            xHealthStatus = pdFAIL;
        }

        /* The medium priority and second high priority tasks are still
         * suspended.  Make sure to resume them before starting again. */
        vTaskResume( xCreatedTasks[ evtMEDIUM_PRIORITY_INDEX ] );
        vTaskResume( xCreatedTasks[ evtHIGHEST_PRIORITY_INDEX_2 ] );

        /* Just keep incrementing to show the task is still executing. */
        xCheckVariable++;
    }
}
/*-----------------------------------------------------------*/

static void prvCheckTaskCounters( portBASE_TYPE xExpectedTask,
                                  portBASE_TYPE xIncrement )
{
    portBASE_TYPE xDummy = 0;

    /* Write to the queue the requested number of times.  The data written is
     * not important. */
    for( xDummy = 0; xDummy < xIncrement; xDummy++ )
    {
        if( xQueueSend( xQueue, &xDummy, evtNO_DELAY ) != pdTRUE )
        {
            /* Did not expect to ever find the queue full. */
            xHealthStatus = pdFAIL;
        }
    }

    /* All the tasks blocked on the queue have a priority higher than the
     * controlling task.  Writing to the queue will therefore have caused this
     * task to be preempted.  By the time this line executes the event task will
     * have executed and incremented its counter.  Increment the expected counter
     * to the same value. */
    ( xExpectedTaskCounters[ xExpectedTask ] ) += xIncrement;

    /* Check the actual counts and expected counts really are the same. */
    if( memcmp( ( void * ) xExpectedTaskCounters, ( void * ) xTaskCounters, sizeof( xExpectedTaskCounters ) ) )
    {
        /* The counters were not the same.  This means a task we did not expect
         * to unblock actually did unblock. */
        xHealthStatus = pdFAIL;
    }
}
/*-----------------------------------------------------------*/

portBASE_TYPE xAreMultiEventTasksStillRunning( void )
{
    static portBASE_TYPE xPreviousCheckVariable = 0;

    /* Called externally to periodically check that this test is still
     * operational. */

    if( xPreviousCheckVariable == xCheckVariable )
    {
        xHealthStatus = pdFAIL;
    }

    xPreviousCheckVariable = xCheckVariable;

    return xHealthStatus;
}
