/***************************************************************************
                          timezonerule.cpp  -  description
                             -------------------
    begin                : Tue Apr 2 2002
    copyright            : (C) 2002 by Jason Harris
    email                : kstars@30doradus.org
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

#include <kdebug.h>
#include <tdelocale.h>

#include "timezonerule.h"
#include "kstarsdatetime.h"

TimeZoneRule::TimeZoneRule() {
	//Build the empty TimeZoneRule.
	StartMonth = 0;
	RevertMonth = 0;
	StartDay = 0;
	RevertDay = 0;
	StartWeek = -1;
	RevertWeek = -1;
	StartTime = TQTime();
	RevertTime = TQTime();
	HourOffset = 0.0;
	dTZ = 0.0;
}

TimeZoneRule::TimeZoneRule( const TQString &smonth, const TQString &sday, const TQTime &stime,
			const TQString &rmonth, const TQString &rday, const TQTime &rtime, const double &dh ) {
	dTZ = 0.0;
	if ( smonth != "0" ) {
		StartMonth = initMonth( smonth );
		RevertMonth = initMonth( rmonth );

		if ( StartMonth && RevertMonth && initDay( sday, StartDay, StartWeek ) &&
					initDay( rday, RevertDay, RevertWeek ) && stime.isValid() && rtime.isValid() ) {
			StartTime = stime;
			RevertTime = rtime;
			HourOffset = dh;
		} else {
			kdWarning() << i18n( "Error parsing TimeZoneRule, setting to empty rule." ) << endl;
			StartMonth = 0;
			RevertMonth = 0;
			StartDay = 0;
			RevertDay = 0;
			StartWeek = -1;
			RevertWeek = -1;
			StartTime = TQTime();
			RevertTime = TQTime();
			HourOffset = 0.0;
		}
	} else { //Empty rule
		StartMonth = 0;
		RevertMonth = 0;
		StartDay = 0;
		RevertDay = 0;
		StartWeek = -1;
		RevertWeek = -1;
		StartTime = TQTime();
		RevertTime = TQTime();
		HourOffset = 0.0;
	}
}

TimeZoneRule::~TimeZoneRule() {
}

void TimeZoneRule::setDST( bool activate ) {
	if ( activate ) {
		kdDebug() << i18n( "Daylight Saving Time active" ) << endl;
		dTZ = HourOffset;
	} else {
		kdDebug() << i18n( "Daylight Saving Time inactive" ) << endl;
		dTZ = 0.0;
	}
}

int TimeZoneRule::initMonth( const TQString &mn ) {
//Check whether the argument is a three-letter English month code.
	TQString ml = mn.lower();
	if ( ml == "jan" ) return 1;
	else if ( ml == "feb" ) return 2;
	else if ( ml == "mar" ) return 3;
	else if ( ml == "apr" ) return 4;
	else if ( ml == "may" ) return 5;
	else if ( ml == "jun" ) return 6;
	else if ( ml == "jul" ) return 7;
	else if ( ml == "aug" ) return 8;
	else if ( ml == "sep" ) return 9;
	else if ( ml == "oct" ) return 10;
	else if ( ml == "nov" ) return 11;
	else if ( ml == "dec" ) return 12;

	kdWarning() << i18n( "Could not parse " ) << mn << i18n( " as a valid month code." ) << endl;
	return false;
}

bool TimeZoneRule::initDay( const TQString &dy, int &Day, int &Week ) {
//Three possible ways to express a day.
//1. simple integer; the calendar date...set Week=0 to indicate that Date is not the day of the week
	bool ok;
	int day = dy.toInt( &ok );
	if ( ok ) {
		Day = day;
		Week = 0;
		return true;
	}

	TQString dl = dy.lower();
//2. 3-letter day of week string, indicating the last of that day of the month
//   ...set Week to 5 to indicate the last weekday of the month
	if ( dl == "mon" ) { Day = 1; Week = 5; return true; }
	else if ( dl == "tue" ) { Day = 2; Week = 5; return true; }
	else if ( dl == "wed" ) { Day = 3; Week = 5; return true; }
	else if ( dl == "thu" ) { Day = 4; Week = 5; return true; }
	else if ( dl == "fri" ) { Day = 5; Week = 5; return true; }
	else if ( dl == "sat" ) { Day = 6; Week = 5; return true; }
	else if ( dl == "sun" ) { Day = 7; Week = 5; return true; }

//3. 1,2 or 3 followed by 3-letter day of week string; this indicates
//   the (1st/2nd/3rd) weekday of the month.
	int wn = dl.left(1).toInt();
	if ( wn >0 && wn <4 ) {
		TQString dm = dl.mid( 1, dl.length() ).lower();
		if ( dm == "mon" ) { Day = 1; Week = wn; return true; }
		else if ( dm == "tue" ) { Day = 2; Week = wn; return true; }
		else if ( dm == "wed" ) { Day = 3; Week = wn; return true; }
		else if ( dm == "thu" ) { Day = 4; Week = wn; return true; }
		else if ( dm == "fri" ) { Day = 5; Week = wn; return true; }
		else if ( dm == "sat" ) { Day = 6; Week = wn; return true; }
		else if ( dm == "sun" ) { Day = 7; Week = wn; return true; }
	}

	kdWarning() << i18n( "Could not parse " ) << dy << i18n( " as a valid day code." ) << endl;
	return false;
}

int TimeZoneRule::findStartDay( const KStarsDateTime &d ) {
	// Determine the calendar date of StartDay for the month and year of the given date.
	ExtDate test;

	// TimeZoneRule is empty, return -1
	if ( isEmptyRule() ) return -1;

	// If StartWeek=0, just return the integer.
	if ( StartWeek==0 ) return StartDay;

	// Since StartWeek was not zero, StartDay is the day of the week, not the calendar date
	else if ( StartWeek==5 ) { // count back from end of month until StartDay is found.
		for ( test = ExtDate( d.date().year(), d.date().month(), d.date().daysInMonth() );
					test.day() > 21; test = test.addDays( -1 ) )
			if ( test.dayOfWeek() == StartDay ) break;
	} else { // Count forward from day 1, 8 or 15 (depending on StartWeek) until correct day of week is found
		for ( test = ExtDate( d.date().year(), d.date().month(), (StartWeek-1)*7 + 1 );
					test.day() < 7*StartWeek; test = test.addDays( 1 ) )
			if ( test.dayOfWeek() == StartDay ) break;
	}
	return test.day();
}

int TimeZoneRule::findRevertDay( const KStarsDateTime &d ) {
	// Determine the calendar date of RevertDay for the month and year of the given date.
	ExtDate test;

	// TimeZoneRule is empty, return -1
	if ( isEmptyRule() ) return -1;

	// If RevertWeek=0, just return the integer.
	if ( RevertWeek==0 ) return RevertDay;

	// Since RevertWeek was not zero, RevertDay is the day of the week, not the calendar date
	else if ( RevertWeek==5 ) { //count back from end of month until RevertDay is found.
		for ( test = ExtDate( d.date().year(), d.date().month(), d.date().daysInMonth() );
					test.day() > 21; test = test.addDays( -1 ) )
			if ( test.dayOfWeek() == RevertDay ) break;
	} else { //Count forward from day 1, 8 or 15 (depending on RevertWeek) until correct day of week is found
		for ( test = ExtDate( d.date().year(), d.date().month(), (RevertWeek-1)*7 + 1 );
					test.day() < 7*RevertWeek; test = test.addDays( 1 ) )
			if ( test.dayOfWeek() == StartDay ) break;
	}
	return test.day();
}

bool TimeZoneRule::isDSTActive( const KStarsDateTime &date ) {
	// The empty rule always returns false
	if ( isEmptyRule() ) return false;

	// First, check whether the month is outside the DST interval.  Note that
	// the interval check is different if StartMonth > RevertMonth (indicating that
	// the DST interval includes the end of the year).
	int month = date.date().month();
	
	if ( StartMonth < RevertMonth ) {
		if ( month < StartMonth || month > RevertMonth ) return false;
	} else {
		if ( month < StartMonth && month > RevertMonth ) return false;
	}
	
	// OK, if the month is equal to StartMonth or Revert Month, we have more
	// detailed checking to do...
	int day = date.date().day();

	if ( month == StartMonth ) {
		int sday = findStartDay( date );
		if ( day < sday ) return false;
		if ( day==sday && date.time() < StartTime ) return false;
	} else if ( month == RevertMonth ) {
		int rday = findRevertDay( date );
		if ( day > rday ) return false;
		if ( day==rday && date.time() > RevertTime ) return false;
	}

	// passed all tests, so we must be in DST.
	return true;
}

void TimeZoneRule::nextDSTChange_LTime( const KStarsDateTime &date ) {
	KStarsDateTime result;

	// return a very remote date if the rule is the empty rule.
	if ( isEmptyRule() ) result = KStarsDateTime( INVALID_DAY );

	else if ( deltaTZ() ) {
		// Next change is reverting back to standard time.

		//y is the year for the next DST Revert date.  It's either the current year, or
		//the next year if the current month is already past RevertMonth
		int y = date.date().year();
		if ( RevertMonth < date.date().month() ) ++y;

		result = KStarsDateTime( ExtDate( y, RevertMonth, 1 ), RevertTime );
		result = KStarsDateTime( ExtDate( y, RevertMonth, findRevertDay( result ) ), RevertTime );

	} else {
		// Next change is starting DST.

		//y is the year for the next DST Start date.  It's either the current year, or
		//the next year if the current month is already past StartMonth
		int y = date.date().year();
		if ( StartMonth < date.date().month() ) ++y;

		result = KStarsDateTime( ExtDate( y, StartMonth, 1 ), StartTime );
		result = KStarsDateTime( ExtDate( y, StartMonth, findStartDay( result ) ), StartTime );
	}

	kdDebug() << i18n( "Next Daylight Savings Time change (Local Time): " ) << result.toString() << endl;
	next_change_ltime = result;
}


void TimeZoneRule::previousDSTChange_LTime( const KStarsDateTime &date ) {
	KStarsDateTime result;

	// return a very remote date if the rule is the empty rule
	if ( isEmptyRule() ) next_change_ltime = KStarsDateTime( INVALID_DAY );

	if ( deltaTZ() ) {
		// Last change was starting DST.

		//y is the year for the previous DST Start date.  It's either the current year, or
		//the previous year if the current month is earlier than StartMonth
		int y = date.date().year();
		if ( StartMonth > date.date().month() ) --y;

		result = KStarsDateTime( ExtDate( y, StartMonth, 1 ), StartTime );
		result = KStarsDateTime( ExtDate( y, StartMonth, findStartDay( result ) ), StartTime );

	} else if ( StartMonth ) {
		//Last change was reverting to standard time.

		//y is the year for the previous DST Start date.  It's either the current year, or
		//the previous year if the current month is earlier than StartMonth
		int y = date.date().year();
		if ( RevertMonth > date.date().month() ) --y;

		result = KStarsDateTime( ExtDate( y, RevertMonth, 1 ), RevertTime );
		result = KStarsDateTime( ExtDate( y, RevertMonth, findRevertDay( result ) ), RevertTime );
	}

	kdDebug() << i18n( "Previous Daylight Savings Time change (Local Time): " ) << result.toString() << endl;
	next_change_ltime = result;
}

/**Convert current local DST change time in universal time */
void TimeZoneRule::nextDSTChange( const KStarsDateTime &local_date, const double TZoffset ) {
	// just decrement timezone offset and hour offset
	KStarsDateTime result = local_date.addSecs( int( (TZoffset + deltaTZ()) * -3600) );

	kdDebug() << i18n( "Next Daylight Savings Time change (UTC): " ) << result.toString() << endl;
	next_change_utc = result;
}

