/*
   <h3>A library package for handling time and date adjustment</h3>
   <p>- <i>Written by M.C.Gregorie, 2011.</i></p>

   <p>The functions in this package are based on the standard C date and
   time functions. Their purpose is to make date adjustments easier,
   e.g. to create a date 5 hours earlier or a month later than a base date,
   which defaults to 'now'. These functions operate on an internal
   stored time_t date/time value.
   There are three groups of functions in this package:</p>

   <p>Functions for setting the stored date/time:</p>
   <ul>
    <li>ajsetnow</li>
    <li>ajsetdate</li>
   </ul>
   
   <p>Functions for adjusting the stored date/time</p>
   <ul>
    <li>ajoffsetdate</li>
    <li>ajmonth</li>
    <li>ajyear</li>
   </ul>

   <p>Control functions</p>
   <ul>
    <li>ajdebug</li>
    <li>ajerror</li>
    <li>ajgeterror</li>
    <li>ajtruncate</li>
   </ul>
   
   <p>Functions for outputting the stored date/time:</p>
   <ul>
    <li>ajgetdate</li>
    <li>ajformatdate</li>
   </ul>

   <p>All functions operate on a hidden global base date, so
   successive operations can be applied to make complex changes
   to a date. The ajgetdate() and ajsetdate() functions can be used
   to preserve the current base date value while you adjust other
   date(s).</p>

   <p>The initial basedate value is set to zero, so you should always
   call ajsetnow() or ajsetdate() to initialise the base date before
   applying changes to it.</p> 
   
   <p>This collection of functions is compiled to form part
   of the <b>libenviron.a</b> library, which must be linked
   if any of of its constituent functions are called.</p>

   <h3>Compilation</h3>
        
   <p>The calling programs should include <i>adjustdate.h</i> and must be
   linked using <i>-lenviron</i>.</p>

   <kbd>
   <b>environ - C programming support functions</b><br>
   Copyright (C) 2005-2011  Martin C Gregorie
   
   This program is free software; you can redistribute it and/or
   modify it under the terms of the Lesser GNU General Public License.
   
   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 Lesser GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
   </kbd>
  
   <p>Contact Martin Gregorie at <i>martin@gregorie.org</i></p>
*/

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>

#include "adjustdate.h"

/*
   Private  definitions
   ====================
*/
#define MAXFMTLTH    80

/*
   Globals used only within this source file
   =========================================
*/
static time_t  basedate     = 0;
static char    *errmsg      = "";
static int     debug        = 0;
static int     truncate     = OFF;

/*
   Prototypes for private functions
   ================================
*/
char *dump_action(int action);
char *dump_basedate();
char *dump_day(int day);
char *dump_element(int element);
char *dump_tm(struct tm *stm);
void fixlastday(struct tm *cd);
int getlastday(int month, int year);

/*
   Functions
   =========
*/

/* 
   Set debugging conditions.
   
   When the debug level is zero no tracing occurs.
   A debug level of 1 traces calls to the functions in this
   package and is intended to help debugging the calling program.
   
   A debug level of 2 or 3 can be used to trace the
   internals of the more complex functions on this package.
*/
void ajdebug(int level)
{
   errmsg="";
   debug = level;
}

/*
   Returns TRUE if the last adjustdate function call set an
   error condition.

   Calling ajgeterror() returns the error.
*/
int ajerror()
{
   return strlen(errmsg);
}

/*
   Return a string describing the last error detected.
*/
char *ajgeterror()
{
   return errmsg;
}

/*
   Return the current base date as a local time string, formatted
   as defined in the supplied format string. The format string
   follows the conventions used by strftime.
   
   Sets an error condition if the required string is too long
   for the internal formatted date buffer.
*/
char *ajformatdate(char *format)
{
   struct tm   *cd;
   static char str[MAXFMTLTH+1];
   size_t n;

   errmsg = "";
   cd = localtime(&basedate);
   n = strftime((char*)&str, MAXFMTLTH, format, cd); 
   if (n >= MAXFMTLTH)
      errmsg = "formatted date truncated";

   if (debug)
      logmsg("'%s' = ajformatdate('%s')", str, format);

   return str;   
}

/*
   Return the current basedate value.
*/
time_t ajgetdate()
{
   errmsg="";
   if (debug)
      logmsg("%d = ajgetdate()", basedate);

   return basedate;
}

