<?php
/*********************************************************************************
 * The contents of this file are subject to the TimeTrex Public License Version
 * 1.1.0 ("License"); You may not use this file except in compliance with the
 * License. You may obtain a copy of the License at http://www.TimeTrex.com/TPL
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied.  See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * All copies of the Covered Code must include on each user interface screen:
 *    (i) the "Powered by TimeTrex" logo and
 *    (ii) the TimeTrex copyright notice
 * in the same form as they appear in the distribution.  See full license for
 * requirements.
 *
 * The Original Code is: TimeTrex Open Source
 * The Initial Developer of the Original Code is TimeTrex Payroll Services
 * Portions created by TimeTrex are Copyright (C) 2004-2007 TimeTrex Payroll Services;
 * All Rights Reserved.
 *
 ********************************************************************************/
/*
 * $Revision: 2237 $
 * $Id: PayPeriodScheduleFactory.class.php 2237 2008-11-11 18:18:20Z ipso $
 * $Date: 2008-11-11 10:18:20 -0800 (Tue, 11 Nov 2008) $
 */

/**
 * @package Module_PayPeriod
 */
class PayPeriodScheduleFactory extends Factory {
	protected $table = 'pay_period_schedule';
	protected $pk_sequence_name = 'pay_period_schedule_id_seq'; //PK Sequence name

	protected $create_initial_pay_periods = FALSE;
	protected $enable_create_initial_pay_periods = TRUE;


	function _getFactoryOptions( $name ) {

		$retval = NULL;
		switch( $name ) {
			case 'type':
				$retval = array(
											5 => TTi18n::gettext('Manual'),
											10  => TTi18n::gettext('Weekly'),
											20  => TTi18n::gettext('Bi-Weekly'),
											30  => TTi18n::gettext('Semi-Monthly'),
											//40  => TTi18n::gettext('Monthly + Advance'),
											50  => TTi18n::gettext('Monthly') //Must have this here, for ROEs
										);
				break;
			case 'startweekday':
				$retval = array(
											0 => TTi18n::gettext('Sunday-Saturday'),
											1 => TTi18n::gettext('Monday-Sunday'),
											2 => TTi18n::gettext('Tuesday-Monday'),
											3 => TTi18n::gettext('Wednesday-Tuesday'),
											4 => TTi18n::gettext('Thursday-Wednesday'),
											5 => TTi18n::gettext('Friday-Thursday'),
											6 => TTi18n::gettext('Saturday-Friday'),

										);
				break;

		}

		return $retval;
	}


	function getCompany() {
		return $this->data['company_id'];
	}
	function setCompany($id) {
		$id = trim($id);

		$clf = new CompanyListFactory();

		if ( $this->Validator->isResultSetWithRows(	'company',
													$clf->getByID($id),
													TTi18n::gettext('Company is invalid')
													) ) {

			$this->data['company_id'] = $id;

			return TRUE;
		}

		return FALSE;
	}

	function getType() {
		//Have to return the KEY because it should always be a drop down box.
		//return Option::getByKey($this->data['status_id'], $this->getOptions('status') );
		return $this->data['type_id'];
	}
	function setType($type) {
		$type = trim($type);

		$key = Option::getByValue($type, $this->getOptions('type') );
		if ($key !== FALSE) {
			$type = $key;
		}

		if ( $this->Validator->inArrayKey(	'type',
											$type,
											TTi18n::gettext('Incorrect Type'),
											$this->getOptions('type')) ) {

			$this->data['type_id'] = $type;

			return TRUE;
		}

		return FALSE;
	}

	function getStartWeekDay() {
		if ( isset($this->data['start_week_day_id']) ) {
			return $this->data['start_week_day_id'];
		}

		return FALSE;
	}
	function setStartWeekDay($val) {
		$val = trim($val);

		$key = Option::getByValue($val, $this->getOptions('startweekday') );
		if ($key !== FALSE) {
			$type = $key;
		}

		if ( $this->Validator->inArrayKey(	'start_week_day',
											$val,
											TTi18n::gettext('Incorrect Start Week Day'),
											$this->getOptions('startweekday')) ) {

			$this->data['start_week_day_id'] = $val;

			return TRUE;
		}

		return FALSE;
	}

	function isUniqueName($name) {
		$ph = array(
					'company_id' => $this->getCompany(),
					'name' => $name,
					);

		$query = 'select id from '. $this->getTable() .' where company_id = ? AND name = ? AND deleted=0';
		$pay_period_schedule_id = $this->db->GetOne($query, $ph);
		Debug::Arr($pay_period_schedule_id,'Unique Pay Period Schedule ID: '. $pay_period_schedule_id, __FILE__, __LINE__, __METHOD__,10);

		if ( $pay_period_schedule_id === FALSE ) {
			return TRUE;
		} else {
			if ($pay_period_schedule_id == $this->getId() ) {
				return TRUE;
			}
		}

		return FALSE;
	}
	function getName() {
		return $this->data['name'];
	}
	function setName($name) {
		$name = trim($name);

		if (	$this->Validator->isLength(	'name',
											$name,
											TTi18n::gettext('Name is invalid'),
											2,50)
				AND	$this->Validator->isTrue(	'name',
												$this->isUniqueName($name),
												TTi18n::gettext('Name is already in use')
												)
						) {

			$this->data['name'] = $name;

			return TRUE;
		}

		return FALSE;
	}

	function getDescription() {
		return $this->data['description'];
	}
	function setDescription($description) {
		$description = trim($description);

		if (	$description == ''
				OR
				$this->Validator->isLength(	'description',
											$description,
											TTi18n::gettext('Description is invalid'),
											2,255) ) {

			$this->data['description'] = $description;

			return TRUE;
		}

		return FALSE;
	}

