/*
 * Copyright (C) 2005 Andrew Beekhof <andrew@beekhof.net>
 *
 * 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
 */

/*
 * Primary reference:
 *	http://en.wikipedia.org/wiki/ISO_8601 (as at 2005-08-01)
 *
 * Secondary references:
 *	http://hydracen.com/dx/iso8601.htm
 *	http://www.personal.ecu.edu/mccartyr/ISOwdALG.txt
 *	http://www.personal.ecu.edu/mccartyr/isowdcal.html
 *	http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm
 *
 */

#include <crm_internal.h>
#include <crm/crm.h>
#include <time.h>
#include <ctype.h>
#include <crm/common/iso8601.h>

/*
 * Andrew's code was originally written for OSes whose "struct tm" contains:
 *	long tm_gmtoff;		:: Seconds east of UTC
 *	const char *tm_zone;	:: Timezone abbreviation
 * Some OSes lack these, instead having:
 *	time_t (or long) timezone;
		:: "difference between UTC and local standard time"
 *	char *tzname[2] = { "...", "..." };
 * I (David Lee) confess to not understanding the details.  So my attempted
 * generalisations for where their use is necessary may be flawed.
 *
 * 1. Does "difference between ..." subtract the same or opposite way?
 * 2. Should it use "altzone" instead of "timezone"?
 * 3. Should it use tzname[0] or tzname[1]?  Interaction with timezone/altzone?
 */
#if defined(HAVE_STRUCT_TM_TM_GMTOFF)
#  define GMTOFF(tm) ((tm)->tm_gmtoff)
#else
/* Note: extern variable; macro argument not actually used.  */
#  define GMTOFF(tm) (-timezone+daylight)
#endif

struct crm_time_s {
    int years;
    int months;                 /* Only for durations */
    int days;
    int seconds;
    int offset;                 /* Seconds */
    bool duration;
};

char *crm_time_as_string(crm_time_t * date_time, int flags);
crm_time_t *parse_date(const char *date_str);

gboolean check_for_ordinal(const char *str);