/*
   Move the date by 'offset' months, adjusting the year as needed.
   Offset may be signed: a positive value increments the month number.
   The day of the month is set to the 'day' value after offsetting the
   month and is checked to be in the range 1 to the last day of the month.

   All range checks on the last day of the month take leap years into account.

   Since it may difficult to determine the last day of the month you
   are altering, you can also use the constants FIRSTDAY, LASTDAY and
   KEEPDAY. The latter causes the day of month to remain unchanged, but
   if the day would be outside the target month, the state of the
   'truncate' flag is used to determine whether to truncate
   the date to the last day of the month or to set the date to a day at
   the start of the following month.

   See also ajoffsetdate() and ajtruncate().

   Sets an error condition if errors are discovered and leaves the date
   unchanged.
*/  
void ajmonth(int offset, int day)
{
   struct tm   *cd;
   int         m;
   time_t      savedate;

   if (debug > 1)
      logmsg("ajmonth(%d,%s) start", offset, dump_day(day)); 
   
   errmsg="";
   savedate = basedate;
   m = offset;
   cd = localtime(&basedate);
   if (debug > 2)
      logmsg("ajmonth start:      %s", dump_tm(cd)); 

   while (m < 0)
   {
      cd->tm_mon--;
      if (cd->tm_mon < 0)
      {
         cd->tm_mon = 11;
         cd->tm_year--;
      }

      m++;
   }
   
   while (m > 0)
   {
      cd->tm_mon++;
      if (cd->tm_mon > 11)
      {
         cd->tm_mon = 0;
         cd->tm_year++;
      }

      m--;
   }

   switch (day)
   {
      case FIRSTDAY: cd->tm_mday = 1; break;
      case LASTDAY:  cd->tm_mday = getlastday(cd->tm_mon, cd->tm_year); break;
      case KEEPDAY:  fixlastday(cd); 
                     break;
      default:       if (day > 0 && day <= 31)
                     {
                        cd->tm_mday = day;
                        fixlastday(cd);
                     }
                     else
                        errmsg = "Day not FIRST/LAST/KEEPDAY or 1-31";
   }
      
   if (debug > 2)
      logmsg("ajmonth adjusted:   %s", dump_tm(cd));

   /*
      If errors were detected, leave the date unchanged.
      Otherwise, Store the revised date/time in basedate.
   */
   if (ajerror())
      basedate = savedate;
   else
      basedate = mktime(cd);
    
   if (debug > 2)
      logmsg("ajmonth normalised: %s", dump_tm(cd)); 

   if (debug)
      logmsg("ajmonth(%d,%s) -> %s",
             offset,
             dump_day(day),
             dump_basedate()); 
}

/*
   Adjust a date/time by adding the signed offset to the element
   of time, which may be DAY, HOUR, MIN, or SEC.

   Sets an error condition if errors are discovered.
*/   
void ajoffsetdate(int offset, int element)
{
   errmsg = "";
   if (debug > 1)
      logmsg("ajoffsetdate(%d,%s) start at %s",
             offset,
             dump_element(element),
             dump_basedate());
              
   switch(element)
   {
      case SEC:  basedate += offset;         break;
      case MIN:  basedate += offset * 60;    break;
      case HOUR: basedate += offset * 3600;  break;
      case DAY:  basedate += offset * 86400; break;
      default:   errmsg = "Invalid time element value";
   }
   
   if (debug)
      logmsg("ajoffsetdate(%d,%s) -> %s",
             offset,
             dump_element(element),
             dump_basedate());
}

/*
   Set the base date and time to 'now'.
*/
void   ajsetnow()
{
   errmsg = "";
   time(&basedate);
   if (debug)
      logmsg("ajsetnow() - %d", basedate);
}

/*
   Set the base date to a known time.
*/
void   ajsetdate(time_t dt)
{
   errmsg="";
   basedate = dt;
   if (debug)
      logmsg("ajsetdate(%d) - %d", dt, basedate);
}

/*
   Set the 'truncate' flag.

   If the flag is ON, days that are outside the permitted range for
   the current month will be set to the last day of the month.

   If the flag is OFF, the month number is incremented, which
   may also affect the year, and the day of the month is set to
   the difference between its original value and the final day
   of the originally selected month.

   Leap years are included in this calculation.

   The default setting for the 'truncate' flag is OFF.

   Sets an error condition if the required setting is not ON or OFF. 
*/

void ajtruncate(int setting)
{
   errmsg = "";
   if (setting == ON || setting == OFF)
      truncate = setting;
   else
      errmsg = "Truncate flag can only be ON or OFF";
}

