<?php namespace ProcessWire;

/**
 * ProcessWire Integer Fieldtype
 *
 * Field that stores an integer value. 
 *
 * For documentation about the fields used in this class, please see:  
 * /wire/core/Fieldtype.php
 * 
 * ProcessWire 3.x, Copyright 2020 by Ryan Cramer
 * https://processwire.com
 * 
 * @todo allow for more integer types (tiny, small, medium, big) and unsigned option
 * 
 */

class FieldtypeInteger extends Fieldtype {

	public static function getModuleInfo() {
		return array(
			'title' => 'Integer',
			'version' => 102,
			'summary' => 'Field that stores an integer',
			'permanent' => true, 
		);
	}

	/**
	 * Get compatible Fieldtypes
	 * 
	 * @param Field $field
	 * @return 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; 
	}

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

	/**
	 * Is value one that should be deleted rather than stored in the DB?
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param mixed $value
	 * @return bool
	 * 
	 */
	public function isDeleteValue(Page $page, Field $field, $value) {
		return $this->isEmptyValue($field, $value); 
	}

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

	/**
	 * Sanitize value to integer or blank string (for no value)
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param int|string|mixed $value
	 * @return int|string
	 * 
	 */
	public function sanitizeValue(Page $page, Field $field, $value) {

		if(is_string($value) && strlen($value) && !ctype_digit(ltrim($value, '-'))) {
			$value = $this->sanitizeValueString($value);
		} else {
			$value = strlen("$value") ? (int) $value : '';
		}

		return $value;

	}

	/**
	 * Sanitize string to integer or blank string if empty
	 * 
	 * @param string $value
	 * @return int|string
	 * 
	 */
	protected function sanitizeValueString($value) {
		
		// string value with one or more non-digit characters
		$value = trim($value);
		
		// trim off common currency symbols
		$value = trim($value, '$€ ');

		if(ctype_digit("$value")) {
			// trimming reduced it to an int

		} else if(preg_match('/^(\de\d|0x\d+|\+\d+)/', $value)) {
			// likely a valid number, but in a non-native format to PW 
			// examples: 1e123213, 0x1234, +123 (intval handles these)
			$value = intval($value);

		} else if(preg_match('/^[^-+\d.]+/', $value)) {
			// string starting with something we don't recognize, let PHP decide
			// example: bd#79
			$value = intval($value);
			if($value === 0) $value = ''; // blank rather than zero

		} else {
			// string value that looks like a number but has some other stuff in it

			// see if there are some definitely non-number chars in there, and truncate
			// the string to that point if we find any
			if(preg_match('/^(-?[\d,. ]+)([^\d,. ]+)/', $value, $matches)) {
				$value = $matches[1];
			}

			// check to see if we're dealing with a potential float val or thousands separators
			if(strpos($value, '.') !== false || strpos($value, ',') !== false || strpos($value, ' ') !== false) {
				// convert float values to rounded integers
				// also handles values with thousands separators
				$value = $this->wire()->sanitizer->float($value, array('blankValue' => ''));
				if($value !== '') $value = round($value);

			} else if(is_numeric($value)) {
				// let PHP decide how to convert it 
				$value = intval($value);

			} else {
				// default: replace non numeric characters
				$negative = substr(trim($value), 0, 1) == '-';
				$value = preg_replace('/[^\d]/', '', $value);
				$value = strlen($value) ? (int) $value : '';
				if($negative && is_int($value)) $value = $value * -1;
			}
		}

		return strlen("$value") ? (int) $value : '';
	}

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

	/**
	 * Get DB schema
	 * 
	 * @param Field $field
	 * @return array
	 * 
	 */
	public function getDatabaseSchema(Field $field) {
		$schema = parent::getDatabaseSchema($field);
		$schema['data'] = 'int NOT NULL';
		return $schema;
	}

	/**
	 * Get Inputfields to configure integer field
	 * 
	 * @param Field $field
	 * @return InputfieldWrapper
	 * 
	 */
	public function ___getConfigInputfields(Field $field) {
		$inputfields = parent::___getConfigInputfields($field); 
		
		/** @var InputfieldRadios $f */
		$f = $this->wire()->modules->get('InputfieldRadios'); 
		$f->label = $this->_('Are blank and 0 equivalent?'); 
		$f->description = $this->_('This affects how ProcessWire matches pages during database find operations.') . ' ' . 
			$this->_('If 0 and blank are equivalent (the Yes option) then a search for **field=0** or **field=""** will produce the same results.') . ' ' . 
			$this->_('If they are not equivalent (the No option) then a search for **field=0** will only match fields containing the value 0, and **field=""** will only match fields with no value.') . ' ' . 
			$this->_('As another example, with the Yes option **field<1** would match both the value 0 and no value, and with the No option it would match only 0.'); 
		$f->attr('name', 'zeroNotEmpty'); 
		$f->addOption(0, $this->_('Yes - Blank and 0 are equivalent'));
		$f->addOption(1, $this->_('No - Blank and 0 have different meanings')); 
		$f->attr('value', (int) $field->get('zeroNotEmpty'));
		$inputfields->add($f); 
	
		/** @var InputfieldInteger $f */
		$f = $this->wire()->modules->get('InputfieldInteger');
		$f->attr('name', 'defaultValue');
		$f->label = $this->_('Default value'); 
		$f->description = $this->_('This value is assigned as the default for this field on pages with no value entered.');
		$f->collapsed = Inputfield::collapsedBlank;
		$f->attr('value', strlen((string) $field->get('defaultValue')) ? (int) $field->get('defaultValue') : ''); 
		$inputfields->add($f);
		
		return $inputfields; 
	}

}

