<?php namespace ProcessWire;

/**
 * ProcessWire Datetime Inputfield
 *
 * Provides input for date and optionally time values. 
 *
 * For documentation about the fields used in this class, please see:  
 * /wire/core/Fieldtype.php
 * 
 * ProcessWire 3.x, Copyright 2024 by Ryan Cramer
 * https://processwire.com
 * 
 * ~~~~~~
 * // get a datetime Inputfield
 * $f = $modules->get('InputfieldDatetime');
 * $f->attr('name', 'test_date'); 
 * $f->label = 'Test date';
 * $f->val(time()); // value is get or set a UNIX timestamp
 * 
 * // date input with jQuery UI datepicker on focus
 * $f->inputType = 'text'; // not necessary as this is the default
 * $f->datepicker = InputfieldDatetime::datepickerFocus;
 * 
 * // date selects
 * $f->inputType = 'select';
 * $f->dateSelectFormat = 'mdy'; // month abbr (i.e. 'Sep'), day, year
 * $f->dateSelectFormat = 'Mdy'; // month full (i.e. 'September'), day, year
 * $f->yearFrom = 2019; // optional year range from
 * $f->yearTo = 2024; // optional year range to
 * 
 * // HTML5 date, time or date+time inputs
 * $f->inputType = 'html';
 * $f->htmlType = 'date'; // or 'time' or 'datetime'
 * ~~~~~~
 * 
 * @property int $value This Inputfield keeps the value in UNIX timestamp format (int).
 * @property string $inputType Input type to use, one of: "text", "select" or "html" (when html type is used, also specify $htmlType).
 * @property int|bool $defaultToday When no value is present, default to today’s date/time?
 * @property int $subYear Substitute year when month+day or time only selections are made (default=2010)
 * @property int $subDay Substitute day when month+year or time only selectinos are made (default=8)
 * @property int $subMonth Substitute month when time-only selections are made (default=4)
 * @property int $subHour Substitute hour when date-only selections are made (default=0)
 * @property int $subMinute Substitute minute when date-only selection are made (default=0)
 * @property bool|int $requiredAttr When combined with "required" option, this also makes it use the HTML5 "required" attribute (default=false).
 * 
 * Properties specific to "text" input type (with optional jQuery UI datepicker)
 * =============================================================================
 * @property int $datepicker jQuery UI datepicker type (see `datepicker*` constants)
 * @property string $yearRange Selectable year range in the format `-30:+20` where -30 is number of years before now and +20 is number of years after now.
 * @property int $timeInputSelect jQuery UI timeSelect type (requires datepicker)—specify 1 to use a `<select>` for time input, or 0 to use a slider (default=0)
 * @property string $dateInputFormat Date input format to use, see WireDateTime::$dateFormats (default='Y-m-d')
 * @property string $timeInputFormat Time input format to use, see WireDateTime::$timeFormats (default='')
 * @property string $placeholder Placeholder attribute text
 * @property string $showAnim Animation type (default='fade')
 * @property bool|int $changeMonth Show a select dropdown to allow changing month? (default=true)
 * @property bool|int $changeYear Show a select dropdown to allow changing year? (default=true)
 * @property bool|int $showButtonPanel Show "Today" and "Done" buttons under the calendar? (default=false)
 * @property int $numberOfMonths Number of month calendars to show together side-by-side (default=1)
 * @property bool|int $showMonthAfterYear Show the month after the year? (default=false)
 * @property bool|int $showOtherMonths Show dates in other months (non-selectable) at the start or end of the current month? (default=false)
 * 
 * Properties specific to "html" input type
 * ========================================
 * @property string $htmlType When "html" is selection for $inputType, this should be one of: "date", "time" or "datetime".
 * @property int $timeStep Refers to the step attribute on time inputs
 * @property string $timeMin Refers to the min attribute on time inputs (HH:MM)
 * @property string $timeMax Refers to the max attribute on time inputs (HH:MM)
 * @property int $dateStep Refers to the step attribute on date inputs
 * @property string $dateMin Refers to the min attribute on date inputs, ISO-8601 (YYYY-MM-DD)
 * @property string $dateMax Refers to the max attribute on date inputs, ISO-8601 (YYYY-MM-DD)
 * 
 * Properties specific to "select" input type
 * ==========================================
 * @property string $dateSelectFormat Format to use for date select, i.e. 'mdy' for 'Sep 1 2024' or 'Mdy' for 'September 1 2024'
 * @property string $timeSelectFormat Format to use for time select
 * @property int $yearFrom First selectable year (default=current year - 100)
 * @property int $yearTo Last selectable year (default=current year + 20)
 * @property bool|int $yearLock Disallow selection of years outside the yearFrom/yearTo range? (default=false)
 * 
 *
 */

