/*
 * 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_CPP
#error "this file cannot be compiled directly"
#endif
#undef INCLUDE_ECXX_PLATFORM_SPECIFIC_APPLICATION_CPP


#include <lwip/apps/sntp.h>

#include "CoreLib.h"
#include "CoreSys.h"
#include "CoreUtil.h"
#include "I2SOutDriver.h"
#include "LwipIntf.h"
#include "Schedule.h"

#ifndef ECXX_NODEMCU_DISABLE_OTA_PROGRAMMING
    #include "OTAProgramming.h"
#else
    #include <ESP8266mDNS.h>
#endif

#include <LittleFS.h>


namespace eCxx {


//
// Internal constants
//
static constexpr uint8_t ActivityIndicatorLED_Pin = NodeMCU::BuiltinLED1;

static constexpr char    NVStorageFileName[]      = "/.__eCxx__emulated_eeprom__.dat";


//
// Include all simple wrapper implementations
//
#define INCLUDE_ECXX_NODEMCU_SIMPLE_WRAPPER_CPP
#include "SimpleWrapper.cpp"


//
// Instantiate the UART, SPI, and TWIMaster, TWISlave, and I2SOut objects
//
inline UART* uart0_instance()
{
    static UART _uart0(UART::UART0);
    return &_uart0;
}


inline UART* uart1_instance()
{
    static UART _uart1(UART::UART1);
    return &_uart1;
}


inline SPI& spi_instance()
{
    static SPI _spi;
    return _spi;
}


inline TWIMaster& twiMaster_instance()
{
    static TWIMaster _twiMaster;
    return _twiMaster;
};


inline TWISlave& twiSlave_instance()
{
    static TWISlave _twiSlave;
    return _twiSlave;
};


//
// A standard NodeMCU application
//
#ifdef ECXX_NODEMCU_DISABLE_OTA_PROGRAMMING
char Application::_hostname[HostnameBufferSize];
#endif

bool                               Application::_inWiFiStationMode = false;

Application::OTAUserStartCallbackF Application::_otaStartCB        = nullptr;
Application::OTAUserEndCallbackF   Application::_otaEndCB          = nullptr;
Application::OTAUserErrorCallbackF Application::_otaErrorCB        = nullptr;

Application::I2SOut::TxCallbackF   Application::_i2sCallback       = nullptr;

uint8_t                            Application::_emulatedEEPROMBuffer[512];


void Application::halt() noexcept
{
    // Uninitialize UART
    uart().flush();
    uart().end();

    // Ensure the activity indicator LED is configured as output and turn it off
    setPinMode(ActivityIndicatorLED_Pin, PinMode::Output);
    setActivityIndicatorLED(false);

    // Loop forever here
    for(;;) {
#ifndef ECXX_NODEMCU_DISABLE_OTA_PROGRAMMING
        // Handle OTA programming
        NodeMCU::OTA.handle();
#else
        // Handle mDNS
        MDNS.update();
#endif
        // Yield
        Application::yield();
        // Blink the activity indicator LED to indicate that the system is halted
        static uint8_t  state     = 0;
        static uint32_t prvMillis = 0;
               uint32_t curMillis = Application::millis();
        if(curMillis - prvMillis < 250) continue;
        prvMillis = curMillis;
        switch(state) {
            case 0  : setActivityIndicatorLED(true ); ++state; break;
            case 1  : setActivityIndicatorLED(false); ++state; break;
            case 2  : setActivityIndicatorLED(true ); ++state; break;
            case 3  : setActivityIndicatorLED(false); ++state; break;
            case 4  :                                 ++state; break;
            default : state = 0;                               break;
        }
    }
}


void Application::softReset() noexcept
{
    // Uninitialize UART
    uart().flush();
    uart().end();

    // Restart the system
    restart();

    // This function does not return
    for(;;);
}


void Application::getSoftUARTBitDelayUS(uint16_t& txBit, uint16_t& rxBit, uint16_t& rxCtr, uint32_t baudrate) noexcept
{
    // Get the CPU frequency
    constexpr uint32_t cpuFreq = Application::getCPUFrequency();

    // Each loop in the __delay_loop4() function will take 4 cycles to execute
    baudrate *= 4;

    // Calculate the bit delay
    const uint16_t bitDelay = ( cpuFreq + baudrate / 2 ) / baudrate;

    // Store the bit delay
    txBit = bitDelay - 4; // 4 = approximated overhead factor
    rxBit = bitDelay - 4; // 4 = approximated overhead factor
    rxCtr = bitDelay / 8;
}


void Application::setPinMode(uint8_t pin, PinMode mode)
{
    // Set pin mode
    switch(mode) {
        case PinMode::InputPullDown16 : NodeMCU::pinMode(pin, INPUT_PULLDOWN_16); break;
        case PinMode::OutputOpenDrain : NodeMCU::pinMode(pin, OUTPUT_OPEN_DRAIN); break;
        case PinMode::Output          : NodeMCU::pinMode(pin, OUTPUT           ); break;
        case PinMode::InputPullUp     : NodeMCU::pinMode(pin, INPUT_PULLUP     ); break;
        default                       : NodeMCU::pinMode(pin, INPUT            ); break;
    }

    // Disable PWM output on the pin
    NodeMCU::stopWaveform(pin);
    NodeMCU::stopPWM(pin);
}


void Application::setActivityIndicatorLED(bool on)
{
    // The activity indicator (built-in) LED is active low
    NodeMCU::digitalWrite(ActivityIndicatorLED_Pin, !on);
}


void Application::digitalWrite(uint8_t pin, bool bit)
{ NodeMCU::digitalWrite(pin, bit); }


bool Application::digitalRead(uint8_t pin)
{ return NodeMCU::digitalRead(pin); }


void Application::analogWrite(uint8_t pin, uint8_t val)
{ NodeMCU::startPWM(pin, val, 255); }


uint16_t Application::analogRead(uint8_t pin)
{
    if(pin != NodeMCU::A0) return 0;

    return system_adc_read();
}


bool Application::nvStorageFormat()
{ return LittleFS.format(); }


bool Application::nvStorageWrite(const void* src, uint16_t size, uint16_t offset)
{
    // Check the size and offset
    if( !size ) return true;
    if( size >= sizeof(_emulatedEEPROMBuffer) || (offset + size) > sizeof(_emulatedEEPROMBuffer) ) return false;

    // Check if the EEPROM data file exists
    if( LittleFS.exists(NVStorageFileName) ) {
        // Open the file for reading
        File f = LittleFS.open(NVStorageFileName, "r");
        if(!f) return false;
        // Read the whole EEPROM data
        const bool success = ( f.read( _emulatedEEPROMBuffer, sizeof(_emulatedEEPROMBuffer) ) == sizeof(_emulatedEEPROMBuffer) );
        f.close();
        if(!success) return false;
    }
    // If the file does not exist, fill the whole EEPROM data buffer with 0xFF
    else {
        memset( _emulatedEEPROMBuffer, 0xFF, sizeof(_emulatedEEPROMBuffer) );
    }

    // Modify the EEPROM data
    memcpy(_emulatedEEPROMBuffer + offset, src, size);

    // Open the file for writing
    File f = LittleFS.open(NVStorageFileName, "w");
    if(!f) return false;

    // Write the whole EEPROM data
    const bool success = ( f.write( _emulatedEEPROMBuffer, sizeof(_emulatedEEPROMBuffer) ) == sizeof(_emulatedEEPROMBuffer) );
    f.flush();
    f.close();

    // Return the status
    return success;
}


bool Application::nvStorageRead(void* dst, uint16_t size, uint16_t offset)
{
    // Check the size and offset
    if( !size ) return true;
    if( size >= sizeof(_emulatedEEPROMBuffer) || (offset + size) > sizeof(_emulatedEEPROMBuffer) ) return false;

    // Check if the EEPROM data file exists
    if( LittleFS.exists(NVStorageFileName) ) {
        // Open the file for reading
        File f = LittleFS.open(NVStorageFileName, "r");
        if(!f) return false;
        // Read the whole EEPROM data
        const bool success = ( f.read( _emulatedEEPROMBuffer, sizeof(_emulatedEEPROMBuffer) ) == sizeof(_emulatedEEPROMBuffer) );
        f.close();
        if(!success) return false;
    }
    // If the file does not exist, fill the whole destination buffer with 0xFF
    else {
        memset(dst, 0xFF, size);
        return true;
    }

    // Copy data to the destination buffer
    memcpy(dst, _emulatedEEPROMBuffer + offset, size);

    // Done
    return true;
}


bool Application::nvFileExists(const char* path)
{ return LittleFS.exists(path); }


NodeMCU::FileSystem::File Application::nvOpenFileForReading(const char* path)
{ return LittleFS.open(path, "r"); }


NodeMCU::FileSystem::File Application::nvOpenFileForWriting(const char* path)
{ return LittleFS.open(path, "w"); }


bool Application::nvRemoveFile(const char* path)
{ return LittleFS.remove(path); }


bool Application::nvRenameFile(const char* pathFrom, const char* pathTo)
{ return LittleFS.rename(pathFrom, pathTo); }


bool Application::nvMakeDirectory(const char* path)
{ return LittleFS.mkdir(path); }


bool Application::nvRemoveDirectory(const char* path)
{ return LittleFS.rmdir(path); }


NodeMCU::FileSystem::Dir Application::nvOpenDirectory(const char* path)
{ return LittleFS.openDir(path); }


uint32_t Application::millis() noexcept
{ return NodeMCU::millis(); }


void Application::delayMS(uint16_t mS) noexcept
{
    const uint32_t start = NodeMCU::millis();

    while(NodeMCU::millis() - start < mS) Application::yield();
}


SPI& Application::spi()
{ return spi_instance(); }


TWIMaster& Application::twiMaster()
{ return twiMaster_instance(); }


TWISlave& Application::twiSlave()
{ return twiSlave_instance(); }


Application::OWIMaster& Application::owiMaster()
{
    static OWIMaster _owimaster;
    return _owimaster;
}


Application::I2SOut& Application::i2sOut()
{
    static I2SOut _i2sOut;
    return _i2sOut;
}


bool Application::attachInterruptArg(uint8_t pin, VoidFuncPtrArg userFunc, void* arg, InterruptMode mode)
{ return NodeMCU::attachInterruptArg(pin, userFunc, arg, mode); }


bool Application::detachInterrupt(uint8_t pin)
{ return NodeMCU::detachInterrupt(pin); }


PGMCharPointer Application::getExtendedOTAUpdaterSimplifiedErrorString()
{ return NodeMCU::Updater.getSimpleErrorString(); }


UART* Application::uart0()
{ return uart0_instance(); }


UART* Application::uart1()
{ return uart1_instance(); }


//
// A standard NodeMCU application - fast-digital-read/write
//
void Application::_preparePinFDR(_PinFDR& pinFDR, uint8_t pin) noexcept
{
    // Check the pin number
    if( !NodeMCU::userGPIOPin<true>(pin) ) return;

    // Store the configurations
    pinFDR._sfr = &GPI;
    pinFDR._bm  = _BV(pin);
}


void Application::_preparePinFDW(_PinFDW& pinFDW, uint8_t pin) noexcept
{
    // Check the pin number
    if( !NodeMCU::userGPIOPin<true>(pin) ) return;

    // Store the configurations
    pinFDW._sfrs = &GPOS;
    pinFDW._sfrc = &GPOC;
    pinFDW._bm   = _BV(pin);
}


void Application::_preparePinFDX(_PinFDX& pinFDX, uint8_t pin) noexcept
{
    // Check the pin number
    if( !NodeMCU::userGPIOPin<true>(pin) ) return;

    // Store the configurations
    pinFDX._sfrr      = &GPI;
    pinFDX._sfrs      = &GPOS;
    pinFDX._sfrc      = &GPOC;
    pinFDX._pin       = pin;
    pinFDX._bm        = _BV(pin);
    pinFDX._usePullUp = true;

    // Initialize the pin as input with pull-up
    pinFDX.setReadMode();
}


//
// A standard NodeMCU application - low level OWI functions
//
// # Using only NodeMCU's internal pull-up resistor (without adding external pull-up resistor),
//   would result in an unstable and even unusable OWI bus.
// # However, if the internal pull-up is not enabled, then the system would hung if we try
//   to search the bus for slave device(s) while nothing is actually connected to it.

/*
 * This code is written based on the information from:
 *     [AN126] 1-Wire Communication Through Software
 *     https://pdfserv.maximintegrated.com/en/an/AN126.pdf
 */
