
/*----------------------------------------------------------------------+
 |                                                                      |
 |      wtime.c -- implementation of wtime functions                    |
 |                                                                      |
 +----------------------------------------------------------------------*/

/*
 *  Author:
 *      Marcel van Kervinck
 *
 *  Created:
 *      2006-12-26
 *
 *  Description:
 *      Implementation of functions defined in wtime.h.
 *
 *  History:
 *      2006-12-26 (marcelk) First version for Mac and Linux.
 *      2007-06-20 (marcelk) Prepare minimal version for use in projects.
 *                           Removed incomplete functions for future redesign.
 *
 *  To do:
 *      -- How to recognize leap seconds on Unix?
 *      -- Make Java version.
 *      -- Speed-up wtime_to_string.
 */

/*----------------------------------------------------------------------+
 |      Copyright                                                       |
 +----------------------------------------------------------------------*/

/*
 *  Copyright (C) 2006-2007, Marcel van Kervinck
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in
 *     the documentation and/or other materials provided with the
 *     distribution.
 *  3. Neither the name of the copyright holder nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
 *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 *  FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
 *  COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 *  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 *  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 *  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 *  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 *  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 *  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

/*----------------------------------------------------------------------+
 |      Includes                                                        |
 +----------------------------------------------------------------------*/

/*
 *  C standard includes
 */
#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

/*
 *  System includes
 */
#include <sys/time.h>

/*
 *  Base include
 */
#include "base.h"

/*
 *  Application includes
 */

/*
 *  Own include
 */
#include "wtime.h"

/*----------------------------------------------------------------------+
 |      Definitions                                                     |
 +----------------------------------------------------------------------*/

#define JULIAN_IS_LEAP(year) ( MOD(year,4)==0 )

/*
 *  In the Gregorian era, year>0.
 *  Therefore we can use C's %-operator safely.
 */
#define GREGORIAN_IS_LEAP(year) (\
        ( (year)%4==0 ) && ( (year)%100!=0 || (year)%400==0 )\
)

/*
 *  It is convenient to use Julian Day Numbers (JDN) internally.
 *  (Note that wtime_t can represent part of the day -1 in many time zones,
 *  so day_number can become negative)
 *  The WTIME_HALF_EPOCH_SECONDS spans half the epoch. We use it to
 *  convert between signed and unsigned seconds. The term 12L*3600
 *  compensates for the half-day shift between JDN and UTC calendar days.
 */
#define WTIME_HALF_EPOCH_SECONDS ((1LL<<37) + (12L*3600))

#define JDN_1970_JAN1   2440588L /* 12:00 */
#define JDN_1752_JAN1   2360976L /* 12:00 */
#define JDN_1752_SEP14  2361222L /* 12:00 */

#define SECONDS_PER_DAY (24L * 3600)

#define JDN_MONDAY_OFFSET 7L

/*----------------------------------------------------------------------+
 |      Function: wtime_init                                            |
 +----------------------------------------------------------------------*/

/*
 *  @TODO: future versions of wtime probably need to be able to read a
 *  configuration from some source (for example, for historical leap seconds).
 *  We want to implement that using function call interface, to prevent
 *  dependencies on file locations and formats. It is unclear how that
 *  fits with one global initialization.
 */
err_t wtime_init(void)
{
        tzset();
        return OK;
}

/*----------------------------------------------------------------------+
 |      Function: wtime_get_current                                     |
 +----------------------------------------------------------------------*/

