/********************************************************************\
 * date.c -- utility functions to handle the date (adjusting, get   * 
 *           current date, etc.) for xacc (X-Accountant)            *
 * Copyright (C) 1997 Robin D. Clark                                *
 * Copyright (C) 1998, 1999, 2000 Linas Vepstas                     *
 *                                                                  *
 * This program is free software; you can redistribute it and/or    *
 * modify it under the terms of the GNU General Public License as   *
 * published by the Free Software Foundation; either version 2 of   *
 * the License, or (at your option) any later version.              *
 *                                                                  *
 * This program 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 General Public License for more details.                     *
 *                                                                  *
 * You should have received a copy of the GNU General Public License*
 * along with this program; if not, contact:                        *
 *                                                                  *
 * Free Software Foundation           Voice:  +1-617-542-5942       *
 * 59 Temple Place - Suite 330        Fax:    +1-617-542-2652       *
 * Boston, MA  02111-1307,  USA       gnu@gnu.org                   *
 *                                                                  *
 *   Author: Rob Clark rclark@cs.hmc.edu                            *
 *                                                                  * 
\********************************************************************/

#define _GNU_SOURCE
#define __EXTENSIONS__

#include "config.h"

#include <ctype.h>

#ifdef HAVE_LANGINFO_D_FMT
#include <langinfo.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <glib.h>

#include "date.h"
#include "gnc-engine-util.h"

#define NANOS_PER_SECOND 1000000000

#ifdef HAVE_LANGINFO_D_FMT
#  define GNC_D_FMT (nl_langinfo (D_FMT))
#else
#  define GNC_D_FMT "%Y-%m-%d"
#endif

/* This is now user configured through the gnome options system() */
static DateFormat dateFormat = DATE_FORMAT_US;

/* This static indicates the debugging module that this .o belongs to. */
static short module = MOD_ENGINE;


/********************************************************************\
\********************************************************************/

static void
timespec_normalize(Timespec *t)
{
  if(t->tv_nsec > NANOS_PER_SECOND)
  {
    t->tv_sec+= (t->tv_nsec / NANOS_PER_SECOND);
    t->tv_nsec= t->tv_nsec % NANOS_PER_SECOND;
  }

  if(t->tv_nsec < - NANOS_PER_SECOND)
  {
    t->tv_sec+= - (-t->tv_nsec / NANOS_PER_SECOND);
    t->tv_nsec = - (-t->tv_nsec % NANOS_PER_SECOND);
  }

  if (t->tv_sec > 0 && t->tv_nsec < 0)
  {
    t->tv_sec--;
    t->tv_nsec = NANOS_PER_SECOND + t->tv_nsec;
  }
  
  if (t->tv_sec < 0 && t->tv_nsec > 0)
  {
    t->tv_sec++;
    t->tv_nsec = - NANOS_PER_SECOND + t->tv_nsec;
  }
  return;
}
  

gboolean
timespec_equal (const Timespec *ta, const Timespec *tb)
{
  if(ta == tb) return TRUE;
  if(ta->tv_sec != tb->tv_sec) return FALSE;
  if(ta->tv_nsec != tb->tv_nsec) return FALSE;
  return TRUE;
}

gint
timespec_cmp(const Timespec *ta, const Timespec *tb)
{
  if(ta == tb) return 0;
  if(ta->tv_sec < tb->tv_sec) return -1;
  if(ta->tv_sec > tb->tv_sec) return 1;
  if(ta->tv_nsec < tb->tv_nsec) return -1;
  if(ta->tv_nsec > tb->tv_nsec) return 1;
  return 0;
}

Timespec
timespec_diff(const Timespec *ta, const Timespec *tb)
{
  Timespec retval;
  retval.tv_sec = ta->tv_sec - tb->tv_sec;
  retval.tv_nsec = ta->tv_nsec - tb->tv_nsec;
  timespec_normalize(&retval);
  return retval;
}