bool Application::owiBegin(OWIData& owiData, uint8_t pin)
{
    // Check the pin number
    if( !NodeMCU::userOWIPin(pin) ) return false;

    // Set pin mode to input with pull-up
    owiData.pin     =     pin;
    owiData.bitmask = _BV(pin);

    NodeMCU::pinMode(owiData.pin, INPUT_PULLUP);

    // Check for the presence of at least one slave
    if( owiReset(owiData) ) return true;

    // No slave was found
    owiEnd(owiData);
    return false;
}


void Application::owiEnd(OWIData& owiData)
{
    // Set pin mode to input
    NodeMCU::pinMode(owiData.pin, INPUT);

    // Clear the OWI data
    owiData.pin     = 0;
    owiData.bitmask = 0;
}


bool Application::owiReset(const OWIData& owiData)
{
    AtomicGuard g;

    // Delay
    delayUS(OWIMaster::Delay_G);

    // Drive bus low and delay
    GPES = owiData.bitmask;
    delayUS(OWIMaster::Delay_H);

    // Release bus and delay
    GPEC = owiData.bitmask;
    delayUS(OWIMaster::Delay_I);

    // Sample bus and delay
    const bool bit = GPI & owiData.bitmask;
    delayUS(OWIMaster::Delay_J);

    // Return true if there is at least one slave present
    return !bit;
}