	function getStartDayOfWeek( $raw = FALSE) {
		if ( isset($this->data['start_day_of_week']) ) {
			return $this->data['start_day_of_week'];
		}

		return FALSE;
	}
	function setStartDayOfWeek($val) {
		$val = trim($val);

		$key = Option::getByValue($val, TTDate::getDayOfWeekArray() );
		if ($key !== FALSE) {
			$val = $key;
		}

		if ( $this->Validator->inArrayKey(	'start_day_of_week',
											$val,
											TTi18n::gettext('Incorrect start day of week'),
											TTDate::getDayOfWeekArray() ) ) {

			$this->data['start_day_of_week'] = $val;

			return TRUE;
		}

		return FALSE;
	}

	function getTransactionDate() {
		if ( isset($this->data['transaction_date']) ) {
			return $this->data['transaction_date'];
		}

		return FALSE;
	}
	function setTransactionDate($val) {
		$val = trim($val);

		$key = Option::getByValue($val, TTDate::getDayOfWeekArray() );
		if ($key !== FALSE) {
			$val = $key;
		}

		if ( $val == 0
				OR $this->Validator->inArrayKey(	'transaction_date',
											$val,
											TTi18n::gettext('Incorrect transaction date'),
											TTDate::getDayOfMonthArray() ) ) {

			$this->data['transaction_date'] = $val;

			return TRUE;
		}

		return FALSE;
	}

	function convertLastDayOfMonth( $val ) {
		if ( $val == -1 ) {
			return 31;
		}

		return $val;
	}

	function getPrimaryDayOfMonth() {
		if ( isset($this->data['primary_day_of_month']) ) {
			return $this->data['primary_day_of_month'];
		}

		return FALSE;
	}
	function setPrimaryDayOfMonth($val) {
		$val = trim($val);

		$key = Option::getByValue($val, TTDate::getDayOfMonthArray() );
		if ($key !== FALSE) {
			$val = $key;
		}

		if ( $val == -1
				OR $this->Validator->inArrayKey(	'primary_day_of_month',
											$val,
											TTi18n::gettext('Incorrect primary day of month'),
											TTDate::getDayOfMonthArray() ) ) {

			$this->data['primary_day_of_month'] = $val;

			return TRUE;
		}

		return FALSE;
	}

	function getSecondaryDayOfMonth() {
		if ( isset($this->data['secondary_day_of_month']) ) {
			return $this->data['secondary_day_of_month'];
		}

		return FALSE;
	}
	function setSecondaryDayOfMonth($val) {
		$val = trim($val);

		$key = Option::getByValue($val, TTDate::getDayOfMonthArray() );
		if ($key !== FALSE) {
			$val = $key;
		}

		if ( $val == -1
				OR $this->Validator->inArrayKey(	'secondary_day_of_month',
											$val,
											TTi18n::gettext('Incorrect secondary day of month'),
											TTDate::getDayOfMonthArray() ) ) {

			$this->data['secondary_day_of_month'] = $val;

			return TRUE;
		}

		return FALSE;
	}

	function getPrimaryTransactionDayOfMonth() {
		if ( isset($this->data['primary_transaction_day_of_month']) ) {
			return $this->data['primary_transaction_day_of_month'];
		}

		return FALSE;
	}
	function setPrimaryTransactionDayOfMonth($val) {
		$val = trim($val);

		$key = Option::getByValue($val, TTDate::getDayOfMonthArray() );
		if ($key !== FALSE) {
			$val = $key;
		}

		if ( $val == -1
				OR $this->Validator->inArrayKey(	'primary_transaction_day_of_month',
											$val,
											TTi18n::gettext('Incorrect primary transaction day of month'),
											TTDate::getDayOfMonthArray() ) ) {

			$this->data['primary_transaction_day_of_month'] = $val;

			return TRUE;
		}

		return FALSE;
	}

	function getSecondaryTransactionDayOfMonth() {
		if ( isset($this->data['secondary_transaction_day_of_month']) ) {
			return $this->data['secondary_transaction_day_of_month'];
		}

		return FALSE;
	}
	function setSecondaryTransactionDayOfMonth($val) {
		$val = trim($val);

		$key = Option::getByValue($val, TTDate::getDayOfMonthArray() );
		if ($key !== FALSE) {
			$val = $key;
		}

		if ( $val == -1
				OR $this->Validator->inArrayKey(	'secondary_transaction_day_of_month',
											$val,
											TTi18n::gettext('Incorrect secondary transaction day of month'),
											TTDate::getDayOfMonthArray() ) ) {

			$this->data['secondary_transaction_day_of_month'] = $val;

			return TRUE;
		}

		return FALSE;
	}

	function getTransactionDateBusinessDay() {
		if ( isset($this->data['transaction_date_bd']) ) {
			return $this->fromBool( $this->data['transaction_date_bd'] );
		}

		return FALSE;
	}
	function setTransactionDateBusinessDay($bool) {
		$this->data['transaction_date_bd'] = $this->toBool($bool);

		return true;
	}

	function getAnchorDate( $raw = FALSE ) {
		if ( isset($this->data['anchor_date']) ) {
			if ( $raw === TRUE ) {
				return $this->data['anchor_date'];
			} else {
				return strtotime( $this->data['anchor_date'] );
			}
		}

		return FALSE;
	}
	function setAnchorDate($epoch) {
		$epoch = trim($epoch);

		if 	(	$this->Validator->isDate(		'anchor_date',
												$epoch,
												TTi18n::gettext('Incorrect start date')) ) {

			$this->data['anchor_date'] = $epoch;

			return TRUE;
		}

		return FALSE;
	}

	function getDayStartTime() {
		if ( isset($this->data['day_start_time']) ) {
			return (int)$this->data['day_start_time'];
		}
		return FALSE;
	}
	function setDayStartTime($int) {
		$int = (int)$int;

		if 	(	$this->Validator->isNumeric(		'day_start_time',
													$int,
													TTi18n::gettext('Incorrect day start time')) ) {
			$this->data['day_start_time'] = $int;

			return TRUE;
		}

		return FALSE;
	}