Timespec
timespec_abs(const Timespec *t)
{
  Timespec retval = *t;

  timespec_normalize(&retval);
  if (retval.tv_sec < 0)
  {
    retval.tv_sec = - retval.tv_sec;
    retval.tv_nsec = - retval.tv_nsec;
  }
  
  return retval;
}

/**
 * timespecCanonicalDayTime
 * given a timepair contains any time on a certain day (local time)
 * converts it to be midday that day.  
 */

Timespec
timespecCanonicalDayTime(Timespec t)
{
  struct tm tm, *result;
  Timespec retval;
  time_t t_secs = t.tv_sec + (t.tv_nsec / NANOS_PER_SECOND);
  result = localtime(&t_secs);
  tm = *result;
  tm.tm_sec = 0;
  tm.tm_min = 0;
  tm.tm_hour = 12;
  tm.tm_isdst = -1;
  retval.tv_sec = mktime(&tm);
  retval.tv_nsec = 0;
  return retval;
}
    
/**
 * setDateFormat
 * set date format to one of US, UK, CE, OR ISO
 * checks to make sure it's a legal value
 * Args: DateFormat: enumeration indicating preferred format
 * returns: nothing
 *
 * Globals: dateFormat
 **/

void setDateFormat(DateFormat df)
{
  if(df >= DATE_FORMAT_FIRST && df <= DATE_FORMAT_LAST)
  {
    dateFormat = df;
  }
  else
  {    /* hack alert - is this what we should be doing here? */
    PERR("non-existent date format set");
  }

  return;
}

/**
 * printDate
 *    Convert a date as day / month / year integers into a localized string
 *    representation
 *
 * Args:   buff - pointer to previously allocated character array; its size
 *                must be at lease MAX_DATE_LENTH bytes.
 *         day - day of the month as 1 ... 31
 *         month - month of the year as 1 ... 12
 *         year - year (4-digit)
 *
 * Return: nothing
 *
 * Globals: global dateFormat value
 */
void 
printDate (char * buff, int day, int month, int year)
{
  if (!buff) return;

  /* Note that when printing year, we use %-4d in format string;
   * this causes a one, two or three-digit year to be left-adjusted
   * when printed (i.e. padded with blanks on the right).  This is 
   * important while the user is editing the year, since erasing a 
   * digit can temporarily cause a three-digit year, and having the 
   * blank on the left is a real pain for the user.  So pad on the 
   * right.
   */
  switch(dateFormat)
  {
    case DATE_FORMAT_UK:
      sprintf (buff, "%2d/%2d/%-4d", day, month, year);
      break;
    case DATE_FORMAT_CE:
      sprintf (buff, "%2d.%2d.%-4d", day, month, year);
      break;
    case DATE_FORMAT_ISO:
      sprintf (buff, "%04d-%02d-%02d", year, month, day);
      break;
    case DATE_FORMAT_LOCALE:
      {
        struct tm tm_str;

        tm_str.tm_mday = day;
        tm_str.tm_mon = month - 1;    /* tm_mon = 0 through 11 */
        tm_str.tm_year = year - 1900; /* this is what the standard 
                                       * says, it's not a Y2K thing */
        tm_str.tm_hour = 0;
        tm_str.tm_min = 0;
        tm_str.tm_sec = 0;
        tm_str.tm_isdst = -1;

        strftime (buff, MAX_DATE_LENGTH, GNC_D_FMT, &tm_str);
      }
      break;

    case DATE_FORMAT_US:
    default:
      sprintf (buff, "%2d/%2d/%-4d", month, day, year);
      break;
  }
}

void 
printDateSecs (char * buff, time_t t)
{
  struct tm *theTime;

  if (!buff) return;

  theTime = localtime (&t);

  printDate (buff, theTime->tm_mday, 
                   theTime->tm_mon + 1,
                   theTime->tm_year + 1900);
}

char * 
xaccPrintDateSecs (time_t t)
{
   char buff[100];
   printDateSecs (buff, t);
   return g_strdup (buff);
}

