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


#ifndef INCLUDE_ECXX_PLATFORM_SPECIFIC_APPLICATION_H
#error "this file cannot be included directly"
#endif
#undef INCLUDE_ECXX_PLATFORM_SPECIFIC_APPLICATION_H


#ifndef ECXX_PLATFORM_NODEMCU_APPLICATION_H
#define ECXX_PLATFORM_NODEMCU_APPLICATION_H


#include <eCxx/Platform/Common/SPI.h>
#include <eCxx/Platform/Common/TWIMaster.h>
#include <eCxx/Platform/Common/TWISlave.h>
#include <eCxx/Platform/Common/UART.h>

#include "I2SOut.h"
#include "PinName.h"


//
// Uncomment this to disable OTA programming support
//
// #define ECXX_NODEMCU_DISABLE_OTA_PROGRAMMING


//
// Function prototypes with "C" linkage
//
extern "C" eCxx::uint32_t system_get_time (              );
extern "C" void           ets_delay_us    (eCxx::uint32_t);

extern "C" void           yield           (              );
extern "C" void           optimistic_yield(eCxx::uint32_t);


//
// Forward declaration of LittleFS classes
//
namespace eCxx {
    namespace NodeMCU {
        namespace FileSystem {
            class Dir;
            class File;
            class FS;
        }
    }
}


//
// Get access to the external LittleFS instance
//
extern eCxx::NodeMCU::FileSystem::FS LittleFS;


namespace eCxx {


//
// Bring out the WString class for convenience
//
using WString = NodeMCU::WString;


//
// A standard NodeMCU application with WiFi Soft access point (AP) mode and over-the-air (OTA) programming support
//
// NodeMCU's ADC voltage range is between 0V to 1V (or to 3.3V if the board has a voltage divider at the ADC input).
//
class Application : public Application_Base, public Application_CRTPBase<Application> {
    public:
        // Forward declaration to the application's configuration structure
        struct Config;

        // Override this from the Application_Base class
        enum class PinMode : uint8_t {
            Input       = static_cast<uint8_t>(Application_Base::PinMode::Input      ),
            InputPullUp = static_cast<uint8_t>(Application_Base::PinMode::InputPullUp),
            Output      = static_cast<uint8_t>(Application_Base::PinMode::Output     ),
            OutputOpenDrain,
            InputPullDown16
        };

        // Explicitly pull these from the Application_Base class
        using Application_Base::InvalidPin;

        using Application_Base::UARTBufferSize;
        using Application_Base::TWIBufferSize;
        using Application_Base::BSRBufferSize;

        using Application_Base::CommunicationTimeoutCycles;
        using Application_Base::MaxScheduledUserCallback;

        using Application_Base::VoidFuncPtr;
        using Application_Base::VoidFuncPtrArg;

        // Declare the OWIMaster class
        using OWIMaster = Application_OWIMaster<Application>;

        // Declare the I2SOut class
        using I2SOut = eCxx::I2SOut;

        // Minimum and maximum ADC pin numbers
        static constexpr uint8_t ADCPinMin = NodeMCU::AMin;
        static constexpr uint8_t ADCPinMax = NodeMCU::AMax;

        // Interrupt mode
        enum class InterruptMode : uint8_t {
            Rising  = 0x01,
            Falling = 0x02,
            Change  = 0x03
        };

        // Error code for over-the-air (OTA) programming
        enum class OTAError : uint8_t {
            AuthError    = 1,
            BeginError   = 2,
            ConnectError = 3,
            ReceiveError = 4,
            EndError     = 5
        };

        // User callback for over-the-air (OTA) programming
        using OTAUserStartCallbackF = eCxx::FunctionalVoidCallback;
        using OTAUserEndCallbackF   = eCxx::FunctionalVoidCallback;
        using OTAUserErrorCallbackF = eCxx::function<void(OTAError)>;

        // Date-time structure
        struct DateTime {
            int8_t   isDST;   // DST flag     ; 1 = DST is in effect ; 0 = DST is not in effect ; -1 = no information is available
            uint16_t yearDay; // [   0,  365] ; the number of full days since the beginning of the year
            uint8_t  weekDay; // [   0,    6] ; the number of full days since Sunday
            uint16_t year;    // [1900, ....] ; the current year
            uint8_t  month;   // [   1,   12] ; the current month
            uint8_t  day;     // [   1,   31] ; the current day
            uint8_t  hour;    // [   0,   23] ; the current hour
            uint8_t  minute;  // [   0,   59] ; the current minute
            uint8_t  second;  // [   0,   60] ; the current second (the extra range is to accommodate for leap seconds)
        };

