<?php namespace ProcessWire;

/**
 * ProcessWire Datetime Fieldtype
 *
 * Holds date and optionally time values. 
 *
 * For documentation about the fields used in this class, please see:  
 * /wire/core/Fieldtype.php
 * 
 * ProcessWire 3.x, Copyright 2022 by Ryan Cramer
 * https://processwire.com
 *
 */

class FieldtypeDatetime extends Fieldtype {
	
	public static function getModuleInfo() {
		return array(
			'title' => 'Datetime',
			'version' => 105,
			'summary' => 'Field that stores a date and optionally time',
		);
	}

	/**
	 * Default date output format
	 * 
	 */
	const defaultDateOutputFormat = 'Y-m-d'; 

	/**
	 * Return all predefined PHP date() formats for use as dates
	 * 
	 * Note: this method moved to the WireDateTime class and is kept here for backwards compatibility.
	 * 
	 * @deprecated Use WireDateTime class instead
	 * @return array
	 *
	 */
	static public function getDateFormats() {
		return WireDateTime::_getDateFormats();
	}

	/**
	 * Return all predefined PHP date() formats for use as times
	 *
	 * Note: this method moved to the WireDateTime class and is kept here for backwards compatibility.
	 *
	 * @deprecated Use WireDateTime class instead
	 * @return array
	 * 
	 */
	static public function getTimeFormats() {
		return WireDateTime::_getTimeFormats();
	}

	/**
	 * Given a date/time string and expected format, convert it to a unix timestamp
	 *
	 * Note: this method moved to the WireDateTime class and is kept here for backwards compatibility.
	 * 
	 * @param string $str Date/time string
	 * @param string $format Format of the date/time string in PHP date syntax
	 * @return int
	 * @deprecated Use WireDateTime class instead
	 *
	 */
	static public function stringToTimestamp($str, $format) {
		$wdt = new WireDateTime();
		return $wdt->stringToTimestamp($str, $format);
	}

	/**
	 * Format a date with the given PHP date() or PHP strftime() format
	 * 
	 * Note: this method moved to the WireDateTime class and is kept here for backwards compatibility.
	 *
 	 * @param int $value Unix timestamp of date
	 * @param string $format date() or strftime() format string to use for formatting
	 * @return string Formatted date string
	 * @deprecated Use WireDateTime class instead
	 *
	 */
	static public function formatDate($value, $format) {
		$wdt = new WireDateTime();
		return $wdt->formatDate($value, $format);
	}

	/**
	 * Given a date() format, convert it to either 'js', 'strftime' or 'regex' format
	 * 
	 * Note: this method moved to the WireDateTime class and is kept here for backwards compatibility.
	 *
	 * @param string $format PHP date() format
	 * @param string $type New format to convert to: either 'js', 'strftime', or 'regex' 
	 * @return string
	 * @deprecated Use WireDateTime class instead
	 *
	 */
	static public function convertDateFormat($format, $type) {
		$wdt = new WireDateTime();
		return $wdt->convertDateFormat($format, $type);
	}

	/*********************************************************************************************************************************
	 *********************************************************************************************************************************/

	/**
	 * Return the Inputfield used for date/time (InputfieldDatetime)
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @return InputfieldDatetime
	 *
	 */
	public function getInputfield(Page $page, Field $field) {
		/** @var InputfieldDatetime $inputfield */
		$inputfield = $this->wire('modules')->get('InputfieldDatetime'); 
		$inputfield->class = $this->className();
		return $inputfield; 
	}