const char *
gnc_print_date (Timespec ts)
{
  static char buff[MAX_DATE_LENGTH];
  time_t t;

  t = ts.tv_sec + (ts.tv_nsec / 1000000000.0);

  printDateSecs (buff, t);

  return buff;
}

/**
 * scanDate
 *    Convert a string into  day / month / year integers according to
 *    the current dateFormat value.
 *
 * Args:   buff - pointer to date string
 *         day -  will store day of the month as 1 ... 31
 *         month - will store month of the year as 1 ... 12
 *         year - will store the year (4-digit)
 *
 * Return: nothing
 *
 * Globals: global dateFormat value
 */
void
scanDate (const char *buff, int *day, int *month, int *year)
{
   char *dupe, *tmp, *first_field, *second_field, *third_field;
   int iday, imonth, iyear;
   struct tm *now;
   time_t secs;

   if (!buff) return;

   dupe = g_strdup (buff);

   tmp = dupe;
   first_field = NULL;
   second_field = NULL;
   third_field = NULL;

   /* use strtok to find delimiters */
   if (tmp) {
      first_field = strtok (tmp, ".,-+/\\()");
      if (first_field) {
         second_field = strtok (NULL, ".,-+/\\()");
         if (second_field) {
            third_field = strtok (NULL, ".,-+/\\()");
         }
      }
   }

   /* if any fields appear blank, use today's date */
   time (&secs);
   now = localtime (&secs);
   iday = now->tm_mday; 
   imonth = now->tm_mon+1;
   iyear = now->tm_year+1900;

   /* get numeric values */
   switch (dateFormat)
   {
     case DATE_FORMAT_LOCALE:
       if (buff[0] != '\0')
       {
         struct tm thetime;

         strptime (buff, GNC_D_FMT, &thetime);

         iday = thetime.tm_mday;
         imonth = thetime.tm_mon + 1;
         iyear = thetime.tm_year + 1900;
       }
       break;
     case DATE_FORMAT_UK:
     case DATE_FORMAT_CE:
       if (first_field) iday = atoi (first_field);
       if (second_field) imonth = atoi (second_field);
       if (third_field) iyear = atoi (third_field);
       break;
     case DATE_FORMAT_ISO:
       if (first_field) iyear = atoi (first_field);
       if (second_field) imonth = atoi (second_field);
       if (third_field) iday = atoi (third_field);
       break;
     case DATE_FORMAT_US:
     default:
       if (first_field) imonth = atoi (first_field);
       if (second_field) iday = atoi (second_field);
       if (third_field) iyear = atoi (third_field);
       break;
   }

   g_free (dupe);

   /* if the year entered is smaller than 100, assume we mean the current
      century (and are not revising some roman emperor's books) */
   if (iyear < 100)
     iyear += ((int) ((now->tm_year+1950-iyear)/100)) * 100;

   if (year) *year=iyear;
   if (month) *month=imonth;
   if (day) *day=iday;
}

/**
 * dateSeparator
 *    Return the field separator for the current date format
 *
 * Args:   none
 *
 * Return: date character
 *
 * Globals: global dateFormat value
 */
char dateSeparator ()
{
  static char locale_separator = '\0';

  switch (dateFormat)
  {
    case DATE_FORMAT_CE:
      return '.';
    case DATE_FORMAT_ISO:
      return '-';
    case DATE_FORMAT_US:
    case DATE_FORMAT_UK:
    default:
      return '/';
    case DATE_FORMAT_LOCALE:
      if (locale_separator != '\0')
        return locale_separator;
      else
      { /* Make a guess */
        char string[256];
        struct tm *tm;
        time_t secs;
        char *s;

        secs = time(NULL);
        tm = localtime(&secs);
        strftime(string, sizeof(string), GNC_D_FMT, tm);

        for (s = string; s != '\0'; s++)
          if (!isdigit(*s))
            return (locale_separator = *s);
      }
  }

  return '\0';
}