static crm_time_t *
crm_get_utc_time(crm_time_t * dt)
{
    crm_time_t *utc = calloc(1, sizeof(crm_time_t));

    utc->years = dt->years;
    utc->days = dt->days;
    utc->seconds = dt->seconds;
    utc->offset = 0;

    if (dt->offset) {
        crm_time_add_seconds(utc, -dt->offset);
    } else {
        /* Durations (which are the only things that can include months, never have a timezone */
        utc->months = dt->months;
    }

    crm_time_log(LOG_TRACE, "utc-source", dt,
                 crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
    crm_time_log(LOG_TRACE, "utc-target", utc,
                 crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
    return utc;
}

crm_time_t *
crm_time_new(const char *date_time)
{
    time_t tm_now;
    crm_time_t *dt = NULL;

    tzset();
    if (date_time == NULL) {
        tm_now = time(NULL);
        dt = calloc(1, sizeof(crm_time_t));
        crm_time_set_timet(dt, &tm_now);
    } else {
        dt = parse_date(date_time);
    }
    return dt;
}

void
crm_time_free(crm_time_t * dt)
{
    if (dt == NULL) {
        return;
    }
    free(dt);
}

static int
year_days(int year)
{
    int d = 365;

    if (crm_time_leapyear(year)) {
        d++;
    }
    return d;
}

/* http://www.personal.ecu.edu/mccartyr/ISOwdALG.txt
 *
 * 5. Find the Jan1Weekday for Y (Monday=1, Sunday=7)
 *  YY = (Y-1) % 100
 *  C = (Y-1) - YY
 *  G = YY + YY/4
 *  Jan1Weekday = 1 + (((((C / 100) % 4) x 5) + G) % 7)
 */
int
crm_time_january1_weekday(int year)
{
    int YY = (year - 1) % 100;
    int C = (year - 1) - YY;
    int G = YY + YY / 4;
    int jan1 = 1 + (((((C / 100) % 4) * 5) + G) % 7);

    crm_trace("YY=%d, C=%d, G=%d", YY, C, G);
    crm_trace("January 1 %.4d: %d", year, jan1);
    return jan1;
}

int
crm_time_weeks_in_year(int year)
{
    int weeks = 52;
    int jan1 = crm_time_january1_weekday(year);

    /* if jan1 == thursday */
    if (jan1 == 4) {
        weeks++;
    } else {
        jan1 = crm_time_january1_weekday(year + 1);
        /* if dec31 == thursday aka. jan1 of next year is a friday */
        if (jan1 == 5) {
            weeks++;
        }

    }
    return weeks;
}

int month_days[14] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29 };

int
crm_time_days_in_month(int month, int year)
{
    if (month == 2 && crm_time_leapyear(year)) {
        month = 13;
    }
    return month_days[month];
}

bool
crm_time_leapyear(int year)
{
    gboolean is_leap = FALSE;

    if (year % 4 == 0) {
        is_leap = TRUE;
    }
    if (year % 100 == 0 && year % 400 != 0) {
        is_leap = FALSE;
    }
    return is_leap;
}

static uint32_t
get_ordinal_days(uint32_t y, uint32_t m, uint32_t d)
{
    int lpc;

    for (lpc = 1; lpc < m; lpc++) {
        d += crm_time_days_in_month(lpc, y);
    }
    return d;
}

void
crm_time_log_alias(int log_level, const char *file, const char *function, int line,
                   const char *prefix, crm_time_t * date_time, int flags)
{
    char *date_s = crm_time_as_string(date_time, flags);

    if (log_level < LOG_CRIT) {
        printf("%s%s%s\n",
               prefix ? prefix : "", prefix ? ": " : "", date_s ? date_s : "__invalid_date__");
    } else {
        do_crm_log_alias(log_level, file, function, line, "%s%s%s",
                         prefix ? prefix : "", prefix ? ": " : "",
                         date_s ? date_s : "__invalid_date__");
    }
    free(date_s);
}

static int
crm_time_get_sec(int sec, uint * h, uint * m, uint * s)
{
    uint hours, minutes, seconds;

    if (sec < 0) {
        seconds = 0 - sec;
    } else {
        seconds = sec;
    }

    hours = seconds / (60 * 60);
    seconds -= 60 * 60 * hours;

    minutes = seconds / (60);
    seconds -= 60 * minutes;

    crm_trace("%d == %.2d:%.2d:%.2d", sec, hours, minutes, seconds);

    *h = hours;
    *m = minutes;
    *s = seconds;

    return TRUE;
}

int
crm_time_get_timeofday(crm_time_t * dt, uint * h, uint * m, uint * s)
{
    return crm_time_get_sec(dt->seconds, h, m, s);
}

int
crm_time_get_timezone(crm_time_t * dt, uint * h, uint * m)
{
    uint s;

    return crm_time_get_sec(dt->seconds, h, m, &s);
}

long long
crm_time_get_seconds(crm_time_t * dt)
{
    int lpc;
    crm_time_t *utc = NULL;
    long long in_seconds = 0;

    utc = crm_get_utc_time(dt);

    for (lpc = 1; lpc < utc->years; lpc++) {
        int dmax = year_days(lpc);

        in_seconds += 60 * 60 * 24 * dmax;
    }

    /* utc->months is an offset that can only be set for a duration
     * By definiton, the value is variable depending on the date to
     * which it is applied
     *
     * Force 30-day months so that something vaguely sane happens
     * for anyone that tries to use a month in this way
     */
    if (utc->months > 0) {
        in_seconds += 60 * 60 * 24 * 30 * utc->months;
    }

    if (utc->days > 0) {
        in_seconds += 60 * 60 * 24 * (utc->days - 1);
    }
    in_seconds += utc->seconds;

    crm_time_free(utc);
    return in_seconds;
}

#define EPOCH_SECONDS 62135596800ULL    /* Calculated using crm_time_get_seconds() */
long long
crm_time_get_seconds_since_epoch(crm_time_t * dt)
{
    return crm_time_get_seconds(dt) - EPOCH_SECONDS;
}

int
crm_time_get_gregorian(crm_time_t * dt, uint * y, uint * m, uint * d)
{
    int months = 0;
    int days = dt->days;

    if(dt->years != 0) {
        for (months = 1; months <= 12 && days > 0; months++) {
            int mdays = crm_time_days_in_month(months, dt->years);

            if (mdays >= days) {
                break;
            } else {
                days -= mdays;
            }
        }

    } else if (dt->months) {
        /* This is a duration including months, don't convert the days field */
        months = dt->months;

    } else {
        /* This is a duration not including months, still don't convert the days field */
    }

    *y = dt->years;
    *m = months;
    *d = days;
    crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days);
    return TRUE;
}