class InputfieldDatetime extends Inputfield {
	
	public static function getModuleInfo() {
		return array(
			'title' => __('Datetime', __FILE__), // Module Title
			'summary' => __('Inputfield that accepts date and optionally time', __FILE__), // Module Summary
			'version' => 108,
			'permanent' => true,
		);
	}

	/**
	 * ISO-8601 date/time formats (default date input format)
	 * 
	 * #pw-internal
	 * 
	 */
	const defaultDateInputFormat = 'Y-m-d';
	const defaultTimeInputFormat = 'H:i';
	const secondsTimeInputFormat = 'H:i:s';


	/**
	 * jQuery UI datepicker: None
	 * 
	 */
	const datepickerNo = 0;
	
	/**
	 * jQuery UI datepicker: Click button to show
	 *
	 */
	const datepickerClick = 1;
	
	/**
	 * jQuery UI datepicker: Inline datepicker always visible (no timepicker support)
	 *
	 */
	const datepickerInline = 2;
	
	/**
	 * jQuery UI datepicker: Show when input focused (recommend option when using datepicker)
	 *
	 */
	const datepickerFocus = 3;


	/**
	 * @var InputfieldDatetimeType[]
	 * 
	 */
	protected $inputTypes = array();

	/**
	 * jQuery UI custom datepicker options set from datepickerOptions() method
	 * 
	 * @var array 
	 * 
	 */
	protected $datepickerOptions = array();
	
	/**
	 * Initialize the date/time inputfield
	 *
	 */
	public function init() {
		
		$this->attr('type', 'text'); 
		$this->attr('size', 25); 
		$this->attr('placeholder', '');
		
		$this->set('defaultToday', 0); 
		$this->set('inputType', 'text'); 
		$this->set('subYear', 2010); 
		$this->set('subMonth', 4); 
		$this->set('subDay', 8); 
		$this->set('subHour', 0); 
		$this->set('subMinute', 0);
		$this->set('requiredAttr', 0);
		
		foreach($this->getInputTypes() as $type) {
			$this->setArray($type->getDefaultSettings()); 
		}

		parent::init();
	}
	
	/**
	 * Return ISO-8601 substitute date (combination of subYear, subMonth, subDay)
	 * 
	 * #pw-internal
	 * 
	 * @return string
	 * 
	 */
	public function subDate() {
		$year = (int) parent::getSetting('subYear');
		$month = (int) parent::getSetting('subMonth');
		$day = (int) parent::getSetting('subDay');
		if($year < 1000 || $year > 2500) $year = (int) date('Y');
		if($month > 12 || $month < 1) $month = 1;
		if($month < 10) $month = "0$month";
		if($day > 31 || $day < 1) $day = 1;
		if($day < 10) $day = "0$day";
		return "$year-$month-$day";
	}

	/**
	 * Return ISO-8601 substitute time (combination of subHour:subMinute:00)
	 * 
	 * #pw-internal
	 * 
	 * @return string
	 *
	 */
	public function subTime() {
		$hour = (int) parent::getSetting('subHour');
		$minute = (int) parent::getSetting('subMinute');
		if($hour > 23 || $hour < 0) $hour = 0;
		if($hour < 10) $hour = "0$hour";
		if($minute > 59 || $minute < 0) $minute = 0;
		if($minute < 10) $minute = "0$minute";
		return "$hour:$minute:00";
	}

