// ---------------------------------------------------------------------------
// - Date.cpp                                                                -
// - standard object library - date class implementation                     -
// ---------------------------------------------------------------------------
// - This program is free software;  you can redistribute it  and/or  modify -
// - it provided that this copyright notice is kept intact.                  -
// -                                                                         -
// - 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.  In no event shall -
// - the copyright holder be liable for any  direct, indirect, incidental or -
// - special damages arising in any way out of the use of this software.     -
// ---------------------------------------------------------------------------
// - copyright (c) 1999-2008 amaury darsch                                   -
// ---------------------------------------------------------------------------

#include "Date.hpp"
#include "Regex.hpp"
#include "Vector.hpp"
#include "Integer.hpp"
#include "Utility.hpp"
#include "QuarkZone.hpp"
#include "Exception.hpp"
#include "cclk.hpp"

namespace afnix {

  // -------------------------------------------------------------------------
  // - private section                                                       -
  // -------------------------------------------------------------------------

  // week day string mapping in us/week reference
  static const long  ATC_MAX_WDAY               = 7;
  static const char* ATC_DAY_NAME[ATC_MAX_WDAY] = {
    "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
  };
  // months string mapping
  static const long  ATC_MAX_YMON               = 12;
  static const char* ATC_MON_NAME[ATC_MAX_YMON] = {
    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
  };

  // number of gregorian days per year  (normal and leap)
  static const long ATC_YDAY_NORM = 365;
  static const long ATC_YDAY_LEAP = 366;
  // number of gregorian days per month (normal and leap)
  static const long ATC_MDAY_NORM[12] = {
    31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
  };
  static const long ATC_MDAY_LEAP[12] = {
    31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
  };

  // format the day string
  static String atc_mapwday (const long wday) {
    if ((wday < 0) || (wday >= ATC_MAX_WDAY))
      throw Exception ("date-error", "day index is ot of range");
    return ATC_DAY_NAME[wday];
  }

  // format the month string
  static String atc_mapymon (const long ymon) {
    if ((ymon < 1) || (ymon > ATC_MAX_YMON))
      throw Exception ("time-error", "month index is out of range");
    return ATC_MON_NAME[ymon-1];
  }

  // return the absolute value
  static inline long abs (const long abs) {
    return (abs < 0) ? -abs : abs;
  }
  static inline t_long abs (const t_long abs) {
    return (abs < 0) ? -abs : abs;
  }

  // return true if the year is a leap year
  static inline bool atc_isleap (const long year) {
    if (year == 0) return true;
    // get absolute year
    long ywrk = abs (year);
    // 0 is a special case
    if ((ywrk % 400) == 0) return true;
    if ((ywrk % 100) == 0) return false;
    if ((ywrk % 4)   == 0) return true;
    return false;
  }

  // get the number of days in a year
  static inline t_long atc_yday_in_year (const long year) {
    t_long yday = atc_isleap (year) ? ATC_YDAY_LEAP : ATC_YDAY_NORM;
    return yday;
  }

  // get the number of seconds in a year
  static inline t_long atc_ysec_in_year (const long year) {
    t_long ysec = atc_yday_in_year (year) * (t_long) Time::DSEC;
    return ysec;
  }

  // get the number of days in a year month
  static inline t_long atc_mday_in_ymon (const long year, const long ymon) {
    if (atc_isleap (year) == true) return ATC_MDAY_LEAP[ymon];
    return ATC_MDAY_NORM[ymon];
  }

  // get the number of seconds in a year month
  static inline t_long atc_msec_in_ymon (const long year, const long ymon) {
    t_long msec = atc_mday_in_ymon (year, ymon) * (t_long) Time::DSEC;
    return msec;
  }

  // trim the day in a year month
  static inline long atc_trim_in_ymon (const long year, const long ymon,
				       const long mday) {
    long maxd = (long) atc_mday_in_ymon (year, ymon);
    return (mday <= maxd) ? mday : maxd;
  }

  // get the number of days upto a year
  static t_long atc_yday_to_year (const long year) {
    // get absolute year
    long ywrk = abs (year);
    // iterate upto the year
    t_long yday = 0LL;
    for (long i = 0; i < ywrk; i++) {
      yday += atc_yday_in_year (i);
    }
    return yday;
  }