	function getTimeZoneOptions() {
		$upf = new UserPreferenceFactory();

		return $upf->getOptions('time_zone');
	}
	function getTimeZone() {
		if ( isset($this->data['time_zone']) ) {
			return $this->data['time_zone'];
		}

		return FALSE;
	}
	function setTimeZone($time_zone) {
		$time_zone = trim($time_zone);

		$key = Option::getByValue($time_zone, $this->getTimeZoneOptions() );
		if ($key !== FALSE) {
			$time_zone = $key;
		}

		if ( $this->Validator->inArrayKey(	'time_zone',
											$time_zone,
											TTi18n::gettext('Incorrect time zone'),
											$this->getTimeZoneOptions() ) ) {

			$this->data['time_zone'] = $time_zone;

			return TRUE;
		}

		return FALSE;
	}

	function setOriginalTimeZone() {
		if ( isset($this->original_time_zone) ) {
			return TTDate::setTimeZone( $this->original_time_zone );
		}

		return FALSE;
	}
	function setPayPeriodTimeZone() {
		$this->original_time_zone = TTDate::getTimeZone();

		return TTDate::setTimeZone( $this->getTimeZone() );
	}
/*
	//Continuous time from the first punch of the day to the last
	//So if continuous time is set to 18hrs, and someone punches in for the first time at
	//11pm. All punches from 11pm + 18hrs are considered for the same day.
	function getContinuousTime() {
		if ( isset($this->data['day_continuous_time']) ) {
			return (int)$this->data['day_continuous_time'];
		}
		return FALSE;
	}
	function setContinuousTime($int) {
		$int = (int)$int;

		if 	(	$this->Validator->isNumeric(		'continuous_time',
													$int,
													TTi18n::gettext('Incorrect continuous time')) ) {
			$this->data['day_continuous_time'] = $int;

			return TRUE;
		}

		return FALSE;
	}
*/
	//
	// Instead of daily continuous time, use minimum time-off between shifts that triggers a new day to start.
	//
	function getNewDayTriggerTime() {
		if ( isset($this->data['new_day_trigger_time']) ) {
			return (int)$this->data['new_day_trigger_time'];
		}
		return FALSE;
	}
	function setNewDayTriggerTime($int) {
		$int = (int)$int;

		if 	(	$this->Validator->isNumeric(		'new_day_trigger_time',
													$int,
													TTi18n::gettext('Incorrect Minimum Time-Off Between Shifts')) ) {
			$this->data['new_day_trigger_time'] = $int;

			return TRUE;
		}

		return FALSE;
	}

	function getMaximumShiftTime() {
		if ( isset($this->data['maximum_shift_time']) ) {
			return (int)$this->data['maximum_shift_time'];
		}
		return FALSE;
	}
	function setMaximumShiftTime($int) {
		$int = (int)$int;

		if 	(	$this->Validator->isNumeric(		'maximum_shift_time',
													$int,
													TTi18n::gettext('Incorrect Maximum Shift Time')) ) {
			$this->data['maximum_shift_time'] = $int;

			return TRUE;
		}

		return FALSE;
	}

	function getUser() {
		$ppsulf = new PayPeriodScheduleUserListFactory();
		$ppsulf->getByPayPeriodScheduleId( $this->getId() );
		foreach ($ppsulf as $pay_period_schedule) {
			$user_list[] = $pay_period_schedule->getUser();
		}

		if ( isset($user_list) ) {
			return $user_list;
		}

		return FALSE;
	}
	function setUser($ids) {
		if ( is_array($ids) ) {
			if ( !$this->isNew() ) {
				//If needed, delete mappings first.
				$ppsulf = new PayPeriodScheduleUserListFactory();
				$ppsulf->getByPayPeriodScheduleId( $this->getId() );

				$user_ids = array();
				foreach ($ppsulf as $pay_period_schedule) {
					$user_id = $pay_period_schedule->getUser();
					Debug::text('Schedule ID: '. $pay_period_schedule->getPayPeriodSchedule() .' User ID: '. $user_id, __FILE__, __LINE__, __METHOD__, 10);

					//Delete users that are not selected.
					if ( !in_array($user_id, $ids) ) {
						Debug::text('Deleting User: '. $user_id, __FILE__, __LINE__, __METHOD__, 10);
						$pay_period_schedule->Delete();
					} else {
						//Save user ID's that need to be updated.
						Debug::text('NOT Deleting User: '. $user_id, __FILE__, __LINE__, __METHOD__, 10);
						$user_ids[] = $user_id;
					}
				}
			}

			//Insert new mappings.
			$ulf = new UserListFactory();

			foreach ($ids as $id) {
				if ( $id != '' AND isset($user_ids) AND !in_array($id, $user_ids) ) {
					$ppsuf = new PayPeriodScheduleUserFactory();
					$ppsuf->setPayPeriodSchedule( $this->getId() );
					$ppsuf->setUser( $id );

					$user_obj = $ulf->getById( $id )->getCurrent();

					if ($this->Validator->isTrue(		'user',
														$ppsuf->Validator->isValid(),
														TTi18n::gettext('Selected Employee is already assigned to another Pay Period').' ('. $user_obj->getFullName() .')' )) {
						$ppsuf->save();
					}
				}
			}

			return TRUE;
		}

		return FALSE;
	}

