/*
 * 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 few modifications) from:
 *     PolledTimeout.h - encapsulation of a polled Timeout
 * ----------------------------------------------------------------------------------
 * Copyright (C) 2018 Daniel Salazar. All rights reserved.
 *
 * 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
 * ----------------------------------------------------------------------------------
 */


#ifndef ECXX_PLATFORM_NODEMCU_POLLED_TIMEOUT_H
#define ECXX_PLATFORM_NODEMCU_POLLED_TIMEOUT_H


#include "CoreLib.h"
#include "CoreSys.h"


namespace eCxx {

namespace NodeMCU {

namespace PolledTimeout {


namespace YieldPolicy {

    struct DoNothing { static void execute() {} };

    struct YieldOrSkip { static void execute() { delay(0); } };

    template <uint32_t DelayMS>
    struct YieldAndDelayMs { static void execute() { delay(DelayMS); } };

} // namespace YieldPolicy


namespace TimePolicy {

    // Time policy based on millis()
    struct TimeSourceMillis {
        typedef eCxx::uint32_t TimeType;

        static constexpr TimeType ticksPerSecond    = 1000;
        static constexpr TimeType ticksPerSecondMax = 1000;

        static inline TimeType time() { return millis(); }
    };

    // Time policy based on getCycleCountIRQ()
    struct TimeSourceCycles
    {
        typedef eCxx::uint32_t TimeType;

        static constexpr TimeType ticksPerSecond    = Application::getCPUFrequency();
        static constexpr TimeType ticksPerSecondMax = 160000000; // 160 MHz

        static TimeType time() { return getCycleCountIRQ(); }
    };

    // "SecondTh" units of TimeType for one second
    template <typename TimeSourceType, uint64_t SecondTh>
    struct TimeUnit {
        using TimeType = typename TimeSourceType::TimeType;

        static constexpr TimeType computeRangeCompensation()
        {
            #define numOfSecThInOneTick ( (1.0 * SecondTh) / ticksPerSecond )
            #define fractional          ( numOfSecThInOneTick - static_cast<int32_t>(numOfSecThInOneTick) )

            return ( fractional == 0 ? 1 : (numOfSecThInOneTick / fractional) + 0.5 );

            #undef numOfSecThInOneTick
            #undef fractional
        }

        static constexpr TimeType ticksPerSecond         = TimeSourceType::ticksPerSecond;
        static constexpr TimeType ticksPerSecondMax      = TimeSourceType::ticksPerSecondMax;

        static constexpr TimeType rangeCompensate        = computeRangeCompensation();

        static constexpr TimeType user2UnitMultiplierMax = (ticksPerSecondMax * rangeCompensate) / SecondTh;
        static constexpr TimeType user2UnitMultiplier    = (ticksPerSecond    * rangeCompensate) / SecondTh;
        static constexpr TimeType user2UnitDivider       = rangeCompensate;

        static constexpr TimeType timeMax                = (eCxx::NumericLimits<TimeType>::max() - 1) / user2UnitMultiplierMax;

        static inline TimeType toTimeTypeUnit(TimeType userUnit)
        { return (userUnit * user2UnitMultiplier) / user2UnitDivider; }

        static inline TimeType toUserUnit(TimeType internalUnit)
        { return (internalUnit * user2UnitDivider) / user2UnitMultiplier; }

        static inline TimeType time()
        { return TimeSourceType::time(); }
    };

    using TimeMillis     = TimeUnit< TimeSourceMillis,       1000 >;
    using TimeFastMillis = TimeUnit< TimeSourceCycles,       1000 >;
    using TimeFastMicros = TimeUnit< TimeSourceCycles,    1000000 >;
    using TimeFastNanos  = TimeUnit< TimeSourceCycles, 1000000000 >;

} // namespace TimePolicy


template <bool PeriodicT, typename YieldPolicyT = YieldPolicy::DoNothing, typename TimePolicyT = TimePolicy::TimeMillis>
class TimeoutTemplate {

    public:
        using TimeType = typename TimePolicyT::TimeType;
        using timeType = typename TimePolicyT::TimeType; // For compatibility with the Arduino library part

        static_assert(eCxx::isIntegral<TimeType>::Value, "template argument substituting TimePolicyT::TimeType is not an integral");
        static_assert(eCxx::isUnsigned<TimeType>::Value, "template argument substituting TimePolicyT::TimeType is not an unsigned integral");

        static constexpr TimeType alwaysExpired   = 0;
        static constexpr TimeType neverExpires    = eCxx::NumericLimits<TimeType>::max();
        static constexpr TimeType rangeCompensate = TimePolicyT::rangeCompensate;

        inline TimeoutTemplate(TimeType userTimeout)
        { reset(userTimeout); }