void Application::owiWriteBit0(const OWIData& owiData)
{
    AtomicGuard g;

    // Drive bus low and delay
    GPES = owiData.bitmask;
    delayUS(OWIMaster::Delay_C);

    // Release bus and delay
    GPEC = owiData.bitmask;
    delayUS(OWIMaster::Delay_D);
}


void Application::owiWriteBit1(const OWIData& owiData)
{
    AtomicGuard g;

    // Drive bus low and delay
    GPES = owiData.bitmask;
    delayUS(OWIMaster::Delay_A);

    // Release bus and delay
    GPEC = owiData.bitmask;
    delayUS(OWIMaster::Delay_B);
}


bool Application::owiReadBit(const OWIData& owiData)
{
    AtomicGuard g;

    // Drive bus low and delay
    GPES = owiData.bitmask;
    delayUS(OWIMaster::Delay_A);

    // Release bus and delay
    GPEC = owiData.bitmask;
    delayUS(OWIMaster::Delay_E);

    // Sample bus and delay
    const bool bit = GPI & owiData.bitmask;
    delayUS(OWIMaster::Delay_F);

    // Return the result
    return bit;
}


//
// A standard NodeMCU application - initialization
//
void Application::initialize(const Application::Config& config)
{
    // Configure the activity indicator LED and turn it off
    setPinMode(ActivityIndicatorLED_Pin, PinMode::Output);
    setActivityIndicatorLED(false);

    // Set the PWM frequency (it must be between 100 and 40000)
    NodeMCU::setPWMFrequency(2000);

    // ### ### ### Blink the activity indicator LED ### ### ###
    for(uint8_t i = 0; i < 3; ++i) {
        setActivityIndicatorLED(true ); delayMS(100);
        setActivityIndicatorLED(false); delayMS(100);
    }

    // Turn off all WiFi modes
    WiFi.softAPdisconnect(true);
    WiFi.disconnect(true);
    delayMS(500);

    // Set MAC address (if specified)
    bool emptyMACAddress = true;
    for(uint8_t i = 0; i < 6; ++i) {
        if( !config.macAddress[i] ) continue;
        emptyMACAddress = false;
        break;
    }

    if(!emptyMACAddress) wifi_set_macaddr( SOFTAP_IF, const_cast<uint8_t*>(config.macAddress) );

    // Read the hostname
    static const char PROGMEM defaultHostname[] = "esp8266";
           _PGMCharPointer    hostname_PGM      = config.hostname.p ? config.hostname.p : defaultHostname;

#ifndef ECXX_NODEMCU_DISABLE_OTA_PROGRAMMING
    char _hostname[HostnameBufferSize];
#endif
    ::strncpy_P( _hostname, hostname_PGM, sizeof(_hostname) );
    _hostname[sizeof(_hostname) - 1] = 0;

    // Read the SSID
    char ssid[SSIDBufferSize];
    ::strncpy_P( ssid, config.ssid.p, sizeof(ssid) );
    ssid[sizeof(ssid) - 1] = 0;

    // Read the passphrase
    char pswd[PassphraseBufferSize];
    ::strncpy_P( pswd, config.passphrase.p, sizeof(pswd) );
    pswd[sizeof(pswd) - 1] = 0;

    // Configure it as a WiFi access point
    WiFi.hostname    ( _hostname       );
    WiFi.setSleepMode( WIFI_NONE_SLEEP );
    WiFi.mode        ( WIFI_AP         );
    WiFi.softAPConfig( IPAddress(config.localIP), IPAddress(config.gateway), IPAddress(config.subnetMask) );
    WiFi.softAP      ( ssid, pswd, config.channel, false, config.maxConnection );

    // ### ### ### Blink the activity indicator LED ### ### ###
    for(uint8_t i = 0; i < 3; ++i) {
        setActivityIndicatorLED(true ); delayMS(100);
        setActivityIndicatorLED(false); delayMS(100);
    }

    // Initialize and mount the file system
    const LittleFSConfig cfg(true);

    LittleFS.setConfig(cfg);
    LittleFS.begin();

    // ### ### ### Blink the activity indicator LED ### ### ###
    for(uint8_t i = 0; i < 3; ++i) {
        setActivityIndicatorLED(true ); delayMS(100);
        setActivityIndicatorLED(false); delayMS(100);
    }

#ifndef ECXX_NODEMCU_DISABLE_OTA_PROGRAMMING

    // OTA callback - start
    NodeMCU::OTA.onStart( []() {
        // Ensure the activity indicator LED is configured as output and turn it off
        Application::setPinMode(ActivityIndicatorLED_Pin, PinMode::Output);
        Application::setActivityIndicatorLED(false);
        // Unmount the file system
        LittleFS.end();
        // Call the user callback (if supplied)
        if(_otaStartCB) _otaStartCB();
    } );

    // OTA callback - progress
    NodeMCU::OTA.onProgress( [](unsigned int, unsigned int) {
        // Blink the activity indicator LED to indicate data transfer
        static bool blState = false;
        blState = !blState;
        Application::setActivityIndicatorLED(blState);
    } );

    // OTA callback - end
    NodeMCU::OTA.onEnd( []() {
        // Turn off the activity indicator LED
        Application::setActivityIndicatorLED(false);
        delayMS(1000);
        // Blink the activity indicator LED to indicate the OTA process has completed
        for(int i = 0; i < 10; ++i) {
            setActivityIndicatorLED(true ); delayMS(25);
            setActivityIndicatorLED(false); delayMS(25);
        }
        // Call the user callback (if supplied)
        if(_otaEndCB) _otaEndCB();
    } );

    // OTA callback - error
    NodeMCU::OTA.onError( [](OTAError otaError) {
        // Call the user callback (if supplied)
        if(_otaErrorCB) _otaErrorCB(otaError);
        // Simply restart on error
        softReset();
    } );

    // Read the OTA password
    if(config.otaPassword.p) {
        ::strncpy_P( pswd, config.otaPassword.p, sizeof(pswd) );
        pswd[sizeof(pswd) - 1] = 0;
    }

    // Configure and enable OTA programming
    NodeMCU::OTA.setHostname(_hostname);
    NodeMCU::OTA.setPort    (config.otaPort);
    NodeMCU::OTA.setPassword(config.otaPassword.p ? pswd : nullptr);

    NodeMCU::OTA.begin();

#else

    // Initialize mDNS
    MDNS.begin(_hostname);

#endif

    // ### ### ### Blink the activity indicator LED ### ### ###
    for(uint8_t i = 0; i < 3; ++i) {
        setActivityIndicatorLED(true ); delayMS(50);
        setActivityIndicatorLED(false); delayMS(50);
    }
}