int
crm_time_get_ordinal(crm_time_t * dt, uint * y, uint * d)
{
    *y = dt->years;
    *d = dt->days;
    return TRUE;
}

int
crm_time_get_isoweek(crm_time_t * dt, uint * y, uint * w, uint * d)
{
    /*
     * Monday 29 December 2008 is written "2009-W01-1"
     * Sunday 3 January 2010 is written "2009-W53-7"
     */
    int year_num = 0;
    int jan1 = crm_time_january1_weekday(dt->years);
    int h = -1;

    CRM_CHECK(dt->days > 0, return FALSE);

/* 6. Find the Weekday for Y M D */
    h = dt->days + jan1 - 1;
    *d = 1 + ((h - 1) % 7);

/* 7. Find if Y M D falls in YearNumber Y-1, WeekNumber 52 or 53 */
    if (dt->days <= (8 - jan1) && jan1 > 4) {
        crm_trace("year--, jan1=%d", jan1);
        year_num = dt->years - 1;
        *w = crm_time_weeks_in_year(year_num);

    } else {
        year_num = dt->years;
    }

/* 8. Find if Y M D falls in YearNumber Y+1, WeekNumber 1 */
    if (year_num == dt->years) {
        int dmax = year_days(year_num);
        int correction = 4 - *d;

        if ((dmax - dt->days) < correction) {
            crm_trace("year++, jan1=%d, i=%d vs. %d", jan1, dmax - dt->days, correction);
            year_num = dt->years + 1;
            *w = 1;
        }
    }

/* 9. Find if Y M D falls in YearNumber Y, WeekNumber 1 through 53 */
    if (year_num == dt->years) {
        int j = dt->days + (7 - *d) + (jan1 - 1);

        *w = j / 7;
        if (jan1 > 4) {
            *w -= 1;
        }
    }

    *y = year_num;
    crm_trace("Converted %.4d-%.3d to %.4d-W%.2d-%d", dt->years, dt->days, *y, *w, *d);
    return TRUE;
}

char *
crm_time_as_string(crm_time_t * date_time, int flags)
{
    char *date_s = NULL;
    char *time_s = NULL;
    char *offset_s = NULL;
    char *result_s = NULL;
    crm_time_t *dt = NULL;
    crm_time_t *utc = NULL;

    if (date_time == NULL) {
        return strdup("");

    } else if (date_time->offset && (flags & crm_time_log_with_timezone) == 0) {
        crm_trace("UTC conversion");
        utc = crm_get_utc_time(date_time);
        dt = utc;
    } else {
        dt = date_time;
    }

    CRM_CHECK(dt != NULL, return NULL);
    if (flags & crm_time_log_duration) {
        uint h = 0, m = 0, s = 0;
        int offset = 0, max = 128;

        date_s = calloc(1, max+1);
        crm_time_get_sec(dt->seconds, &h, &m, &s);

        if (date_s == NULL) {
            goto done;
        }

        if(dt->years) {
            offset += snprintf(date_s+offset, max-offset, "%4d year%s ", dt->years, dt->years>1?"s":"");
        }
        if(dt->months) {
            offset += snprintf(date_s+offset, max-offset, "%2d month%s ", dt->months, dt->months>1?"s":"");
        }
        if(dt->days) {
            offset += snprintf(date_s+offset, max-offset, "%2d day%s ", dt->days, dt->days>1?"s":"");
        }
        if(dt->seconds) {
            offset += snprintf(date_s+offset, max-offset, "%d seconds ( ", dt->seconds);
            if(h) {
                offset += snprintf(date_s+offset, max-offset, "%d hour%s ", h, h>1?"s":"");
            }
            if(m) {
                offset += snprintf(date_s+offset, max-offset, "%d minute%s ", m, m>1?"s":"");
            }
            if(s) {
                offset += snprintf(date_s+offset, max-offset, "%d second%s ", s, s>1?"s":"");
            }
            offset += snprintf(date_s+offset, max-offset, ")");
        }
        goto done;
    }

    if (flags & crm_time_log_date) {
        date_s = calloc(1, 32);
        if (date_s == NULL) {
            goto done;

        } else if (flags & crm_time_seconds) {
            unsigned long long s = crm_time_get_seconds(date_time);

            snprintf(date_s, 31, "%lld", s); /* Durations may not be +ve */
            goto done;

        } else if (flags & crm_time_epoch) {
            unsigned long long s = crm_time_get_seconds_since_epoch(date_time);

            snprintf(date_s, 31, "%lld", s); /* Durations may not be +ve */
            goto done;

        } else if (flags & crm_time_weeks) {
            /* YYYY-Www-D */
            uint y, w, d;

            if (crm_time_get_isoweek(dt, &y, &w, &d)) {
                snprintf(date_s, 31, "%d-W%.2d-%d", y, w, d);
            }

        } else if (flags & crm_time_ordinal) {
            /* YYYY-DDD */
            uint y, d;

            if (crm_time_get_ordinal(dt, &y, &d)) {
                snprintf(date_s, 31, "%d-%.3d", y, d);
            }

        } else {
            /* YYYY-MM-DD */
            uint y, m, d;

            if (crm_time_get_gregorian(dt, &y, &m, &d)) {
                snprintf(date_s, 31, "%.4d-%.2d-%.2d", y, m, d);
            }
        }
    }

    if (flags & crm_time_log_timeofday) {
        uint h, m, s;

        time_s = calloc(1, 32);
        if (time_s == NULL) {
            goto cleanup;
        }

        if (crm_time_get_timeofday(dt, &h, &m, &s)) {
            snprintf(time_s, 31, "%.2d:%.2d:%.2d", h, m, s);
        }

        if (dt->offset != 0) {
            crm_time_get_sec(dt->offset, &h, &m, &s);
        }

        offset_s = calloc(1, 32);
        if ((flags & crm_time_log_with_timezone) == 0 || dt->offset == 0) {
            crm_trace("flags %6x %6x", flags, crm_time_log_with_timezone);
            snprintf(offset_s, 31, "Z");

        } else {
            snprintf(offset_s, 31, " %c%.2d:%.2d", dt->offset < 0 ? '-' : '+', h, m);
        }
    }

  done:
    result_s = calloc(1, 100);

    snprintf(result_s, 100, "%s%s%s%s",
             date_s ? date_s : "", (date_s != NULL && time_s != NULL) ? " " : "",
             time_s ? time_s : "", offset_s ? offset_s : "");

  cleanup:
    free(date_s);
    free(time_s);
    free(offset_s);
    crm_time_free(utc);

    return result_s;
}

