<?php
/**
* @version	$Id: date_formatter.php 15137 2012-03-04 08:06:21Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2011 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

defined('FULL_PATH') or die('restricted access!');

class kDateFormatter extends kFormatter {

	/**
	 * Current Language
	 *
	 * @var LanguagesItem
	 */
	var $language = null;

	/**
	 * Create date formatter
	 *
	 * @access public
	 */
	public function __construct()
	{
		parent::__construct();

		$this->language = $this->Application->recallObject('lang.current');
	}

	/**
	 * Sets mixed format (date + time) for field if not set directly
	 *
	 * @param Array $field_options options of field
	 * @param Array $format separate formats for date & time
	 * @param string $type destination key in field_options to store mixed format
	 */
	function SetMixedFormat(&$field_options, &$format, $type)
	{
		if (!isset($field_options[$type])) {
			// default value is date+separator+time
			$field_options[$type] = '_regional_DateTimeFormat';
		}

		if ($field_options[$type] == '_regional_DateTimeFormat') {
			$field_options[$type] = $format['date'].$field_options['date_time_separator'].$format['time'];
		}
		else if(preg_match('/_regional_(.*)/', $field_options[$type], $regs)) {
			$field_options[$type] = $this->language->GetDBField($regs[1]);
		}
		$format['mixed'] = $field_options[$type];
	}

	/**
	 * Returns separate formats for date,time,combined for input & display formats
	 *
	 * @param Array $field_options options of field
	 * @param string $type type of requested information = {mixed,date,time}
	 * @return Array display & input formats
	 */
	function GetSeparateFormats(&$field_options, $type)
	{
		if ($type == 'mixed') {
			if (!isset($field_options['date_time_separator'])) $field_options['date_time_separator'] = ' ';

			$display_format = Array ();
			$input_format = Array ();

			list ($display_format['date'], $input_format['date']) = $this->GetSeparateFormats($field_options, 'date');
			list ($display_format['time'], $input_format['time']) = $this->GetSeparateFormats($field_options, 'time');

			$this->SetMixedFormat($field_options, $display_format, 'format');
			$this->SetMixedFormat($field_options, $input_format, 'input_format');

			return Array ($display_format, $input_format);
		}
		else {
			// 1. set display format
			if (isset($field_options[$type.'_format'])) {
				$format = $field_options[$type.'_format'];
			}
			else {
				$format = $this->language->GetDBField(ucfirst($type).'Format');
			}

			// 2. set input format
			if (isset($field_options['input_'.$type.'_format'])) {
				$input_format = $field_options['input_'.$type.'_format'];
			}
			else {
				$input_format = $this->language->GetDBField('Input'.ucfirst($type).'Format');
			}

			return Array ($format, $input_format);
		}
	}

	/**
	 * The method is supposed to alter config options or configure object in some way based on its usage of formatters
	 * The methods is called for every field with formatter defined when configuring item.
	 * Could be used for adding additional VirtualFields to an object required by some special Formatter
	 *
	 * @param string $field_name
	 * @param array $field_options
	 * @param kDBBase $object
	 */
	function PrepareOptions($field_name, &$field_options, &$object)
	{
		list ($display_format, $input_format) = $this->GetSeparateFormats($field_options, 'mixed');

		$field_options['sub_fields'] = Array('date' => $field_name.'_date', 'time' => $field_name.'_time');
		if (!isset($field_options['use_timezone'])) {
			// apply timezone from server
			$field_options['use_timezone'] = true;
		}

		// 1. add field to indicate, that date is already combined into one field
		$add_fields = Array (
			$field_name . '_combined' => Array ('type' => 'int', 'default' => 0),
		);

		// 2. add DATE virtual field
		$opts = Array('master_field' => $field_name, 'formatter' => 'kDateFormatter', 'format' => $display_format['date'], 'input_format' => $input_format['date']);

		$copy_options = Array ('type', 'default', 'required', 'use_timezone', 'error_msgs');
		foreach ($copy_options as $copy_option) {
			if ( array_key_exists($copy_option, $field_options) ) {
				$opts[$copy_option] = $field_options[$copy_option];
			}
		}

		$add_fields[$field_name . '_date'] = $opts;

		// 3. add TIME virtual field
		$opts['format'] = $display_format['time'];
		$opts['input_format'] = $input_format['time'];
		$add_fields[$field_name . '_time'] = $opts;

		$filter_type = getArrayValue($field_options, 'filter_type');
		if ( $filter_type == 'range' ) {
			$opts['format'] = $field_options['format'];
			$add_fields[$field_name . '_rangefrom'] = $opts;
			$add_fields[$field_name . '_rangeto'] = $opts;
		}

		if ( !$object->isVirtualField($field_name) ) {
			// adding calculated field to format date directly in the query
			$object->addCalculatedField($field_name . '_date', '%1$s.' . $field_name);
			$object->addCalculatedField($field_name . '_time', '%1$s.' . $field_name);
		}

		$virtual_fields = $object->getVirtualFields();
		$add_fields = kUtil::array_merge_recursive($add_fields, $virtual_fields);
		$object->setVirtualFields($add_fields);
	}

	/**
	 * Used for split fields like timestamp -> date, time
	 * Called from DBItem to update sub fields values after loading item
	 *
	 * @param string $field
	 * @param string $value
	 * @param Array $options
	 * @param kDBItem $object
	 * @return void
	 * @access public
	 */
	public function UpdateSubFields($field, $value, &$options, &$object)
	{
		$sub_fields = getArrayValue($options, 'sub_fields');

		if ( !$sub_fields || !isset($value) || !$value ) {
			return ;
		}

		$object->SetDBField($sub_fields['date'], $value);
		$object->SetDBField($sub_fields['time'], $value);
	}

	/**
	 * Used for split fields like timestamp -> date, time
	 * Called from DBItem Validate (before validation) to get back master field value from its sub_fields
	 *
	 * @param string $field
	 * @param mixed $value
	 * @param Array $options
	 * @param kDBItem $object
	 */
	function UpdateMasterFields($field, $value, &$options, &$object)
	{
		$sub_fields = getArrayValue($options, 'sub_fields');
		$master_field = getArrayValue($options, 'master_field');

		if ( $master_field ) {
			// when in one of sub_fields - call update for master_field to update its value from sub_fields [are you following ? :) ]
			$opt = $object->GetFieldOptions($master_field);
			$this->UpdateMasterFields($master_field, null, $opt, $object);
		}
		elseif ( $sub_fields && !$object->GetDBField($field . '_combined') ) {
			// when in master field - set own value from sub_fields
			if ( $object->GetDBField($sub_fields['date']) != '' && $object->GetDBField($sub_fields['time']) == '' ) {
				// when time is not supplied, then use "midnight" (or unit config override)
				$empty_time = getArrayValue($options, 'empty_time');
				if ( $empty_time === false ) {
					$empty_time = adodb_mktime(0, 0, 0);
				}
				$object->SetDBField($sub_fields['time'], $empty_time);
			}
			elseif ( $object->GetDBField($sub_fields['time']) != '' && $object->GetDBField($sub_fields['date']) == '' ) {
				// when date is not supplied, then use "1970-01-01 00:00:00" instead (or unit config override)
				$empty_date = getArrayValue($options, 'empty_date');
				if ( $empty_date === false ) {
					$empty_date = adodb_mktime(0, 0, 0, 1, 1, 1970);
				}
				$object->SetDBField($sub_fields['date'], $empty_date);
			}

			$input_format['date'] = $object->GetFieldOption($sub_fields['date'], 'input_format');
			$input_format['time'] = $object->GetFieldOption($sub_fields['time'], 'input_format');

			$object->SetField($field, $object->GetField($sub_fields['date'], $input_format['date']) . $options['date_time_separator'] . $object->GetField($sub_fields['time'], $input_format['time']));
		}
	}

	/**
	 * Formats value of a given field
	 *
	 * @param string $value
	 * @param string $field_name
	 * @param kDBItem|kDBList $object
	 * @param string $format
	 * @return string
	 */
	function Format($value, $field_name, &$object, $format=null)
	{
		if ( is_null($value) ) return '';
		if ( !is_numeric($value) ) {
			return $value; // for leaving badly formatted date on the form
		}
		settype($value, 'int');
		if ( !is_int($value) ) {
			return $value;
		}

		$options = $object->GetFieldOptions($field_name);
		if ( isset($format) ) {
			$options['format'] = $format;
		}

		if (preg_match('/_regional_(.*)/', $options['format'], $regs)) {
			// when such type of format is given directly to kDBBase::GetField
			$options['format'] = $this->language->GetDBField($regs[1]);
		}

		if ($options['format'] == '_input_') {
			// use input format instead of output format
			$options['format'] = $options['input_format'];
		}

		if (!$options['use_timezone']) {
			return adodb_gmdate($options['format'], $value);
		}

		$format = defined($options['format']) ? constant($options['format']) : $options['format'];
		$dt_separator = getArrayValue($options, 'date_time_separator');

		if ($dt_separator) {
			$format = trim($format, $dt_separator);
		}

		return adodb_date($format, $value);
	}

	function HumanFormat($format)
	{
		$patterns = Array('/m/',
											'/n/',
											'/d/',
											'/j/',
											'/y/',
											'/Y/',
											'/h|H/',
											'/g|G/',
											'/i/',
											'/s/',
											'/a|A/');
		$replace = Array(	'mm',
											'm',
											'dd',
											'd',
											'yy',
											'yyyy',
											'hh',
											'h',
											'mm',
											'ss',
											'AM');
		$res = preg_replace($patterns, $replace, $format);
		return $res;
	}

	function SQLFormat($format)
	{
		$mapping = Array(
			'/%/' => '%%',
			'/(?<!%)a/' => '%p', // Lowercase Ante meridiem and Post meridiem => MySQL provides only uppercase
			'/(?<!%)A/' => '%p', // Uppercase Ante meridiem and Post meridiem
			'/(?<!%)d/' => '%d', // Day of the month, 2 digits with leading zeros
			'/(?<!%)D/' => '%a', // A textual representation of a day, three letters
			'/(?<!%)F/' => '%M', // A full textual representation of a month, such as January or March
			'/(?<!%)g/' => '%l', // 12-hour format of an hour without leading zeros
			'/(?<!%)G/' => '%k', // 24-hour format of an hour without leading zeros
			'/(?<!%)h/' => '%h', // 12-hour format of an hour with leading zeros
			'/(?<!%)H/' => '%H', // 24-hour format of an hour with leading zeros
			'/(?<!%)i/' => '%i', // Minutes with leading zeros
				'/(?<!%)I/' => 'N/A', // Whether or not the date is in daylights savings time

				'/(?<!%)S/' => 'N/A', // English ordinal suffix for the day of the month, 2 characters, see below
				'/jS/' => '%D', //  MySQL can't return separate suffix, but could return date with suffix
			'/(?<!%)j/' => '%e', // Day of the month without leading zeros
			'/(?<!%)l/' => '%W', // A full textual representation of the day of the week
				'/(?<!%)L/' => 'N/A', // Whether it's a leap year
			'/(?<!%)m/' => '%m', // Numeric representation of a month, with leading zeros
			'/(?<!%)M/' => '%b', // A short textual representation of a month, three letters
			'/(?<!%)n/' => '%c', // Numeric representation of a month, without leading zeros
				'/(?<!%)O/' => 'N/A', // Difference to Greenwich time (GMT) in hours
				'/(?<!%)r/' => 'N/A', // RFC 2822 formatted date
			'/(?<!%)s/' => '%s', // Seconds, with leading zeros
				// S and jS moved before j - see above
				'/(?<!%)t/' => 'N/A', // Number of days in the given month
				'/(?<!%)T/' => 'N/A', // Timezone setting of this machine
				'/(?<!%)U/' => 'N/A', // Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT)
			'/(?<!%)w/' => '%w', // Numeric representation of the day of the week
			'/(?<!%)W/' => '%v', // ISO-8601 week number of year, weeks starting on Monday (added in PHP 4.1.0)
			'/(?<!%)Y/' => '%Y', // A full numeric representation of a year, 4 digits
			'/(?<!%)y/' => '%y', // A two digit representation of a year
				'/(?<!%)z/' => 'N/A', // The day of the year (starting from 0) => MySQL starts from 1
				'/(?<!%)Z/' => 'N/A', // Timezone offset in seconds. The offset for timezones west of UTC is always negative, and for those east of UTC is always positive.
		);

		$patterns = array_keys($mapping);
		$replacements = array_values($mapping);

		$res = preg_replace($patterns, $replacements, $format);
		return $res;
	}

	/**
	 * Converts formatted date+time to timestamp and validates format
	 *
	 * @param mixed $value
	 * @param string $field_name
	 * @param kDBItem $object
	 * @return mixed
	 * @access public
	 */
	public function Parse($value, $field_name, &$object)
	{
		$options = $object->GetFieldOptions($field_name);

		$dt_separator = getArrayValue($options,'date_time_separator');
		if($dt_separator) $value = trim($value, $dt_separator);
		if($value == '') return NULL;
		//return strtotime($value);

		$format = $options['input_format'];
		if ($dt_separator) $format = trim($format, $dt_separator);

		$error_params = Array( $this->HumanFormat($format), adodb_date($format), 'value' => $value );

		$hour = 0;
		$minute = 0;
		$second = 0;
		$month = 1;
		$day = 1;
		$year = 1970;

		$patterns['n'] = '([0-9]{1,2})';
		$patterns['m'] = '([0-9]{1,2})';
		$patterns['d'] = '([0-9]{1,2})';
		$patterns['j'] = '([0-9]{1,2})';
		$patterns['Y'] = '([0-9]{4})';
		$patterns['y'] = '([0-9]{2})';
		$patterns['G'] = '([0-9]{1,2})';
		$patterns['g'] = '([0-9]{1,2})';
		$patterns['H'] = '([0-9]{2})';
		$patterns['h'] = '([0-9]{2})';
		$patterns['i'] = '([0-9]{2})';
		$patterns['s'] = '([0-9]{2})';
		$patterns['a'] = '(am|pm)';
		$patterns['A'] = '(AM|PM)';

		$holders_mask = '/' . preg_replace('/[a-zA-Z]{1}/i', '([a-zA-Z]{1})', preg_quote($format, '/')) . '/';
		if (!preg_match($holders_mask, $format, $holders)) {
			$object->SetError($field_name, 'bad_date_format', null, $error_params);
			return $value;
		}

		$values_mask = '/^' . preg_quote($format, '/') . '$/';
		foreach ($patterns as $key => $val) {
			$values_mask = str_replace($key, $val, $values_mask);
		}

		if (!preg_match($values_mask, $value, $values)) {
			$object->SetError($field_name, 'bad_date_format', null, $error_params);
			return $value;
		}

		for ($i = 1; $i < count($holders); $i++) {
			switch ($holders[$i]) {
				case 'n':
				case 'm':
					$month = $values[$i];
					$month = preg_replace('/^0{1}/', '', $month);
					break;
				case 'd':
					$day = $values[$i];
					$day = preg_replace('/^0{1}/', '', $day);
					break;
				case 'Y':
					$year = $values[$i];
					break;
				case 'y':
					$year = $values[$i] >= 70 ? 1900 + $values[$i] : 2000 + $values[$i];
					break;
				case 'H':
				case 'h':
				case 'G':
				case 'g':
					$hour = $values[$i];
					$hour = preg_replace('/^0{1}/', '', $hour);
					break;
				case 'i':
					$minute = $values[$i];
					$minute = preg_replace('/^0{1}/', '', $minute);
					break;
				case 's':
					$second = $values[$i];
					$second = preg_replace('/^0{1}/', '', $second);
					break;
				case 'a':
				case 'A':
					if ($hour <= 12) { // if AM/PM used with 24-hour - could happen :)
						if ($values[$i] == 'pm' || $values[$i] == 'PM') {
							$hour += 12;
							if ($hour == 24) $hour = 12;
						}
						elseif ($values[$i] == 'am' || $values[$i] == 'AM') {
							if ($hour == 12) $hour = 0;
						}
					}
					break;
			}
		}

		//echo "day: $day, month: $month, year: $year, hour: $hour, minute: $minute<br>";

		/*if (!($year >= 1970 && $year <= 2037)) {
			$object->SetError($field_name, 'bad_date_format', null, $error_params);
			return $value;
		}*/

		if (!($month >= 1 && $month <= 12)) {
			$object->SetError($field_name, 'bad_date_format', null, $error_params);
			return $value;
		}

		$months_days = Array ( 1 => 31,2 => 28, 3 => 31, 4 => 30,5 => 31,6 => 30, 7 => 31, 8 => 31,9 => 30,10 => 31,11 => 30,12 => 31);
		if ($year % 4 == 0) $months_days[2] = 29;

		if (!($day >=1 && $day <= $months_days[$month])) {
			$object->SetError($field_name, 'bad_date_format', null, $error_params);
			return $value;
		}

		if (!($hour >=0 && $hour <= 23)) {
			$object->SetError($field_name, 'bad_date_format', null, $error_params);
			return $value;
		}

		if (!($minute >=0 && $minute <= 59)) {
			$object->SetError($field_name, 'bad_date_format', null, $error_params);
			return $value;
		}

		if (!($second >=0 && $second <= 59)) {
			$object->SetError($field_name, 'bad_date_format', null, $error_params);
			return $value;
		}

		if (!$options['use_timezone']) {
			return adodb_gmmktime($hour, $minute, $second, $month, $day, $year);
		}

		return adodb_mktime($hour, $minute, $second, $month, $day, $year);
	}

	function GetSample($field, &$options, &$object)
	{
		return $this->Format( adodb_mktime(), $field, $object, $options['input_format']);
	}
}