//
// A standard NodeMCU application - event processing
//
uint32_t Application::processEvent(bool blinkActivityIndicatorLED_)
{
#ifndef ECXX_NODEMCU_DISABLE_OTA_PROGRAMMING
    // Handle OTA programming
    NodeMCU::OTA.handle();
#else
    // Handle mDNS
    MDNS.update();
#endif

    // Yield
    Application::yield();

    // Call the scheduled I2S callback once
    if(_i2sCallback) {
        // Call the callback and then remove it
        (*_i2sCallback)();
        _i2sCallback = nullptr;
        // Yield
        Application::yield();
    }

    // Call user callback(s) with interrupt disabled
    callScheduledAtomicUserCallback();

    // Blink the activity indicator LED to indicate activity and return the current millis()
    if(blinkActivityIndicatorLED_) {
        return blinkActivityIndicatorLEDAndGetMillis();
    }
    // Return the current millis() only
    else {
        return Application::millis();
    }
}


//
// A standard NodeMCU application - switch WiFi to station mode
//
bool Application::switchToWiFiStationMode(PGMCharPointer ssid_, PGMCharPointer passphrase_)
{
    // Simply return if it is already in WiFi station mode
    if(_inWiFiStationMode) return true;

    // Read the SSID
    char ssid[SSIDBufferSize];
    ::strncpy_P( ssid, ssid_.p, sizeof(ssid) );
    ssid[sizeof(ssid) - 1] = 0;

    // Read the passphrase
    char pswd[PassphraseBufferSize];
    ::strncpy_P( pswd, passphrase_.p, sizeof(pswd) );
    pswd[sizeof(pswd) - 1] = 0;

    // Get the access point MAC address
    uint8_t macAddress[6];
    WiFi.softAPmacAddress(macAddress);

    // Turn off all WiFi modes
    WiFi.softAPdisconnect(true);
    WiFi.disconnect(true);
    delayMS(500);

    // Clear all state up callbacks
    LwipIntf::clearAllStateUpCB();

    // Set station MAC address
    wifi_set_macaddr(STATION_IF, macAddress);

    // Configure it as a WiFi station
#ifndef ECXX_NODEMCU_DISABLE_OTA_PROGRAMMING
    WiFi.hostname( NodeMCU::OTA.getHostname().c_str() );
#else
    WiFi.hostname( _hostname );
#endif
    WiFi.setSleepMode( WIFI_NONE_SLEEP );
    WiFi.mode        ( WIFI_STA        );
    WiFi.begin       ( ssid, pswd      );
    delayMS(500);

    // Wait until connected or timeout
    uint8_t cnt = 25;
    while( WiFi.status() != WL_CONNECTED ) {
        if(!--cnt) return false;
        delayMS(500);
    }

#ifndef ECXX_NODEMCU_DISABLE_OTA_PROGRAMMING
    // Reinitialize mDNS via OTA
    NodeMCU::OTA.reinitializeMDNS();
#else
    // Uninitialize mDNS
    MDNS.close();
    Application::delayMS(500);
    // Reinitialize mDNS
    MDNS.begin( _hostname );
#endif

    // Set flag
    _inWiFiStationMode = true;

    // Done
    return true;
}