static int
crm_time_parse_sec(const char *time_str)
{
    int rc;
    uint hour = 0;
    uint minute = 0;
    uint second = 0;

    rc = sscanf(time_str, "%d:%d:%d", &hour, &minute, &second);
    if (rc == 1) {
        rc = sscanf(time_str, "%2d%2d%2d", &hour, &minute, &second);
    }

    if (rc > 0 && rc < 4) {
        crm_trace("Got valid time: %.2d:%.2d:%.2d", hour, minute, second);
        if (hour >= 24) {
            crm_err("Invalid hour: %d", hour);
        } else if (minute >= 60) {
            crm_err("Invalid minute: %d", minute);
        } else if (second >= 60) {
            crm_err("Invalid second: %d", second);
        } else {
            second += (minute * 60);
            second += (hour * 60 * 60);
        }
    } else {
        crm_err("Bad time: %s (%d)", time_str, rc);
    }
    return second;
}

static int
crm_time_parse_offset(const char *offset_str)
{
    int offset = 0;

    tzset();
    if (offset_str == NULL) {
#if defined(HAVE_STRUCT_TM_TM_GMTOFF)
        time_t now = time(NULL);
        struct tm *now_tm = localtime(&now);
#endif
        int h_offset = GMTOFF(now_tm) / (3600);
        int m_offset = (GMTOFF(now_tm) - (3600 * h_offset)) / (60);

        if (h_offset < 0 && m_offset < 0) {
            m_offset = 0 - m_offset;
        }
        offset += (60 * 60 * h_offset);
        offset += (60 * m_offset);

    } else if (offset_str[0] == 'Z') {

    } else if (offset_str[0] == '+' || offset_str[0] == '-' || isdigit((int)offset_str[0])) {
        gboolean negate = FALSE;

        if (offset_str[0] == '-') {
            negate = TRUE;
            offset_str++;
        }
        offset = crm_time_parse_sec(offset_str);
        if (negate) {
            offset = 0 - offset;
        }
    }
    return offset;
}