	function getPreviousBusinessDay($epoch) {

		Debug::Text('Epoch: '. TTDate::getDate('DATE+TIME', $epoch ), __FILE__, __LINE__, __METHOD__, 10);

		$holiday_epochs = array();

		$user_ids = $this->getUser();

		if ( count($user_ids) > 0 ) {
			$hlf = new HolidayListFactory();
			$hlf->getByPolicyGroupUserIdAndStartDateAndEndDate( $user_ids, $epoch-(86400*14), $epoch+(86400*2) );
			if ( $hlf->getRecordCount() > 0 ) {
				foreach( $hlf as $h_obj ) {
					Debug::Text('Found Holiday Epoch: '. TTDate::getDate('DATE+TIME', $h_obj->getDateStamp() ) .' Name: '. $h_obj->getName() , __FILE__, __LINE__, __METHOD__, 10);
					$holiday_epochs[] = $h_obj->getDateStamp();
				}

				//Debug::Arr($holiday_epochs, 'Holiday Epochs: ', __FILE__, __LINE__, __METHOD__, 10);
			}

			while( TTDate::isWeekDay($epoch) == FALSE OR in_array( TTDate::getBeginDayEpoch( $epoch ), $holiday_epochs) ) {
				Debug::text('<b>FOUND WeekDay/HOLIDAY!</b>', __FILE__, __LINE__, __METHOD__, 10);
				$epoch -= 86400;
			}

		}

		return $epoch;
	}