//
// A standard NodeMCU application - date and time functions
//
bool Application::_configNTP_waitUntilActualTimeIsSet()
{
    // Disable obtaining SNTP servers from DHCP
    sntp_servermode_dhcp(0);

    // Yield immediately for safety
    Application::yield(true);

    // Wait until the actual time is set with a timeout ~25 S (50 * ~500 mS)
    uint8_t timoutCnt = 50;

    for(;;) {
        // Delay for a while
        delayMS(500);
        // Until the actual time is set, it remains 0
        const time_t tmt = time(nullptr);
        if(tmt) {
            localtime(&tmt);
            return true;
        }
        // Check for timeout
        if(!timoutCnt) break;
        --timoutCnt;
    }

    // Failed
    sntp_stop();
    return false;
}


bool Application::configNTP(const char* tz, const char* server1, const char* server2, const char* server3)
{
    // Return if it is not in WiFi station mode
    if(!_inWiFiStationMode) return false;

    // Reset the current date-time
    const timeval tv = { 0, 0 };

    settimeofday(&tv, nullptr);

    // Configure NTP
    NodeMCU::configTime(tz, server1, server2, server3);

    // Wait until the actual time is set
    return _configNTP_waitUntilActualTimeIsSet();
}


bool Application::configNTP(int32_t timezoneSec, int32_t daylightOffsetSec, const char* server1, const char* server2, const char* server3)
{
    // Return if it is not in WiFi station mode
    if(!_inWiFiStationMode) return false;

    // Reset the current date-time
    const timeval tv = { 0, 0 };

    settimeofday(&tv, nullptr);

    // Configure NTP
    NodeMCU::configTime(timezoneSec, daylightOffsetSec, server1, server2, server3);

    // Wait until the actual time is set
    return _configNTP_waitUntilActualTimeIsSet();
}


