/*
 * Copyright (C) 2020-2021 Aloysius Indrayanto
 *
 * This file is part of the eCxx library, see LICENSE file for the license details.
 */

/*
 * ----------------------------------------------------------------------------------
 * The code in this file is copied (with a slight modifications) from:
 *     Schedule.cpp - allow to trigger from SYS stack a registered user code to be
 *                    then executed in user stack (CONT stack) without the common
 *                    restrictions from system context
 * ----------------------------------------------------------------------------------
 * Copyright (C) 2020 esp8266/Arduino
 *
 * This file is part of the esp8266 core for Arduino environment.
 *
 * This library is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any later
 * version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with this library; if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 * ----------------------------------------------------------------------------------
 */


#include <assert.h>

#include "PolledTimeout.h"
#include "Schedule.h"


namespace eCxx {

namespace NodeMCU {


struct ScheduledFuncList {
    using SchedFuncT = ScheduledFunctionalVoid;

    SchedFuncT         mFunc = nullptr;
    ScheduledFuncList* mNext = nullptr;
};

static ScheduledFuncList* sFirst  = nullptr;
static ScheduledFuncList* sLast   = nullptr;
static ScheduledFuncList* sUnused = nullptr;
static eCxx::int32_t      sCount  = 0;


struct RecurrentFuncList {
    using RecFuncT = ScheduledFunctionalBool;

    RecFuncT                      mFunc = nullptr;
    RecFuncT                      alarm = nullptr;
    RecurrentFuncList*            mNext = nullptr;
    PolledTimeout::periodicFastUs callNow;

    inline RecurrentFuncList(PolledTimeout::periodicFastUs interval)
    : callNow(interval)
    {}
};

static RecurrentFuncList* rFirst = nullptr;
static RecurrentFuncList* rLast  = nullptr;


// Returns a pointer to an unused ScheduledFuncList;
// if none are available allocates a new one, or nullptr if limit is reached
static ScheduledFuncList* ICACHE_RAM_ATTR get_fn_unsafe()
{
    constexpr eCxx::int32_t SCHEDULED_FN_MAX_COUNT = 32;

    ScheduledFuncList* result = nullptr;

    // Try to get an item from unused items list
    if(sUnused) {
        result  = sUnused;
        sUnused = sUnused->mNext;
    }

    // If no unused item found and the count is not too high, allocate a new one
    else if(sCount < SCHEDULED_FN_MAX_COUNT) {
        result = new(eCxx::nothrow) ScheduledFuncList;
        if(result) ++sCount;
    }

    return result;
}


static void recycle_fn_unsafe(ScheduledFuncList* fn)
{
    fn->mFunc = nullptr;
    fn->mNext = sUnused;
    sUnused   = fn;
}


extern bool ICACHE_RAM_ATTR scheduleFunction(const ScheduledFunctionalVoid& fn)
{
    if(!fn) return false;

    eCxx::AtomicGuard g;

    ScheduledFuncList* item = get_fn_unsafe();
    if(!item) return false;

    item->mFunc = fn;
    item->mNext = nullptr;

    if(sFirst) sLast->mNext = item;
    else       sFirst       = item;
               sLast        = item;

    return true;
}


extern bool ICACHE_RAM_ATTR scheduleRecurrentFunction(const ScheduledFunctionalBool& fn, uint32_t repeatUS, const ScheduledFunctionalBool& alarm)
{
    // ~26800000us (26.8s)
    assert( repeatUS < decltype(RecurrentFuncList::callNow)::neverExpires );

    if(!fn) return false;

    RecurrentFuncList* item = new (std::nothrow) RecurrentFuncList(repeatUS);
    if(!item) return false;

    item->mFunc = fn;
    item->alarm = alarm;

    eCxx::AtomicGuard g;

    if(rLast) rLast->mNext = item;
    else      rFirst       = item;
              rLast        = item;

    return true;
}


void runScheduledFunctions()
{
    // Yield every 100 mS
    PolledTimeout::periodicFastMs yieldNow(100);

    // Prevent scheduling of new functions during this run
    ScheduledFuncList* stop = sLast;
    bool               done = false;

    while(sFirst && !done) {
        done = (sFirst == stop);

        sFirst->mFunc();

        if(true) {
            // Remove function from stack
            eCxx::AtomicGuard g;

            ScheduledFuncList* toRecycle = sFirst;

            if(sLast == sFirst) sLast = nullptr; // Removing sLast
            sFirst = sFirst->mNext;

            recycle_fn_unsafe(toRecycle);
        }

        if(yieldNow) {
            // Scheduled functions might last too long for watchdog et al.
            esp_schedule();
            contYield(g_ContData);
        }
    }
}


void runScheduledRecurrentFunctions()
{
    // Yield every 100 mS
    PolledTimeout::periodicFastMs yieldNow(100);

    // Scheduled functions are removed only from this function so that it
    // is never called from an interrupt handler (always on CONT stack)
    RecurrentFuncList* current = rFirst;
    if(!current) return;

    // Fence is like a mutex; because this function is never called from an ISR,
    // AtomicGuard is not needed
    static bool fence = false;

    if(fence) return;
    fence = true;

    // Prevent scheduling of new functions during this run
    RecurrentFuncList* prev = nullptr;
    RecurrentFuncList* stop = rLast;
    bool               done = false;

    do {

        done = (current == stop);

        const bool wakeup  = ( current->alarm && current->alarm() );
              bool callNow = current->callNow;

        if( (wakeup || callNow) && !current->mFunc() ) {
            // Remove function from stack
            eCxx::AtomicGuard g;

            RecurrentFuncList* toDitch = current;

            if(rLast == current) rLast = prev; // removing rLast

            current = current->mNext;

            if(prev) prev->mNext = current;
            else     rFirst      = current;

            delete(toDitch);
        }
        else {
            prev    = current;
            current = current->mNext;
        }

        if(yieldNow) {
            // Scheduled functions might last too long for watchdog et al.
            esp_schedule();
            contYield(g_ContData);
        }

    } while(current && !done);

    fence = false;
}


} // namespace NodeMCU

} // namespace eCxx


#ifdef __ECXX_NODEMCU_SCHEDULE_USE_ECXX_FUNCTION

//
// Function wrapper with "C++" linkage for compatibility with the Arduino library part
//
extern bool ICACHE_RAM_ATTR schedule_function(const std::function<void(void)>& fn)
{
    return eCxx::NodeMCU::scheduleFunction( [fn]() {
        fn();
    } );
}

#else

//
// Function aliases with "C++" linkage for compatibility with the Arduino library part
//

// make mangle CODE='#include <functional>\n namespace eCxx { namespace NodeMCU { extern bool scheduleFunction(const std::function<void()>& fn) { return false; } } }'
extern bool schedule_function(const std::function<void(void)>& fn) __ECXX_ATTR_ALIAS("_ZN4eCxx7NodeMCU16scheduleFunctionERKSt8functionIFvvEE");

#endif