  // get the number of seconds upto a year
  static t_long atc_ysec_to_year (const long year) {
    t_long ysec = atc_yday_to_year (year) * (t_long) Time::DSEC;
    return ysec;
  }

  // get the number of days upto a month in a specific year
  static t_long atc_yday_to_ymon (const long year, const long ymon) {
    t_long yday = 0LL;
    if (atc_isleap (year) == true) {
      for (long i = 0; i < ymon; i++) yday += ATC_MDAY_LEAP[i];
    } else {
      for (long i = 0; i < ymon; i++) yday += ATC_MDAY_NORM[i];
    }
    return yday;
  }

  // get the number of seconds upto a month in a specific year
  static t_long atc_ysec_to_ymon (const long year, const long ymon) {
    t_long ysec = atc_yday_to_ymon (year, ymon) * (t_long) Time::DSEC;
    return ysec;
  }

  // number of day upto a date
  static t_long atc_yday_to_date (const long year, const long ymon, 
				  const long mday) {
    t_long yday = 0LL;
    yday += atc_yday_to_year (year);
    yday += atc_yday_to_ymon (year, ymon);
    yday += mday;
    return yday;
  }

  // number of seconds upto a date
  static t_long atc_ysec_to_date (const long year, const long ymon, 
				  const long mday) {
    t_long ysec = atc_yday_to_date (year, ymon, mday) * (t_long) Time::DSEC;
    return ysec;
  }

  // find the year from an atc clock
  static long atc_year_from_tclk (const t_long tclk) {
    // get the absolute clock
    t_long wclk = abs (tclk);
    // compute the year by iteration
    long   year = 0;
    long   ymax = Utility::maxlong ();
    t_long cclk = 0LL;
    while (year < ymax) {
      cclk += atc_ysec_in_year (year);
      if (cclk > wclk) break;
      year++;
    }
    return year;
  }

  // find the month in a year from an atc clock
  static long atc_ymon_from_tclk (const t_long tclk, const long year) {
    // get the absolute clock
    t_long wclk = abs (tclk);
    // compute the month by iteration
    long   ymon = 0;
    t_long cclk = 0LL;
    for (long i = 0; i < ATC_MAX_YMON; i++) {
      cclk += atc_msec_in_ymon (year, i);
      if (cclk > wclk) break;
      ymon++;
    }
    return ymon;
  }

  // the date sructure
  struct s_date {
    long d_year; // year
    long d_yday; // day in year
    long d_ymon; // month in year [0:11]
    long d_mday; // month in day  [0:30]
    long d_wday; // day in week   [0:6]
    // create a default date
    s_date (void) {
      d_year = 0;
      d_yday = 0;
      d_ymon = 0;
      d_mday = 0;
      d_wday = 6;
    }
    // create a specific date
    s_date (const t_long tclk) {
      // get the absolute clock
      t_long wclk = abs (tclk);
      // get the year
      d_year = atc_year_from_tclk (wclk);
      // update the remaining seconds
      t_long secs = wclk - atc_ysec_to_year (d_year);
      // get the year day
      d_yday = (long) (secs / (t_long) Time::DSEC);
      // get the year month
      d_ymon = atc_ymon_from_tclk (secs, d_year);
      // update the remaining seconds
      secs -= atc_ysec_to_ymon (d_year, d_ymon);
      // get the month day
      d_mday = (long) (secs / (t_long) Time::DSEC);
      if (tclk >= 0) {
	// get the week day (0000-00-00 is a saturday)
	d_wday = ((wclk / (t_long)Time::DSEC) + 6) % 7;
      } else {
	// update the sign
	d_year = -d_year;
	// get the week day (0000-00-00 is a saturday)
	d_wday = (13 - (atc_yday_in_year (d_year) - d_yday)) % 7;
      }
    }
  };

  // -------------------------------------------------------------------------
  // - class section                                                        -
  // -------------------------------------------------------------------------

  // create a current date

  Date::Date (void) {
    p_date = new s_date (d_tclk);
  }

  // create a specific date

  Date::Date (const t_long tclk) : Time (tclk) {
    p_date = new s_date (d_tclk);
  }

