//=============================================================================
// File:       datetime.cpp
// Contents:   Definitions for DwDateTime
// Maintainer: Doug Sauder <dwsauder@fwb.gulf.net>
// WWW:        http://www.fwb.gulf.net/~dwsauder/mimepp.html
//
// Copyright (c) 1996, 1997 Douglas W. Sauder
// All rights reserved.
//
// IN NO EVENT SHALL DOUGLAS W. SAUDER BE LIABLE TO ANY PARTY FOR DIRECT,
// INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF
// THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF DOUGLAS W. SAUDER
// HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// DOUGLAS W. SAUDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT
// NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS"
// BASIS, AND DOUGLAS W. SAUDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE,
// SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
//
//=============================================================================

#define DW_IMPLEMENTATION

#include <mimelib/config.h>
#include <mimelib/debug.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <mimelib/string.h>
#include <mimelib/datetime.h>
#include <mimelib/token.h>
#include <time.h>

static char lWeekDay[7][4]
    = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
static char lMonth[12][4]
    = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
       "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

extern "C" int ParseRfc822Date(const char *str, struct tm *tms, int *z);
extern "C" int ParseDate(const char *str, struct tm *tms, int *z);
static DwInt32 ymd_to_jdnl(int year, int mon, int day, int julian);
static void jdnl_to_ymd(DwInt32 jdn, int *year, int *mon, int *day, int julian);
static DwUint32 my_inv_gmtime(struct tm* ptms);

const char* const DwDateTime::sClassName = "DwDateTime";


int DwDateTime::sDefaultZone = 0;
int DwDateTime::sIsDefaultZoneSet = 0;
DwDateTime* (*DwDateTime::sNewDateTime)(const DwString&,
    DwMessageComponent*) = 0;


DwDateTime* DwDateTime::NewDateTime(const DwString& aStr,
    DwMessageComponent* aParent)
{
    if (sNewDateTime) {
        return sNewDateTime(aStr, aParent);
    }
    else {
        return new DwDateTime(aStr, aParent);
    }
}


void DwDateTime::SetDefaultZone(int aZone)
{
    sDefaultZone = aZone;
    sIsDefaultZoneSet = 1;
}


DwDateTime::DwDateTime()
{
    Init();
    mIsModified = 1;
}


DwDateTime::DwDateTime(const DwDateTime& aDateTime)
  : DwFieldBody(aDateTime)
{
    mYear   = aDateTime.mYear;
    mMonth  = aDateTime.mMonth;
    mDay    = aDateTime.mDay;
    mHour   = aDateTime.mHour;
    mMinute = aDateTime.mMinute;
    mSecond = aDateTime.mSecond;
    mZone   = aDateTime.mZone;
}


DwDateTime::DwDateTime(const DwString& aStr, DwMessageComponent* aParent)
  : DwFieldBody(aStr, aParent)
{
    Init();
    mIsModified = 0;
}


void DwDateTime::Init()
{
    mClassId = kCidDateTime;
    mClassName = DwDateTime::sClassName;
    // Check if default time zone is set
    if (sIsDefaultZoneSet == 0) {
        // Use calls to gmtime() and localtime() to get the time difference
        // between local time and UTC (GMT) time.
        time_t t_now = time((time_t*) 0);
#if defined(HAVE_GMTIME_R)
        struct tm utc;
        gmtime_r(&t_now, &utc);
        struct tm local;
        localtime_r(&t_now, &local);
#else
        struct tm utc = *gmtime(&t_now);
        struct tm local = *localtime(&t_now);
#endif
        DwUint32 t_local = my_inv_gmtime(&local);
        DwUint32 t_utc = my_inv_gmtime(&utc);
        sDefaultZone = (int) (t_local - t_utc)/60;
        sIsDefaultZoneSet = 1;
    }
    // Set the time zone from the default time zone
    mZone = sDefaultZone;
    // Get the current calendar time
    time_t t_now = time((time_t*) 0);
    // Set year, month, day, hour, minute, and second from calendar time
    _FromCalendarTime(t_now);
}


DwDateTime::~DwDateTime()
{
}


const DwDateTime& DwDateTime::operator = (const DwDateTime& aDateTime)
{
    if (this == &aDateTime) return *this;
    DwFieldBody::operator = (aDateTime);
    mYear   = aDateTime.mYear;
    mMonth  = aDateTime.mMonth;
    mDay    = aDateTime.mDay;
    mHour   = aDateTime.mHour;
    mMinute = aDateTime.mMinute;
    mSecond = aDateTime.mSecond;
    mZone   = aDateTime.mZone;
    return *this;
}


DwUint32 DwDateTime::AsUnixTime() const
{
    struct tm tt;
    tt.tm_year = mYear - 1900;
    tt.tm_mon  = mMonth - 1;
    tt.tm_mday = mDay;
    tt.tm_hour = mHour;
    tt.tm_min  = mMinute;
    tt.tm_sec  = mSecond;
    DwUint32 t = my_inv_gmtime(&tt);
    t = (t == (DwUint32) -1) ? 0 : t;
    t -= mZone*60;
    return t;
}


