<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2012 - 2013 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * flDate Class
 *
 * Class to provide date related functions.
 *
 * @package fusionLib
 */
class flDate {

	/**#@+
	 * Standard date formats.
	 */
	const ATOM = '%Y-%m-%dT%H:%i:%s%P';
	const COOKIE = '%l, %d-%M-%y %H:%i:%s %T';
	const ISO8601 = '%Y-%m-%dT%H:%i:%s%O';
	const RFC822 = '%D, %d %M %y %H:%i:%s %O';
	const RFC850 = '%l, %d-%M-%y %H:%i:%s %T';
	const RFC1036 = '%D, %d %M %y %H:%i:%s %O';
	const RFC1123 = '%D, %d %M %Y %H:%i:%s %O';
	const RFC2822 = '%D, %d %M %Y %H:%i:%s %O';
	const RFC3339 = '%Y-%m-%dT%H:%i:%s%P';
	const RSS = '%D, %d %M %Y %H:%i:%s %O';
	const W3C = '%Y-%m-%dT%H:%i:%s%P';

	const TIME12 = '%g:%i:%s %A';
	const TIME24 = '%H:%i:%s';
	const TIME12SHORT = '%g:%i %A';
	const TIME24SHORT = '%H:%i';
	const DATE = '%Y-%m-%d';
	const DATE2 = '%Y-%M-%d';
	const DATETIME = '%Y-%m-%d %H:%i:%s';
	const DATETIMESHORT = '%Y-%m-%d %H:%i';
	/**#@-*/

	/**
	 * The time value.
	 *
	 * @var DateTime
	 */
	protected $value;

	/**
	 * The default format to use for strings.
	 *
	 * @var string
	 */
	protected $defaultFmt = flDate::DATETIME;

	/**
	 * Construct the date object.
	 *
	 * If the timezone is specified in $date then it will be used for setting the date then the value of $tz applied, otherwise the
	 * value in $date is assumed to be in $tz timezone.
	 *
	 * @param string $date The date and time to initialise the object with, yyyy-mm-dd hh:mm:ss, @12345 to set a timestamp or null for current date and time.
	 * @param string $tz The timezone for $date.
	 */
	function __construct($date = null, $tz = 'UTC') {
		$this->value = new DateTime($date, $zone = new DateTimeZone($tz));
		$this->value->setTimezone($zone);
	}

	/**
	 * Set the timezone.
	 *
	 * @param string $tz The timezone to use.
	 * @return flDate This object.
	 */
	function setTimezone($tz) {
		$this->value->setTimezone(new DateTimeZone($tz));
		return $this;
	}

	/**
	 * Set the time.
	 *
	 * @param int $hour The hour (0 - 23)
	 * @param int $minute The minute (0 - 59)
	 * @param int $second The seconds (0 - 59)
	 * @return flDate This object.
	 */
	function setTime($hour, $minute, $second = 0) {
		$this->value->setTime($hour, $minute, $second);
		return $this;
	}

	/**
	 * Set the date.
	 *
	 * @param int $year The year.
	 * @param int $month The month (1 - 12)
	 * @param int $day The day of the month (1 - 31)
	 * @return flDate This object.
	 */
	function setDate($year, $month, $day) {
		$this->value->setDate($year, $month, $day);
		return $this;
	}

	/**
	 * Set the default format string to use.
	 *
	 * @param string $fmt The default format string to use in toString.
	 * @return flDate This object.
	 */
	function setDefaultFormat($fmt) {
		$this->defaultFmt = $fmt;
		return $this;
	}

	/**
	 * Test if a year is a leap year.
	 *
	 * @param int $year The year to test.
	 * @return bool true if leap year (366 days) of false if not (365 days).
	 */
	protected function isLeapYear($year) {

		// If the year is evenly divisible by 4 then could be
		if($year % 4 == 0) {
			// If year not evenly divisible by 100 then leap
			if($year % 100 != 0)
				return true;

			// If year not evenly divisible by 400 then leap
			if($year % 400 == 0)
				return true;
		}

		// Not a leap year
		return false;
	}

	/**
	 * Calculate a timestamp since 00:00:00 UTC on 1 January 1970.
	 *
	 * @return float The timestamp.
	 */
	function getTimestamp() {
		static $daysInMonth = array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);

		// Clone force to UTC
		$t1 = clone $this;
		$t1->setTimezone('UTC');

		$time = 0;

		// Find days in years since 1970
		$year = $t1->toString('%Y');
		for($c=1970;$c<$year;$c++)
			$time += $this->isLeapYear($c) ? 366 : 365;

		// Find days since start of year
		$isLeap = $this->isLeapYear($year);
		$month = $t1->toString('%m') - 1;
		for($c=0;$c<$month;$c++)
			$time += $c == 1 && $isLeap ? 29 : $daysInMonth[$c];

		// Add in day of month
		$time += $t1->toString('%d') - 1;

		// Convert to seconds
		$time *= 86400;

		// Add in hours, minutes & seconds
		$time += $t1->toString('%H') * 3600;
		$time += $t1->toString('%i') * 60;
		$time += $t1->toString('%s');

