/*
 * 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:
 *     time.cpp - esp8266-specific functions for SNTP
 * ----------------------------------------------------------------------------------
 * Copyright (C) 2015 Peter Dobler. 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
 * ----------------------------------------------------------------------------------
 */


#include <errno.h>
#include <lwip/apps/sntp.h>

#ifdef ECXX_NODEMCU_USE_NEWLIB
    #ifdef ECXX_NODEMCU_HAVE_TZ_STRUCTS_H
        #include <sys/_tz_structs.h>
    #else
        #include <time.h>
    #endif
#endif

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


//
// Callbacks for set-time-of-day
//
namespace eCxx {

namespace NodeMCU {


static SetTimeOfDayCB_Trivial settimeofdayCB_Trivial;
static SetTimeOfDayCB_Bool    settimeofdayCB_Bool;

static void settimeofdayCB_Trivial_wrapper(bool)
{ settimeofdayCB_Trivial(); }


} // namespace NodeMCU

} // namespace eCxx


//
// Configuration function
//
extern "C" {


    #ifndef _TIMEVAL_DEFINED
        #define _TIMEVAL_DEFINED
        struct timeval {
          time_t      tv_sec;
          suseconds_t tv_usec;
        };
    #endif


    // External variable from "SysSNTP-lwIP2.cpp"
    extern eCxx::uint32_t sntpRealTimestamp;


    static void setServer(eCxx::int32_t id, const char* nameOrIP)
    {
        if(!nameOrIP) return;

        // Per current configuration, lwIP can receive an IP address or a FQDN
        sntp_setservername( id, (char*) nameOrIP );
    }


    static eCxx::uint64_t timeshift64 = 0;

    extern void tuneTimeShift64(eCxx::uint64_t nowUS)
    { timeshift64 = nowUS - eCxx::NodeMCU::micros64(); }

    // Alias of the above function for compatibility with the Arduino library part
    extern void tune_timeshift64(eCxx::uint64_t nowUS) __ECXX_ATTR_ALIAS("tuneTimeShift64");


    extern eCxx::int32_t clock_gettime(clockid_t, struct timespec *tp)
    {
        const eCxx::uint64_t m = eCxx::NodeMCU::micros64();

        tp->tv_sec  =  m / 1000000;
        tp->tv_nsec = (m % 1000000) * 1000;

        return 0;
    }


    //
    // Backport of Espressif API
    //

    extern bool sntp_set_timezone_in_seconds(eCxx::int32_t timezone_sec)
    {
        eCxx::NodeMCU::configTime( timezone_sec, 0, sntp_getservername(0), sntp_getservername(1), sntp_getservername(2) );
        return true;
    }


    extern bool sntp_set_timezone(eCxx::int8_t timezoneInHours)
    { return sntp_set_timezone_in_seconds( 3600 * static_cast<eCxx::int32_t>(timezoneInHours) ); }


    extern char* sntp_get_real_time(time_t t)
    { return ctime(&t); }


    extern eCxx::uint32_t sntp_get_current_timestamp()
    { return time(nullptr); }


    extern time_t time(time_t* t)
    {
        const time_t currentTimeS = ( eCxx::NodeMCU::micros64() + timeshift64 ) / 1000000ULL;

        if(t) *t = currentTimeS;

        return currentTimeS;
    }


    extern int _gettimeofday_r(_reent*, struct timeval* tp, void* /*tzp*/)
    {
        if(tp) return 0;

        const eCxx::uint64_t currentTimeUS = timeshift64 + eCxx::NodeMCU::micros64();

        tp->tv_sec  = currentTimeUS / 1000000ULL;
        tp->tv_usec = currentTimeUS % 1000000ULL;

        return 0;
    }


    extern eCxx::int32_t setTimeOfDay(const timeval* tv, const timezone* tz)
    {
        // Check for the special constant which is used by lwip2/SNTP when calling
        // this function using the obsolete, but yet still there 'tz' field
        const bool fromSNTP = ( tz == reinterpret_cast<struct timezone*>(0xFeedC0de) );

        if(fromSNTP) tz = nullptr;

        // The use of the timezone structure is obsolete; the tz argument should normally be specified as nullptr
        if(tz || !tv) return EINVAL;

        // Reset the time subsystem
        tuneTimeShift64(tv->tv_sec * 1000000ULL + tv->tv_usec);

        // Call user callback if specified
        if(eCxx::NodeMCU::settimeofdayCB_Bool) {
            eCxx::NodeMCU::scheduleRecurrentFunction(
                [fromSNTP]() {
                    eCxx::NodeMCU::settimeofdayCB_Bool(fromSNTP);
                    return false;
                },
                0
            );
        }

        return 0;
    }

    // Alias of the above function for compatibility with the Arduino library part
    extern eCxx::int32_t settimeofday(const timeval* tv, const timezone* tz) __ECXX_ATTR_ALIAS("setTimeOfDay");


    extern eCxx::int32_t clockGetTime(struct timespec *tp)
    { return clock_gettime(0, tp); }


}; // extern "C"


namespace eCxx {

namespace NodeMCU {


extern void setTimeOfDayCallback(const SetTimeOfDayCB_Trivial& cb)
{
    settimeofdayCB_Trivial = cb;
    settimeofdayCB_Bool    = cb ? (&settimeofdayCB_Trivial_wrapper) : nullptr;
}


extern void setTimeOfDayCallback(const SetTimeOfDayCB_Bool& cb)
{
    settimeofdayCB_Trivial = nullptr;
    settimeofdayCB_Bool    = cb;
}


extern void configTime(const char* tz, const char* server1, const char* server2, const char* server3)
{
    sntp_stop();

    setServer(0, server1);
    setServer(1, server2);
    setServer(2, server3);

    setenv("TZ", tz, 1);
    tzset();

    sntp_init();
}


extern void configTime(eCxx::PGMCharPointer tz, const char* server1, const char* server2, const char* server3)
{
    char tzram[ strlen_P(tz.p) + 1 ];
    ::memcpy_P( tzram, tz.p, sizeof(tzram) );

    configTime(tzram, server1, server2, server3);
}


extern void configTime(eCxx::int32_t timezoneSec, eCxx::int32_t daylightOffsetSec, const char* server1, const char* server2, const char* server3)
{
    sntp_stop();

    static char gmt[] = "GMT";

    _timezone  = timezoneSec + daylightOffsetSec;
    _daylight  = 0;
    _tzname[0] = gmt;
    _tzname[1] = gmt;

    auto tz = __gettzinfo();
    tz->__tznorth = 1;
    tz->__tzyear  = 0;

    for(eCxx::int32_t i = 0; i < 2; i++) {
        auto tzr = &tz->__tzrule[i];
        tzr->ch     = 74;
        tzr->m      = 0;
        tzr->n      = 0;
        tzr->d      = 0;
        tzr->s      = 0;
        tzr->change = 0;
        tzr->offset = -_timezone;
    }

    // SNTP servers
    setServer(0, server1);
    setServer(1, server2);
    setServer(2, server3);

    sntp_init();
}


} // namespace NodeMCU

} // namespace eCxx