	function getNextPayPeriod($end_date = NULL) {
		if ( !$this->Validator->isValid() ) {
			return FALSE;
		}

		//Manual Pay Period Schedule, skip repeating...
		if ( $this->getType() == 5 ) {
			return FALSE;
		}

		$pplf = new PayPeriodListFactory();

		//Debug::text('PP Schedule ID: '. $this->getId(), __FILE__, __LINE__, __METHOD__, 10);
		//Debug::text('PP Schedule Name: '. $this->getName(), __FILE__, __LINE__, __METHOD__, 10);
		Debug::text('PP Schedule Type ('.$this->getType().'): '. Option::getByKey($this->getType(), $this->getOptions('type') ), __FILE__, __LINE__, __METHOD__, 10);
		//Debug::text('Anchor Date: '. $this->getAnchorDate() ." - ". TTDate::getDate('DATE+TIME', $this->getAnchorDate() ), __FILE__, __LINE__, __METHOD__, 10);
		//Debug::text('Primary Date: '. $this->getPrimaryDate() ." - ". TTDate::getDate('DATE+TIME', $this->getPrimaryDate() ), __FILE__, __LINE__, __METHOD__, 10);
		//Debug::text('Secondary Date: '. $this->getSecondaryDate() ." - ". TTDate::getDate('DATE+TIME', $this->getPrimaryDate() ), __FILE__, __LINE__, __METHOD__, 10);

		$last_pay_period_is_new = FALSE;
		if ( $end_date != '' AND $end_date != 0 ) {
			Debug::text('End Date is set: '. TTDate::getDate('DATE+TIME', $end_date), __FILE__, __LINE__, __METHOD__, 10);
			$last_pay_period_end_date = $end_date;
		} else {
			Debug::text('Checking for Previous pay periods...', __FILE__, __LINE__, __METHOD__, 10);
			//Get the last pay period schedule in the database.
			$pplf->getByPayPeriodScheduleId( $this->getId(), NULL, NULL, NULL, array('start_date' => 'desc') );
			$last_pay_period = $pplf->getCurrent();
			if ( $last_pay_period->isNew() ) {
				$last_pay_period_is_new = TRUE;

				Debug::text('No Previous pay periods...', __FILE__, __LINE__, __METHOD__, 10);

				//Do this so a rollover doesn't happen while we're calculating.
				//$last_pay_period_end_date = TTDate::getTime();
				//This causes the pay period schedule to jump ahead one month. So set this to be beginning of the month.
				$last_pay_period_end_date = TTDate::getBeginMonthEpoch();
			} else {
				Debug::text('Previous pay periods found... ID: '. $last_pay_period->getId(), __FILE__, __LINE__, __METHOD__, 10);
				$last_pay_period_end_date = $last_pay_period->getEndDate();
			}
			unset($last_pay_period, $pplf);
		}
		Debug::text('aLast Pay Period End Date: '. TTDate::getDate('DATE+TIME', $last_pay_period_end_date) .' ('.$last_pay_period_end_date .')', __FILE__, __LINE__, __METHOD__, 10);

		//FIXME: This breaks having pay periods with different daily start times.
		//However, without it, I think DST breaks pay periods.
		//$last_pay_period_end_date = TTDate::getEndDayEpoch( $last_pay_period_end_date + 1 ) - 86400;
		$last_pay_period_end_date = TTDate::getEndDayEpoch( $last_pay_period_end_date - (86400/2) );
		Debug::text('bLast Pay Period End Date: '. TTDate::getDate('DATE+TIME', $last_pay_period_end_date) .' ('.$last_pay_period_end_date .')', __FILE__, __LINE__, __METHOD__, 10);

		if ( $this->getDayStartTime() != 0 ) {
			Debug::text('Daily Start Time is set, adjusting Last Pay Period End Date by: '. TTDate::getHours( $this->getDayStartTime() ), __FILE__, __LINE__, __METHOD__, 10);
			//Next adjust last_pay_period_end_date (which becomes the start date) to DayStartTime because then there could be a gap if they
			//change this mid-schedule. The End Date will take care of it after the first pay period.
			$last_pay_period_end_date = TTDate::getTimeLockedDate( TTDate::getBeginDayEpoch($last_pay_period_end_date) + $this->getDayStartTime(), $last_pay_period_end_date);
			Debug::text('cLast Pay Period End Date: '. TTDate::getDate('DATE+TIME', $last_pay_period_end_date) .' ('.$last_pay_period_end_date .')', __FILE__, __LINE__, __METHOD__, 10);
		}

		$insert_pay_period = 1; //deprecate primary pay periods.
		switch ( $this->getType() ) {
			case 10: //Weekly
			case 20: //Bi-Weekly
				$last_pay_period_end_day_of_week = TTDate::getDayOfWeek( $last_pay_period_end_date );
				Debug::text('Last Pay Period End Day Of Week: '. $last_pay_period_end_day_of_week .' Start Day Of Week: '. $this->getStartDayOfWeek(), __LINE__, __METHOD__, 10);
				if ( $last_pay_period_end_day_of_week != $this->getStartDayOfWeek() ) {
					Debug::text('zTmp Pay Period End Date: '. 'next '. TTDate::getDayOfWeekByInt( $this->getStartDayOfWeek() ), __FILE__, __LINE__, __METHOD__, 10);
					//$tmp_pay_period_end_date = strtotime('next '. TTDate::getDayOfWeekByInt( $this->getStartDayOfWeek() ), $last_pay_period_end_date )-1;
					$tmp_pay_period_end_date = strtotime('next '. TTDate::getDayOfWeekByInt( $this->getStartDayOfWeek() ), $last_pay_period_end_date );

					//strtotime doesn't keep time when using "next", it resets it to midnight on the day, so we need to adjust for that.
					$tmp_pay_period_end_date = TTDate::getTimeLockedDate( TTDate::getBeginDayEpoch($tmp_pay_period_end_date) + $this->getDayStartTime(), $tmp_pay_period_end_date)-1;
				} else {
					$tmp_pay_period_end_date = $last_pay_period_end_date;

					//This should fix a bug where if they are creating a new pay period schedule
					//starting on Monday with the anchor date of 01-Jul-08, it would start on 01-Jul-08 (Tue)
					//rather moving back to the Monday.
					if ( TTDate::getDayOfMonth( $tmp_pay_period_end_date ) != TTDate::getDayOfMonth( $tmp_pay_period_end_date+1 ) ) {
						Debug::text('Right on day boundary, minus an additional second to account for difference...', __FILE__, __LINE__, __METHOD__, 10);
						$tmp_pay_period_end_date--;
					}
				}
				Debug::text('aTmp Pay Period End Date: '. TTDate::getDate('DATE+TIME', $tmp_pay_period_end_date) .' ('.$tmp_pay_period_end_date .')', __FILE__, __LINE__, __METHOD__, 10);

				$start_date = $tmp_pay_period_end_date+1;

				if ( $this->getType() == 10 ) { //Weekly
					$tmp_pay_period_end_date = TTDate::getMiddleDayEpoch($start_date) + (86400*7); //Add one week
				} elseif ( $this->getType() == 20 ) { //Bi-Weekly
					$tmp_pay_period_end_date = TTDate::getMiddleDayEpoch($start_date) + (86400*14); //Add two weeks
				}

				//Use Begin Day Epoch to nullify DST issues.
				$end_date = TTDate::getBeginDayEpoch( $tmp_pay_period_end_date )-1;
				$transaction_date = TTDate::getMiddleDayEpoch( TTDate::getMiddleDayEpoch($end_date) + ($this->getTransactionDate()*86400) );

				break;
			case 30: //Semi-monthly
				$tmp_last_pay_period_end_day_of_month = TTDate::getDayOfMonth( $last_pay_period_end_date+1 );
				Debug::text('bLast Pay Period End Day Of Month: '. $tmp_last_pay_period_end_day_of_month, __FILE__, __LINE__, __METHOD__, 10);

				if ( $tmp_last_pay_period_end_day_of_month == $this->convertLastDayOfMonth( $this->getPrimaryDayOfMonth() ) ) {
					$insert_pay_period = 1;
					$primary = TRUE;
				} elseif ( $tmp_last_pay_period_end_day_of_month == $this->convertLastDayOfMonth( $this->getSecondaryDayOfMonth() ) ) {
					$insert_pay_period = 2;
					$primary = FALSE;
				} else {
					Debug::text('Finding if Primary or Secondary is closest...', __FILE__, __LINE__, __METHOD__, 10);

					$primary_date_offset = TTDate::getDateOfNextDayOfMonth( $last_pay_period_end_date, NULL, $this->convertLastDayOfMonth( $this->getPrimaryDayOfMonth() ) ) - $last_pay_period_end_date;
					$secondary_date_offset = TTDate::getDateOfNextDayOfMonth( $last_pay_period_end_date, NULL, $this->convertLastDayOfMonth( $this->getSecondaryDayOfMonth() ) ) - $last_pay_period_end_date;
					Debug::text('Primary Date Offset: '. TTDate::getDays( $primary_date_offset ) .' Secondary Date Offset: '. TTDate::getDays( $secondary_date_offset ), __FILE__, __LINE__, __METHOD__, 10);

					if ( $primary_date_offset <= $secondary_date_offset ) {
						$insert_pay_period = 1;
						$primary = TRUE;

						$last_pay_period_end_date = TTDate::getDateOfNextDayOfMonth( $last_pay_period_end_date, NULL, $this->convertLastDayOfMonth( $this->getPrimaryDayOfMonth() ) );
					} else {
						$insert_pay_period = 2;
						$primary = FALSE;

						$last_pay_period_end_date = TTDate::getDateOfNextDayOfMonth( $last_pay_period_end_date, NULL, $this->convertLastDayOfMonth( $this->getSecondaryDayOfMonth() ) );
					}
					$last_pay_period_end_date = TTDate::getBeginDayEpoch( $last_pay_period_end_date );
				}
				unset($tmp_last_pay_period_end_day_of_month);
				Debug::text('cLast Pay Period End Date: '. TTDate::getDate('DATE+TIME', $last_pay_period_end_date) .' ('.$last_pay_period_end_date .') Primary: '. (int)$primary, __FILE__, __LINE__, __METHOD__, 10);

				$start_date = $last_pay_period_end_date+1;

				if ( $primary == TRUE ) {
					$end_date = TTDate::getBeginDayEpoch( TTDate::getDateOfNextDayOfMonth( $start_date, NULL, $this->convertLastDayOfMonth( $this->getSecondaryDayOfMonth() ) ) ) -1;
					$transaction_date = TTDate::getMiddleDayEpoch( TTDate::getDateOfNextDayOfMonth( TTDate::getMiddleDayEpoch($end_date), NULL, $this->convertLastDayOfMonth( $this->getPrimaryTransactionDayOfMonth() ) ) );
				} else {
					$end_date = TTDate::getBeginDayEpoch( TTDate::getDateOfNextDayOfMonth( $start_date, NULL, $this->convertLastDayOfMonth( $this->getPrimaryDayOfMonth() ) ) ) -1;
					$transaction_date = TTDate::getMiddleDayEpoch( TTDate::getDateOfNextDayOfMonth( TTDate::getMiddleDayEpoch($end_date), NULL, $this->convertLastDayOfMonth( $this->getSecondaryTransactionDayOfMonth() ) ) );
				}

				break;
			case 50: //Monthly
				$start_date = $last_pay_period_end_date+1;
				$end_date = TTDate::getDateOfNextDayOfMonth( $start_date+86400, NULL, $this->convertLastDayOfMonth( $this->getPrimaryDayOfMonth() ) );

				//Use Begin Day Epoch to nullify DST issues.
				$end_date = TTDate::getBeginDayEpoch( TTDate::getBeginMinuteEpoch($end_date) )-1;

				$transaction_date = TTDate::getMiddleDayEpoch( TTDate::getDateOfNextDayOfMonth( $end_date, NULL, $this->convertLastDayOfMonth( $this->getPrimaryTransactionDayOfMonth() ) ) );

				break;
		}

		if (  $this->getDayStartTime() != 0 ) {
			Debug::text('Daily Start Time is set, adjusting End Date by: '. TTDate::getHours( $this->getDayStartTime() ) .' Start Date: '. TTDate::getDate('DATE+TIME', $start_date), __FILE__, __LINE__, __METHOD__, 10);

			//We already account for DayStartTime in weekly/bi-weekly start_date cases above, so skip applying it again here.
			if ( $this->getType() != 10 AND $this->getType() != 20 ) {
				$start_date = $start_date + $this->getDayStartTime();
			}
			$end_date = $end_date + $this->getDayStartTime();

			//Need to do this, otherwise transaction date could be earlier then end date.
			$transaction_date = $transaction_date + $this->getDayStartTime();
		}

		Debug::text('aStart Date('. $start_date .'): '. TTDate::getDate('DATE+TIME', $start_date), __FILE__, __LINE__, __METHOD__, 10);
		Debug::text('aEnd Date('. $end_date .'): '. TTDate::getDate('DATE+TIME', $end_date), __FILE__, __LINE__, __METHOD__, 10);
		Debug::text('aPay Date('. $transaction_date .'): '. TTDate::getDate('DATE+TIME', $transaction_date), __FILE__, __LINE__, __METHOD__, 10);


		//Handle last day of the month flag for primary and secondary dates here
		if ( ( $this->getType() == 30
					AND (
						( $insert_pay_period == 1
						AND ( $this->getPrimaryDayOfMonth() == 31
							OR $this->getPrimaryDayOfMonth() == -1 )
						)
						OR ( $insert_pay_period == 2
							AND ( $this->getSecondaryDayOfMonth() == 31
								OR $this->getSecondaryDayOfMonth() == -1 )
							)
						)
			 )
			 OR
			 (
				$this->getType() == 50 AND ( $this->getPrimaryDayOfMonth() == 31 OR $this->getPrimaryDayOfMonth() == -1 )
			 ) ) {

			Debug::text('Last day of the month set for start date: ', __FILE__, __LINE__, __METHOD__, 10);
			if ( $this->getDayStartTime() > 0 ) {
				//Minus one day, THEN add daily start time, otherwise it will go past the month boundary
				$end_date = (TTDate::getEndMonthEpoch($end_date)-86400) + ( $this->getDayStartTime() ); //End month epoch is 23:59:59, so don't minus one.
			} else {
				$end_date = TTDate::getEndMonthEpoch($end_date) + ( $this->getDayStartTime() ); //End month epoch is 23:59:59, so don't minus one.
			}
		}

		//Handle "last day of the month" for transaction dates.
		if ( $this->getPrimaryDayOfMonth() == 31 OR $this->getPrimaryDayOfMonth() == -1 ) {
			//Debug::text('LDOM set for Primary: ', __FILE__, __LINE__, __METHOD__, 10);
			$transaction_date = TTDate::getEndMonthEpoch($transaction_date);
		}

		//Handle "always business day" flag for transaction dates here.
		if ( $this->getTransactionDateBusinessDay() == TRUE ) {
			$transaction_date = $this->getPreviousBusinessDay($transaction_date);
		}

		if ( $transaction_date < $end_date ) {
			$transaction_date = $end_date;
		}

		Debug::text('Start Date: '. TTDate::getDate('DATE+TIME', $start_date), __FILE__, __LINE__, __METHOD__, 10);
		Debug::text('End Date: '. TTDate::getDate('DATE+TIME', $end_date), __FILE__, __LINE__, __METHOD__, 10);
		Debug::text('Pay Date: '. TTDate::getDate('DATE+TIME', $transaction_date), __FILE__, __LINE__, __METHOD__, 10);
		Debug::text("<br><br>\n\n", __FILE__, __LINE__, __METHOD__, 10);

		$this->next_start_date = $start_date;
		$this->next_end_date = $end_date;
		$this->next_transaction_date = $transaction_date;

		//Its a primary pay period
		if ($insert_pay_period == 1) {
			$this->next_primary = TRUE;
		} else {
			$this->next_primary = FALSE;
		}

		return TRUE;
	}

