<?php namespace ProcessWire;

/**
 * ProcessWire Float Fieldtype
 *
 * Field that stores a floating point number. 
 *
 * 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
 *
 */

class FieldtypeFloat extends Fieldtype {

	public static function getModuleInfo() {
		return array(
			'title' => __('Float', __FILE__),
			'summary' => __('Field that stores a floating point number', __FILE__),
			'version' => 108,
			'permanent' => true, 
		);
	}

	/**
	 * Get compatible Fieldtypes
	 * 
	 * @param Field $field
	 * @return null|Fieldtypes
	 * 
	 */
	public function ___getCompatibleFieldtypes(Field $field) {
		$fieldtypes = parent::___getCompatibleFieldtypes($field);
		$hasDecimal = $this->wire()->modules->isInstalled('FieldtypeDecimal');
		foreach($fieldtypes as $type) {
			if($hasDecimal && wireInstanceOf($type, 'FieldtypeDecimal')) continue;
			if(!$type instanceof FieldtypeInteger && !$type instanceof FieldtypeFloat && $type != 'FieldtypeText') {
				$fieldtypes->remove($type); 
			}
		}
		return $fieldtypes; 
	}

	/**
	 * Get blank value
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @return string
	 * 
	 */
	public function getBlankValue(Page $page, Field $field) {
		return ''; 
	}

	/**
	 * Is given value considered empty to this Fieldtype?
	 * 
	 * @param Field $field
	 * @param mixed $value
	 * @return bool
	 * 
	 */
	public function isEmptyValue(Field $field, $value) {
		if($value === "0" || $value === 0 || $value === "0.0" || $value === 0.0) {
			// when zeroNotEmpty option is set, we don't count a literal "0" is being a blank value
			if($field->get('zeroNotEmpty')) return false;
		}
		return empty($value);
	}

	/**
	 * Sanitize value
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param float|int|string $value
	 * @return float|string
	 * 
	 */
	public function sanitizeValue(Page $page, Field $field, $value) {
		if(!strlen("$value")) return '';
		if(!is_float($value) && !is_int($value)) {
			$value = $this->wire()->sanitizer->float((string) $value, array('blankValue' => ''));
		}
		$precision = $field->get('precision');
		if($precision === null || $precision === '' || $precision < 0) {
			$value = (float) $value;
		} else {
			$value = round((float) $value, $precision);
		}
		return $value; 
	}

	/**
	 * Get Inputfield for this Fieldtype
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @return Inputfield
	 * 
	 */
	public function getInputfield(Page $page, Field $field) {
		/** @var InputfieldFloat $inputfield */
		$inputfield = $this->wire()->modules->get('InputfieldFloat'); 
		$inputfield->addClass($this->className());
		$inputfield->precision = $field->get('precision'); 
		return $inputfield; 
	}

	/**
	 * Sleep value for DB storage
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param string|float|int|null $value
	 * @return string
	 * 
	 */
	public function ___sleepValue(Page $page, Field $field, $value) {
		if($value === null) return ''; // blank value
		$precision = $field->get('precision'); 
		if(is_null($precision) || $precision === '' || $precision < 0) {
			$precision = self::getPrecision($value);
		}
		if(!is_string($value)) {
			$value = number_format((float) $value, $precision, '.', '');
		}
		return $value;
	}

	/**
	 * Get precision of given value 
	 * 
	 * @param string|float $value
	 * @param int $min minimum precision to return
	 * @return int
	 * 
	 */
	public static function getPrecision($value, $min = 1) {
		$value = (float) $value;
		if(stripos("$value", 'E-')) {
			// negative exponent
			list($v, $precision) = explode('E-', strtoupper("$value"), 2);
			$precision = (int) $precision;
			if(strpos($v, '.') !== false) $precision += strlen(strstr($v, '.'))-1;
		} else if(stripos("$value", 'E')) {
			// positive exponent
			list($v, $precision) = explode('E', strtoupper("$value"), 2);
			$precision = (int) ltrim($precision, '+');
			if(strpos($v, '.') !== false) $precision = (strlen(strstr($v, '.')) - 1) - $precision;
		} else if(strpos("$value", '.') === false) {
			$precision = 0;
		} else {
			// decimal
			/*
			$remainder = ceil($value) - $value;
			$precision = strlen(ltrim("$remainder", '0., '));
			*/
			list(, $v) = explode('.', "$value");
			$precision = strlen(rtrim($v, '0'));
		}
		if($precision < $min) $precision = $min; 
		return $precision;
	}