        // Date-time mode
        enum class DateTimeMode : uint8_t {
            UTC   = 0,
            Local = 1
        };

        // Network interface description structure
        struct NetIfDesc {
            int32_t     ifNumber;
            char        ifName    [ 3]; // netif.name ( 2 characters        )            + null terminator
            char        ifHostname[33]; // hostname   (32 characters maximum)            + null terminator
            char        ifMAC     [18]; // 6 octets   ( 2 characters each   ) + 5 colons + null terminator
            bool        isLinkUp;
            bool        isLocal;
            bool        isLegacy;
            bool        isIPv6;
        #if LWIP_IPV6
            char        address   [40]; // 8 groups   (4 characters each    ) + 7 colons + null terminator
        #else
            char        address   [16]; // 4 fields   (3 characters each    ) + 3 dots   + null terminator
        #endif
            char        netmask   [16]; // 4 fields   (3 characters each    ) + 3 dots   + null terminator
            char        gateway   [16]; // 4 fields   (3 characters each    ) + 3 dots   + null terminator
        };

    public:
        static void halt() noexcept __ECXX_ATTR_NORETURN;
        static void softReset() noexcept __ECXX_ATTR_NORETURN;

        static void getSoftUARTBitDelayUS(uint16_t& txBit, uint16_t& rxBit, uint16_t& rxCtr, uint32_t baudrate) noexcept;

        static void setPinMode(uint8_t pin, PinMode mode);
        static void setActivityIndicatorLED(bool on);

        static void digitalWrite(uint8_t pin, bool bit);
        static bool digitalRead(uint8_t pin);

        static void analogWrite(uint8_t pin, uint8_t val);
        static uint16_t analogRead(uint8_t pin);

        // WARNING : # In NodeMCU, "offset" is actually appended to the NV-storage file name!
        //           # To avoid too many files stored in the FS, format the FS every once in a while.
        static bool nvStorageFormat();
        static bool nvStorageWrite(const void* src, uint16_t size, uint16_t offset = 0);
        static bool nvStorageRead (      void* dst, uint16_t size, uint16_t offset = 0);

        // Please refer to https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html for more details
        static bool nvFileExists(const char* path);
        static NodeMCU::FileSystem::File nvOpenFileForReading(const char* path);
        static NodeMCU::FileSystem::File nvOpenFileForWriting(const char* path);
        static bool nvRemoveFile(const char* path);
        static bool nvRenameFile(const char* pathFrom, const char* pathTo);
        static bool nvMakeDirectory(const char* path);
        static bool nvRemoveDirectory(const char* path);
        static NodeMCU::FileSystem::Dir nvOpenDirectory(const char* path);

        static inline uint32_t micros() noexcept __ECXX_ATTR_ALWAYS_INLINE;
        static        uint32_t millis() noexcept;

        static inline void delayUS(uint16_t uS) noexcept __ECXX_ATTR_ALWAYS_INLINE;
        static        void delayMS(uint16_t mS) noexcept;

        static inline UART& uart();

        static SPI& spi();

        static TWIMaster& twiMaster();
        static TWISlave& twiSlave();

        static OWIMaster& owiMaster();

        static I2SOut& i2sOut();

        static inline void yield(bool immediate = false) noexcept __ECXX_ATTR_ALWAYS_INLINE;
        static constexpr uint32_t getCPUFrequency() noexcept;

        // NOTE : The user function must be in IRAM (declared using ICACHE_RAM_ATTR)
        static bool attachInterruptArg(uint8_t pin, VoidFuncPtrArg userFunc, void* arg, InterruptMode mode);
        static bool detachInterrupt(uint8_t pin);

        static void initialize(const Application::Config& config);
        static uint32_t processEvent(bool blinkActivityIndicatorLED = true);

    public:
        static bool switchToWiFiStationMode(PGMCharPointer ssid, PGMCharPointer passphrase);
        static inline bool inWiFiStationMode() noexcept;

    private:
        static bool _configNTP_waitUntilActualTimeIsSet();

    public:
        // NOTE : NTP can only be used in WiFi station mode
        static bool configNTP(const char* tz, const char* server1, const char* server2 = nullptr, const char* server3 = nullptr);
        static bool configNTP(int32_t timezoneSec, int32_t daylightOffsetSec, const char* server1, const char* server2 = nullptr, const char* server3 = nullptr);