  // create a date by iso specification

  Date::Date (const String& date) {
    p_date = nilp;
    setdate (date);
  }

  // create a date by specific elements
  
  Date::Date (const long year, const long ymon, const long mday) : Time (0) {
    p_date = nilp;
    setdate (year, ymon, mday);
  }

  // create a date by specific elements
  
  Date::Date (const long year, const long ymon, const long mday,
	      const long hour, const long mins, const long secs) : Time (0) {
    p_date = nilp;
    setdate (year, ymon, mday, hour, mins, secs);
  }

  // copy construct this date

  Date::Date (const Date& that) {
    that.rdlock ();
    p_date = nilp;
    settime (that.gettime ());
    that.unlock ();
  }

  // destroy this date

  Date::~Date (void) {
    delete p_date;
  }

  // return the class name

  String Date::repr (void) const {
    return "Date";
  }

  // return a clone of this object

  Object* Date::clone (void) const {
    return new Date (*this);
  }

  // set the date by specific time

  void Date::settime (const t_long tclk) {
    wrlock ();
    try {
      delete p_date;
      Time::settime (tclk);
      p_date = new s_date (d_tclk);
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set a date by an iso specification

  void Date::setdate (const String& date) {
    wrlock ();
    try {
      // extract the date component
      Regex re ("[($d$d$d$d)-($d$d)-($d$d)T($d$d):($d$d):($d$d)Z]");
      if (re == date) {
	if (re.length () != 6) {
	  throw Exception ("internal-error", "invalid parsed date", date);
	}
	// get the date data
	long year = re.getint (0);
	long ymon = re.getint (1);
	long mday = re.getint (2);
	long hour = re.getint (3);
	long mins = re.getint (4);
	long secs = re.getint (5);
	// set the date
	setdate (year, ymon, mday, hour, mins, secs);
      } else {
	throw Exception ("date-error", "invalid date format", date);
      }
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set the date by specific elements
  
  void Date::setdate (const long year, const long ymon, const long mday) {
    wrlock ();
    try {
      t_long tclk = atc_ysec_to_date (year, ymon-1, mday-1);
      if (year >= 0) {
	settime (tclk);
      } else {
	settime (-tclk);
      }
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set the date by specific elements
  
  void Date::setdate (const long year, const long ymon, const long mday,
		      const long hour, const long mins, const long secs) {
    wrlock ();
    try {
      // get the date clock
      t_long tclk = atc_ysec_to_date (year, ymon-1, mday-1);
      // add the time
      tclk += (t_long) (hour * HSEC + mins * MSEC + secs);
      // set the sign and time
      if (year >= 0) {
	settime (tclk);
      } else {
	settime (-tclk);
      }
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // add years to the current date

  void Date::addyear (const long num) {
    wrlock ();
    try {
      // get the current date
      long year = getyear ();
      long ymon = getymon ();
      long mday = getmday ();
      long hour = gethour (true);
      long mins = getmins (true);
      long secs = getsecs (true);
      // add the year
      year += num;
      // check if the day is valid
      mday = atc_trim_in_ymon (year, ymon, mday);
      // update the date
      setdate (year, ymon, mday, hour, mins, secs);
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // add months to the current date

  void Date::addymon (const long num) {
    wrlock ();
    try {
      // get the current date
      long year = getyear ();
      long ymon = getymon ();
      long mday = getmday ();
      long hour = gethour (true);
      long mins = getmins (true);
      long secs = getsecs (true);
      // compute number of year and remaining month
      long ynum = num / 12;
      long mnum = num % 12;
      // add the year and month
      year += ynum;
      ymon += mnum;
      // update year to add and month
      year += ymon / 12;
      ymon  = ymon % 12;
      // check if the day is valid
      mday = atc_trim_in_ymon (year, ymon, mday);
      // update the date
      setdate (year, ymon, mday, hour, mins, secs);
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return the date year

  long Date::getyear (void) const {
    rdlock ();
    long result = p_date->d_year;
    unlock ();
    return result;
  }

  // return the day in the year 

  long Date::getyday (void) const {
    rdlock ();
    long result = p_date->d_yday + 1;
    unlock ();
    return result;
  }

  // return the month in the year 

  long Date::getymon (void) const {
    rdlock ();
    long result = p_date->d_ymon + 1;
    unlock ();
    return result;
  }

  // return the day in the month

  long Date::getmday (void) const {
    rdlock ();
    long result = p_date->d_mday + 1;
    unlock ();
    return result;
  }

  // return the day in the week

  long Date::getwday (void) const {
    rdlock ();
    long result = p_date->d_wday;
    unlock ();
    return result;
  }

  // return the base day reference time

  t_long Date::getbday (void) const {
    rdlock ();
    t_long result = (d_tclk / DSEC) * DSEC;
    unlock ();
    return result;
  }

  // return the week day name

  String Date::mapwday (void) const {
    rdlock ();
    try {
      long wday = getwday ();
      String result = atc_mapwday (wday);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return the month name

  String Date::mapymon (void) const {
    rdlock ();
    try {
      long ymon = getymon ();
      String result = atc_mapymon (ymon);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // format the date

  String Date::todate (void) const {
    rdlock ();
    try {
      // check the year
      long year = getyear ();
      if (year < 0) {
	throw Exception ("date-error", 
			 "ISO-8601 does not define negative year");
      }
      if (year > 9999) {	
	throw Exception ("date-error", 
			 "ISO-8601 does not define year above 9999");
      }
      // format the year
      String result;
      if (year < 10) {
	result += "000";
      } else if (year < 100) {
	result += "00";
      } else if (year < 1000) {
	result += "0";
      }
      result += year;
      // format the month
      long ymon = getymon ();
      if (ymon < 10) {
	result += "-0";
      } else {
	result += "-";
      }
      result += ymon;
      // format the day
      long mday = getmday ();
      if (mday < 10) {
	result += "-0";
      } else {
	result += "-";
      }
      result += mday;
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // format the time

  String Date::totime (const bool utc) const {
    rdlock ();
    try {
      String result = Time::format (utc);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // format the date in a general form
  
  String Date::format (const bool utc) const {
    rdlock ();
    try {
      // format the week day
      String result = mapwday ();
      // add the month
      result += ' ';
      result += mapymon ();
      // add the day
      result += ' ';
      result += getmday ();
      // format the time
      result += ' ';
      result += totime (utc);
      // add the year
      result += ' ';
      result += getyear ();
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // format the date in the ISO-8601 form
  
  String Date::toiso (const bool utc) const {
    rdlock ();
    try {
      // format the date
      String result = todate ();
      // format the time
      result += 'T';
      result += Time::toiso (utc);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // format the date in the RFC-2822 form

  String Date::torfc (void) const {
    rdlock ();
    try {
      // ap the week day
      String result = mapwday ();
      // add the month day
      long mday = getmday ();
      if (mday < 10) {
	result += ", 0";
      } else {
	result += ", ";
      }
      result += mday;
      // add the month
      result += ' ';
      result += mapymon ();
      // add the year
      long year = getyear ();
      if (year < 10) {
	result += " 000";
      } else if (year < 100) {
	result += " 00";
      } else if (year < 1000) {
	result += " 0";
      } else {
	result += " ";
      }
      result += year;
      // add the time/zone
      result += ' ';
      result += Time::torfc ();
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    } 
  }

  // -------------------------------------------------------------------------
  // - object section                                                        -
  // -------------------------------------------------------------------------

  // the quark zone
  static const long QUARK_ZONE_LENGTH = 12;
  static QuarkZone  zone (QUARK_ZONE_LENGTH);

  // the object supported quarks
  static const long QUARK_TODATE  = zone.intern ("to-date");
  static const long QUARK_TOTIME  = zone.intern ("to-time");
  static const long QUARK_GETYEAR = zone.intern ("year");
  static const long QUARK_GETYDAY = zone.intern ("year-day");
  static const long QUARK_GETYMON = zone.intern ("month");
  static const long QUARK_GETMDAY = zone.intern ("day");
  static const long QUARK_ADDYEAR = zone.intern ("add-years");
  static const long QUARK_ADDYMON = zone.intern ("add-months");  
  static const long QUARK_GETWDAY = zone.intern ("week-day");
  static const long QUARK_MAPWDAY = zone.intern ("map-day");
  static const long QUARK_MAPYMON = zone.intern ("map-month");
  static const long QUARK_SETDATE = zone.intern ("set-date");
  static const long QUARK_GETBDAY = zone.intern ("get-base-day");

  // create a new object in a generic way
 
  Object* Date::mknew (Vector* argv) {
    long argc = (argv == nilp) ? 0 : argv->length ();
    // create a default time object
    if (argc == 0) return new Date;
    if (argc == 1) {
      Object* obj = argv->get (0);
      // check for an integer
      Integer* iobj = dynamic_cast <Integer*> (obj);
      if (iobj != nilp) return new Date (iobj->tointeger ());
      // check for a string
      String* sobj = dynamic_cast <String*> (obj);
      if (sobj != nilp) return new Date (*sobj);
      // invalid object
      throw Exception ("type-error", "invalid object for date",
		       Object::repr (obj));
    }
    // check for 3 arguments
    if (argc == 3) {
      long year = argv->getint (0);
      long ymon = argv->getint (1);
      long mday = argv->getint (2);
      return new Date (year, ymon, mday);
    }
    // check for 6 arguments
    if (argc == 6) {
      long year = argv->getint (0);
      long ymon = argv->getint (1);
      long mday = argv->getint (2);
      long hour = argv->getint (3);
      long mins = argv->getint (4);
      long secs = argv->getint (5);
      return new Date (year, ymon, mday, hour, mins, secs);
    }
    throw Exception ("argument-error",
                     "too many argument with date constructor");
  }
 
  // return true if the given quark is defined

  bool Date::isquark (const long quark, const bool hflg) const {
    rdlock ();
    if (zone.exists (quark) == true) {
      unlock ();
      return true;
    }
    bool result = hflg ? Time::isquark (quark, hflg) : false;
    unlock ();
    return result;
  }

  // apply this object with a set of arguments and a quark
  
  Object* Date::apply (Runnable* robj, Nameset* nset, const long quark,
		       Vector* argv) {
    // get the number of arguments
    long argc = (argv == nilp) ? 0 : argv->length ();

    // check for 0 arguments
    if (argc == 0) {
      if (quark == QUARK_TODATE)  return new String  (todate  ());
      if (quark == QUARK_TOTIME)  return new String  (totime  (false));
      if (quark == QUARK_GETYEAR) return new Integer (getyear ());
      if (quark == QUARK_GETYDAY) return new Integer (getyday ());
      if (quark == QUARK_GETYMON) return new Integer (getymon ());
      if (quark == QUARK_GETMDAY) return new Integer (getmday ());
      if (quark == QUARK_GETWDAY) return new Integer (getmday ());
      if (quark == QUARK_GETBDAY) return new Integer (getbday ());
      if (quark == QUARK_MAPWDAY) return new String  (mapwday ());
      if (quark == QUARK_MAPYMON) return new String  (mapymon ());
    }
    // check for 1 argument
    if (argc == 1) {
      if (quark == QUARK_TOTIME) {
	bool utc = argv->getbool (0);
	return new String (totime (utc));
      }
      if (quark == QUARK_ADDYEAR) {
	long num = argv->getint (0);
	addyear (num);
	return nilp;
      }
      if (quark == QUARK_ADDYMON) {
	long num = argv->getint (0);
	addymon (num);
	return nilp;
      }
      if (quark == QUARK_SETDATE) {
	String date = argv->getstring (0);
	setdate (date);
	return nilp;
      }
    }
    // check for 3 arguments
    if (argc == 3) {
      if (quark == QUARK_SETDATE) {
	long year = argv->getint (0);
	long ymon = argv->getint (1);
	long mday = argv->getint (2);
	setdate (year, ymon, mday);
	return nilp;
      }
    }
    // check for 6 arguments
    if (argc == 6) {
      if (quark == QUARK_SETDATE) {
	long year = argv->getint (0);
	long ymon = argv->getint (1);
	long mday = argv->getint (2);
	long hour = argv->getint (3);
	long mins = argv->getint (4);
	long secs = argv->getint (5);
	setdate (year, ymon, mday, hour, mins, secs);
	return nilp;
      }
    }
    // call the time method
    return Time::apply (robj, nset, quark, argv);
  }
}