void Application::setCurrentDateTime(int64_t microSecondsSinceUnixEpoch)
{
    const int64_t  sec = microSecondsSinceUnixEpoch       / 1000000ULL;
    const int64_t usec = microSecondsSinceUnixEpoch - sec * 1000000ULL;

    const timeval tv = {
        static_cast< decltype(timeval::tv_sec ) >( sec),
        static_cast< decltype(timeval::tv_usec) >(usec)
    };

    settimeofday(&tv, nullptr);
}


const Application::DateTime Application::currentDateTime(DateTimeMode dtm)
{
    const time_t tmt = time(nullptr);
    const tm*    now = (dtm == DateTimeMode::Local) ? localtime(&tmt) : gmtime(&tmt);

    DateTime dt;

         if(now->tm_isdst > 0) dt.isDST =  1;
    else if(now->tm_isdst < 0) dt.isDST = -1;
    else                       dt.isDST =  0;

    dt.yearDay = now->tm_yday;
    dt.weekDay = now->tm_wday;
    dt.year    = now->tm_year + 1900;
    dt.month   = now->tm_mon  +    1;
    dt.day     = now->tm_mday;
    dt.hour    = now->tm_hour;
    dt.minute  = now->tm_min;
    dt.second  = now->tm_sec;

    return dt;
}