/*
   Move the date by 'offset' years, leaving the time unchanged.
   Offset may be signed: a positive value increments the year.
   'action' must be one of the constants FIRSTDAY, LASTDAY and
   KEEPDATE. It specifies the changes, if any, to be made to the
   month and day of month. FIRSTDAY sets the date to the 1st of
   January for the adjusted year while LASTDAY sets the date to
   the 31st of December for the adjusted year.

   KEEPDATE leaves the month and day of month unchanged, but can
   be affected by day of the month truncation if the offset moves the
   date from a leap year to a non-leap year and the the original
   date was the 29th of February.

   See also ajtruncate().

   You can make additional adjustments with ajmonth() and ajoffsetdate().

   Sets an error condition if errors are discovered.
*/   
void ajyear(int offset, int action)
{
   struct tm   *cd;
   int         m;

   if (debug > 1)
      logmsg("ajyear(%d,%s) start", offset, dump_action(action)); 
   
   errmsg="";
   cd = localtime(&basedate);
   if (debug > 2)
      logmsg("ajyear start:      %s", dump_tm(cd)); 

   cd->tm_year += offset;
   switch (action)
   {
      case FIRSTDAY: cd->tm_mday = 1; cd->tm_mon = 0;   break;
      case LASTDAY:  cd->tm_mday = 31; cd->tm_mon = 11; break;
      case KEEPDATE: break;
      default:       errmsg = "Invalid action specified";
   }

   fixlastday(cd);                     
   if (debug > 2)
      logmsg("ajyear adjusted:   %s", dump_tm(cd));

   /*
      Only update the basedate if no errors occurred.
   */
   if (!ajerror())
      basedate = mktime(cd);
    
   if (debug > 2)
      logmsg("ajyear normalised: %s", dump_tm(cd)); 

   if (debug)
      logmsg("ajyear(%d,%s) -> %s",
             offset,
             dump_action(action),
             dump_basedate()); 
}

/*
   An internal function used to display an action argument.
*/
char *dump_action(int action)
{
   static char str[9];

   switch(action)
   {
      case FIRSTDAY: strcpy(str, "FIRSTDAY");   break;
      case LASTDAY:  strcpy(str, "LASTDAY");    break;
      case KEEPDAY:  strcpy(str, "KEEPDATE");   break;
      default:       snprintf(str, 8, "%d", action);
   }

   return str;
}

/*
   An internal function used to display the basedate.
*/
char *dump_basedate()
{  
   static char str[MAXFMTLTH+1];
   struct tm *dt = localtime(&basedate);
   strftime(str, MAXFMTLTH, "%04Y-%02m-%02e %02H:%02M:%02S", dt);
   return str;
}

/*
   An internal function used to display a day argument.
*/
char *dump_day(int day)
{
   static char str[9];

   switch(day)
   {
      case FIRSTDAY: strcpy(str, "FIRSTDAY"); break;
      case LASTDAY:  strcpy(str, "LASTDAY");  break;
      case KEEPDAY:  strcpy(str, "KEEPDAY");  break;
      default:       snprintf(str, 8, "%d", day);
   }

   return str;
}

/*
   An internal function used to display a time element argument.
*/
char *dump_element(int element)
{
   static char str[25];

   switch(element)
   {
      case SEC:   strcpy(str, "SEC");  break;
      case MIN:   strcpy(str, "MIN");  break;
      case HOUR:  strcpy(str, "HOUR"); break;
      case DAY:   strcpy(str, "DAY");  break;
      default:    snprintf(str, 24, "%d", element);
   }

   return str;
}

/*
   An internal function used to display the tm structure content.
*/
char *dump_tm(struct tm *stm)
{
   static char str[50];

   snprintf(str, 50,
            "%s %04d-%02d-%02d %02d:%02d:%02d wkd=%d yrd=%d",
            (stm->tm_isdst ? "DST," : "" ),
            stm->tm_year,
            stm->tm_mon,
            stm->tm_mday,
            stm->tm_hour,
            stm->tm_min,
            stm->tm_sec,
            stm->tm_wday,
            stm->tm_yday);
   return str;
}

/*
   An internal function used to correct cases where the day of month
   is outside the day range for the month.
*/
void fixlastday(struct tm *cd)
{
   int orig_day   = cd->tm_mday;
   int orig_month = cd->tm_mon;
   int orig_year  = cd->tm_year;
   int lastday    = getlastday(cd->tm_mon, cd->tm_year);
   int diff       = cd->tm_mday - lastday;

   if (debug > 1)
      logmsg("fixlastday: %d:%d:%d last=%d diff=%d",
             orig_year + 1900, orig_month, orig_day, lastday, diff);

   if (diff > 0)
   {
      cd->tm_mday = lastday;
      if (!truncate)
      {
         cd->tm_mday = diff;
         cd->tm_mon++;
         if (cd->tm_mon > 11)
         {
            cd->tm_mon = 0;
            cd->tm_year++;
         }
      }

      if (debug > 1)
         logmsg("fixlastday: %d:%d:%d -> %d:%d:%d",
                orig_year + 1900,  orig_month, orig_day,
                cd->tm_year +1900, cd->tm_mon, cd->tm_mday);
   }

}

/*
   An internal function used to calculate the last day of a month.
*/
int getlastday(int month, int year)
{
   int months[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
   int day;

   day = months[month];
   if (((year + 1900) % 4) == 0 && ((year + 1900) % 400) != 0)
      day++;

   if (debug > 1)
      logmsg("%d = getlastdayq(%d, %d)", day, month, year);
      
   return day;
}
