<?php namespace ProcessWire;

/**
 * Inputfield for floating point numbers
 * 
 * ProcessWire 3.x, Copyright 2023 by Ryan Cramer
 * https://processwire.com
 *
 * @property int|null|string $precision Decimals precision (or -1 to disable rounding in 3.0.193+)
 * @property int $digits Total digits, for when used in decimal mode (default=0)
 * @property string $inputType Input type to use, one of "text" or "number"
 * @property int|float $min
 * @property int|float $max
 * @property int|float|string $step
 * @property int $size
 * @property string $placeholder
 * @property int|float $initValue Initial/default value (when used as independent Inputfield)
 * @property int|float|string $defaultValue Initial/default value (when used with FieldtypeInteger)
 * @property bool|int $noE Convert “123E-3” and “123E3” type numbers to real numbers in the <input>? 3.0.193+
 * 
 */

class InputfieldFloat extends InputfieldInteger {
	
	public static function getModuleInfo() {
		return array(
			'title' => __('Float', __FILE__), // Module Title
			'summary' => __('Floating point number with precision', __FILE__), // Module Summary
			'version' => 105,
			'permanent' => true, 
		);
	}

	/**
	 * Construct
	 * 
	 */
	public function __construct() {
		$this->set('precision', 2); 
		$this->set('digits', 0);
		$this->set('noE', 0);
		parent::__construct();
	}

	/**
	 * Module init
	 * 
	 */
	public function init() {
		parent::init();
		$this->attr('step', 'any'); // HTML5 attr required to support decimals with 'number' types
	}

	/**
	 * Get configured precision setting, or if given a value, precision of the value
	 * 
	 * @param float|string|null $value
	 * @return int|string Returns integer of precision or blank string if none defined
	 * 
	 */
	protected function getPrecision($value = null) {
		if($value !== null) return FieldtypeFloat::getPrecision($value);
		$precision = $this->precision;
		return $precision === null || $precision === '' || $precision < 0 ? '' : (int) $precision;
	}

	/**
	 * Sanitize value 
	 * 
	 * @param float|string $value
	 * @return float|string
	 * 
	 */
	protected function sanitizeValue($value) {
		if(!strlen("$value")) {
			$value = '';
		} else if($this->digits > 0) {
			$value = (string) $value;
			if(!is_numeric("$value")) {
				$value = $this->wire()->sanitizer->float($value, array(
					'precision' => (int) $this->precision, 
					'getString' => 'F', 
					'blankValue' => '',
				));
			}
		} else if(!is_float($value) && !is_int($value)) {
			$value = $this->wire()->sanitizer->float($value, array('blankValue' => ''));
			if(!strlen("$value")) $value = '';
		} else {
			$precision = $this->precision;
			if($precision === null || $precision === '') $precision = $this->getPrecision($value);
			$value = is_int($precision) && $precision > 0 ? round((float) $value, $precision) : $value;
		}
		return $value;
	}
	
	/**
	 * Typecast value to float, override from InputfieldInteger
	 *
	 * @param string|int|float $value
	 * @return float
	 *
	 */
	protected function typeValue($value) {
		return (float) $value;
	}

	/**
	 * Does the value have an E in it like ”123E-3” or "123E3” ?
	 * 
	 * @param string $value
	 * @return bool
	 * @since 3.0.193
	 * 
	 */
	public function hasE($value) {
		$value = strtoupper((string) $value);
		if(strpos($value, 'E') === false) return false;
		$value = str_replace(array('-', '.', ',', ' '), '', $value);
		list($a, $b) = explode('E', $value, 2);
		$b = trim($b, '+-');
		return ctype_digit("$a$b"); 
	}

	/**
	 * Override method from Inputfield to convert locale specific decimals for input[type=number]
	 * 
	 * @param array|null $attributes
	 * @return string
	 * 
	 */
	public function getAttributesString(?array $attributes = null) {
		if(is_null($attributes)) $attributes = $this->getAttributes();
		if($attributes['type'] === 'number') { 
			$value = isset($attributes['value']) ? $attributes['value'] : null;
			if(is_float($value) || (is_string($value) && strlen($value))) {
				// the HTML5 number input type requires "." as the decimal
				$value = $this->localeConvertValue($value);
				$attributes['value'] = $value;
			}
			if(empty($attributes['step']) || $attributes['step'] === 'any') {
				$precision = (int) $this->precision;
				if($precision === 0 && $value !== null) {
					$precision = $this->getPrecision($value);
				}
				if($precision > 0) {
					$attributes['step'] = '.' . ($precision > 1 ? str_repeat('0', $precision - 1) : '') . '1';
				}
			}
		} else if($this->digits > 0 && empty($attributes['inputmode'])) {
			$attributes['inputmode'] = 'decimal';
		}
		if(!empty($attributes['value']) && $this->noE && $this->hasE($attributes['value'])) {
			$attributes['value'] = $this->wire()->sanitizer->float($attributes['value'], array('getString' => true));
		}
		if($this->precision > 0 && $this->digits > 0) {
			if(isset($attributes['value']) && strlen("$attributes[value]")) {
				$f = $attributes['type'] === 'number' ? 'F' : 'f'; // F=non-locale aware, f=locale aware
				$attributes['value'] = sprintf("%.{$this->precision}$f", (float) $attributes['value']);
			}
		}
		return parent::getAttributesString($attributes);
	}

	/**
	 * Convert floats with non "." decimal points to use "." decimal point according to locale
	 * 
	 * @param float|string $value
	 * @return string|float Returns string representation of float when value was converted
	 * 
	 */
	protected function localeConvertValue($value) {
		if(!strlen("$value")) return $value; 
		if(ctype_digit(str_replace(array('.', 'E', 'e', '-', '+'), '', "$value"))) return $value;
		$locale = localeconv();
		$decimal = $locale['decimal_point'];
		if($decimal === '.' || strpos($value, $decimal) === false) return $value;
		$parts = explode($decimal, $value, 2);
		$value = implode('.', $parts);
		return $value;
	}

	/**
	 * Inputfield config
	 * 
	 * @return InputfieldWrapper
	 * 
	 */
	public function getConfigInputfields() {
		$inputfields = parent::getConfigInputfields();
		if($this->hasFieldtype === false) {
			// when used without FieldtypeFloat
			$f = $inputfields->InputfieldInteger;
			$f->attr('name', 'precision');
			$f->label = $this->_('Number of decimal digits to round to');
			$f->description = $this->_('Or use a negative number like `-1` to disable rounding.');
			$f->attr('value', $this->precision);
			$f->attr('size', 8);
			$inputfields->add($f);
		} else {
			// precision is configured with FieldtypeFloat
		}
		// @todo anyone other than me want a config setting for $noE ?
		return $inputfields;
	}

}