void DwDateTime::FromUnixTime(DwUint32 aTime)
{
    _FromUnixTime(aTime);
    SetModified();
}


void DwDateTime::_FromUnixTime(DwUint32 aTime)
{
    time_t t = aTime + mZone*60;
#if defined(HAVE_GMTIME_R)
    struct tm tt;
    gmtime_r(&t, &tt);
#else
    struct tm tt = *gmtime(&t);
#endif
    mYear   = tt.tm_year + 1900;
    mMonth  = tt.tm_mon + 1;
    mDay    = tt.tm_mday;
    mHour   = tt.tm_hour;
    mMinute = tt.tm_min;
    mSecond = tt.tm_sec;
}

void DwDateTime::FromCalendarTime(time_t aTime)
{
    _FromCalendarTime(aTime);
    SetModified();
}


void DwDateTime::_FromCalendarTime(time_t aTime)
{
    // Note: the broken-down time is the only portable representation.
    // ANSI does not even require that time_t be an integer type; it could
    // be a double.  And, it doesn't even have to be in seconds.

    // Get the broken-down time.
#if defined(HAVE_GMTIME_R)
    struct tm tms_utc;
    gmtime_r(&aTime, &tms_utc);
#else
    struct tm tms_utc = *gmtime(&aTime);
#endif
    // Convert to UNIX time, using portable routine
    DwUint32 t_unix = my_inv_gmtime(&tms_utc);
    // Set from the UNIX time
    _FromUnixTime(t_unix);
}


DwInt32 DwDateTime::DateAsJulianDayNum() const
{
    DwInt32 jdn = ymd_to_jdnl(mYear, mMonth, mDay, -1);
    return jdn;
}


void DwDateTime::DateFromJulianDayNum(DwInt32 aJdn)
{
    jdnl_to_ymd(aJdn, &mYear, &mMonth, &mDay, -1);
    SetModified();
}


DwInt32 DwDateTime::TimeAsSecsPastMidnight() const
{
    DwInt32 n = mHour;
    n *= 60;
    n += mMinute;
    n *= 60;
    n += mSecond;
    return n;
}


void DwDateTime::TimeFromSecsPastMidnight(DwInt32 aSecs)
{
    mSecond = (int) (aSecs % 60);
    aSecs /= 60;
    mMinute = (int) (aSecs % 60);
    aSecs /= 60;
    mHour = (int) (aSecs % 24);
    SetModified();
}


void DwDateTime::Parse()
{
    mIsModified = 0;
    char buffer[80];
    char *str;
    int mustDelete;
    // Allocate memory from heap only in rare instances where the buffer
    // is too small.
    if (mString.length() >= 80) {
        mustDelete = 1;
        str = new char [mString.length()+1];
    }
    else {
        mustDelete = 0;
        str = buffer;
    }
    strncpy(str, mString.data(), mString.length());
    str[mString.length()] = 0;
    str[79] = 0;
    struct tm tms;
    int zone;
    int err = ParseRfc822Date(str, &tms, &zone);
    if ( err == -1 )  // try another format
        err = ParseDate(str, &tms, &zone);
    if (!err) {
        mYear   = tms.tm_year + 1900;
        mMonth  = tms.tm_mon+1;
        mDay    = tms.tm_mday;
        mHour   = tms.tm_hour;
        mMinute = tms.tm_min;
        mSecond = tms.tm_sec;
        mZone   = zone;
    }
    else /* if (err) */ {
        mYear   = 1970;
        mMonth  = 1;
        mDay    = 1;
        mHour   = 0;
        mMinute = 0;
        mSecond = 0;
        mZone   = 0;
    }
    if (mustDelete) {
        delete[] str;
    }
}


void DwDateTime::Assemble()
{
    if (!mIsModified) return;
    // Find the day of the week
    DwInt32 jdn = DateAsJulianDayNum();
    int dow = (int) ((jdn+1)%7);
    char sgn = (mZone < 0) ? '-' : '+';
    int z = (mZone < 0) ? -mZone : mZone;
    char buffer[80];
    snprintf(buffer, sizeof(buffer), "%s, %d %s %4d %02d:%02d:%02d %c%02d%02d",
        lWeekDay[dow], mDay, lMonth[(mMonth-1)%12], mYear,
        mHour, mMinute, mSecond, sgn, z/60%24, z%60);
    mString = buffer;
    mIsModified = 0;
}


DwMessageComponent* DwDateTime::Clone() const
{
    return new DwDateTime(*this);
}


#if defined (DW_DEBUG_VERSION)
void DwDateTime::PrintDebugInfo(std::ostream& aStrm, int /*aDepth*/) const
{
    aStrm <<
    "---------------- Debug info for DwDateTime class ---------------\n";
    _PrintDebugInfo(aStrm);
}
#else
void DwDateTime::PrintDebugInfo(std::ostream& , int) const {}
#endif // defined (DW_DEBUG_VERSION)