	/**
	 * Get predefined setups for newly created fields of this type
	 *
	 * #pw-internal
	 * #pw-hooker
	 *
	 * @return array
	 * @since 3.0.213
	 *
	 */
	public function ___getFieldSetups() {
		return array(
			'datetime' => array(
				'title' => $this->_x('Date and Time', 'datetime-setup'), // label for datetime field setup
				'dateOutputFormat' => $this->_x('j F Y g:i a', 'datetime-setup'), // default date/time output format for datetime (PHP dateformat)
				'dateInputFormat' => $this->_x('Y-m-d', 'datetime-setup'), // default date input format for datetime (PHP dateformat)
				'timeInputFormat' => $this->_x('g:i a', 'datetime-setup'), // default time input format for datetime (PHP dateformat)
				'inputType' => $this->_x('html', 'datetime-setup'), // default input type to use for datetime - one of: html, text, select
				'htmlType' => 'datetime',
			),
			'date' => array(
				'title' => $this->_x('Date', 'date-setup'), // label for date-only fields setup
				'dateOutputFormat' => $this->_x('j F Y', 'date-setup'), // default output format for date (PHP dateformat)
				'dateInputFormat' => $this->_x('Y-m-d', 'date-setup'), // default input format for date (PHP dateformat)
				'timeInputFormat' => '', 
				'inputType' => $this->_x('html', 'date-setup'), // default input type to use for date - one of: html, text, select
				'htmlType' => 'date',
			),
			'time' => array(
				'title' => $this->_x('Time', 'time-setup'), // label for time-only field setup
				'dateOutputFormat' => $this->_x('g:i a', 'time-setup'), // default output format for time (PHP dateformat)
				'dateInputFormat' => '',
				'timeInputFormat' => $this->_x('g:i a', 'time-setup'), // default input format for time (PHP dateformat)
				'inputType' => $this->_x('html', 'time-setup'), // default input type to use for time - one of: html, text, select
				'htmlType' => 'time',
			),
		);
	}

	/**
	 * Sanitize value, per Fieldtype interface
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param string|int|\DateTime $value
	 * @return int
	 *	
 	 */
	public function sanitizeValue(Page $page, Field $field, $value) {
		return $this->_sanitizeValue($value); 
	}

	/**
	 * Sanitize a value assumed to be either a timestamp or in strtotime() compatible format
	 * 
	 * @param string|int|\DateTime
	 * @return int|string Returns unix timestamp integer or blank string if empty or invalid value
	 *
	 */
	protected function _sanitizeValue($value) {
		if(empty($value)) {
			// empty value
			$value = '';
		} else if(is_int($value)) {
			// value okay as-is
		} else if($value instanceof \DateTime) {
			// instance of DateTime 
			$value = $value->getTimestamp();
		} else if(ctype_digit(ltrim("$value", '-'))) {
			// already a timestamp
			$value = (int) $value;
		} else {
			// convert date string to time
			$value = strtotime($value);
			if($value === false) $value = '';
		}
		return $value; 
	}

	/**
	 * Format the value for output, according to selected format and language
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param int $value
	 * @return string
	 *
	 */
	public function ___formatValue(Page $page, Field $field, $value) {
		$format = '';
		if($this->languages && !$this->user->language->isDefault()) $format = $field->get("dateOutputFormat{$this->user->language}"); 
		if(!$format) $format = $field->get('dateOutputFormat');
		return $this->wire('datetime')->formatDate($value, $format);
	}

	/**
	 * Export value
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param int $value
	 * @param array $options
	 * @return string
	 * 
	 */
	public function ___exportValue(Page $page, Field $field, $value, array $options = array()) {
		if(!$value) return '';
		return date('Y-m-d H:i:s', $value); 
	}
	
	/**
	 * Return whether the given value is considered empty or not
	 *
	 * @param Field $field
	 * @param mixed $value
	 * @return bool
	 *
	 */
	public function isEmptyValue(Field $field, $value) {
		
		if($value instanceof Selector) {
			// PageFinder is asking if it should let this Fieldtype handle the operator/value
			// combination with potential empty value present in a Selector
			$selector = $value;
			$op = substr($selector->operator, 0, 1);
			// tell PageFinder we will handle greater-than/less-than conditions in our getMatchQuery()
			if($op === '>' || $op === '<') return true;
		}
		
		// note: 0000-00-00 intentionally returns true, which is what $value is when PageFinder is testing
		// whether the Fieldtype recognizes an empty ISO-8601 date that it will convert to matching null
		
		$value = trim($value, '-0 ');
		
		return !strlen($value); 
	}