err_t wtime_get_current(wtime_t *current)
{
        err_t err = OK;

        struct timeval tv;
        struct timezone tz;

        int r = gettimeofday(&tv, &tz);
        if (r != 0) {
                RAISE_ERRNO(gettimeofday);
        }

        unsigned long long seconds = tv.tv_sec;
        seconds += (long long)JDN_1970_JAN1 * SECONDS_PER_DAY -
                WTIME_HALF_EPOCH_SECONDS;

        long fraction = tv.tv_usec;

        int zone;

        // @TODO: should use gm_gmtoff
        switch (-tz.tz_minuteswest)
        {
        case -12*60   : zone = wtime_zone_w1200; break;
        case -11*60-30: zone = wtime_zone_w1130; break;
        case -11*60   : zone = wtime_zone_w1100; break;
        case -10*60-30: zone = wtime_zone_w1030; break;
        case -10*60   : zone = wtime_zone_w1000; break;
        case  -9*60-30: zone = wtime_zone_w0930; break;
        case  -9*60   : zone = wtime_zone_w0900; break;
        case  -8*60-30: zone = wtime_zone_w0830; break;
        case  -8*60   : zone = wtime_zone_w0800; break;
        case  -7*60-30: zone = wtime_zone_w0730; break;
        case  -7*60   : zone = wtime_zone_w0700; break;
        case  -6*60-30: zone = wtime_zone_w0630; break;
        case  -6*60   : zone = wtime_zone_w0600; break;
        case  -5*60-30: zone = wtime_zone_w0530; break;
        case  -5*60   : zone = wtime_zone_w0500; break;
        case  -4*60-30: zone = wtime_zone_w0430; break;
        case  -4*60   : zone = wtime_zone_w0400; break;
        case  -3*60-30: zone = wtime_zone_w0330; break;
        case  -3*60   : zone = wtime_zone_w0300; break;
        case  -2*60-30: zone = wtime_zone_w0230; break;
        case  -2*60   : zone = wtime_zone_w0200; break;
        case  -1*60-30: zone = wtime_zone_w0130; break;
        case  -1*60   : zone = wtime_zone_w0100; break;
        case  -0*60-30: zone = wtime_zone_w0030; break;

        case   0*60   : zone = wtime_zone_z0000; break;

        case   0*60+30: zone = wtime_zone_e0030; break;
        case   1*60   : zone = wtime_zone_e0100; break;
        case   1*60+30: zone = wtime_zone_e0130; break;
        case   2*60   : zone = wtime_zone_e0200; break;
        case   2*60+30: zone = wtime_zone_e0230; break;
        case   3*60   : zone = wtime_zone_e0300; break;
        case   3*60+30: zone = wtime_zone_e0330; break;
        case   4*60   : zone = wtime_zone_e0400; break;
        case   4*60+30: zone = wtime_zone_e0430; break;
        case   5*60   : zone = wtime_zone_e0500; break;
        case   5*60+30: zone = wtime_zone_e0530; break;
        case   6*60   : zone = wtime_zone_e0600; break;
        case   6*60+30: zone = wtime_zone_e0630; break;
        case   7*60   : zone = wtime_zone_e0700; break;
        case   7*60+30: zone = wtime_zone_e0730; break;
        case   8*60   : zone = wtime_zone_e0800; break;
        case   8*60+30: zone = wtime_zone_e0830; break;
        case   9*60   : zone = wtime_zone_e0900; break;
        case   9*60+30: zone = wtime_zone_e0930; break;
        case  10*60   : zone = wtime_zone_e1000; break;
        case  10*60+30: zone = wtime_zone_e1030; break;
        case  11*60   : zone = wtime_zone_e1100; break;
        case  11*60+30: zone = wtime_zone_e1130; break;
        case  12*60   : zone = wtime_zone_e1200; break;
 
        case  13*60   : zone = wtime_zone_e1300x; break;
        case  14*60   : zone = wtime_zone_e1400x; break;
        case   5*60+45: zone = wtime_zone_e0545x; break;
        case   8*60+45: zone = wtime_zone_e0845x; break;
        case  12*60+45: zone = wtime_zone_e1245x; break;

        default:
                if (-tz.tz_minuteswest > 0 && -tz.tz_minuteswest < 1024) {
                        /*
                         *  Solar time, minutes west of GMT
                         */
                        zone = wtime_zone_solar_east;
                        fraction = (fraction / 1000) << 10;
                        fraction |= -tz.tz_minuteswest;
                } else
                if (-tz.tz_minuteswest < 0 && -tz.tz_minuteswest > -1024) {
                        /*
                         *  Solar time, minutes east of GMT
                         */
                        zone = wtime_zone_solar_west;
                        fraction = (fraction / 1000) << 10;
                        fraction |= 1024 - tz.tz_minuteswest;
                } else {
                        /*
                         *  Unrepresentable time zone, mark as unknown local
                         *  @TODO: alternative: hard-wire to UTC time
                         *  or just throw an exception
                         */
                        zone = wtime_zone_unknown_local;

                        seconds += (-tz.tz_minuteswest * 60LL);
                }
                break;
        }

        *current = (seconds << 26) | (fraction << 6) | zone;
cleanup:
        return err;
}

