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


#ifndef ECXX_PLATFORM_STREAM_SAMPLER_H
#define ECXX_PLATFORM_STREAM_SAMPLER_H


#include <eCxx/Platform/Common/Application.h>


namespace eCxx {


//
// A simple data-stream sampler
//
// # This sampler collects a representative sample of data-stream that retains the characteristics
//   of the data-stream and can be used to estimate many important aggregates in the data-stream.
// # This sampler is intended to be used for very low speed transmission (equivalent to <= ~ 300 baud).
// # This sampler only takes data-stream samples and does not process/decode them further.
//
class StreamSampler : public Uncopyable {
    public:
        // Calibration setting
        class CalibrationSetting {
            public:
                inline CalibrationSetting(
                    ActiveState activeState       = ActiveState::Low, // If the line is idle high, then set it to ActiveState::Low (and vice versa)
                    uint16_t    initialTimeoutMS  = 2500,             // The amount of milliseconds to wait for the line to become idle for the first time
                    uint16_t    endBlockTimeoutMS =   30,             // The amount of milliseconds to wait before the current data stream (block) is assumed to be complete
                    uint8_t     noiseTimeUS       =  100,             // The amount of microseconds that a level changed is assumed to be from noise
                    uint8_t     calibrateCount    =    6,             // The approximate number of times the calibration process should be executed
                    uint8_t     tolerance         =   20,             // Maximum tolerance for timing (in %)
                    uint16_t    totalTimeoutMS    = 5000              // Timeout for the whole calibration process (not including the initial timeout)
                );

            private:
                // Calibration setting
                ActiveState _activeState;
                uint16_t    _initialTimeoutMS;
                uint16_t    _endBlockTimeoutMS;
                uint8_t     _noiseTimeUS;
                uint8_t     _calibrateCount;
                uint8_t     _tolerance;
                uint16_t    _totalTimeoutMS;

            private:
                friend class StreamSampler;

                inline void resetAllState() const noexcept;
                inline void resetRunningState() const noexcept;
                inline bool done() const noexcept;

                // Accumulative state
                mutable          uint8_t  _totalStep;    // Total calibration steps done

                mutable          uint32_t _minLTimeTot;  // Total minimum low  state length (in uS)
                mutable          uint32_t _minHTimeTot;  // Total minimum high state length (in uS)
                mutable          uint32_t _maxLTimeTot;  // Total maximum low  state length (in uS)
                mutable          uint32_t _maxHTimeTot;  // Total maximum high state length (in uS)

                mutable          uint16_t _bitCountTot;  // Total maximum number of bits

                // Running state
                enum class CState : uint8_t {
                    Start,
                    InProgress,
                    Done
                };

                mutable volatile CState   _state;

                mutable          uint32_t _lastMicros;
                mutable          bool     _lastBit;

                mutable          uint16_t _minLTime;  // Minimum low  state length (in uS)
                mutable          uint16_t _minHTime;  // Minimum high state length (in uS)
                mutable          uint16_t _maxLTime;  // Maximum low  state length (in uS)
                mutable          uint16_t _maxHTime;  // Maximum high state length (in uS)

                mutable          uint8_t  _bitCount;  // Maximum number of bits
        };

        // Bitstream timing specification
        struct TimingSpecification {
            uint16_t endBlockTimeoutMS; // The amount of milliseconds to wait before the current data stream (block) is assumed to be complete
            uint8_t  noiseTimeUS;       // The amount of microseconds that a level changed is assumed to be from noise

            uint16_t minLTimeUS0;       // minimum low  state length (in uS) - lower range
            uint16_t minLTimeUS1;       // minimum low  state length (in uS) - upper range
            uint16_t minHTimeUS0;       // minimum high state length (in uS) - lower range
            uint16_t minHTimeUS1;       // minimum high state length (in uS) - upper range

            uint16_t maxLTimeUS0;       // maximum low  state length (in uS) - lower range
            uint16_t maxLTimeUS1;       // maximum low  state length (in uS) - upper range
            uint16_t maxHTimeUS0;       // maximum high state length (in uS) - lower range
            uint16_t maxHTimeUS1;       // maximum high state length (in uS) - upper range

            uint8_t  minLTimeBits;      // minimum low  state length (in bits)
            uint8_t  minHTimeBits;      // minimum high state length (in bits)
            uint8_t  maxLTimeBits;      // maximum low  state length (in bits)
            uint8_t  maxHTimeBits;      // maximum high state length (in bits)

            uint8_t  maxBitCount;       // Maximum number of bits that will be received (in multiple of 8)

            inline TimingSpecification();

            inline void reset() noexcept;
            inline bool valid() const noexcept;
        };

        // User receive callback
        using RxCallbackF = eCxx::FunctionalVoidCallback_ConstUInt8Ptr_UInt8;

    public:
       StreamSampler();
       ~StreamSampler();

       bool begin(uint8_t pin);
       bool begin(uint8_t pin, const TimingSpecification& ts);
       void end();

       inline void setTimingSpecification(TimingSpecification& ts) noexcept;
       inline const TimingSpecification& timingSpecification() const noexcept;