/**Convert current local DST change time in universal time */
void TimeZoneRule::previousDSTChange( const KStarsDateTime &local_date, const double TZoffset ) {
	// just decrement timezone offset
	KStarsDateTime result = local_date.addSecs( int( TZoffset * -3600) );

	// if prev DST change is a revert change, so the revert time is in daylight saving time
	if ( result.date().month() == RevertMonth )
		result = result.addSecs( int(HourOffset * -3600) );
	
	kdDebug() << i18n( "Previous Daylight Savings Time change (UTC): " ) << result.toString() << endl;
	next_change_utc = result;
}

void TimeZoneRule::reset_with_ltime( KStarsDateTime &ltime, const double TZoffset, const bool time_runs_forward,
		const bool automaticDSTchange ) {

/**There are some problems using local time for getting next daylight saving change time.
	1. The local time is the start time of DST change. So the local time doesn't exists and must
		  corrected.
	2. The local time is the revert time. So the local time exists twice.
	3. Neither start time nor revert time. There is no problem.

	Problem #1 is more complicated and we have to change the local time by reference.
	Problem #2 we just have to reset status of DST.
	
	automaticDSTchange should only set to true if DST status changed due to running automatically over
	a DST change time. If local time will changed manually the automaticDSTchange should always set to
	false, to hold current DST status if possible (just on start and revert time possible).
	*/

	//don't need to do anything for empty rule
	if ( isEmptyRule() ) return;

	// check if DST is active before resetting with new time
	bool wasDSTactive(false);
	
	if ( deltaTZ() != 0.0 ) { 
		wasDSTactive = true;
	}

	// check if current time is start time, this means if a DST change happend in last hour(s)
	bool active_with_houroffset = isDSTActive(ltime.addSecs( int(HourOffset * -3600) ) );
	bool active_normal = isDSTActive( ltime );

	// store a valid local time
	KStarsDateTime ValidLTime = ltime;

	if ( active_with_houroffset != active_normal && ValidLTime.date().month() == StartMonth ) {
		// current time is the start time
		kdDebug() << "Current time = Starttime: invalid local time due to daylight saving time" << endl;

		// set a correct local time because the current time doesn't exists
		// if automatic DST change happend, new DST setting is the opposite of current setting
		if ( automaticDSTchange ) {
			// revert DST status
			setDST( !wasDSTactive );
			// new setting DST is inactive, so subtract hour offset to new time
			if ( wasDSTactive ) {
				// DST inactive
				ValidLTime = ltime.addSecs( int( HourOffset * - 3600) );
			} else {
				// DST active
				// add hour offset to new time
				ValidLTime = ltime.addSecs( int( HourOffset * 3600) );
			}
		} else {  // if ( automaticDSTchange )
		// no automatic DST change happend, so stay in current DST mode
			setDST( wasDSTactive );
			if ( wasDSTactive ) {
				// DST active
				// add hour offset to current time, because time doesn't exists
				ValidLTime = ltime.addSecs( int( HourOffset * 3600) );
			} else {
				// DST inactive
				// subtrace hour offset to current time, because time doesn't exists
				ValidLTime = ltime.addSecs( int( HourOffset * -3600) );
			}
		}  // else { // if ( automaticDSTchange )

	} else {  // if ( active_with_houroffset != active_normal && ValidLTime.date().month() == StartMonth )
		// If current time was not start time, so check if current time is revert time
		// this means if a DST change happend in next hour(s)
		active_with_houroffset = isDSTActive(ltime.addSecs( int(HourOffset * 3600) ) );
		if ( active_with_houroffset != active_normal && RevertMonth == ValidLTime.date().month() ) {
			// current time is the revert time
			kdDebug() << "Current time = Reverttime" << endl;

			// we don't kneed to change the local time, because local time always exists, but
			// some times exists twice, so we have to reset DST status
			if ( automaticDSTchange ) {
				// revert DST status
				setDST( !wasDSTactive );
			} else {
				// no automatic DST change so stay in current DST mode
				setDST( wasDSTactive );
			}

		} else {
			//Current time was neither starttime nor reverttime, so use normal calculated DST status
			setDST( active_normal );
		}
	}  // if ( active_with_houroffset != active_normal && ValidLTime.date().month() == StartMonth )

//	kdDebug() << "Using Valid Local Time = " << ValidLTime.toString() << endl;

	if (time_runs_forward) {
		// get next DST change time in local time
		nextDSTChange_LTime( ValidLTime );
		nextDSTChange( next_change_ltime, TZoffset );
	} else {
		// get previous DST change time in local time
		previousDSTChange_LTime( ValidLTime );
		previousDSTChange( next_change_ltime, TZoffset );
	}
	ltime = ValidLTime;
}


bool TimeZoneRule::equals( TimeZoneRule *r ) {
	if ( StartDay == r->StartDay && RevertDay == r->RevertDay &&
			StartWeek == r->StartWeek && RevertWeek == r->RevertWeek &&
			StartMonth == r->StartMonth && RevertMonth == r->RevertMonth &&
			StartTime == r->StartTime && RevertTime == r->RevertTime &&
			isEmptyRule() == r->isEmptyRule() )
		return true;
	else
		return false;
}