		return $time;
	}

	/**
	 * Set the object to the given timestamp.
	 *
	 * @param float $time The timestamp since 00:00:00 UTC on 1 January 1970.
	 * @return flDate This object.
	 */
	function setTimestamp($time) {
		$tz = $this->value->getTimezone();
		$this->value = new DateTime('@' . $time, new DateTimeZone('UTC'));
		$this->value->setTimezone($tz);
		return $this;
	}

	/**
	 * Time between the current value and and earlier value.
	 *
	 * @param flDate $since The date object to return how long ago it was.
	 * @return array offset, units where units one of years, months, days, hours, minutes, seconds.
	 * @throws flDateException
	 */
	function timeSpan($since) {
		$secs = $this->getTimestamp() - $since->getTimestamp();
		if($secs < 0)
			throw new flDateException('The date passed to timeSpan must be before the current time.');

		if($years = floor($secs / 31536000))
			return array('offset' => $years, 'units' => 'years', 'string' => sprintf(_n('%d Year Ago', '%d Years Ago', $years, 'fusionLib'), $years));
		$secs -= $years * 31536000;

		if($months = floor($secs / 2628000))
			return array('offset' => $months, 'units' => 'months', 'string' => sprintf(_n('%d Month Ago', '%d Months Ago', $months, 'fusionLib'), $months));
		$secs -= $months * 2628000;

		if($days = floor($secs / 86400))
			return array('offset' => $days, 'units' => 'days', 'string' => sprintf(_n('%d Day Ago', '%d Days Ago', $days, 'fusionLib'), $days));
		$secs -= $days * 86400;

		if($hours = floor($secs / 3600))
			return array('offset' => $hours, 'units' => 'hours', 'string' => sprintf(_n('%d Hour Ago', '%d Hours Ago', $hours, 'fusionLib'), $hours));
		$secs -= $hours * 3600;

		if($mins = floor($secs / 60))
			return array('offset' => $mins, 'units' => 'minutes', 'string' => sprintf(_n('%d Minute Ago', '%d Minutes Ago', $mins, 'fusionLib'), $mins));
		$secs -= $mins * 60;

		return array('offset' => $secs, 'units' => 'seconds', 'string' => sprintf(_n('%d Second Ago', '%d Seconds Ago', $secs, 'fusionLib'), $secs));
	}

	/**
	 * Format a date into a string. Date codes are proceeded with % in the same way as MySQL DATE_FORMAT.
	 *
	 * @param string $fmt The format string for the date or null to use the default.
	 * @return string The formatted date.
	 */
	function toString($fmt = null) {
		if($fmt === null)
			$fmt = $this->defaultFmt;

		// Translate day and month names
		fusionLib::bindTextDomain('fusionLib');
		if(strpos($fmt, '%D') !== false) {
			$shortDay = array(__('Sun'), __('Mon'), __('Tue'), __('Wed'), __('Thu'), __('Fri'), __('Sat'));
			$fmt = str_replace('%D', $shortDay[$this->value->format('w')], $fmt);
		}
		if(strpos($fmt, '%l') !== false) {
			$shortDay = array(__('Sunday'), __('Monday'), __('Tuesday'), __('Wednesday'), __('Thursday'), __('Friday'), __('Saturday'));
			$fmt = str_replace('%l', $shortDay[$this->value->format('w')], $fmt);
		}
		if(strpos($fmt, '%M') !== false) {
			$shortDay = array(__('Jan'), __('Feb'), __('Mar'), __('Apr'), __('May'), __('Jun'), __('Jul'), __('Aug'), __('Sep'), __('Oct'), __('Nov'), __('Dec'));
			$fmt = str_replace('%M', $shortDay[$this->value->format('n') - 1], $fmt);
		}
		if(strpos($fmt, '%F') !== false) {
			$shortDay = array(__('January'), __('February'), __('March'), __('April'), __('May'), __('June'), __('July'), __('August'), __('September'), __('October'), __('November'), __('December'));
			$fmt = str_replace('%F', $shortDay[$this->value->format('n') - 1], $fmt);
		}

		return $this->value->format(str_replace('%\\', '', preg_replace("/([a-z]+?){1}/i", "\\\\\\1", $fmt)));
	}

	/**
	 * Return the date as a string.
	 *
	 * @return string The date as a string.
	 */
	function __toString() {
		return $this->toString();
	}

	/**
	 * Clone internal objects.
	 */
	function __clone() {
		$this->value = clone $this->value;
	}

	/**
	 * Compares this date object with the passed date and time object.
	 *
	 * @param flDate $with The object to compare with this.
	 * @return int -1 if this is < $with, 0 if they are equal, 1 if this is > $with
	 */
	function compare($with) {

		// Clone objects and force to UTC
		$t1 = clone $this;
		$t2 = clone $with;
		$t1->setTimezone('UTC');
		$t2->setTimezone('UTC');

		$el = array('%Y', '%m', '%d', '%H', '%i', '%s');
		foreach($el as $e) {
			$dt = $t1->toString($e) - $t2->toString($e);
			if($dt > 0)
				return 1;
			else if($dt < 0)
				return -1;
		}

		return 0;
	}
}