       inline bool valid() const noexcept;

       bool calibrate(const CalibrationSetting& cs) __ECXX_ATTR_OPTIMIZE_S;
       bool receive(RxCallbackF rxcb, ActiveState activeState = ActiveState::Low);

    private:
        enum class OperationalMode : uint8_t {
            None,
            Calibration,
            Receiving
        };

        static void callUserCallback_impl(void* arg) __ECXX_ATTR_OPTIMIZE_2;

        void ICACHE_RAM_ATTR calibrate_impl(bool bit, uint32_t micros) __ECXX_ATTR_OPTIMIZE_2;
        void ICACHE_RAM_ATTR receive_impl(bool bit, uint32_t micros) __ECXX_ATTR_OPTIMIZE_2;

    private:
        uint8_t                   _pin;
        Application::_PinFDR      _pinFDR;

        TimingSpecification       _tSpec;
        const CalibrationSetting* _cSet;

        RxCallbackF               _rxUserCB;
        ActiveState               _rxActiveState;
        uint8_t                   _rxCurBuffIdx;
        uint8_t                   _rxBuff[2][Application::BSRBufferSize];
        uint8_t                   _rxPosIdx;
        uint8_t                   _rxBitMask;
        uint8_t                   _rxBitCount[2];
        uint32_t                  _rxLastMicros;
        bool                      _rxLastBit;
        bool                      _rxStart;

        static void ICACHE_RAM_ATTR gpio_isr(void* arg) __ECXX_ATTR_OPTIMIZE_2;
};


//
// Inline member functions of the StreamSampler::CalibrationSetting class
//
inline StreamSampler::CalibrationSetting::CalibrationSetting(
    ActiveState activeState,
    uint16_t    initialTimeoutMS,
    uint16_t    endBlockTimeoutMS,
    uint8_t     noiseTimeUS,
    uint8_t     calibrateCount,
    uint8_t     tolerance,
    uint16_t    totalTimeoutMS
)
: _activeState      (activeState      )
, _initialTimeoutMS (initialTimeoutMS )
, _endBlockTimeoutMS(endBlockTimeoutMS)
, _noiseTimeUS      (noiseTimeUS      )
, _calibrateCount   (calibrateCount   )
, _tolerance        (tolerance        )
, _totalTimeoutMS   (totalTimeoutMS   )
{
         if(_endBlockTimeoutMS <    5) _endBlockTimeoutMS =    5;
    else if(_endBlockTimeoutMS > 1000) _endBlockTimeoutMS = 1000;

         if(_noiseTimeUS <  10) _noiseTimeUS =  10;
    else if(_noiseTimeUS > 240) _noiseTimeUS = 240;

         if(_calibrateCount < 2) _calibrateCount = 2;
    else if(_calibrateCount > 8) _calibrateCount = 8;

         if(_tolerance <  5) _tolerance =  5;
    else if(_tolerance > 35) _tolerance = 35;
}


inline void StreamSampler::CalibrationSetting::resetAllState() const noexcept
{
    // Accumulative state
    _totalStep   = 0;

    _minLTimeTot = 0;
    _minHTimeTot = 0;
    _maxLTimeTot = 0;
    _maxHTimeTot = 0;

    _bitCountTot = 0;

    // Running state
    resetRunningState();
}


inline void StreamSampler::CalibrationSetting::resetRunningState() const noexcept
{
    _state    = CState::Start;

    _minLTime = -1;
    _minHTime = -1;
    _maxLTime =  0;
    _maxHTime =  0;

    _bitCount =  0;
}


inline bool StreamSampler::CalibrationSetting::done() const noexcept
{ return _state == CState::Done; }


//
// Inline member functions of the StreamSampler::TimingSpecification class
//
inline StreamSampler::TimingSpecification::TimingSpecification()
{ reset(); }


inline void StreamSampler::TimingSpecification::reset() noexcept
{
    endBlockTimeoutMS = 0;
    noiseTimeUS       = 0;

    minLTimeUS0       = 0;
    minLTimeUS1       = 0;
    minHTimeUS0       = 0;
    minHTimeUS1       = 0;

    maxLTimeUS0       = 0;
    maxLTimeUS1       = 0;
    maxHTimeUS0       = 0;
    maxHTimeUS1       = 0;

    minLTimeBits      = 0;
    minHTimeBits      = 0;
    maxLTimeBits      = 0;
    maxHTimeBits      = 0;

    maxBitCount       = 0;
}


inline bool StreamSampler::TimingSpecification::valid() const noexcept
{ return endBlockTimeoutMS && noiseTimeUS && maxBitCount; }


//
// Inline member functions of the StreamSampler class
//
inline void StreamSampler::setTimingSpecification(TimingSpecification& ts) noexcept
{ _tSpec = ts; }


inline const StreamSampler::TimingSpecification& StreamSampler::timingSpecification() const noexcept
{ return _tSpec; }


inline bool StreamSampler::valid() const noexcept
{ return _pin != Application::InvalidPin && _tSpec.valid(); }


} // namespace eCxx


#endif