static crm_time_t *
crm_time_parse(const char *time_str, crm_time_t * a_time)
{
    uint h, m, s;
    char *offset_s = NULL;
    crm_time_t *dt = a_time;

    tzset();
    if (a_time == NULL) {
        dt = calloc(1, sizeof(crm_time_t));
    }

    if (time_str) {
        dt->seconds = crm_time_parse_sec(time_str);

        offset_s = strstr(time_str, "Z");
        if (offset_s == NULL) {
            offset_s = strstr(time_str, " ");
        }
    }

    if (offset_s) {
        while (isspace(offset_s[0])) {
            offset_s++;
        }
    }
    dt->offset = crm_time_parse_offset(offset_s);
    crm_time_get_sec(dt->offset, &h, &m, &s);
    crm_trace("Got tz: %c%2.d:%.2d", dt->offset < 0 ? '-' : '+', h, m);
    return dt;
}

crm_time_t *
parse_date(const char *date_str)
{
    char *time_s;
    crm_time_t *dt = NULL;

    int year = 0;
    int month = 0;
    int week = 0;
    int day = 0;
    int rc = 0;

    CRM_CHECK(date_str != NULL, return NULL);
    CRM_CHECK(strlen(date_str) > 0, return NULL);

    if (date_str[0] == 'T' || date_str[2] == ':') {
        /* Just a time supplied - Infer current date */
        dt = crm_time_new(NULL);
        dt = crm_time_parse(date_str, dt);
        goto done;

    } else {
        dt = calloc(1, sizeof(crm_time_t));
    }

    if (safe_str_eq("epoch", date_str)) {
        dt->days = 1;
        dt->years = 1970;
        crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
        return dt;
    }

    /* YYYY-MM-DD */
    rc = sscanf(date_str, "%d-%d-%d", &year, &month, &day);
    if (rc == 1) {
        /* YYYYMMDD */
        rc = sscanf(date_str, "%4d%2d%2d", &year, &month, &day);
    }
    if (rc == 3) {
        if (month > 12) {
            crm_err("Invalid month: %d", month);
        } else if (day > 31) {
            crm_err("Invalid day: %d", day);
        } else {
            dt->years = year;
            dt->days = get_ordinal_days(year, month, day);
            crm_trace("Got gergorian date: %.4d-%.3d", year, dt->days);
        }
        goto done;
    }

    /* YYYY-DDD */
    rc = sscanf(date_str, "%d-%d", &year, &day);
    if (rc == 2) {
        crm_trace("Got ordinal date");
        if (day > year_days(year)) {
            crm_err("Invalid day: %d (max=%d)", day, year_days(year));
        } else {
            dt->days = day;
            dt->years = year;
        }
        goto done;
    }

    /* YYYY-Www-D */
    rc = sscanf(date_str, "%d-W%d-%d", &year, &week, &day);
    if (rc == 3) {
        crm_trace("Got week date");
        if (week > crm_time_weeks_in_year(year)) {
            crm_err("Invalid week: %d (max=%d)", week, crm_time_weeks_in_year(year));
        } else if (day < 1 || day > 7) {
            crm_err("Invalid day: %d", day);
        } else {
            /*
             * http://en.wikipedia.org/wiki/ISO_week_date
             *
             * Monday 29 December 2008 is written "2009-W01-1"
             * Sunday 3 January 2010 is written "2009-W53-7"
             *
             * Saturday 27 September 2008 is written "2008-W37-6"
             *
             * http://en.wikipedia.org/wiki/ISO_week_date
             * If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it is in week 01.
             * If 1 January is on a Friday, Saturday or Sunday, it is in week 52 or 53 of the previous year.
             */
            int jan1 = crm_time_january1_weekday(year);

            crm_trace("Jan 1 = %d", jan1);

            dt->years = year;
            crm_time_add_days(dt, (week - 1) * 7);

            if (jan1 <= 4) {
                crm_time_add_days(dt, 1 - jan1);
            } else {
                crm_time_add_days(dt, 8 - jan1);
            }

            crm_time_add_days(dt, day);
        }
        goto done;
    }

    crm_err("Couldn't parse %s", date_str);
  done:

    time_s = strstr(date_str, " ");
    if (time_s == NULL) {
        time_s = strstr(date_str, "T");
    }

    if (dt && time_s) {
        time_s++;
        crm_time_parse(time_s, dt);
    }

    crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);

    CRM_CHECK(crm_time_check(dt), return NULL);

    return dt;
}

