/*
   File:        dates.c
   Author:      A. Moore
   Created:     Tue Jul 23 15:42:17 EDT 2002
   Description: Date processing

   Copyright 2002, The Auton Lab, CMU
*/

/* THIS FILE IS OBSOLETE. ALL OF ITS FUNCTIONALITY
 * NOW RESIDES IN UTILS/AM_TIME.C */
#include "dates.h"

void fake_function_to_appease_gcc(void)
{
return;
}

#ifdef PAIN

void free_date(date *x)
{
  AM_FREE(x,date);
}

void fprintf_date(FILE *s,char *m1,date *x,char *m2)
{
  char *buff;

  buff = mk_printf("%s -> year",m1);
  fprintf_int(s,buff,x->year,m2);
  free_string(buff);

  buff = mk_printf("%s -> day",m1);
  fprintf_int(s,buff,x->day,m2);
  free_string(buff);

  buff = mk_printf("%s -> month",m1);
  fprintf_int(s,buff,x->month,m2);
  free_string(buff);
}

void pdate(date *x)
{
  fprintf_date(stdout,"date",x,"\n");
}

date *mk_copy_date(date *old)
{
  date *nu = AM_MALLOC(date);

  nu -> year = old -> year;
  nu -> day = old -> day;
  nu -> month = old -> month;

  return nu;
}

int month_length_non_leapyear(int month)
{
  static int month_length[] = { 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 ,
                                31 , 30 , 31 };
  my_assert(sizeof(month_length) == 12 * sizeof(int));

  if ( month < 1 || month > 12 )
    my_error("month_length_non_leapyear: month < 1 or month > 12");

  return month_length[month-1];
}

/* Returns TRUE if and only "year" is a leapyear */

bool is_leap_year(int year)
{
  bool result;
  if ( year % 4 != 0 )
    result = FALSE;
  else if ( year % 400 == 0 )
    result = TRUE;
  else if ( year % 100 == 0 )
    result = FALSE;
  else
    result = TRUE;

  return result;
}

/* Input: 1 <= month <= 12.
   Output: Number of days in month in given year. */
int month_to_length(int year,int month)
{
  int result = month_length_non_leapyear(month);
  if ( month == 2 && is_leap_year(year) )
    result += 1;
  return result;
}

/* Input: 1 <= month <= 12.
   Output: Pointer to static string (should not be freed) 
           representing month. */
char *month_to_name(int month)
{
  static char *names[] = { "JAN" , "FEB" , "MAR" , "APR" , "MAY" , "JUN" , 
                           "JUL" , "AUG" , "SEP" , "OCT" , "NOV" , "DEC" };
  my_assert(sizeof(names) == sizeof(char *) * 12);
  my_assert_always(month >= 1 && month <= 12);
  
  return names[month-1];
}

#define MAX_YEAR 2500

/* Input: 1800 <= year <= MAX_YEAR, 1 <= month <= 12,
          1 <= day <= days in month

   Output: Representation of date. */
date *mk_date(int year,int month,int day)
{
  date *x = AM_MALLOC(date);

  if ( year < 1800 ) my_errorf("Can't make a date involving year %d. I only\n"
			       "work with years at or after 1800AD\n",year);

  if ( year > MAX_YEAR ) my_errorf("Can't make a date involving year %d. I only\n"
			       "work with years at or before MAX_YEARAD\n",year);

  if ( month < 1 || month > 12 )
    my_errorf("When making date, month should be >= 1 and <= 12. So %s is\n"
	      "no good.\n",month);

  if ( day < 1 || day > month_to_length(year,month) )
    my_errorf("Can't have day-of-month = %d for month %s of year %d\n",
	      day,month_to_name(month),year);

  x -> year = year;
  x -> day = day;
  x -> month = month;

  return x;
}

/* Accessor */
int date_year(date *d)
{
  return d->year;
}

/* Accessor */
int date_month(date *d)
{
  return d->month;
}

/* Accessor */
int date_day(date *d)
{
  return d->day;
}

int days_since_jan1(date *d)
{
  static int days_before_month[12];
  static bool days_before_month_defined = FALSE;
  if ( !days_before_month_defined )
  {
    int i;
    days_before_month[0] = 0;
    for ( i = 2 ; i <= 12 ; i++ )
      days_before_month[i-1] = days_before_month[i-2] + 
                               month_length_non_leapyear(i-1);
  }

  if ( d->month < 1 || d->month > 12 )
    my_error("days_since_jan1: month < 1 or month > 12");

  return days_before_month[d->month-1] + d->day-1 +
         ((is_leap_year(d->year) && d->month >= 3) ? 1 : 0);
}