        static void setCurrentDateTime(int64_t microSecondsSinceUnixEpoch);
        static const DateTime currentDateTime(DateTimeMode dtm = DateTimeMode::Local);

    public:
        // NOTE : Set the result buffer to nullptr to get the number of interfaces only
        static size_t getNetIfDescList(NetIfDesc* resBuff, size_t buffLen);

    public:
        // NOTE : # Periodic interrupt and PWM will interfere with each other
        //        # The user function must be in IRAM (declared using ICACHE_RAM_ATTR)
        static void attachPeriodicInterrupt(VoidFuncPtr userFunc, uint16_t frequency);
        static void detachPeriodicInterrupt();

    public:
        static inline void setOTAStartUserCallback(OTAUserStartCallbackF ucb) noexcept;
        static inline void setOTAEndUserCallback(OTAUserEndCallbackF ucb) noexcept;
        static inline void setOTAErrorUserCallback(OTAUserErrorCallbackF ucb) noexcept;

        static PGMCharPointer getExtendedOTAUpdaterSimplifiedErrorString();

        static UART* uart0();
        static UART* uart1(); // WARNING : this UART is Tx only!

    public:
        // Fast-digital-read wrapper class
        class _PinFDR {
            private:
                friend class Application;

                volatile uint32_t* _sfr; // GPI
                uint32_t           _bm;  // Bitmask

            public:
                inline _PinFDR() noexcept __ECXX_ATTR_ALWAYS_INLINE
                : _sfr(nullptr), _bm(0)
                {}

                inline bool operator()() const noexcept __ECXX_ATTR_ALWAYS_INLINE
                { return *_sfr & _bm; }

                inline volatile uint32_t* sfrGPI() const noexcept __ECXX_ATTR_ALWAYS_INLINE
                { return _sfr; }

                inline uint32_t bitmask() const noexcept __ECXX_ATTR_ALWAYS_INLINE
                { return _bm; }
        };

        // Fast-digital-write wrapper class
        class _PinFDW {
            private:
                friend class Application;

                volatile uint32_t* _sfrs; // GPOS
                volatile uint32_t* _sfrc; // GPOC
                uint32_t           _bm;   // Bitmask

            public:
                inline _PinFDW() noexcept __ECXX_ATTR_ALWAYS_INLINE
                : _sfrs(nullptr), _sfrc(nullptr), _bm(0)
                {}

                inline void operator()(bool bit) const noexcept __ECXX_ATTR_ALWAYS_INLINE
                {
                    if(bit) *_sfrs = _bm;
                    else    *_sfrc = _bm;
                }

                inline volatile uint32_t* sfrGPOS() const noexcept __ECXX_ATTR_ALWAYS_INLINE
                { return _sfrs; }

                inline volatile uint32_t* sfrGPOC() const noexcept __ECXX_ATTR_ALWAYS_INLINE
                { return _sfrc; }

                inline uint32_t bitmask() const noexcept __ECXX_ATTR_ALWAYS_INLINE
                { return _bm; }
        };

        // Fast-digital-read-and-write wrapper class
        // (by default, when in read mode, the pin is configured with pull-up)
        class _PinFDX {
            private:
                friend class Application;

                volatile uint32_t* _sfrr;      // GPI
                volatile uint32_t* _sfrs;      // GPOS
                volatile uint32_t* _sfrc;      // GPOC
                uint8_t            _pin;       // Pin
                uint32_t           _bm;        // Bitmask

                bool               _usePullUp; // Use pull-up flag

            public:
                inline _PinFDX() noexcept __ECXX_ATTR_ALWAYS_INLINE
                : _sfrr(nullptr), _sfrs(nullptr), _sfrc(nullptr), _pin(0), _bm(0), _usePullUp(true)
                {}

                inline void setUsePullUp(bool usePullUp) noexcept __ECXX_ATTR_ALWAYS_INLINE
                { _usePullUp = usePullUp; }

                inline void setReadMode() const noexcept __ECXX_ATTR_ALWAYS_INLINE
                {
                    Application::setPinMode(
                        _pin,
                        _usePullUp ? Application::PinMode::InputPullUp : Application::PinMode::Input
                    );
                }

                inline void setWriteMode(bool initialBit) const noexcept __ECXX_ATTR_ALWAYS_INLINE
                {
                    if(initialBit) *_sfrs = _bm;
                    else           *_sfrc = _bm;

                    Application::setPinMode(_pin, Application::PinMode::Output);
                }