static int
parse_int(const char *str, int field_width, int uppper_bound, int *result)
{
    int lpc = 0;
    int offset = 0;
    int intermediate = 0;
    gboolean fraction = FALSE;
    gboolean negate = FALSE;

    CRM_CHECK(str != NULL, return FALSE);
    CRM_CHECK(result != NULL, return FALSE);

    *result = 0;

    if (strlen(str) <= 0) {
        return FALSE;
    }

    if (str[offset] == 'T') {
        offset++;
    }

    if (str[offset] == '.' || str[offset] == ',') {
        fraction = TRUE;
        field_width = -1;
        offset++;
    } else if (str[offset] == '-') {
        negate = TRUE;
        offset++;
    } else if (str[offset] == '+' || str[offset] == ':') {
        offset++;
    }

    for (; (fraction || lpc < field_width) && isdigit((int)str[offset]); lpc++) {
        if (fraction) {
            intermediate = (str[offset] - '0') / (10 ^ lpc);
        } else {
            *result *= 10;
            intermediate = str[offset] - '0';
        }
        *result += intermediate;
        offset++;
    }
    if (fraction) {
        *result = (int)(*result * uppper_bound);

    } else if (uppper_bound > 0 && *result > uppper_bound) {
        *result = uppper_bound;
    }
    if (negate) {
        *result = 0 - *result;
    }
    if (lpc > 0) {
        crm_trace("Found int: %d.  Stopped at str[%d]='%c'", *result, lpc, str[lpc]);
        return offset;
    }
    return 0;
}

crm_time_t *
crm_time_parse_duration(const char *interval_str)
{
    gboolean is_time = FALSE;
    crm_time_t *diff = NULL;

    CRM_CHECK(interval_str != NULL, goto bail);
    CRM_CHECK(strlen(interval_str) > 0, goto bail);
    CRM_CHECK(interval_str[0] == 'P', goto bail);
    interval_str++;

    diff = calloc(1, sizeof(crm_time_t));

    while (isspace((int)interval_str[0]) == FALSE) {
        int an_int = 0, rc;
        char ch = 0;

        if (interval_str[0] == 'T') {
            is_time = TRUE;
            interval_str++;
        }

        rc = parse_int(interval_str, 10, 0, &an_int);
        if (rc == 0) {
            break;
        }
        interval_str += rc;

        ch = interval_str[0];
        interval_str++;

        crm_trace("Testing %c=%d, rc=%d", ch, an_int, rc);

        switch (ch) {
            case 0:
                return diff;
                break;
            case 'Y':
                diff->years = an_int;
                break;
            case 'M':
                if (is_time) {
                    /* Minutes */
                    diff->seconds += an_int * 60;
                } else {
                    diff->months = an_int;
                }
                break;
            case 'W':
                diff->days += an_int * 7;
                break;
            case 'D':
                diff->days += an_int;
                break;
            case 'H':
                diff->seconds += an_int * 60 * 60;
                break;
            case 'S':
                diff->seconds += an_int;
                break;
            default:
                goto bail;
                break;
        }
    }
    return diff;

  bail:
    free(diff);
    return NULL;
}

crm_time_period_t *
crm_time_parse_period(const char *period_str)
{
    gboolean invalid = FALSE;
    const char *original = period_str;
    crm_time_period_t *period = NULL;

    CRM_CHECK(period_str != NULL, return NULL);
    CRM_CHECK(strlen(period_str) > 0, return NULL);

    tzset();
    period = calloc(1, sizeof(crm_time_period_t));

    if (period_str[0] == 'P') {
        period->diff = crm_time_parse_duration(period_str);
    } else {
        period->start = parse_date(period_str);
    }

    period_str = strstr(original, "/");
    if (period_str) {
        CRM_CHECK(period_str[0] == '/', invalid = TRUE;
                  goto bail);
        period_str++;

        if (period_str[0] == 'P') {
            period->diff = crm_time_parse_duration(period_str);
        } else {
            period->end = parse_date(period_str);
        }

    } else if (period->diff != NULL) {
        /* just aduration starting from now */
        period->start = crm_time_new(NULL);

    } else {
        invalid = TRUE;
        CRM_CHECK(period_str != NULL, goto bail);
    }

    /* sanity checks */
    if (period->start == NULL && period->end == NULL) {
        crm_err("Invalid time period: %s", original);
        invalid = TRUE;

    } else if (period->start == NULL && period->diff == NULL) {
        crm_err("Invalid time period: %s", original);
        invalid = TRUE;

    } else if (period->end == NULL && period->diff == NULL) {
        crm_err("Invalid time period: %s", original);
        invalid = TRUE;
    }

  bail:
    if (invalid) {
        free(period->start);
        free(period->end);
        free(period->diff);
        free(period);
        return NULL;
    }
    if (period->end == NULL && period->diff == NULL) {
    }

    if (period->start == NULL) {
        period->start = crm_time_subtract(period->end, period->diff);

    } else if (period->end == NULL) {
        period->end = crm_time_add(period->start, period->diff);
    }

    crm_time_check(period->start);
    crm_time_check(period->end);

    return period;
}