	function createNextPayPeriod($end_date = NULL, $offset = NULL) {
		if ( $end_date == NULL OR $end_date == '' ) {
			$end_date = NULL;
		}

		if ( $offset == NULL OR $offset == '' ) {
			$offset = 86400; //24hrs
		}

		if ( $this->getType() == 5 ) {
			return FALSE;
		}

		Debug::text('Current TimeZone: '. TTDate::getTimeZone(), __FILE__, __LINE__, __METHOD__, 10);
		//Handle timezones in this function rather then getNextPayPeriod()
		//Because if we set the timezone back to the original in that function, it
		//gets written to the database in the "original" timezone, not the proper timezone.
		$this->setPayPeriodTimeZone();
		Debug::text('Pay Period TimeZone: '. TTDate::getTimeZone(), __FILE__, __LINE__, __METHOD__, 10);

		Debug::text('End Date ('. $end_date.'): '. TTDate::getDate('DATE+TIME', $end_date ), __FILE__, __LINE__, __METHOD__,10);

		$this->getNextPayPeriod($end_date);

		Debug::text('Next pay period starts: '. TTDate::getDate('DATE+TIME', $this->getNextStartDate() ), __FILE__, __LINE__, __METHOD__,10);

		//If the start date is within 24hrs of now, insert the next pay period.
		if ( $this->getNextStartDate() <= ( TTDate::getTime() + $offset ) ) {
			Debug::text('Insert new pay period. Start Date: '. $this->getNextStartDate() .' End Date: '. $this->getNextEndDate() , __FILE__, __LINE__, __METHOD__,10);

			$ppf = new PayPeriodFactory();
			$ppf->setCompany( $this->getCompany() );
			$ppf->setPayPeriodSchedule( $this->getId() );
			$ppf->setStatus(10);
			$ppf->setStartDate( $this->getNextStartDate() );
			$ppf->setEndDate( $this->getNextEndDate() );
			$ppf->setTransactionDate( $this->getNextTransactionDate() );

			$ppf->setPrimary( $this->getNextPrimary() );
			$ppf->setEnableImportData( TRUE ); //Import punches when creating new pay periods.
			if ( $ppf->isValid() ) {
				$new_pay_period_id = $ppf->Save();
				Debug::text('New Pay Period ID: '. $new_pay_period_id, __FILE__, __LINE__, __METHOD__,10);

				if ( $new_pay_period_id != '' ) {
					/*
					//
					//Handle this in PayPeriodFactory now.
					//
					//Check for any user_date rows in this time period that are not assigned to a pay period.
					//Get all users assigned to this pp schedule
					$udlf = new UserDateListFactory();
					$udlf->getByUserIdAndStartDateAndEndDateAndEmptyPayPeriod( $this->getUser(), $this->getNextStartDate(), $this->getNextEndDate() );
					Debug::text(' New Pay Period ID: '. $new_pay_period_id .' Pay Period orphaned User Date Rows: '. $udlf->getRecordCount(), __FILE__, __LINE__, __METHOD__,10);
					if ( $udlf->getRecordCount() > 0 ) {
						foreach( $udlf as $ud_obj ) {
							$ud_obj->setPayPeriod( $new_pay_period_id );
							if ( $ud_obj->isValid() ) {
								$ud_obj->Save();
							}
						}
					}
					*/

					$this->setOriginalTimeZone();

					return TRUE;
				} else {
					Debug::text('aSaving Pay Period Failed!', __FILE__, __LINE__, __METHOD__,10);
				}
			} else {
				Debug::text('bSaving Pay Period Failed!', __FILE__, __LINE__, __METHOD__,10);
			}

		} else {
			Debug::text('***NOT inserting or changing status of new pay period yet, not within offset.', __FILE__, __LINE__, __METHOD__,10);
		}

		$this->setOriginalTimeZone();

		return FALSE;
	}