/*
   Let X = { a : a >= lo && a < hi && a % n == 0 }
   This function returns |X| (efficiently)

   PRE: hi >= lo
*/
int num_round_between(int lo,int hi,int n)
{
  int q = lo % n;
  int start = (q == 0) ? lo : (lo + n - q); /* start = min member of X */
  int p = hi % n;
  int end = (p == 0) ? (hi - n) : (hi - p); /* end = max member of X */

  my_assert_always(hi >= lo);

  my_assert(end >= start - n);

  return (end + n - start) / n;
}

int leap_years_since_1800(int year)
{
  return num_round_between(1800,year,4) -
         num_round_between(1800,year,100) +
         num_round_between(1800,year,400);
}

/* a daycode is an integer representing a given date.

   Tomorrow is always represented by a daycode of one greater than today,
   thus given two dates, the number of days between them equals the
   difference of their daycodes. */
int date_to_daycode(date *d)
{
  int days = days_since_jan1(d);
  int years = d->year-1800;
  int leapyears = leap_years_since_1800(d->year);
  int result = days + 365 * years + leapyears;
               
  /*
  printf("days = %d\n",days);
  printf("years = %d\n",years);
  printf("leapyears = %d\n",leapyears);
  printf("result = %d\n",result);
  */
  return result;
}

/* If parse error... returns -1 and MAKES and stores message in *r_errmess */
/* If OK sets *r_errmess to NULL */
int string_to_daycode(char *string,char **r_errmess)
{
  date *d = mk_date_from_string(string,r_errmess);
  int result = (d == NULL) ? -1 : date_to_daycode(d);
  if ( d != NULL ) free_date(d);
  return result;
}

/* If parse error... returns -1 and MAKES and stores message in *r_errmess */
/* If OK sets *r_errmess to NULL */
int daycode_from_args(char *key,int argc,char *argv[],int default_daycode,
		      char **r_errmess)
{
  char *defstring = mk_string_from_daycode(default_daycode);
  char *s = string_from_args(key,argc,argv,defstring);
  int daycode = string_to_daycode(s,r_errmess);
  free_string(defstring);
  return daycode;
}

/* Returns number of days from a until b.
   0 if same date
   > 0 if b later than a
   < 0 if b earlier than a
*/
int days_between(date *a,date *b)
{
  return date_to_daycode(b) - date_to_daycode(a);
}

/* day_of_week is a number such that 0 <= day_of_week <= 6 with

   0 represents Monday
   1 represents Tuesday
      :
   5 represents Saturday
   6 represents Sunday

   Input: day_of_week
   Output: Static pointer to string that names day_of_week
*/
char *day_of_week_to_name(int day_of_week)
{
  static char *names[] = { "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN" };
  my_assert(sizeof(names) == sizeof(char *) * 7);
  my_assert_always(day_of_week >= 0 && day_of_week < 7);
  
  return names[day_of_week];
}

/* MAKES and returns a string in "MMM-DD-YYYY" where MMM is the three-letter
   text name of the month */
char *mk_string_from_date(date *d)
{
  return mk_printf("%3s-%02d-%04d",month_to_name(d->month),d->day,d->year);
}

/* MAKES and returns a string in "MMM-DD-YYYY" where MMM is the three-letter
   text name of the month */
char *mk_string_from_daycode(int daycode)
{
  date *d = mk_date_from_daycode(daycode);
  char *s = mk_string_from_date(d);
  free_date(d);
  return s;
}