#if defined (DW_DEBUG_VERSION)
void DwDateTime::_PrintDebugInfo(std::ostream& aStrm) const
{
    DwFieldBody::_PrintDebugInfo(aStrm);
    aStrm << "Date:             "
        << mYear << '-' << mMonth << '-' << mDay << ' '
        << mHour << ':' << mMinute << ':' << mSecond << ' '
        << mZone << '\n';
}
#else
void DwDateTime::_PrintDebugInfo(std::ostream& ) const {}
#endif // defined (DW_DEBUG_VERSION)


void DwDateTime::CheckInvariants() const
{
#if defined (DW_DEBUG_VERSION)
    DwFieldBody::CheckInvariants();
    assert(mYear >= 0);
    assert(1 <= mMonth && mMonth <= 12);
    assert(1 <= mDay && mDay <= 31);
    assert(0 <= mHour && mHour < 24);
    assert(0 <= mMinute && mMinute < 60);
    assert(0 <= mSecond && mSecond < 60);
    assert(-12*60 <= mZone && mZone <= 12*60);
#endif // defined (DW_DEBUG_VERSION)
}


#ifdef PAPAL                    /* Pope Gregory XIII's decree */
#define LASTJULDATE 15821004L   /* last day to use Julian calendar */
#define LASTJULJDN  2299160L    /* jdn of same */
#else                           /* British-American usage */
#define LASTJULDATE 17520902L   /* last day to use Julian calendar */
#define LASTJULJDN  2361221L    /* jdn of same */
#endif


static DwInt32 ymd_to_jdnl(int year, int mon, int day, int julian)
{
    DwInt32 jdn;

    if (julian < 0)         /* set Julian flag if auto set */
        julian = (((year * 100L) + mon) * 100 + day  <=  LASTJULDATE);

    if (year < 0)              /* adjust BC year */
        year++;

    if (julian)
        jdn = 367L * year - 7 * (year + 5001L + (mon - 9) / 7) / 4
        + 275 * mon / 9 + day + 1729777L;
    else
        jdn = (DwInt32)(day - 32075)
            + 1461L * (year + 4800L + (mon - 14) / 12) / 4
            + 367 * (mon - 2 - (mon - 14) / 12 * 12) / 12
            - 3 * ((year + 4900L + (mon - 14) / 12) / 100) / 4;

    return jdn;
}


static void jdnl_to_ymd(DwInt32 jdn, int *year, int *mon, int *day, int julian)
{
    DwInt32 x, z, m, d, y;
    DwInt32 daysPer400Years = 146097L;
    DwInt32 fudgedDaysPer4000Years = 1460970L + 31;

    if (julian < 0)                 /* set Julian flag if auto set */
        julian = (jdn <= LASTJULJDN);

    x = jdn + 68569L;
    if (julian) {
        x += 38;
        daysPer400Years = 146100L;
        fudgedDaysPer4000Years = 1461000L + 1;
    }
    z = 4 * x / daysPer400Years;
    x = x - (daysPer400Years * z + 3) / 4;
    y = 4000 * (x + 1) / fudgedDaysPer4000Years;
    x = x - 1461 * y / 4 + 31;
    m = 80 * x / 2447;
    d = x - 2447 * m / 80;
    x = m / 11;
    m = m + 2 - 12 * x;
    y = 100 * (z - 49) + y + x;

    *year = (int)y;
    *mon = (int)m;
    *day = (int)d;

    if (*year <= 0)                   /* adjust BC years */
        (*year)--;
}

#define JDN_JAN_1_1970  2440588L

/*
 * Converts broken-down time to time in seconds since 1 Jan 1970 00:00.
 * Pays no attention to time zone or daylight savings time.  Another way
 * to think about this function is that it is the inverse of gmtime().
 * One word of caution: the values in the broken down time must be
 * correct.
 *
 * This function is different from mktime() in three ways:
 * 1. mktime() accepts a broken-down local time and converts it to a scalar
 *    UTC time.  Thus, mktime() takes time zone and daylight savings time
 *    information into account when computing the scalar time.  (This makes
 *    mktime() highly non-portable).
 * 2. mktime() will adjust for non-standard values, such as a tm_mday member
 *    that is out of range.  This function does no such conversion.
 * 3. mktime() sets the struct fields tm_yday, tm_wday, and tm_isdst to
 *    their correct values on output.  This function does not.
 */
static DwUint32 my_inv_gmtime(struct tm* ptms)
{
    DwInt32 jdn;
    DwUint32 t;

    jdn = ymd_to_jdnl(ptms->tm_year+1900, ptms->tm_mon+1,
        ptms->tm_mday, -1);
    t = jdn - JDN_JAN_1_1970;  /* days    */
    t = 24*t + ptms->tm_hour;  /* hours   */
    t = 60*t + ptms->tm_min;   /* minutes */
    t = 60*t + ptms->tm_sec;   /* seconds */
    return t;
}