/********************************************************************\
 * iso 8601 datetimes should look like 1998-07-02 11:00:00.68-05
\********************************************************************/
/* hack alert -- this routine returns incorrect values for 
 * dates before 1970 */

static Timespec
gnc_iso8601_to_timespec(const char *str, int do_localtime)
{
  char buf[4];
  Timespec ts;
  struct tm stm;
  long int nsec =0;

  ts.tv_sec=0;
  ts.tv_nsec=0;
  if (!str) return ts;

  stm.tm_year = atoi(str) - 1900;
  str = strchr (str, '-'); if (str) { str++; } else { return ts; }
  stm.tm_mon = atoi(str) - 1;
  str = strchr (str, '-'); if (str) { str++; } else { return ts; }
  stm.tm_mday = atoi(str);

  str = strchr (str, ' '); if (str) { str++; } else { return ts; }
  stm.tm_hour = atoi(str);
  str = strchr (str, ':'); if (str) { str++; } else { return ts; }
  stm.tm_min = atoi(str);
  str = strchr (str, ':'); if (str) { str++; } else { return ts; }
  stm.tm_sec = atoi (str);

  /* the decimal point, optionally present ... */
  /* hack alert -- this algo breaks if more than 9 decimal places present */
  if (strchr (str, '.')) 
  { 
     int decimals, i, multiplier=1000000000;
     str = strchr (str, '.') +1;
     decimals = strcspn (str, "+- ");
     for (i=0; i<decimals; i++) multiplier /= 10;
     nsec = atoi(str) * multiplier;
  }
  stm.tm_isdst = -1;

  /* timezone format can be +hh or +hhmm or +hh.mm (or -) */
  str += strcspn (str, "+-");
  buf[0] = str[0];
  buf[1] = str[1];
  buf[2] = str[2];
  buf[3] = 0;
  stm.tm_hour -= atoi(buf);

  str +=3;
  if ('.' == *str) str++;
  if (isdigit (*str) && isdigit (*(str+1)))
  {
     int cyn;
     /* copy sign from hour part */
     if ('+' == buf[0]) { cyn = -1; } else { cyn = +1; } 
     buf[0] = str[0];
     buf[1] = str[1];
     buf[2] = str[2];
     buf[3] = 0;
     stm.tm_min += cyn * atoi(buf);
  }

  /* adjust for the local timezone */
  if (do_localtime)
  {
    struct tm *tm;
    int tz_hour;
    time_t secs = mktime (&stm);

    /* The call to localtime is 'bogus', but it forces 'timezone' to be set.
     * Note that we must use the accurate date, since the value of 'gnc_timezone'
     * includes daylight savings corrections for that date. */
    tm = localtime (&secs);   
    tz_hour = gnc_timezone(tm)/3600;
    stm.tm_hour -= tz_hour;
    stm.tm_min -= (gnc_timezone(tm) - 3600*tz_hour)/60;
  }

  /* compute number of seconds */
  ts.tv_sec = mktime (&stm);
  ts.tv_nsec = nsec;

  return ts;
}

Timespec
gnc_iso8601_to_timespec_local(const char *str)
{
   return gnc_iso8601_to_timespec(str, 1);
}

Timespec
gnc_iso8601_to_timespec_gmt(const char *str)
{
   return gnc_iso8601_to_timespec(str, 0);
}

/********************************************************************\
\********************************************************************/

