/* Copyright (c) 2015-2016 Skyward Experimental Rocketry
 * Authors: Alain Carlucci, Federico Terraneo, Matteo Piazzolla
 *
 * 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.
 */

#include "TaskScheduler.h"

#include <diagnostic/SkywardStack.h>

#include <algorithm>

using namespace std;
using namespace miosix;

namespace Boardcore
{

TaskScheduler::TaskScheduler(miosix::Priority priority)
    : ActiveObject(STACK_MIN_FOR_SKYWARD, priority)
{
    // Create dynamically the tasks vector because of too much space
    tasks = new std::array<Task, TASKS_SIZE>();

    // Initialize the vector elements
    for (size_t i = 1; i < TASKS_SIZE; i++)
    {
        function_t function;
        (*tasks)[i] = makeTask(function, 0, i, false, Policy::SKIP);
    }
}

TaskScheduler::~TaskScheduler() { delete tasks; }

size_t TaskScheduler::addTask(function_t function, uint32_t period,
                              Policy policy, int64_t startTick)
{
    Lock<FastMutex> lock(mutex);
    size_t id = 1;

    // Find a suitable id for the new task
    for (; id < TASKS_SIZE; id++)
    {
        if ((*tasks)[id].valid == false)
        {
            break;
        }
    }

    // Check if in the corresponding id there's already a task
    if ((*tasks)[id].valid)
    {
        // Unlock the mutex for expensive operation
        Unlock<FastMutex> unlock(mutex);

        LOG_ERR(logger, "Full task scheduler, id = {:zu}", id);
        return 0;
    }

    // Register the task into the map
    (*tasks)[id] = makeTask(function, period, id, true, policy);

    if (policy == Policy::ONE_SHOT)
    {
        startTick += period;
    }

    // Add the task first event in the agenda, performs in-place construction
    agenda.emplace(id, startTick);
    condvar.broadcast();  // Signals the run thread

    return id;
}

bool TaskScheduler::removeTask(size_t id)
{
    Lock<FastMutex> lock(mutex);

    // Check if the task is actually present
    if ((*tasks)[id].valid == false)
    {
        // Unlock the mutex for expensive operation
        Unlock<FastMutex> unlock(mutex);

        LOG_ERR(logger, "Attempting to remove a task not registered");
        return false;
    }

    // Set the validity of the task to false
    (*tasks)[id].valid = false;

    return true;
}

bool TaskScheduler::start()
{
    // This check is necessary to prevent task normalization if the scheduler is
    // already stopped
    if (running)
    {
        return false;
    }

    // Normalize the tasks start time if they precede the current tick
    normalizeTasks();

    return ActiveObject::start();
}

void TaskScheduler::stop()
{
    stopFlag = true;      // Signal the run function to stop
    condvar.broadcast();  // Wake the run function even if there are no tasks

    ActiveObject::stop();
}

vector<TaskStatsResult> TaskScheduler::getTaskStats()
{
    Lock<FastMutex> lock(mutex);

    vector<TaskStatsResult> result;

    for (auto const& task : (*tasks))
    {
        if (task.valid)
        {
            result.push_back(fromTaskIdPairToStatsResult(task));
        }
    }

    return result;
}

void TaskScheduler::normalizeTasks()
{
    int64_t currentTick = getTick();

    std::priority_queue<Event> newAgenda;
    while (agenda.size() > 0)
    {
        Event event = agenda.top();
        agenda.pop();

        if (event.nextTick < currentTick)
        {
            Task& task = (*tasks)[event.taskId];
            event.nextTick +=
                ((currentTick - event.nextTick) / task.period + 1) *
                task.period;
        }

        newAgenda.push(event);
    }
    agenda = newAgenda;
}

void TaskScheduler::run()
{
    Lock<FastMutex> lock(mutex);

    while (true)
    {
        while (agenda.size() == 0 && !shouldStop())
            condvar.wait(mutex);

        // Exit if the ActiveObject has been stopped
        if (shouldStop())
        {
            return;
        }

        int64_t startTick = getTick();
        Event nextEvent   = agenda.top();
        Task& nextTask    = (*tasks)[nextEvent.taskId];

        // If the task has the SKIP policy and its execution was missed, we need
        // to move it forward to match the period
        if (nextEvent.nextTick < startTick && nextTask.policy == Policy::SKIP)
        {
            agenda.pop();
            enqueue(nextEvent, startTick);
        }
        else if (nextEvent.nextTick <= startTick)
        {
            agenda.pop();

            // Execute the task function
            if (nextTask.valid)
            {
                {
                    Unlock<FastMutex> unlock(lock);

                    try
                    {
                        nextTask.function();
                    }
                    catch (...)
                    {
                        // Update the failed statistic
                        nextTask.failedEvents++;
                    }
                }

                // Enqueue only on a valid task
                updateStats(nextEvent, startTick, getTick());
                enqueue(nextEvent, startTick);
            }
        }
        else
        {
            Unlock<FastMutex> unlock(lock);

            Thread::sleepUntil(nextEvent.nextTick);
        }
    }
}

void TaskScheduler::updateStats(const Event& event, int64_t startTick,
                                int64_t endTick)
{
    constexpr float tickToMs = 1000.f / TICK_FREQ;
    Task& task               = (*tasks)[event.taskId];

    // Activation stats
    float activationError = startTick - event.nextTick;
    task.activationStats.add(activationError * tickToMs);

    // Period stats
    int64_t lastCall = task.lastCall;
    if (lastCall >= 0)
        task.periodStats.add((startTick - lastCall) * tickToMs);

    // Update the last call tick to the current start tick for the next
    // iteration
    task.lastCall = startTick;

    // Workload stats
    task.workloadStats.add(endTick - startTick);
}

void TaskScheduler::enqueue(Event event, int64_t startTick)
{
    constexpr float msToTick = TICK_FREQ / 1000.f;

    Task& task = (*tasks)[event.taskId];
    switch (task.policy)
    {
        case Policy::ONE_SHOT:
            // If the task is one shot we won't push it to the agenda and we'll
            // remove it from the tasks map.
            task.valid = false;
            return;
        case Policy::SKIP:
            // Updated the missed events count
            task.missedEvents += (startTick - event.nextTick) / task.period;

            // Compute the number of periods between the tick the event should
            // have been run and the tick it actually run. Than adds 1 and
            // multiply the period to get the next execution tick still aligned
            // to the original one.
            // E.g. If a task has to run once every 2 ticks and start at tick 0
            // but for whatever reason the first execution is at tick 3, then
            // the next execution will be at tick 4.
            event.nextTick +=
                ((startTick - event.nextTick) / task.period + 1) * task.period;
            break;
        case Policy::RECOVER:
            event.nextTick += task.period * msToTick;
            break;
    }

    // Re-enqueue the event in the agenda and signals the run thread
    agenda.push(event);
    condvar.broadcast();
}

TaskScheduler::Task TaskScheduler::makeTask(function_t function,
                                            uint32_t period, size_t id,
                                            bool validity, Policy policy)
{
    return Task{function, period, id, validity, policy, -1, {}, {}, {}, 0, 0};
}

}  // namespace Boardcore