	/**
	 * Match a date/time value in the database, as used by PageFinder
	 * 
	 * @param PageFinderDatabaseQuerySelect $query
	 * @param string $table
	 * @param string $subfield
	 * @param string $operator
	 * @param int|string $value
	 * @return DatabaseQuerySelect
	 * @throws WireException if given invalid operator
	 *
	 */
	public function getMatchQuery($query, $table, $subfield, $operator, $value) {
		
		$database = $this->wire()->database;
		$intValue = $this->_sanitizeValue($value);
		$table = $database->escapeTable($table);
		$subfield = $subfield ? $database->escapeCol($subfield) : 'data';
		$minDT = '1000-01-01 00:00:00'; // $maxDT = '9999-12-31 23:59:59';
		
		if(is_string($value) && in_array($operator, array('%=', '^='))) {
			// partial date string match
			if(!ctype_digit($value)) {
				$value = str_replace(array('/', '.'), '-', trim($value));
			}
			if(!ctype_digit(str_replace(array('-', ' '), '', $value))) {
				throw new WireException("Invalid partial date string '$value' (numbers, hyphens and space only)");
			}
			$value = str_replace(array('%', '_'), '', $value);
			$value = $operator === '^=' ? "$value%" : "%$value%";
			$query->where("$table.$subfield LIKE ?", $value);
			
		} else if(!$database->isOperator($operator)) {
			// invalid operator
			throw new WireException("$this invalid date operator: $operator");

		} else if(is_int($intValue)) {
			// matching a populated value that successfully converted to unix timestamp
			$dateString = date('Y-m-d H:i:s', $intValue);
			if($dateString !== false) {
				$query->where("$table.$subfield$operator?", $dateString);
			}
			
		} else {
			// matching an empty value
			if(in_array($operator, array('!=', '>', '>='))) {
				// match NOT empty (!=0, >0)
				$query->where("$table.$subfield>=?", $minDT);
				
			} else if(in_array($operator, array('=', '<', '<='))) {
				// match empty (=0, <0, <=0): match null or value below unix timestamp range
				// this includes 0000-00-00 when present and used by MySQL version
				$query->where("$table.$subfield IS NULL OR $table.$subfield<?", $minDT);
				
			} else {
				// unsupported operator
				throw new WireException("$this operator cannot be used here: $operator"); 
			}
		}
		
		return $query; 
	}

	/**
	 * Get selector info
	 * 
	 * @param Field $field
	 * @param array $data
	 * @return array
	 * 
	 */
	public function ___getSelectorInfo(Field $field, array $data = array()) {
		$a = parent::___getSelectorInfo($field, $data); 
		$a['operators'] = array('=', '!=', '>', '>=', '<', '<=', '%=', '^=', '=""', '!=""'); 
		return $a;
	}

	/**
	 * Return database schema used by this field
	 * 
	 * @param Field $field
	 * @return array
	 *
	 */
	public function getDatabaseSchema(Field $field) {
		$schema = parent::getDatabaseSchema($field); 
		$schema['data'] = 'datetime NOT NULL';
		$schema['keys']['data'] = 'KEY data (data)'; 
		return $schema;
	}

	/**
	 * Convert value from timestamp to Y-m-d H:i:s date string
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param string|int $value
	 * @return string
	 *
	 */
	public function ___sleepValue(Page $page, Field $field, $value) {
		$value = $this->_sanitizeValue($value); 
		return is_int($value) ? date('Y-m-d H:i:s', $value) : '';
	}

	/**
	 * Convert value from Y-m-d H:i:s string to timestamp
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param string $value
	 * @return int
	 *
	 */
	public function ___wakeupValue(Page $page, Field $field, $value) {
		if(empty($value)) return '';
		$value = strtotime($value); 
		if($value === false) $value = '';
		return $value; 
	}

	/**
	 * Get compatible Fieldtypes
	 * 
	 * @param Field $field
	 * @return Fieldtypes
	 * @throws WireException
	 *
	 */
	public function ___getCompatibleFieldtypes(Field $field) {
		$fieldtypes = $this->wire(new Fieldtypes());
		foreach($this->wire('fieldtypes') as $fieldtype) {
			if($fieldtype instanceof FieldtypeDatetime) $fieldtypes->add($fieldtype);
		}
		return $fieldtypes;
	}