//
// A standard NodeMCU application - get network interface description list
//
size_t Application::getNetIfDescList(NetIfDesc* resBuff, size_t buffLen)
{
    // For converting MAC address to hexadecimal string
    static constexpr char hexDigit[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

    // For counting the number of addresses
    size_t count = 0;

    // Walk through the addresses
    for(auto a : addrList) {

        // Increment the counter and skip if the buffer is full (the counter reaches beyond the buffer size)
        ++count;
        if(count > buffLen) continue;

        // Get and clear the current buffer
        NetIfDesc& rb = resBuff[count - 1];
        memset( &rb, 0, sizeof(rb) );

        // Copy the data to the result buffer
        rb.ifNumber = a.ifnumber();
        rb.isLinkUp = a.ifUp();
        rb.isLocal  = a.isLocal();
        rb.isLegacy = a.isLegacy();
        rb.isIPv6   = a.isV6();

        strncpy( rb.ifName,     a.ifname().c_str(),   sizeof(rb.ifName    ) - 1 );
        strncpy( rb.ifHostname, a.ifhostname(),       sizeof(rb.ifHostname) - 1 );

        strncpy( rb.address,    a.toString().c_str(), sizeof(rb.address   ) - 1 );

        if(rb.isLegacy) {
            strncpy( rb.netmask, a.netmask().toString().c_str(), sizeof(rb.netmask) - 1 );
            strncpy( rb.gateway, a.gw     ().toString().c_str(), sizeof(rb.gateway) - 1 );
        }

        const uint8_t* ifMAC = a.ifmac();
        rb.ifMAC[ 0] = hexDigit[ ifMAC[0] >> 4    ];
        rb.ifMAC[ 1] = hexDigit[ ifMAC[0] &  0x0F ];
        rb.ifMAC[ 2] = ':';
        rb.ifMAC[ 3] = hexDigit[ ifMAC[1] >> 4    ];
        rb.ifMAC[ 4] = hexDigit[ ifMAC[1] &  0x0F ];
        rb.ifMAC[ 5] = ':';
        rb.ifMAC[ 6] = hexDigit[ ifMAC[2] >> 4    ];
        rb.ifMAC[ 7] = hexDigit[ ifMAC[2] &  0x0F ];
        rb.ifMAC[ 8] = ':';
        rb.ifMAC[ 9] = hexDigit[ ifMAC[3] >> 4    ];
        rb.ifMAC[10] = hexDigit[ ifMAC[3] &  0x0F ];
        rb.ifMAC[11] = ':';
        rb.ifMAC[12] = hexDigit[ ifMAC[4] >> 4    ];
        rb.ifMAC[13] = hexDigit[ ifMAC[4] &  0x0F ];
        rb.ifMAC[14] = ':';
        rb.ifMAC[15] = hexDigit[ ifMAC[5] >> 4    ];
        rb.ifMAC[16] = hexDigit[ ifMAC[5] &  0x0F ];

    } // for addrList

    return count;
}


//
// A standard NodeMCU application - periodic interrupt
//
void Application::attachPeriodicInterrupt(VoidFuncPtr userFunc, uint16_t frequency)
{
    // Abort if the ISR is not in IRAM
    if( !NodeMCU::addressInIRAM(userFunc) ) {
        ::printf_P( PGM_STR("Periodic ISR is not in IRAM!\r\n").p );
        abort();
    }

    NodeMCU::timer1InitISR();

    NodeMCU::timer1AttachInterrupt(userFunc);
    NodeMCU::timer1Enable(TIM_DIV16, TIM_EDGE, TIM_LOOP);

    NodeMCU::timer1Write(5000000UL / frequency);
}


void Application::detachPeriodicInterrupt()
{
    NodeMCU::timer1Disable();
    NodeMCU::timer1DetachInterrupt();
}


} // namespace eCxx


//
// Include the UART wrapper implementation and the ISR routines implementation
//
#define INCLUDE_ECXX_NODEMCU_UART_IMPL
#include "UART.cpp"