                inline bool read() const noexcept __ECXX_ATTR_ALWAYS_INLINE
                { return *_sfrr & _bm; }

                inline void write(bool bit) const noexcept __ECXX_ATTR_ALWAYS_INLINE
                {
                    if(bit) *_sfrs = _bm;
                    else    *_sfrc = _bm;
                }

                inline volatile uint32_t* sfrGPI() const noexcept __ECXX_ATTR_ALWAYS_INLINE
                { return _sfrr; }

                inline volatile uint32_t* sfrGPOS() const noexcept __ECXX_ATTR_ALWAYS_INLINE
                { return _sfrs; }

                inline volatile uint32_t* sfrGPOC() const noexcept __ECXX_ATTR_ALWAYS_INLINE
                { return _sfrc; }

                inline uint32_t bitmask() const noexcept __ECXX_ATTR_ALWAYS_INLINE
                { return _bm; }
        };

        // Fast-digital-read/write initialization functions
        static void _preparePinFDR(_PinFDR& pinFDR, uint8_t pin) noexcept;
        static void _preparePinFDW(_PinFDW& pinFDW, uint8_t pin) noexcept;
        static void _preparePinFDX(_PinFDX& pinFDX, uint8_t pin) noexcept;

    private:
        // OWIMaster low-level functions
        friend class Application_OWIMaster<Application>;

        class OWIData {
            public:
                inline OWIData()
                : pin(0)
                , bitmask(0)
                {}

                inline bool valid() const
                { return bitmask != 0; }

            private:
                friend class Application;

                uint8_t  pin;
                uint32_t bitmask;
        };

        static bool owiBegin(OWIData& owiData, uint8_t pin);
        static void owiEnd(OWIData& owiData);

        static bool owiReset(const OWIData& owiData);

        static void owiWriteBit0(const OWIData& owiData);
        static void owiWriteBit1(const OWIData& owiData);
        static bool owiReadBit(const OWIData& owiData);

    private:
        static constexpr uint8_t HostnameBufferSize   = 32 + 1; // Maximum 32 characters + null terminator
        static constexpr uint8_t SSIDBufferSize       = 31 + 1; // Maximum 31 characters + null terminator
        static constexpr uint8_t PassphraseBufferSize = 63 + 1; // Maximum 63 characters + null terminator

    private:
        static constexpr uint32_t    _cpuFrequency = F_CPU;

#ifdef ECXX_NODEMCU_DISABLE_OTA_PROGRAMMING
        static char                  _hostname[HostnameBufferSize];
#endif

        static bool                  _inWiFiStationMode;

        static OTAUserStartCallbackF _otaStartCB;
        static OTAUserEndCallbackF   _otaEndCB;
        static OTAUserErrorCallbackF _otaErrorCB;

        static I2SOut::TxCallbackF   _i2sCallback;

        static uint8_t               _emulatedEEPROMBuffer[512] __ECXX_ATTR_SEC_NOINIT;

        static inline void __delay_loop4(uint16_t cnt) noexcept __ECXX_ATTR_ALWAYS_INLINE;

        friend I2SOut;
        friend class HWInfo;
        friend class OneWireSoftUART;
};


inline void Application::__delay_loop4(uint16_t cnt) noexcept
{
    // Maximum delay at 1 MHz CPU is about 65536 * 4 = 262144 uS = 262.144 mS
    __asm__ __volatile__ (
        "1: addi %0, %0, -1 \n\t"
        "   bnez.n %0, 1b   \n\t"
        : "=a"(cnt)
        :  "0"(cnt)
    );
}


inline uint32_t Application::micros() noexcept
{ return ::system_get_time(); }


inline void Application::delayUS(uint16_t uS) noexcept
{ ::ets_delay_us(uS); }


inline UART& Application::uart()
{ return *uart0(); }


inline void Application::yield(bool immediate) noexcept
{
    if(immediate)
        ::yield();
    else
        ::optimistic_yield(5000);
}


constexpr uint32_t Application::getCPUFrequency() noexcept
{ return _cpuFrequency; }


inline bool Application::inWiFiStationMode() noexcept
{ return _inWiFiStationMode; }


inline void Application::setOTAStartUserCallback(OTAUserStartCallbackF ucb) noexcept
{ _otaStartCB = ucb; }


inline void Application::setOTAEndUserCallback(OTAUserEndCallbackF ucb) noexcept
{ _otaEndCB = ucb; }


inline void Application::setOTAErrorUserCallback(OTAUserErrorCallbackF ucb) noexcept
{ _otaErrorCB = ucb; }


//
// Application configuration structure
//
struct Application::Config {
    uint32_t       cpuFreq;