/*----------------------------------------------------------------------+
 |      Function: wtime_to_string                                       |
 +----------------------------------------------------------------------*/

static const int days_in_month[2][12] = {
        /*
         *  regular year
         */
        { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
        /*
         *  leap year
         */
        { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
};

/*
   Convert a wtime_t timestamp to string using default selection between
   Julian or Gregorian calendar system (Julian with astronomical years,
   that is, including year 0). For rendering in another calendar system,
   one should use a specific calendar class that can do this.

   @TODO: export Gregorian and astronomical Julian and real Julian
   @TODO: export week number and day of week
   @TODO: accept strfdate-like formats
 */

static 
err_t write_invalid_string(char *buf, wtime_t timestamp)
{
        sprintf(buf, "{Invalid-wtime-%016llx}", timestamp);
        return OK;
}

err_t wtime_to_string(char *buf, int size, wtime_t timestamp)
{
        err_t err = OK;

        /*
         *  Simple check if called provides sufficient buffer space
         */
        if (size < WTIME_STRING_LEN_MAX + 1) {
                RAISE(WTIME_ERR_BUFFER_TOO_SMALL);
        }

        int zone = timestamp & 63;

        long long seconds = (timestamp >> 26);
        int leap_second = 0;
        long fraction = (timestamp >> 6) & 0xfffffL;
        int fraction_digits = 6;

        if (zone == wtime_zone_solar_west || zone == wtime_zone_solar_east) {
                fraction >>= 10;
                if (fraction < 1000) {
                        leap_second = 0;
                        fraction_digits = 3;
                } else  {
                        if (fraction >= 1010) {
                                check( write_invalid_string(buf, timestamp) );
                                RETURN;
                        }
                        leap_second = 1;
                        fraction -= 1000;
                        fraction_digits = 1;
                }
        } else {
                /*
                 *        0 ...  999999 micro seconds
                 *  1000000 ... 1009999 0.1 ms within leap seconds
                 *  1010000 ... 1015807 (5808 positions) invalid
                 *  1015808 ... 1048576 (0xf8000..0xfffff)
                 *                      15-bit space for messages (future
                 *                      extension for wtime broadcasting
                 *                      stations: embed control messages at a
                 *                      rate of 128 bytes per second: 7 bits
                 *                      address and 8 bit data)
                 */

                if (fraction >= 1000000) {
                        if (fraction >= 1010000) {
#if 0 /* timestamp-encoded messaging not defined yet */
                                if (fraction >= 0xf8000L) {
                                        int offset = (fraction - 0xf8000L) >> 8;
                                        int value  = (fraction - 0xf8000L) & 255;
                                        abort();
                                }
#endif
                                /* invalid wtime_t timestamp value */
                                check( write_invalid_string(buf, timestamp) );
                                RETURN;
                        }
                        leap_second = 1;
                        fraction -= 1000000;
                        fraction_digits = 4;
                }
        }

        int ix = 0;

        /* localize seconds, determine zone offset */

        int zone_offset;

        switch (zone) {
        case wtime_zone_invalid:
        case wtime_zone_reserved_55:
        case wtime_zone_reserved_56:
        case wtime_zone_reserved_57:
        case wtime_zone_reserved_58:
        case wtime_zone_reserved_59:
        case wtime_zone_reserved_60:
                check( write_invalid_string(buf, timestamp) );
                RETURN;

        case wtime_zone_solar_west:
                zone_offset = ((timestamp >> 6) & 0x3ff) - 0x400;
                break;
        case wtime_zone_solar_east:
                zone_offset = (timestamp >> 6) & 0x3ff;
                break;
        case wtime_zone_e1300x:
                zone_offset = 13*60;
                break;
        case wtime_zone_e1400x:
                zone_offset = 14*60;
                break;
        case wtime_zone_e0545x:
                zone_offset = 5*60+45;
                break;
        case wtime_zone_e0845x:
                zone_offset = 8*60+45;
                break;
        case wtime_zone_e1245x:
                zone_offset = 12*60+45;
                break;
        default:
                assert(wtime_zone_w1200 <= zone && zone <= wtime_zone_e1200);
                zone_offset = (zone - wtime_zone_z0000) * 30;
                break;
        }

        seconds += zone_offset * 60;
        seconds += WTIME_HALF_EPOCH_SECONDS;

        // @TODO: rewrite so we can use / and % here
        long day_number = DIV(seconds, SECONDS_PER_DAY);
        long day_second = MOD(seconds, SECONDS_PER_DAY);

        int year;
        int year_day;
        int is_leap_year;

        /* find the year */
        if (day_number < JDN_1752_SEP14) {
                /*
                 *  Julian calendar
                 */
                year = 1752;
                long year_jan1 = JDN_1752_JAN1;
                is_leap_year = JULIAN_IS_LEAP(year);
                while (day_number < year_jan1) {
                        year--;
                        is_leap_year = JULIAN_IS_LEAP(year);
                        year_jan1 -= is_leap_year ? 366 : 365;
                }
                year_day = day_number - year_jan1;
        } else {
                /*
                 *  Gregorian calendar
                 */
                year = 1752;
                long year_jan1 = JDN_1752_SEP14 -
                        (31+29+31+30+31+30+31+31+14) + 1;

                for (;;) {
                        is_leap_year = GREGORIAN_IS_LEAP(year);

                        long next_jan1 = year_jan1 + (is_leap_year ? 366 : 365);

                        if (day_number < next_jan1) break; /* found it */

                        year++;
                        year_jan1 = next_jan1;
                }
                year_day = day_number - year_jan1;
        }

        /*
         *  find the month
         */
        int month_day = 1 + year_day;

        // @TODO: replace with binary search
        int month = 1;
        while (month_day > days_in_month[is_leap_year][month-1]) {
                month_day -= days_in_month[is_leap_year][month-1];
                month++;
        }

#if 0
        assert(day_number + JDN_MONDAY_OFFSET >= 0);
        int week_day = 1 + (day_number + JDN_MONDAY_OFFSET) % 7;
#endif

        /*
         *  Only accept leap second encoding at the end of a full minute
         */
        if (leap_second != 0) {
                if (day_second % 60 != 59) {
                        check( write_invalid_string(buf, timestamp) );
                        RETURN;
                }
        }

        /*
         *  For readability, we print a space between date, time and time zone.
         *  Although IS0-8601 doesn't mandate the 'T' separator, it doesn't
         *  allow spaces. In this function, readability prevails.
         */
        sprintf(buf+ix,
                // YYYY-MM-DD      HH:MM:SS.xxx...        +ZZ:ZZ
                "%04d-%02d-%02d %02ld:%02ld:%02ld.%0*ld %c%02d%s%02d",
                year,
                month,
                month_day,
                day_second / 3600,
                (day_second / 60) % 60,
                (day_second % 60) + leap_second,
                fraction_digits,
                fraction,
                (zone_offset < 0) ? '-' : '+',
                abs(zone_offset) / 60,
                /*
                 *  We only use the colon (':') separator inside the timezone
                 *  if we have a really strange solar-based zone, like +01:23.
                 *  We omit the colon for zones that are exact multiples of
                 *  15 minutes.
                 */
                (abs(zone_offset) % 15 != 0) ? ":" : "",
                abs(zone_offset) % 60);

cleanup:
        return err;
}

/*----------------------------------------------------------------------+
 |                                                                      |
 +----------------------------------------------------------------------*/