char * 
gnc_timespec_to_iso8601_buff (Timespec ts, char * buff)
{
  int len;
  int tz_hour, tz_min;
  char cyn;
  time_t tmp;
  struct tm parsed;

  tmp = ts.tv_sec;
  localtime_r(&tmp, &parsed);

  tz_hour = gnc_timezone (&parsed) / 3600;
  tz_min = (gnc_timezone (&parsed) - 3600*tz_hour) / 60;
  if (0>tz_min) { tz_min +=60; tz_hour --; }
  if (60<=tz_min) { tz_min -=60; tz_hour ++; }

  /* we also have to print the sign by hand, to work around a bug
   * in the glibc 2.1.3 printf (where %+02d fails to zero-pad)
   */
  cyn = '-';
  if (0>tz_hour) { cyn = '+'; tz_hour = -tz_hour; }

  len = sprintf (buff, "%4d-%02d-%02d %02d:%02d:%02d.%06ld %c%02d%02d",
                 parsed.tm_year + 1900,
                 parsed.tm_mon + 1,
                 parsed.tm_mday,
                 parsed.tm_hour,
                 parsed.tm_min,
                 parsed.tm_sec,
                 ts.tv_nsec / 1000,
                 cyn,
                 tz_hour,
                 tz_min);

  /* return pointer to end of string */
  buff += len;
  return buff;
}


/********************************************************************\
\********************************************************************/
/* hack alert -- this routine returns incorrect values for 
 * dates before 1970 */

time_t 
xaccDMYToSec (int day, int month, int year)
{
  struct tm stm;
  time_t secs;

  stm.tm_year = year - 1900;
  stm.tm_mon = month - 1;
  stm.tm_mday = day;
  stm.tm_hour = 0;
  stm.tm_min = 0;
  stm.tm_sec = 0;
  stm.tm_isdst = -1;

  /* compute number of seconds */
  secs = mktime (&stm);

  return secs;
}

time_t
xaccScanDateS (const char *str)
{
  int month, day, year;

  scanDate (str, &day, &month, &year);

  return xaccDMYToSec (day,month,year);
}

#define THIRTY_TWO_YEARS 0x3c30fc00LL

static Timespec
gnc_dmy2timespec_internal (int day, int month, int year, gboolean start_of_day)
{
  Timespec result;
  struct tm date;
  long long secs = 0;
  long long era = 0;

  year -= 1900;

  /* make a crude attempt to deal with dates outside the range of Dec
   * 1901 to Jan 2038. Note we screw up centennial leap years here so
   * hack alert */
  if ((2 > year) || (136 < year)) 
  {
    era = year / 32;
    year %= 32;
    if (0 > year) { year += 32; era -= 1; } 
  }

  date.tm_year = year;
  date.tm_mon = month - 1;
  date.tm_mday = day;

  if (start_of_day)
  {
    date.tm_hour = 0;
    date.tm_min = 0;
    date.tm_sec = 0;
  }
  else
  {
    date.tm_hour = 23;
    date.tm_min = 59;
    date.tm_sec = 59;
  }

  date.tm_isdst = -1;

  /* compute number of seconds */
  secs = mktime (&date);

  secs += era * THIRTY_TWO_YEARS;

  result.tv_sec = secs;
  result.tv_nsec = 0;

  return result;
}

Timespec
gnc_dmy2timespec (int day, int month, int year)
{
  return gnc_dmy2timespec_internal (day, month, year, TRUE);
}

Timespec
gnc_dmy2timespec_end (int day, int month, int year)
{
  return gnc_dmy2timespec_internal (day, month, year, FALSE);
}

/********************************************************************\
\********************************************************************/

long int
gnc_timezone (struct tm *tm)
{
  g_return_val_if_fail (tm != NULL, 0);

#ifdef HAVE_STRUCT_TM_GMTOFF
  /* tm_gmtoff is seconds *east* of UTC and is
   * already adjusted for daylight savings time. */
  return -(tm->tm_gmtoff);
#else
  /* timezone is seconds *west* of UTC and is
   * not adjusted for daylight savings time.
   * In Spring, we spring forward, wheee! */
  return timezone - (tm->tm_isdst > 0 ? 60 * 60 : 0);
#endif
}


void
timespecFromTime_t( Timespec *ts, time_t t )
{
    ts->tv_sec = t;
    ts->tv_nsec = 0;
}

/********************** END OF FILE *********************************\
\********************************************************************/