	function getNextStartDate() {
		if ( isset($this->next_start_date) ) {
			return $this->next_start_date;
		}

		return FALSE;
	}

	function getNextEndDate() {
		if ( isset($this->next_end_date) ) {
			return $this->next_end_date;
		}

		return FALSE;
	}

	function getNextTransactionDate() {
		if ( isset($this->next_transaction_date) ) {
			return $this->next_transaction_date;
		}

		return FALSE;
	}

	function getNextAdvanceEndDate() {
		if ( isset($this->next_advance_end_date) ) {
			return $this->next_advance_end_date;
		}

		return FALSE;
	}

	function getNextAdvanceTransactionDate() {
		if ( isset($this->next_advance_transaction_date) ) {
			return $this->next_advance_transaction_date;
		}

		return FALSE;
	}

	function getNextPrimary() {
		if ( isset($this->next_primary) ) {
			return $this->next_primary;
		}

		return FALSE;
	}

	function getCurrentPayPeriodNumber($epoch = NULL, $end_date_epoch = NULL) {
		//EPOCH MUST BE TRANSACTION DATE!!!
		//End Date Epoch must be END DATE of pay period

		//Don't return pay period number if its a manual schedule.
		if ( $this->getType() == 5 ) {
			return FALSE;
		}

		//FIXME: Turn this query in to a straight count(*) query for even more speed.
		if ($epoch == NULL OR $epoch == '') {
			$epoch = TTDate::getTime();
		}
		//Debug::text('Epoch: '. TTDate::getDate('DATE+TIME',$epoch) .' - End Date Epoch: '. TTDate::getDate('DATE+TIME',$end_date_epoch) , __FILE__, __LINE__, __METHOD__, 10);

/*
		//FIXME: If a company starts with TimeTrex half way through the year, this will be incorrect.
		//Because it only counts pay periods that exist, not pay periods that WOULD have existed.
		$pplf = new PayPeriodListFactory();
		$pplf->getByPayPeriodScheduleIdAndStartTransactionDateAndEndTransactionDate( $this->getId(), TTDate::getBeginYearEpoch( $epoch ), $epoch );
		$retval = $pplf->getRecordCount();

		Debug::text('Current Pay Period: '. $retval , __FILE__, __LINE__, __METHOD__, 10);
*/


		//Half Fixed method here. We cache the results so to speed it up, but there still might be a faster way to do this.
		//FIXME: Perhaps some type of hybrid system like the above unless they have less then a years worth of
		//pay periods, then use this method below?
		$id = $this->getId().$epoch.$end_date_epoch;

		$retval = $this->getCache($id);

		if ( $retval === FALSE ) {
			//FIXME: I'm sure there is a quicker way to do this.
			$next_transaction_date = 0;
			$next_end_date = $end_date_epoch;
			$end_year_epoch = TTDate::getEndYearEpoch( $epoch );
			$i=0;

			while ( $next_transaction_date <= $end_year_epoch AND $i < 100 ) {
				//Debug::text('I: '. $i .' Looping: Transaction Date: '. TTDate::getDate('DATE+TIME',$next_transaction_date) .' - End Year Epoch: '. TTDate::getDate('DATE+TIME',$end_year_epoch) , __FILE__, __LINE__, __METHOD__, 10);
				$this->getNextPayPeriod( $next_end_date );

				$next_transaction_date = $this->getNextTransactionDate();
				$next_end_date = $this->getNextEndDate();

				if ( $next_transaction_date <= $end_year_epoch ) {
					$i++;
				}
			}

			Debug::text('i: '. $i , __FILE__, __LINE__, __METHOD__, 10);

			$retval = $this->getAnnualPayPeriods() - $i;
			Debug::text('Current Pay Period: '. $retval , __FILE__, __LINE__, __METHOD__, 10);

			//Cache results
			$this->saveCache($retval,$id);
		}

		return $retval;
	}