void
crm_time_set(crm_time_t * target, crm_time_t * source)
{
    crm_trace("target=%p, source=%p, offset=%d", target, source);

    CRM_CHECK(target != NULL && source != NULL, return);

    target->years = source->years;
    target->days = source->days;
    target->months = source->months;    /* Only for durations */
    target->seconds = source->seconds;
    target->offset = source->offset;

    crm_time_log(LOG_TRACE, "source", source,
                 crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
    crm_time_log(LOG_TRACE, "target", target,
                 crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
}

static void
ha_set_tm_time(crm_time_t * target, struct tm *source)
{
    int h_offset = 0;
    int m_offset = 0;

    if (source->tm_year > 0) {
        /* years since 1900 */
        target->years = 1900 + source->tm_year;
    }

    if (source->tm_yday >= 0) {
        /* days since January 1 [0-365] */
        target->days = 1 + source->tm_yday;
    }

    target->seconds = 0;
    if (source->tm_hour >= 0) {
        target->seconds += 60 * 60 * source->tm_hour;
    }
    if (source->tm_min >= 0) {
        target->seconds += 60 * source->tm_min;
    }
    if (source->tm_sec >= 0) {
        target->seconds += source->tm_sec;
    }

    /* tm_gmtoff == offset from UTC in seconds */
    h_offset = GMTOFF(source) / (3600);
    m_offset = (GMTOFF(source) - (3600 * h_offset)) / (60);
    crm_trace("Offset (s): %ld, offset (hh:mm): %.2d:%.2d", GMTOFF(source), h_offset, m_offset);

    target->offset = 0;
    target->offset += 60 * 60 * h_offset;
    target->offset += 60 * m_offset;
}

void
crm_time_set_timet(crm_time_t * target, time_t * source)
{
    ha_set_tm_time(target, localtime(source));
}

crm_time_t *
crm_time_add(crm_time_t * dt, crm_time_t * value)
{
    crm_time_t *utc = NULL;
    crm_time_t *answer = NULL;

    CRM_CHECK(dt != NULL && value != NULL, return NULL);

    answer = calloc(1, sizeof(crm_time_t));
    crm_time_set(answer, dt);

    utc = crm_get_utc_time(value);

    answer->years += utc->years;
    crm_time_add_months(answer, utc->months);
    crm_time_add_days(answer, utc->days);
    crm_time_add_seconds(answer, utc->seconds);

    crm_time_free(utc);
    return answer;
}

crm_time_t *
crm_time_calculate_duration(crm_time_t * dt, crm_time_t * value)
{
    crm_time_t *utc = NULL;
    crm_time_t *answer = NULL;

    CRM_CHECK(dt != NULL && value != NULL, return NULL);

    utc = crm_get_utc_time(value);
    answer = crm_get_utc_time(dt);
    answer->duration = TRUE;

    answer->years -= utc->years;
    if(utc->months != 0) {
        crm_time_add_months(answer, -utc->months);
    }
    crm_time_add_days(answer, -utc->days);
    crm_time_add_seconds(answer, -utc->seconds);

    crm_time_free(utc);
    return answer;
}

crm_time_t *
crm_time_subtract(crm_time_t * dt, crm_time_t * value)
{
    crm_time_t *utc = NULL;
    crm_time_t *answer = NULL;

    CRM_CHECK(dt != NULL && value != NULL, return NULL);

    answer = calloc(1, sizeof(crm_time_t));
    crm_time_set(answer, dt);
    utc = crm_get_utc_time(value);

    answer->years -= utc->years;
    if(utc->months != 0) {
        crm_time_add_months(answer, -utc->months);
    }
    crm_time_add_days(answer, -utc->days);
    crm_time_add_seconds(answer, -utc->seconds);

    return answer;
}

bool
crm_time_check(crm_time_t * dt)
{
    int ydays = 0;

    CRM_CHECK(dt != NULL, return FALSE);

    ydays = year_days(dt->years);
    crm_trace("max ydays: %d", ydays);

    CRM_CHECK(dt->days > 0, return FALSE);
    CRM_CHECK(dt->days <= ydays, return FALSE);

    CRM_CHECK(dt->seconds >= 0, return FALSE);
    CRM_CHECK(dt->seconds < 24 * 60 * 60, return FALSE);

    return TRUE;
}

#define do_cmp_field(l, r, field)					\
    if(rc == 0) {                                                       \
		if(l->field > r->field) {				\
			crm_trace("%s: %d > %d",			\
				    #field, l->field, r->field);	\
			rc = 1;                                         \
		} else if(l->field < r->field) {			\
			crm_trace("%s: %d < %d",			\
				    #field, l->field, r->field);	\
			rc = -1;					\
		}							\
    }

int
crm_time_compare(crm_time_t * a, crm_time_t * b)
{
    int rc = 0;
    crm_time_t *t1 = NULL;
    crm_time_t *t2 = NULL;

    if (a == NULL && b == NULL) {
        return 0;
    } else if (a == NULL) {
        return -1;
    } else if (b == NULL) {
        return 1;
    }

    t1 = crm_get_utc_time(a);
    t2 = crm_get_utc_time(b);

    do_cmp_field(t1, t2, years);
    do_cmp_field(t1, t2, days);
    do_cmp_field(t1, t2, seconds);

    crm_time_free(t1);
    crm_time_free(t2);
    return rc;
}

void
crm_time_add_seconds(crm_time_t * a_time, int extra)
{
    int days = 0;
    int seconds = 24 * 60 * 60;

    crm_trace("Adding %d seconds to %d (max=%d)", extra, a_time->seconds, seconds);

    a_time->seconds += extra;
    while (a_time->seconds >= seconds) {
        a_time->seconds -= seconds;
        days++;
    }

    while (a_time->seconds < 0) {
        a_time->seconds += seconds;
        days--;
    }
    crm_time_add_days(a_time, days);
}

void
crm_time_add_days(crm_time_t * a_time, int extra)
{
    int lower_bound = 1;
    int ydays = crm_time_leapyear(a_time->years) ? 366 : 365;

    crm_trace("Adding %d days to %.4d-%.3d", extra, a_time->years, a_time->days);

    a_time->days += extra;
    while (a_time->days > ydays) {
        a_time->years++;
        a_time->days -= ydays;
        ydays = crm_time_leapyear(a_time->years) ? 366 : 365;
    }

    if(a_time->duration) {
        lower_bound = 0;
    }

    while (a_time->days < lower_bound) {
        a_time->years--;
        a_time->days += crm_time_leapyear(a_time->years) ? 366 : 365;
    }
}

void
crm_time_add_months(crm_time_t * a_time, int extra)
{
    int lpc;
    uint32_t y, m, d, dmax;

    crm_time_get_gregorian(a_time, &y, &m, &d);
    crm_trace("Adding %d months to %.4d-%.2d-%.2d", extra, y, m, d);

    if (extra > 0) {
        for (lpc = extra; lpc > 0; lpc--) {
            m++;
            if (m == 13) {
                m = 1;
                y++;
            }
        }
    } else {
        for (lpc = -extra; lpc > 0; lpc--) {
            m--;
            if (m == 0) {
                m = 12;
                y--;
            }
        }
    }

    dmax = crm_time_days_in_month(m, y);
    if (dmax < d) {
        /* Preserve day-of-month unless the month doesn't have enough days */
        d = dmax;
    }

    crm_trace("Calculated %.4d-%.2d-%.2d", y, m, d);

    a_time->years = y;
    a_time->days = get_ordinal_days(y, m, d);

    crm_time_get_gregorian(a_time, &y, &m, &d);
    crm_trace("Got %.4d-%.2d-%.2d", y, m, d);
}

void
crm_time_add_minutes(crm_time_t * a_time, int extra)
{
    crm_time_add_seconds(a_time, extra * 60);
}

void
crm_time_add_hours(crm_time_t * a_time, int extra)
{
    crm_time_add_seconds(a_time, extra * 60 * 60);
}

void
crm_time_add_weeks(crm_time_t * a_time, int extra)
{
    crm_time_add_days(a_time, extra * 7);
}

void
crm_time_add_years(crm_time_t * a_time, int extra)
{
    a_time->years += extra;
}
