/*---------------------------------------------------------------------------*/
/* time.h                                                                    */
/* copyright (c) innovaphone 2015                                            */
/*                                                                           */
/*---------------------------------------------------------------------------*/

#include <platform.h>
#include <sys/time.h>
#include "sdk/common/interface/time.h"

//-----------------------------------------------------------------------------
// Internal used function and types, copied from the firmware time function
// (common/box/box.h/.cpp).
//-----------------------------------------------------------------------------

#define SECSPERMIN      60
#define MINSPERHOUR     60
#define HOURSPERDAY     24
#define DAYSPERWEEK     7
#define DAYSPERNYEAR    365
#define DAYSPERLYEAR    366
#define SECSPERHOUR     (SECSPERMIN * MINSPERHOUR)
#define SECSPERDAY      ((long) SECSPERHOUR * HOURSPERDAY)
#define MONSPERYEAR     12

#define JULIAN_DAY      0   /* Jn - Julian day */
#define DAY_OF_YEAR     1   /* n - day of year */
#define MONTH_NTH_DAY_OF_WEEK   2   /* Mm.n.d - month, week, day of week */

//#define YEAR_BASE       1900
//#define EPOCH_YEAR      1970
//#define EPOCH_WDAY      4

#define is_digit(c)     ((c) >= '0' && (c) <= '9')
#define isleap(y)       (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0))