	/**
	 * Get all date/time input types
	 * 
	 * @return InputfieldDatetimeType[]
	 * 
	 */
	public function getInputTypes() {
		
		if(count($this->inputTypes)) {
			return $this->inputTypes;
		}
		
		$path = dirname(__FILE__) . '/';
		require_once($path . 'InputfieldDatetimeType.php');
		$dir = new \DirectoryIterator($path . 'types/');
		
		foreach($dir as $file) {
			if($file->isDir() || $file->isDot() || $file->getExtension() != 'php') continue;
			require_once($file->getPathname());
			$className = wireClassName($file->getBasename('.php'), true);
			/** @var InputfieldDatetimeType $type */
			$type = $this->wire(new $className($this));
			$name = $type->getTypeName();
			$this->inputTypes[$name] = $type;
		}
		
		return $this->inputTypes;
	}

	/**
	 * Get current date/time input type instance
	 * 
	 * @param string $typeName
	 * @return InputfieldDatetimeType
	 * 
	 */
	public function getInputType($typeName = '') {
		$inputTypes = $this->getInputTypes();
		if(!$typeName) $typeName = $this->inputType;
		if(!$typeName || !isset($inputTypes[$typeName])) $typeName = 'text';
		return $inputTypes[$typeName];
	}

	/**
	 * Set property
	 * 
	 * @param string $key
	 * @param mixed $value
	 * @return Inputfield|WireData
	 * 
	 */
	public function set($key, $value) {
		if($key === 'dateMin' || $key === 'dateMax') {
			if(is_int($value)) $value = date(self::defaultDateInputFormat, $value);
		} else if($key === 'timeMin' || $key === 'timeMax') {
			if(is_int($value)) $value = date(self::defaultTimeInputFormat, $value); 
		}
		return parent::set($key, $value); 
	}
	
	/**
	 * Called before the render method, from a hook in the Inputfield class
	 *
	 * We are overriding it here and checking for a datepicker, so that we can make sure 
	 * jQuery UI is loaded before the InputfieldDatetime.js
	 * 
	 * @param Inputfield|null $parent
	 * @param bool $renderValueMode
	 * @return bool
	 *
	 */
	public function renderReady(?Inputfield $parent = null, $renderValueMode = false) {
		$this->addClass("InputfieldNoFocus", 'wrapClass');
		$this->getInputType()->renderReady();
		return parent::renderReady($parent, $renderValueMode); 
	}

	/**
	 * Render the date/time inputfield
	 * 
	 * @return string
	 *
	 */
	public function ___render() {
		return $this->getInputType()->render();
	}

	/**
	 * Render value for presentation, non-input
	 *
	 */
	public function ___renderValue() {
		
		$out = $this->getInputType()->renderValue();
		if($out) return $out;
		
		$value = $this->attr('value');
		if(!$value) return '';
		$format = self::defaultDateInputFormat . ' ';
		if($this->timeStep > 0 && $this->timeStep < 60) {
			$format .= self::secondsTimeInputFormat;
		} else {
			$format .= self::defaultTimeInputFormat;
		}
		
		return $this->wire()->datetime->formatDate($value, trim($format));
	}

	/**
	 * Process input
	 * 
	 * @param WireInputData $input
	 * @return Inputfield|InputfieldDatetime
	 * 
	 */
	public function ___processInput(WireInputData $input) {
		
		$valuePrevious = $this->val();
		$value = $this->getInputType()->processInput($input);
		
		if($value === false) {
			// false indicates type is not processing input
			parent::___processInput($input);
			$value = $this->getAttribute('value');
		} else {
			$this->setAttribute('value', $value); 
		}
	
		if($value !== $valuePrevious) {
			$this->trackChange('value', $valuePrevious, $value);
			$parent = $this->getParent();
			if($parent) $parent->trackChange($this->name);
		}

		return $this;
	}