	function getAnnualPayPeriods() {
		switch ( $this->getType() ) {
			case 5:
				//We need the annual number of pay periods calculated for manual pay period schedules if we
				//are to have any hope of calculating taxes correctly.
				//Get all the pay periods, take the first day, last day, and the total number to figure out an average
				//number of days per period.
				//Alternatively have them manually specify the number, but this required adding a field to the table.
				$retval = FALSE;

				if ( $this->getId() > 0 ) {
					$pplf = new PayPeriodListFactory();
					$retarr = $pplf->getFirstStartDateAndLastEndDateByPayPeriodScheduleId( $this->getId() );
					if ( is_array($retarr) AND isset($retarr['first_start_date']) AND isset($retarr['last_end_date']) ) {
						$retarr['first_start_date'] = TTDate::strtotime( $retarr['first_start_date'] );
						$retarr['last_end_date']= TTDate::strtotime( $retarr['last_end_date'] );

						$days_per_period = ( ( $retarr['last_end_date'] - $retarr['first_start_date'] ) / $retarr['total']) / 86400;
						$retval = floor(365 / round( $days_per_period ) );
						Debug::text('First Start Date: '. TTDate::getDate('DATE+TIME', $retarr['first_start_date']) .' Last End Date: '. TTDate::getDate('DATE+TIME', $retarr['last_end_date']) .' Total PP: '. $retarr['total'] .' Average Days/Period: '. $days_per_period .'('. round($days_per_period).') Annual Pay Periods: '. $retval, __FILE__, __LINE__, __METHOD__, 10);
					}
					unset($pplf, $retarr);
				}

				break;
			case 10:
				$retval = 52;
				break;
			case 20:
				$retval = 26;
				break;
			case 30:
				$retval = 24; //Semi-monthly
				break;
			case 40:
				$retval = 12; //Monthly + advance, deductions only once per month
				break;
			case 50:
				$retval = 12;
				break;
			default:
				return FALSE;
				break;
		}

		return $retval;
	}

	function getEnableInitialPayPeriods() {
		if ( isset($this->enable_create_initial_pay_periods) ) {
			return $this->enable_create_initial_pay_periods;
		}

		return FALSE;
	}

	function setEnableInitialPayPeriods( $val ) {
		$this->enable_create_initial_pay_periods = (bool)$val;

		return TRUE;
	}

	function getCreateInitialPayPeriods() {
		if ( isset($this->create_initial_pay_periods) ) {
			return $this->create_initial_pay_periods;
		}

		return FALSE;
	}

	function setCreateInitialPayPeriods( $val ) {
		$this->create_initial_pay_periods = (bool)$val;

		return TRUE;
	}

	function preSave() {
		$this->StartTransaction();

		if ( $this->isNew() == TRUE ) {
			$this->setCreateInitialPayPeriods( TRUE );
		}

		if ( $this->getDeleted() == TRUE ) {
			//Delete pay periods assigned to this schedule.
			$pplf = new PayPeriodListFactory();
			$pplf->getByPayPeriodScheduleId( $this->getId() );
			if ( $pplf->getRecordCount() > 0 ) {
				Debug::text('Delete Pay Periods: '. $pplf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10);
				foreach( $pplf as $pp_obj ) {
					$pp_obj->setDeleted(TRUE);
					$pp_obj->Save();
				}
			}

		}

		return TRUE;
	}

	function Validate() {
		if ( $this->getDeleted() == TRUE ) {
			return TRUE;
		}

		return TRUE;
	}

	function postSave() {
		if ( $this->getEnableInitialPayPeriods() == TRUE AND $this->getCreateInitialPayPeriods() == TRUE ) {
			$ppslf = new PayPeriodScheduleListFactory();
			$pay_period_schedule_obj = $ppslf->getById( $this->getId() )->getCurrent();

			$pay_period_schedule_obj->createNextPayPeriod( $pay_period_schedule_obj->getAnchorDate() );
			Debug::text('New Pay Period Schdule, creating pay periods start from ('.$pay_period_schedule_obj->getAnchorDate().'): '. TTDate::getDate('DATE+TIME', $pay_period_schedule_obj->getAnchorDate() ), __FILE__, __LINE__, __METHOD__, 10);

			//Create pay periods up until now, at most 104.
			for($i=0; $i <= 104; $i++ ) {
				if ( $pay_period_schedule_obj->createNextPayPeriod() == FALSE ) {
					Debug::text('createNextPayPeriod returned false, stopping loop.', __FILE__, __LINE__, __METHOD__, 10);
					break;
				}
			}
		}

		$this->CommitTransaction();

		return TRUE;
	}

	function addLog( $log_action ) {
		return TTLog::addEntry( $this->getId(), $log_action, TTi18n::getText('Pay Period Schedule'), NULL, $this->getTable() );
	}

}
?>