static const char mon_lengths[2][MONSPERYEAR] = {
    { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
    { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
};

struct rule {
    int     r_type;         /* type of rule--see below */
    int     r_day;          /* day number of rule */
    int     r_week;         /* week number of rule */
    int     r_mon;          /* month number of rule */
    long    r_time;         /* transition time of rule */
};


/*
** Given a pointer into a time zone string, scan until a character that is not
** a valid character in a zone name is found.  Return a pointer to that
** character.
*/

static const char *getzname(const char * strp)
{
    char c;
    while ((c = *strp) != '\0' && !is_digit(c) && c != ',' && c != '-' && c != '+')
        ++strp;
    return strp;
}

/*
** Given a pointer into a time zone string, extract a number from that string.
** Check that the number is within a specified range; if it is not, return 0.
** Otherwise, return a pointer to the first character not part of the number.
*/

static const char *getnum(const char * strp, int * const nump, const int min, const int max)
{
    char c;
    int  num;

    if (strp == 0 || !is_digit(c = *strp))
        return 0;
    num = 0;
    do {
        num = num * 10 + (c - '0');
        if (num > max)
            return 0;   /* illegal value */
        c = *++strp;
    } while (is_digit(c));
    if (num < min)
        return 0;       /* illegal value */
    *nump = num;
    return strp;
}

/*
** Given a pointer into a time zone string, extract a number of seconds,
** in hh[:mm[:ss]] form, from the string.
** If any error occurs, return 0.
** Otherwise, return a pointer to the first character not part of the number
** of seconds.
*/

static const char * getsecs(const char * strp, long * const secsp)
{
    int num;

    /*
    ** `HOURSPERDAY * DAYSPERWEEK - 1' allows quasi-Posix rules like
    ** "M10.4.6/26", which does not conform to Posix,
    ** but which specifies the equivalent of
    ** ``02:00 on the first Sunday on or after 23 Oct''.
    */
    strp = getnum(strp, &num, 0, HOURSPERDAY * DAYSPERWEEK - 1);
    if (strp == 0)
        return 0;
    *secsp = num * (long)SECSPERHOUR;
    if (*strp == ':') {
        ++strp;
        strp = getnum(strp, &num, 0, MINSPERHOUR - 1);
        if (strp == 0)
            return 0;
        *secsp += num * SECSPERMIN;
        if (*strp == ':') {
            ++strp;
            /* `SECSPERMIN' allows for leap seconds.  */
            strp = getnum(strp, &num, 0, SECSPERMIN);
            if (strp == 0)
                return 0;
            *secsp += num;
        }
    }
    return strp;
}

/*
** Given a pointer into a time zone string, extract an offset, in
** [+-]hh[:mm[:ss]] form, from the string.
** If any error occurs, return 0.
** Otherwise, return a pointer to the first character not part of the time.
*/

static const char * getoffset(const char * strp, long * const offsetp)
{
    int neg = 0;

    if (*strp == '-') {
        neg = 1;
        ++strp;
    }
    else if (*strp == '+')
        ++strp;
    strp = getsecs(strp, offsetp);
    if (strp == 0)
        return 0;       /* illegal time */
    if (neg)
        *offsetp = -*offsetp;
    return strp;
}

/*
** Given a pointer into a time zone string, extract a rule in the form
** date[/time].  See POSIX section 8 for the format of "date" and "time".
** If a valid rule is not found, return 0.
** Otherwise, return a pointer to the first character not part of the rule.
*/

static const char * getrule(const char * strp, struct rule * const rulep)
{
    if (*strp == 'J') {
        /*
        ** Julian day.
        */
        rulep->r_type = JULIAN_DAY;
        ++strp;
        strp = getnum(strp, &rulep->r_day, 1, DAYSPERNYEAR);
    }
    else if (*strp == 'M') {
        /*
        ** Month, week, day.
        */
        rulep->r_type = MONTH_NTH_DAY_OF_WEEK;
        ++strp;
        strp = getnum(strp, &rulep->r_mon, 1, MONSPERYEAR);
        if (strp == 0)
            return 0;
        if (*strp++ != '.')
            return 0;
        strp = getnum(strp, &rulep->r_week, 1, 5);
        if (strp == 0)
            return 0;
        if (*strp++ != '.')
            return 0;
        strp = getnum(strp, &rulep->r_day, 0, DAYSPERWEEK - 1);
    }
    else if (is_digit(*strp)) {
        /*
        ** Day of year.
        */
        rulep->r_type = DAY_OF_YEAR;
        strp = getnum(strp, &rulep->r_day, 0, DAYSPERLYEAR - 1);
    }
    else  return 0;       /* invalid format */
    if (strp == 0)
        return 0;
    if (*strp == '/') {
        /*
        ** Time specified.
        */
        ++strp;
        strp = getsecs(strp, &rulep->r_time);
    }
    else  rulep->r_time = 2 * SECSPERHOUR;    /* default = 2:00:00 */
    return strp;
}

/*
** Given the Epoch-relative time of January 1, 00:00:00 UTC, in a year, the
** year, a rule, and the offset from UTC at the time that rule takes effect,
** calculate the Epoch-relative time that rule takes effect.
*/

static time_t transtime(const time_t janfirst, const int year, const struct rule * const rulep, const long offset)
{
    int     leapyear;
    time_t  value;
    int     i;
    int     d, m1, yy0, yy1, yy2, dow;

    leapyear = isleap(year);
    value = 0;

    switch (rulep->r_type) {

    case JULIAN_DAY:
        /*
        ** Jn - Julian day, 1 == January 1, 60 == March 1 even in leap
        ** years.
        ** In non-leap years, or if the day number is 59 or less, just
        ** add SECSPERDAY times the day number-1 to the time of
        ** January 1, midnight, to get the day.
        */
        value = janfirst + (rulep->r_day - 1) * SECSPERDAY;
        if (leapyear && rulep->r_day >= 60)
            value += SECSPERDAY;
        break;

    case DAY_OF_YEAR:
        /*
        ** n - day of year.
        ** Just add SECSPERDAY times the day number to the time of
        ** January 1, midnight, to get the day.
        */
        value = janfirst + rulep->r_day * SECSPERDAY;
        break;

    case MONTH_NTH_DAY_OF_WEEK:
        /*
        ** Mm.n.d - nth "dth day" of month m.
        */
        value = janfirst;
        for (i = 0; i < rulep->r_mon - 1; ++i)
            value += mon_lengths[leapyear][i] * SECSPERDAY;

        /*
        ** Use Zeller's Congruence to get day-of-week of first day of
        ** month.
        */
        m1 = (rulep->r_mon + 9) % 12 + 1;
        yy0 = (rulep->r_mon <= 2) ? (year - 1) : year;
        yy1 = yy0 / 100;
        yy2 = yy0 % 100;
        dow = ((26 * m1 - 2) / 10 +
            1 + yy2 + yy2 / 4 + yy1 / 4 - 2 * yy1) % 7;
        if (dow < 0)
            dow += DAYSPERWEEK;

        /*
        ** "dow" is the day-of-week of the first day of the month.  Get
        ** the day-of-month (zero-origin) of the first "dow" day of the
        ** month.
        */
        d = rulep->r_day - dow;
        if (d < 0)
            d += DAYSPERWEEK;
        for (i = 1; i < rulep->r_week; ++i) {
            if (d + DAYSPERWEEK >=
                mon_lengths[leapyear][rulep->r_mon - 1])
                break;
            d += DAYSPERWEEK;
        }

        /*
        ** "d" is the day-of-month (zero-origin) of the day we want.
        */
        value += d * SECSPERDAY;
        break;
    }

    /*
    ** "value" is the Epoch-relative time of 00:00:00 UTC on the day in
    ** question.  To get the Epoch-relative time of the specified local
    ** time on that day, add the transition time and the current offset
    ** from UTC.
    */
    return value + rulep->r_time + offset;
}

long get_local_offset(dword time, timezone_info_t & tm)
{
    //  get local offset for an arbitrary time in the past or future.
    //  'time' is in seconds since begin of unix epoch (01.01.1970 00:00 - 19.01.2038 00:00).
    if (tm.numTransits <= 2) {
        //	no DST configured, always the same offset
        return tm.tzStdOff;
    }
    if (time < tm.transits[0].utc) {
        //  time before the first transit in 1970, correct offset is next transit
        return tm.transits[1].off;
    }
    //	get a starting index in the transit table.
    //	dividing 'time' by the number of seconds per normal year may result in an index
    //	one less than the exact one but it's not worth to spend more effort just to save
    //	one additional cycle over the table.
    long i = (long)((time / (365 * 24 * 60 * 60)));
    for (; i + 1 < tm.numTransits; i++) {
        if (time < tm.transits[i + 1].utc) {
            //	time between two transits, first offset applies
            return (tm.transits[i + 0].off);
        }
    }
    //	time behind last transit in 2037, correct offset is there
    return tm.transits[i].off;
}


//-----------------------------------------------------------------------------
// ITime
//-----------------------------------------------------------------------------

long64 ITime::TimeStampMilliseconds()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    unsigned long long ts_ms = 1000 * (unsigned long long)tv.tv_sec + (unsigned long long)tv.tv_usec / 1000;
    return ts_ms;
}

long64 ITime::GetMonotonicTime()
{
#ifdef __linux__
    struct timespec spec;
    clock_gettime(CLOCK_MONOTONIC, &spec);
    return spec.tv_sec;
#else
	struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec;
#endif
}

long64 ITime::GetMonotonicUsec()
{
#ifdef __linux__
    struct timespec spec;
    clock_gettime(CLOCK_MONOTONIC, &spec);
    return (ulong64)spec.tv_sec * 1000000 + spec.tv_nsec / 1000;
#else
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return (ulong64)tv.tv_sec * 1000000 + tv.tv_usec;
#endif
}

size_t ITime::GetTimeStamp(char * buf, unsigned sz)
{
    struct timeval now;
    int rc;
    rc = gettimeofday(&now, NULL);
    if (rc == 0) {
        struct tm *tm;
        size_t n;
        tm = localtime(&now.tv_sec);
        n = strftime(buf, sz, "%Y-%m-%d %H:%M:%S", tm);
        if (n != 0) {
            return (n + snprintf(&buf[n], sz - n, ".%06u", (unsigned)now.tv_usec));
        }
    }
    buf[0] = '\0';
    return 0;
}

size_t ITime::FormatTimeStampISO(char * buf, unsigned length, long64 timeMs)
{
    time_t sec = (time_t)(timeMs / 1000);
    buf[0] = '\0';
    struct tm *tm;
    tm = gmtime(&sec);
    return strftime(buf, length, "%Y-%m-%dT%H:%M:%SZ", tm);
}


size_t ITime::FormatTimeStampRFC1123(char * buf, unsigned length, long64 timeMs)
{
    time_t sec = (time_t)(timeMs / 1000);
    buf[0] = '\0';
    if (length < 30) {
        return 0;
    }
    struct tm *tm;
    tm = gmtime(&sec);
    const char * wkday [] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
    const char * month [] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
    return _sprintf(buf, "%s, %2.2u %s %u %2.2u:%2.2u:%2.2u GMT",
        wkday[tm->tm_wday], tm->tm_mday, month[tm->tm_mon], tm->tm_year + 1900,
        tm->tm_hour, tm->tm_min, tm->tm_sec);
}


bool ITime::ParseTimeZoneString(const char * tz, timezone_info_t & ti, int * errPos)
{
    const char * p;
    const char * stdname;
    const char * dstname;
    long         stdoffset;
    long         dstoffset;
    struct rule  start;
    struct rule  end;
    time_t       janfirst;
    time_t       starttime;
    time_t       endtime;

    ti = timezone_info_t(); // initialize ti with an empty structure.

    if (errPos) *errPos = 0;
    if (!(p = tz)) return (false);

    stdoffset = dstoffset = 0;

    stdname = p;
    p = getzname(p);
    if ((p - stdname) < 3 || !*p) goto reject;
    p = getoffset(p, &stdoffset);
    if (!p) goto reject;

    if (*p)
    {
        dstname = p;
        p = getzname(p);
        if ((p - dstname) < 3) goto reject;
        if (*p && *p != ',' && *p != ';') {
            p = getoffset(p, &dstoffset);
            if (p == 0) goto reject;
        }
        else {
            dstoffset = stdoffset - SECSPERHOUR;
        }

        if (*p != ',' && *p != ';') goto reject;
        p++;

        if ((p = getrule(p, &start)) == 0) goto reject;
        if (*p++ != ',') goto reject;
        if ((p = getrule(p, &end)) == 0) goto reject;
        if (*p) goto reject;

        /* Two transitions per year, from EPOCH_YEAR to 2037 */
        janfirst = 0;
        for (int year = EPOCH_YEAR; year <= 2037; ++year) {
            starttime = transtime(janfirst, year, &start, stdoffset);
            endtime = transtime(janfirst, year, &end, dstoffset);
            if (starttime > endtime) {
                ti.transits[ti.numTransits].utc = endtime;
                ti.transits[ti.numTransits].off = -stdoffset; /* DST ends */
            }
            else {
                ti.transits[ti.numTransits].utc = starttime;
                ti.transits[ti.numTransits].off = -dstoffset; /* DST begins */
            }
            ++ti.numTransits;
            if (starttime > endtime) {
                ti.transits[ti.numTransits].utc = starttime;
                ti.transits[ti.numTransits].off = -dstoffset; /* DST begins */
            }
            else {
                ti.transits[ti.numTransits].utc = endtime;
                ti.transits[ti.numTransits].off = -stdoffset; /* DST ends */
            }
            ++ti.numTransits;
            janfirst += (isleap(year) ? DAYSPERLYEAR : DAYSPERNYEAR) * SECSPERDAY;
        }

        ti.tzStdOff = -stdoffset;
        ti.tzDstOff = -dstoffset;
    }
    else
    {
        ti.numTransits = 2;
        ti.transits[0].utc = 0;
        ti.transits[0].off = -stdoffset;
        ti.transits[1].utc = 0x7fffffff;
        ti.transits[1].off = -stdoffset;

        ti.tzStdOff = -stdoffset;
        ti.tzDstOff = -stdoffset;
    }

    //debug->printf("NTP - TZ %s (%i/%i)", name, tz_std_off, tz_dst_off);

    return (true);

reject:
    //debug->printf("NTP - TZ %s - error at %i", name, p - name);
    if (errPos) *errPos = p - tz;
    return (false);
}


long64 ITime::UTCTimeToLocalTime(long64 timeMsUtc, timezone_info_t & ti)
{
    long64 diff = get_local_offset(timeMsUtc / 1000, ti) * 1000;
    return timeMsUtc + diff;
}


long64 ITime::TimeStampMilliseconds(timezone_info_t & ti)
{
    return UTCTimeToLocalTime(TimeStampMilliseconds(), ti);
}


size_t ITime::FormatTimeStamp(char * buf, unsigned length, const char * formatStr, long64 timeMs)
{
    time_t tt = time_t(timeMs / 1000);
    tm * ts = gmtime(&tt);
    return strftime(buf, length, formatStr, ts);
}


size_t ITime::FormatTimeStamp(char * buf, unsigned length, const char * formatStr, long64 timeMs, timezone_info_t & ti)
{
    return FormatTimeStamp(buf, length, formatStr, UTCTimeToLocalTime(timeMs, ti));
}


void ITime::GetTimeStruct(long64 timeMs, time_tm_t * t)
{
    time_t tt = time_t(timeMs / 1000);
    struct tm tM;
    gmtime_r(&tt, &tM);
    t->tmHour = tM.tm_hour;
    t->tmIsDst = tM.tm_isdst;
    t->tmMDay = tM.tm_mday;
    t->tmMin = tM.tm_min;
    t->tmMon = tM.tm_mon;
    t->tmSec = tM.tm_sec;
    t->tmWDay = tM.tm_wday;
    t->tmYDay = tM.tm_yday;
    t->tmYear = tM.tm_year;
}


void ITime::GetTimeStruct(long64 timeMs, time_tm_t * t, timezone_info_t & ti)
{
    time_t tt = time_t(UTCTimeToLocalTime(timeMs, ti) / 1000);
    struct tm tM;
    gmtime_r(&tt, &tM);
    t->tmHour = tM.tm_hour;
    t->tmIsDst = tM.tm_isdst;
    t->tmMDay = tM.tm_mday;
    t->tmMin = tM.tm_min;
    t->tmMon = tM.tm_mon;
    t->tmSec = tM.tm_sec;
    t->tmWDay = tM.tm_wday;
    t->tmYDay = tM.tm_yday;
    t->tmYear = tM.tm_year;
}


long64 ITime::TimeStructToMilliseconds(time_tm_t * ts)
{
    struct tm t;
    t.tm_hour = ts->tmHour;
    t.tm_isdst = ts->tmIsDst;
    t.tm_mday = ts->tmMDay;
    t.tm_min = ts->tmMin;
    t.tm_mon = ts->tmMon;
    t.tm_sec = ts->tmSec;
    t.tm_wday = ts->tmWDay;
    t.tm_yday = ts->tmYDay;
    t.tm_year = ts->tmYear;
    time_t result = mktime(&t);
    return (result == -1 ? 0 : long64(result) * 1000);
}


bool ITime::NormalizeTimeStruct(time_tm_t * ts)
{
    struct tm t;
    t.tm_hour = ts->tmHour;
    t.tm_isdst = ts->tmIsDst;
    t.tm_mday = ts->tmMDay;
    t.tm_min = ts->tmMin;
    t.tm_mon = ts->tmMon;
    t.tm_sec = ts->tmSec;
    t.tm_wday = ts->tmWDay;
    t.tm_yday = ts->tmYDay;
    t.tm_year = ts->tmYear;
    time_t result = mktime(&t);
    ts->tmHour = t.tm_hour;
    ts->tmIsDst = t.tm_isdst;
    ts->tmMDay = t.tm_mday;
    ts->tmMin = t.tm_min;
    ts->tmMon = t.tm_mon;
    ts->tmSec = t.tm_sec;
    ts->tmWDay = t.tm_wday;
    ts->tmYDay = t.tm_yday;
    ts->tmYear = t.tm_year;

    return result != -1;
}