	/**
	 * Capture setting of the 'value' attribute and convert string dates to unix timestamp
	 * 
	 * @param string $key
	 * @param mixed $value
	 * @return Inputfield|InputfieldDatetime
	 *
	 */
	public function setAttribute($key, $value) {
		if($key === 'value') {
			if(empty($value) && "$value" !== "0") {
				// empty value that’s not 0
				$value = '';
			} else if(is_int($value) || ctype_digit("$value")) {
				// unix timestamp
				$value = (int) $value;
			} else if(strlen($value) > 8 && $value[4] === '-' && $value[7] === '-' && ctype_digit(substr($value, 0, 4))) {
				// ISO-8601, i.e. 2010-04-08 02:48:00
				$value = $this->wire()->datetime->strtotime($value); 
				if(!$value) $value = '';
			} else {
				$value = $this->getInputType()->sanitizeValue($value);
			}
		}
		return parent::setAttribute($key, $value); 
	}

	/**
	 * Get or set jQuery UI datepicker (and timepicker) options
	 * 
	 * <https://api.jqueryui.com/datepicker/>
	 * 
	 * Can also be used with several timepicker options:
	 * <https://trentrichardson.com/examples/timepicker/#tp-options>
	 * 
	 * ~~~~~
	 * // set custom options
	 * $inputfield->datepickerOptions([ 'changeMonth' => false, 'changeYear' => false ]); 
	 * 
	 * // set the defaults for all datepicker fields/inputs to start from
	 * $config->js('InputfieldDatetimeDatepickerDefaults', [ 'showButtonPanel' => true ]); 
	 * 
	 * // override settings for all datetime fields/inputs
	 * $config->js('InputfieldDatetimeDatepickerOptions', [ 'showAnim' => 'drop'  ]);
	 * 
	 * // override settings for all datetime fields/inputs (from JS)
	 * $.extend(ProcessWire.config.InputfieldDatetimeDatepickerOptions, { 'showAnim': 'drop' }); 
	 * 
	 * ~~~~~
	 * 
	 * @param array $options
	 * @return array
	 * 
	 */
	public function datepickerOptions($options = array()) {
		if(count($options)) {
			$this->datepickerOptions = array_merge($this->datepickerOptions, $options);
		}
		return $this->datepickerOptions;
	}
	
	/**
	 * Date/time Inputfield configuration, per field
	 *
	 */
	public function ___getConfigInputfields() {

		$inputfields = parent::___getConfigInputfields();
		$inputTypes = $this->getInputTypes();
		$modules = $this->wire()->modules;

		/** @var InputfieldRadios $f */
		$f = $modules->get('InputfieldRadios');
		$f->attr('name', 'inputType'); 
		$f->label = $this->_('Input Type'); 
		$f->icon = 'calendar';
		
		foreach($inputTypes as $inputTypeName => $inputType) {
			$f->addOption($inputTypeName, $inputType->getTypeLabel());
		}
		
		$inputTypeVal = $this->getSetting('inputType');
		if(!$inputTypeVal) $inputTypeVal = 'text';
		if(!isset($inputTypes[$inputTypeVal])) $inputTypeVal = 'text';
		$f->val($inputTypeVal);
		$inputfields->add($f);

		foreach($inputTypes as $inputTypeName => $inputType) {
			/** @var InputfieldFieldset $fieldset */
			$fieldset = $modules->get('InputfieldFieldset');
			$fieldset->attr('name', '_' . $inputTypeName . 'Options');
			$fieldset->label = $inputType->getTypeLabel();
			$fieldset->showIf = 'inputType=' . $inputTypeName;
			$inputType->getConfigInputfields($fieldset);
			$inputfields->add($fieldset);
		}
		
		/** @var InputfieldCheckbox $f */
		$f = $modules->get('InputfieldCheckbox');
		$f->setAttribute('name', 'defaultToday');
		$f->attr('value', 1);
		if($this->defaultToday) $f->attr('checked', 'checked');
		$f->label = $this->_('Default to today’s date?');
		$f->description = $this->_('If checked, this field will hold the current date when no value is entered.'); // Default today description
		$inputfields->append($f);

		return $inputfields; 
	}
}