    PGMCharPointer hostname;

    PGMCharPointer ssid;
    PGMCharPointer passphrase;
    uint8_t        channel;
    uint8_t        maxConnection;
    uint8_t        macAddress[6];
    uint8_t        localIP[4];
    uint8_t        gateway[4];
    uint8_t        subnetMask[4];

    PGMCharPointer otaPassword;
    uint16_t       otaPort;

    inline Config(PGMCharPointer ssid, PGMCharPointer passphrase, PGMCharPointer otaPassword = nullptr, PGMCharPointer hostname = nullptr);
    inline Config(PGMCharPointer ssid, PGMCharPointer passphrase, uint8_t channel, const uint8_t (&macAddress_)[6], PGMCharPointer otaPassword = nullptr, PGMCharPointer hostname = nullptr);

    inline void setChannel(uint8_t channel);
    inline void setMaxConnection(uint8_t maxConnection);
    inline void setOTAPort(uint16_t otaPort);

    inline void setMACAddress(uint8_t o1, uint8_t o2, uint8_t o3, uint8_t o4, uint8_t o5, uint8_t o6);
    inline void setLocalIP   (uint8_t o1, uint8_t o2, uint8_t o3, uint8_t o4);
    inline void setGateway   (uint8_t o1, uint8_t o2, uint8_t o3, uint8_t o4);
    inline void setSubnetMask(uint8_t o1, uint8_t o2, uint8_t o3, uint8_t o4);
};


inline Application::Config::Config(PGMCharPointer ssid_, PGMCharPointer passphrase_, PGMCharPointer otaPassword_, PGMCharPointer hostname_)
: Config(
    ssid_,
    passphrase_,
    1,
    eCxx::makeArray<eCxx::uint8_t, 0x1A, 0xFE, 0x34, 0xC2, 0x32, 0xA9>(),
    otaPassword_,
    hostname_
)
{}


inline Application::Config::Config(PGMCharPointer ssid_, PGMCharPointer passphrase_, uint8_t channel_, const uint8_t (&macAddress_)[6], PGMCharPointer otaPassword_, PGMCharPointer hostname_)
: cpuFreq      ( F_CPU )
, hostname     ( hostname_ )
, ssid         ( ssid_ )
, passphrase   ( passphrase_ )
, channel      ( channel_ )
, maxConnection( 4 )
, macAddress   { macAddress_[0], macAddress_[1], macAddress_[2], macAddress_[3], macAddress_[4], macAddress_[5] }
, localIP      { 192, 168,   0, 100 }
, gateway      { 192, 168,   0, 100 }
, subnetMask   { 255, 255, 255,   0 }
, otaPassword  ( otaPassword_ )
, otaPort      ( 8266 )
{}


inline void Application::Config::setChannel(uint8_t channel_)
{ channel = channel_; }


inline void Application::Config::setMaxConnection(uint8_t maxConnection_)
{ maxConnection = maxConnection_; }


inline void Application::Config::setOTAPort(uint16_t otaPort_)
{ otaPort = otaPort_; }


inline void Application::Config::setMACAddress(uint8_t o1, uint8_t o2, uint8_t o3, uint8_t o4, uint8_t o5, uint8_t o6)
{
    macAddress[0] = o1;
    macAddress[1] = o2;
    macAddress[2] = o3;
    macAddress[3] = o4;
    macAddress[4] = o5;
    macAddress[5] = o6;
}


inline void Application::Config::setLocalIP(uint8_t o1, uint8_t o2, uint8_t o3, uint8_t o4)
{
    localIP[0] = o1;
    localIP[1] = o2;
    localIP[2] = o3;
    localIP[3] = o4;
}


inline void Application::Config::setGateway(uint8_t o1, uint8_t o2, uint8_t o3, uint8_t o4)
{
    gateway[0] = o1;
    gateway[1] = o2;
    gateway[2] = o3;
    gateway[3] = o4;
}


inline void Application::Config::setSubnetMask(uint8_t o1, uint8_t o2, uint8_t o3, uint8_t o4)
{
    subnetMask[0] = o1;
    subnetMask[1] = o2;
    subnetMask[2] = o3;
    subnetMask[3] = o4;
}


} // namespace eCxx


#endif