char upcase_char(char c)
{
  return (c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c;
}

/* Returns the month (1 <= month <= 12) represented by the string.
   Just looks at the first three chars of string, thus

     "decadent" will return 12 (= december)

     Returns -1 if string matches no month */
int name_to_month(char *s)
{
  int result = -1;
  if ( strlen(s) >= 3 )
  {
    int m;
    char c0 = upcase_char(s[0]);
    char c1 = upcase_char(s[1]);
    char c2 = upcase_char(s[2]);

    for ( m = 1 ; result < 0 && m <= 12 ; m++ )
    {
      char *name = month_to_name(m);
      if ( c0 == name[0] && c1 == name[1] && c2 == name[2] )
	result = m;
    }
  }
  return result;
}

date *mk_date_knowing_year(char *s,char *year_string,char *month_string,
			   char *day_string,char **r_errmess)
{
  int year = -77;
  date *d = NULL;

  *r_errmess = NULL;

  if ( *r_errmess == NULL && !is_a_number(year_string) )
    *r_errmess = mk_printf("Cannot find a year in this date: %s. The string\n"
			   "%s doesn't work as a year.\n",s,year_string);

  if ( *r_errmess == NULL )
  {
    year = atoi(year_string);
    if ( year < 1800 || year > MAX_YEAR )
      *r_errmess = mk_printf("I insist on years between 1800 and MAX_YEAR.\n"
			     "The year %s from date string %s does not work\n",
			     year_string,s);
  }

  if ( *r_errmess == NULL )
  {
    int month = -77;
    char *use_day_string = day_string;

    if ( !is_a_number(day_string) )
    {
      month = name_to_month(day_string);
      if ( month >= 0 && is_a_number(month_string) )
	use_day_string = month_string;
      else
	*r_errmess = mk_printf("The day-of-month is %s in date-string %s.\n"
			       "But that is not a number.\n",day_string,s);
    }
    else
    {
      if ( is_a_number(month_string) )
	month = atoi(month_string);
      else
      {
	month = name_to_month(month_string);
	if ( month < 0 )
	  *r_errmess = mk_printf("The month in date-string %s is %s. But\n"
				 "that is not a month I have heard of\n");
      }
    }

    if ( *r_errmess == NULL && (month < 1 || month > 12) )
      *r_errmess = mk_printf("The month in date-string %s is %s. But\n"
			     "it should be between 1 and 12");
    
    if ( *r_errmess == NULL && !is_a_number(use_day_string) )
	*r_errmess = mk_printf("The day-of-month is %s in the date-string %s\n"
			       "but that is not a number",use_day_string,s);

    if ( *r_errmess == NULL )
    {
      int day = atoi(use_day_string);
      if ( day < 1 || day > month_to_length(year,month) )
	*r_errmess = mk_printf("The day-of-month is %s in the date-string %s\n"
			       "but for month %s in year %d it should be\n"
			       "between 1 and %d\n",use_day_string,s,
			       month_to_name(month),year,
			       month_to_length(year,month));
      else
	d = mk_date(year,month,day);
    }
  }

  return d;
}

bool must_be_year(char *s)
{
  return is_a_number(s) && atoi(s) >= 1000;
}

/* Parses a date. A date may be 1,2 or 3 components separated by 
   "ignore characters" where "ignore characters" are :-/ and space

   Figures out which of the components is the year by looking for 
   something between 1800 and max_year

   Then assume the earlier of the two remaining components is the month
   (thus failing to parse UK format dates).

   And the later of the two components is day of month.

   Can handle month being given by name (in any case) using the same
   rules as name_to_month() above.

   It DOES handle sates of the form DD-MMM-YYYY successfully, even those
   are UK style, but only if MMM is text format.

   If a parse error returns NULL, and MAKES and RETURNS an error message
   (which must later be free_string()'d in *r_errmess.

   If happy, returns date d, and sets *r_errmess to NULL.
*/
date *mk_date_from_string(char *s,char **r_errmess)
{
  string_array *sa = mk_broken_string_using_seppers(s," -:/");
  int size = string_array_size(sa);
  date *d = NULL;

  *r_errmess = NULL;

  if ( size == 0 || size > 3 )
    *r_errmess = mk_printf("The string \"%s\" can't be parsed as a date",s);
  else 
  {
    char *s0 = (size>0) ? string_array_ref(sa,0) : NULL;
    char *s1 = (size>1) ? string_array_ref(sa,1) : NULL;
    char *s2 = (size>2) ? string_array_ref(sa,2) : NULL;
    
    if ( size == 3 )
    {
      if ( must_be_year(s0) )
	d = mk_date_knowing_year(s,s0,s1,s2,r_errmess);
      else if ( must_be_year(s1) )
	d = mk_date_knowing_year(s,s1,s0,s2,r_errmess);
      else
	d = mk_date_knowing_year(s,s2,s0,s1,r_errmess);
    }
    else if ( size == 2 )
    {
      if ( must_be_year(s0) )
	d = mk_date_knowing_year(s,s0,s1,"1",r_errmess);
      else
	d = mk_date_knowing_year(s,s1,s0,"1",r_errmess);
    }
    else
      d = mk_date_knowing_year(s,s0,"1","1",r_errmess);
  }

  free_string_array(sa);

  my_assert(d == NULL || *r_errmess == NULL);
  my_assert(d != NULL || *r_errmess != NULL);

  return d;
}

/* Returns 0 for monday, 1 for tuesday, ... 5 for saturday, 6 for sunday */
int daycode_to_day_of_week(int daycode)
{
  int result = (daycode+2) % 7;
  return result;
}

/* Returns 0 for monday, 1 for tuesday, ... 5 for saturday, 6 for sunday */
int date_to_day_of_week(date *d)
{
  int daycode = date_to_daycode(d);
  return daycode_to_day_of_week(daycode);
}

/* Same as mk_date_from_string above, except my_error()'s if parse error */
date *mk_date_from_string_simple(char *s)
{
  char *errmess;
  date *d = mk_date_from_string(s,&errmess);
  if ( d == NULL ) my_error(errmess);
  return d;
}

#define MAX_DAYCODES ((MAX_YEAR + 1 - 1800) * 366)

int global_num_daycodes = -1;
int global_daycode_to_year[MAX_DAYCODES];
int global_daycode_to_month[MAX_DAYCODES];
int global_daycode_to_day[MAX_DAYCODES];

/* Makes the date d such that date_to_daycode(d) = daycode */
date *mk_date_from_daycode(int daycode)
{
  date *d;

  if ( global_num_daycodes < 0 )
  {
    int y;
    int daycode = 0;
    printf("Filling date cache...\n");
    for ( y = 1800 ; y <= MAX_YEAR ; y++ )
    {
      int m;
      for ( m = 1 ; m <= 12 ; m++ )
      {
	int day;
	int days_in_month = month_to_length(y,m);
	for ( day = 1 ; day <= days_in_month ; day++ )
	{
	  my_assert(daycode < MAX_DAYCODES);
	  global_daycode_to_year[daycode] = y;
	  global_daycode_to_month[daycode] = m;
	  global_daycode_to_day[daycode] = day;
	  daycode += 1;
	}
      }
    }
    printf("...finished filling date cache\n");
    global_num_daycodes = daycode;
  }

  if ( daycode < 0 )
    my_errorf("mk_date_from_daycode: daycode = %d < 0",daycode);
  else if ( daycode >= global_num_daycodes )
    my_errorf("mk_date_from_daycode: daycode = %d would go beyond MAX_YEAR",
	      daycode);

  d = mk_date(global_daycode_to_year[daycode],
	      global_daycode_to_month[daycode],
	      global_daycode_to_day[daycode]);

  my_assert(daycode == date_to_daycode(d));

  return d;
}

/* Change d so that it is "num_days" later.
   If num_days is -ve, moves to appropriate earlier date. */
void increment_date(date *d,int num_days)
{
  int old_daycode = date_to_daycode(d);
  int new_daycode = old_daycode + num_days;
  date *temp = mk_date_from_daycode(new_daycode);
  d->year = temp->year;
  d->month = temp->month;
  d->day = temp->day;
  free_date(temp);
}

void date_main(int argc,char *argv[])
{
  char *s = string_from_args("date",argc,argv,NULL);
  if ( s == NULL )
    my_errorf("You must put date <string> on command line");
  else
  {
    date *d = mk_date_from_string_simple(s);
    char *s2 = mk_string_from_date(d);
    int daycode = date_to_daycode(d);
    int day_of_week = date_to_day_of_week(d);
    date *d2 = mk_date_from_daycode(daycode);
    char *d2_string = mk_string_from_date(d2);

    pdate(d);

    printf("daycode = %d\nday_of_week = %d\n",daycode,day_of_week);

    printf("The string %s is parsed as date %s, which is a %s\n",
	   s,s2,day_of_week_to_name(day_of_week));

    printf("mk_date_from_daycode(%d) = %s\n",daycode,d2_string);
	   
    free_string(s2);
    free_string(d2_string);
    free_date(d);
    free_date(d2);
  }
}

/************ Begin dates *************/

void void_free_date(void *data)
{
  free_date((date *)data);
}

void *void_mk_copy_date(void *data)
{
  return (void *) mk_copy_date((date *)data);
}

void void_fprintf_date(FILE *s,char *m1,void *data,char *m2)
{
  fprintf_date(s,m1,(date *) data,m2);
}

dates *mk_empty_dates(void)
{
  dates *array = AM_MALLOC(dates);
  array -> genarr = mk_empty_generic_array(void_free_date,
					   void_mk_copy_date,
					   void_fprintf_date);
  return array;
}

void add_to_dates(dates *array,date *element)
{
  add_to_generic_array(array->genarr,(void *)element);
}

void add_pointer_to_dates(dates *array,date *element)
{
  add_pointer_to_generic_array(array->genarr,(void *)element);
}

int dates_size(dates *array)
{
  return(generic_array_size(array->genarr));
}

date *dates_ref(dates *array,int index)
{
  return (date *) generic_array_ref(array->genarr,index);
}
  
void fprintf_dates(FILE *s,char *m1,dates *array,char *m2)
{
  fprintf_generic_array(s,m1,array->genarr,m2);
}

void pdates(dates *array)
{
  fprintf_dates(stdout,"dates",array,"\n");
}

void free_dates(dates *array)
{
  free_generic_array(array->genarr);
  AM_FREE(array,dates);
}

dates *mk_copy_dates(dates *array)
{
  dates *new_dates = AM_MALLOC(dates);
  new_dates -> genarr = mk_copy_generic_array(array->genarr);
  return new_dates;
}

void dates_set(dates *array,int index,date *d)
{
  generic_array_set(array->genarr,index,(void *) d);
}

void dates_remove(dates *array,int index)
{
  generic_array_remove(array->genarr,index);
}

/************** End dates ******************/
#endif 