	/**
	 * Field configuration screen
	 * 
	 * @param Field $field
	 * @return InputfieldWrapper
	 *
	 */
	public function ___getConfigInputfields(Field $field) {

		$modules = $this->wire()->modules;
		$inputfields = parent::___getConfigInputfields($field);
		$wdt = $this->wire('datetime'); /** @var WireDateTime $wdt */
		$dateOutputFormat = (string) $field->get('dateOutputFormat');

		/** @var InputfieldSelect $f */
		$f = $modules->get('InputfieldSelect'); 
		$f->attr('name', '_dateOutputFormat'); 
		$f->label = $this->_('Date Output Format'); 
		$f->description = $this->_('Select the format to be used when outputting dates with this field.') . ' ';
		$f->description .= $this->_('For relative date/time formats, see the Time Output Format.'); 
		$f->icon = 'calendar';
		$f->addOption('', $this->_('None'));
		$f->columnWidth = 50;
		$date = strlen(date('jn')) < 4 ? time() : strtotime('2016-04-08 5:10:02 PM');
		$found = false;
		foreach($wdt->getDateFormats() as $format) {
			$dateFormatted = (string) $wdt->formatDate($date, $format); 	
			if($format == 'U') $dateFormatted .= " " . $this->_('(unix timestamp)');
			$f->addOption($format, "$dateFormatted [$format]"); 
			if(!$found && strpos($dateOutputFormat, $format) !== false) {
				$f->attr('value', $format); 
				$found = true; 
			}
		}
		$f->attr('onchange', "$('#Inputfield_dateOutputFormat').val($(this).val() + ' ' + $('#Inputfield__timeOutputFormat').val());"); 
		$inputfields->add($f); 

		/** @var InputfieldSelect $f */
		$f = $modules->get('InputfieldSelect'); 
		$f->attr('name', '_timeOutputFormat'); 
		$f->label = $this->_('Time Output Format'); 
		$f->description = $this->_('If you want to output time in addition to the date, select the format to be used when outputting time with this field. This will be combined with the date format.'); 
		$f->icon = 'clock-o';
		$f->addOption('', $this->_('None'));
		$f->columnWidth = 50;
		$date = strtotime('5:10:02 PM');
		foreach($wdt->getTimeFormats() as $format) {
			$timeFormatted = $wdt->formatDate($date, $format); 	
			$f->addOption($format, "$timeFormatted [$format]"); 
			if(strpos($dateOutputFormat, $format) !== false) {
				list(,$test) = explode($format, $dateOutputFormat);
				if(!strlen($test)) $f->attr('value', $format);
			}
		}
		$f->attr('onchange', "$('#Inputfield_dateOutputFormat').val($('#Inputfield__dateOutputFormat').val() + ' ' + $(this).val());"); 
		//$f->collapsed = Inputfield::collapsedBlank;
		$inputfields->add($f);

		/** @var InputfieldText $f */
		$f = $modules->get("InputfieldText"); 
		$f->attr('name', 'dateOutputFormat'); 
		$f->attr('value', $field->get('dateOutputFormat')); 
		$f->attr('size', 20); 
		$f->label = $this->_('Date/Time Output Format Code');
		$f->description = $this->_('The date/time will be output according to the format below. This is automatically built from the date/time selections above, but you may change it as needed to suit your needs.') . ' ';
		$f->description .= $this->_('See the [PHP date](https://www.php.net/manual/en/function.date.php) function reference for more information on how to customize this format. Alternatively, you may use a [PHP strftime](https://www.php.net/manual/en/function.strftime.php) format if desired for localization.'); 
		$f->icon = 'code';
		$f->collapsed = Inputfield::collapsedYes; 
		if($this->languages) {
			$f->useLanguages = true; 
			foreach($this->languages as $language) {
				/** @var Language $language */
				if($language->isDefault()) continue; 
				$f->set("value$language", (string) $field->get('dateOutputFormat' . $language)); 
			}
		}
		$inputfields->add($f); 

		return $inputfields; 
	}
}