	/**
	 * Get DB schema for this Fieldtype
	 * 
	 * @param Field $field
	 * @return array
	 * 
	 */
	public function getDatabaseSchema(Field $field) {
		$schema = parent::getDatabaseSchema($field); 
		$schema['data'] = ($field->get('colType') === 'double' ? 'double' : 'float') . ' NOT NULL';
		return $schema;
	}

	/**
	 * Set the database data column type (updating the DB table)
	 * 
	 * @param Field $field
	 * @param string $newType Specify either 'float' or 'double'
	 * @return string Returns the colum
	 * @since 3.0.241
	 * 
	 */
	public function setColumnType(Field $field, $newType) {
		$oldType = $this->getColumnType($field);
		if($newType != 'double') $newType = $this->getDefaultColumnType();
		if($oldType === $newType) return $oldType;
		$database = $this->wire()->database;
		$table = $field->getTable();
		$database->exec("ALTER TABLE $table MODIFY `data` $newType NOT NULL");
		$field->warning("Changed '$field->name' type from '$oldType' to '$newType'", Notice::debug);
		$field->set('colType', $newType);
		return $newType;
	}

	/**
	 * Get the current database data column type
	 * 
	 * @param Field $field
	 * @return string 
	 * @since 3.0.241
	 * 
	 */
	protected function getColumnType(Field $field) {
		$database = $this->wire()->database;
		$table = $field->getTable();
		try {
			$cols = $database->getColumns($table, true);
			$type = $cols['data']['type'];
		} catch(\Exception $e) {
			$this->error($e->getMessage());
			$type = $this->getDefaultColumnType();
		}
		return $type;
	}
	
	protected function getDefaultColumnType() {
		return 'float';
	}
	
	/**
	 * Get field configuration
	 * 
	 * @param Field $field
	 * @return InputfieldWrapper
	 * 
	 */
	public function ___getConfigInputfields(Field $field) {
		$inputfields = parent::___getConfigInputfields($field);
		$precision = $field->get('precision');
		if($precision === null) $precision = 2; 
		
		$colType = strtolower($this->getColumnType($field));
		$newType = $field->get('colType');
		if(empty($newType)) $newType = $colType;
		if($newType != $colType) $colType = $this->setColumnType($field, $newType);
		
		$f = $inputfields->InputfieldRadios;
		$f->attr('name', 'colType'); 
		$f->label = $this->_('Database column type'); 
		$f->addOption('float', $this->_('Float (single-precision)'));
		$f->addOption('double', $this->_('Double (double-precision)'));
		$f->val($colType);
		$inputfields->add($f);
		
		$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.');
		if($precision !== '') $f->val($precision); 
		$f->attr('size', 8); 
		$inputfields->append($f);
	
		/** @var FieldtypeInteger $ft */
		$ft = $this->wire()->modules->get('FieldtypeInteger');
		// use the same 'zeroNotEmpty' setting as FieldtypeInteger
		$f = $ft->___getConfigInputfields($field)->getChildByName('zeroNotEmpty'); 
		if($f) $inputfields->add($f); 

		return $inputfields; 
	}

	/**
	 * Convert float string with commas to float value
	 * 
	 * @param string $str
	 * @return float|string
	 * @deprecated Use $sanitizer->float($value, [ 'blankValue' = '' ]) instead
	 * 
	 */
	public static function strToFloat($str) { 
		return wire('sanitizer')->float($str, array('blankValue' => ''));
	}
	
	/*
	public function formatValue(Page $page, Field $field, $value) {
		// @todo add support for number_format options
		return $value; 	
	}
	*/

	/*
	public function getMatchQuery($query, $table, $subfield, $operator, $value) {
		if(!is_int($value) && !is_float($value)) {
			$value = self::strToFloat((string) $value);
		}
		return parent::getMatchQuery($query, $table, $subfield, $operator, $value); 
	}
	*/


}