        inline bool ICACHE_RAM_ATTR expired()
        {
            YieldPolicyT::execute();
            if(PeriodicT) return expiredRetrigger();
            return expiredOneShot();
        }

        inline operator bool() __ECXX_ATTR_ALWAYS_INLINE
        { return expired(); }

        inline bool canExpire() const
        { return !_neverExpires; }

        inline bool canWait() const
        { return _timeout != alwaysExpired; }

        // Reset so that it will trigger after this new timeout
        inline void ICACHE_RAM_ATTR reset(TimeType newUserTimeout)
        {
            reset();
            _timeout      = TimePolicyT::toTimeTypeUnit(newUserTimeout);
            _neverExpires = (newUserTimeout < 0) || (newUserTimeout > timeMax());
        }

        // Reset so that it will trigger after the timeout previously set
        inline void ICACHE_RAM_ATTR reset()
        { _start = TimePolicyT::time(); }

        // Reset to just expired so that on next poll the check will immediately trigger for the user
        // (also change timeout after next immediate trigger)
        inline void ICACHE_RAM_ATTR resetAndSetExpired (const TimeType newUserTimeout)
        {
            reset(newUserTimeout);
            _start -= _timeout;
        }

        // Reset to just expired so that on next poll the check will immediately trigger for the user
        inline void ICACHE_RAM_ATTR resetAndSetExpired()
        {
            reset();
            _start -= _timeout;
        }

        inline void resetToNeverExpires ()
        {
            _timeout      = alwaysExpired + 1; // Because canWait() has precedence
            _neverExpires = true;
        }

        inline TimeType getTimeout() const
        { return TimePolicyT::toUserUnit(_timeout); }

        static constexpr TimeType timeMax()
        { return TimePolicyT::timeMax; }

    private:
        inline bool ICACHE_RAM_ATTR checkExpired(TimeType internalUnit) const
        { return ( !_neverExpires ) && ( (internalUnit - _start) >= _timeout ); }

        inline bool ICACHE_RAM_ATTR expiredRetrigger()
        {
            if( !canWait() ) return true;

            const TimeType current = TimePolicyT::time();

            if( checkExpired(current) ) {
                const uint32_t n = (current - _start) / _timeout; // Check how many _timeouts periods have elapsed
                _start += n  * _timeout;
                return true;
            }

            return false;
        }

        inline bool ICACHE_RAM_ATTR expiredOneShot() const
        { return !canWait() || checkExpired( TimePolicyT::time() ); }

    private:
        TimeType _timeout;
        TimeType _start;
        bool     _neverExpires;
};


// Time policy based on millis()
// timeMax() is ( (2 ^ 32) - 2 ) mS or 49.7 days
using oneShotMs  = TimeoutTemplate<false>;
using periodicMs = TimeoutTemplate<true>;


// Time policy based on getCycleCountIRQ()
// timeMax() values:
//     Ms -> 26843      mS (26.80 seconds)
//     Us -> 26843545   uS (26.80 seconds)
//     Ns -> 1073741823 nS ( 1.07 seconds)
using oneShotFastMs  = TimeoutTemplate<false, YieldPolicy::DoNothing, TimePolicy::TimeFastMillis>;
using oneShotFastUs  = TimeoutTemplate<false, YieldPolicy::DoNothing, TimePolicy::TimeFastMicros>;
using oneShotFastNs  = TimeoutTemplate<false, YieldPolicy::DoNothing, TimePolicy::TimeFastNanos >;

using periodicFastMs = TimeoutTemplate<true,  YieldPolicy::DoNothing, TimePolicy::TimeFastMillis>;
using periodicFastUs = TimeoutTemplate<true,  YieldPolicy::DoNothing, TimePolicy::TimeFastMicros>;
using periodicFastNs = TimeoutTemplate<true,  YieldPolicy::DoNothing, TimePolicy::TimeFastNanos >;


} // namespace PolledTimeout

} // namespace NodeMCU

} // namespace eCxx


//
// For compatibility with the Arduino library part
//
namespace esp8266 {
    namespace polledTimeout {

        template <bool PeriodicT, typename YieldPolicyT = eCxx::NodeMCU::PolledTimeout::YieldPolicy::DoNothing, typename TimePolicyT = eCxx::NodeMCU::PolledTimeout::TimePolicy::TimeMillis>
        using timeoutTemplate = eCxx::NodeMCU::PolledTimeout::TimeoutTemplate<PeriodicT, YieldPolicyT, TimePolicyT>;

        typedef eCxx::NodeMCU::PolledTimeout::oneShotMs     oneShot;
        typedef eCxx::NodeMCU::PolledTimeout::oneShotMs     oneShotMs;
        typedef eCxx::NodeMCU::PolledTimeout::oneShotFastMs oneShotFastMs;

    }
}


#endif
