<?php namespace ProcessWire;

/**
 * ProcessWire Text Fieldtype
 *
 * Basic Field that stores text, typically a single line. 
 *
 * For documentation about the fields used in this class, please see:  
 * /wire/core/Fieldtype.php
 * 
 * ProcessWire 3.x, Copyright 2023 by Ryan Cramer
 * https://processwire.com
 *
 *
 */

class FieldtypeText extends Fieldtype {

	public static function getModuleInfo() {
		return array(
			'title' => 'Text',
			'version' => 102,
			'summary' => 'Field that stores a single line of text',
			'permanent' => true, 
		);
	}

	/**
	 * Are text formatters allowed for this Fieldtype?
	 *
	 * Descending classes can override with the allowTextFormatters(false) method. 
	 *
	 */
	private $allowTextFormatters = true; 

	/**
	 * Provides a way for descending classes to disable text formatters where they aren't applicable
	 *
	 * @param bool|null $allow True to allow them, false to disallow or NULL not to do anything
	 * @return bool Current state of $allowTextFormatters
	 *
	 */
	protected function allowTextFormatters($allow = null) {
		if(!is_null($allow)) $this->allowTextFormatters = $allow ? true : false;
		return $this->allowTextFormatters; 
	}

	/**
	 * Return all Fieldtypes derived from FieldtypeText, which we will consider compatible
	 * 
	 * @param Field $field
	 * @return Fieldtypes
	 *
	 */
	public function ___getCompatibleFieldtypes(Field $field) {
		$fieldtypes = $this->wire(new Fieldtypes());
		foreach($this->wire()->fieldtypes as $fieldtype) {
			if($fieldtype instanceof FieldtypeText) {
				$fieldtypes->add($fieldtype);
			} else {
				$className = $fieldtype->className();
				if($className == 'FieldtypeSelector') $fieldtypes->add($fieldtype);
			}
		}
		return $fieldtypes; 
	}

	/**
	 * Sanitize value for storage
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param string $value
	 * @return string
	 *
	 */
	public function sanitizeValue(Page $page, Field $field, $value) {
		return $value; 
	}

	/**
	 * Format value for output
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param string $value
	 * @return string
	 *
	 */
	public function ___formatValue(Page $page, Field $field, $value) {

		$value = (string) $value; 
		$textformatters = $field->get('textformatters');

		if($this->allowTextFormatters() && is_array($textformatters)) {
			$modules = $this->wire()->modules;
			foreach($textformatters as $name) {
				/** @var Textformatter $textformatter */
				$textformatter = $modules->get($name);
				if($textformatter) $textformatter->formatValue($page, $field, $value); 
			}
		}

		return $value; 
	}

	/**
	 * Return whether the given value is considered empty or not
	 *
	 * This an be anything that might be present in a selector value and thus is
	 * typically a string. However, it may be used outside of that purpose so you
	 * shouldn't count on it being a string.
	 *
	 * @param Field $field
	 * @param mixed $value
	 * @return bool
	 *
	 */
	public function isEmptyValue(Field $field, $value) {
		return !strlen("$value"); 
	}

	/**
	 * Return the associated Inputfield
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @return Inputfield
	 *
	 */
	public function getInputfield(Page $page, Field $field) {
		$modules = $this->wire()->modules;
		$inputfieldClass = $field->get('inputfieldClass');
		/** @var Inputfield $inputfield */
		$inputfield = $inputfieldClass ? $modules->getModule($inputfieldClass) : null;
		if(!$inputfield) $inputfield = $modules->get('InputfieldText');
		return $inputfield;
	}

	/**
	 * Update a query to match the text with a fulltext index
	 * 
	 * @param PageFinderDatabaseQuerySelect $query
	 * @param string $table
	 * @param string $subfield
	 * @param string $operator
	 * @param int|string $value
	 * @return DatabaseQuerySelect
	 *
	 */
	public function getMatchQuery($query, $table, $subfield, $operator, $value) {
		$ft = new DatabaseQuerySelectFulltext($query); 
		$ft->match($table, $subfield, $operator, $value); 
		return $query; 
	}

	/**
	 * Return the database schema in specified format
	 * 
	 * @param Field $field
	 * @return array
	 *
	 */
	public function getDatabaseSchema(Field $field) {
		$schema = parent::getDatabaseSchema($field);
		$len = $this->wire()->database->getMaxIndexLength();
		$schema['data'] = 'text NOT NULL';
		$schema['keys']['data_exact'] = "KEY `data_exact` (`data`($len))"; 
		$schema['keys']['data'] = 'FULLTEXT KEY `data` (`data`)'; 
		return $schema;
	}

	/**
	 * Hook called when field is about to be saved
	 *
	 * @param Field $field
	 * @since 3.0.212
	 *
	 */
	public function ___saveFieldReady(Field $field) {
		parent::___saveFieldReady($field);
		if(!$field->id && $this->allowTextFormatters() && $this->wire()->page->process == 'ProcessField') {
			// default Textformatter for newly created fields in ProcessField
			$value = $field->get('textformatters');
			if(!is_array($value) || !count($value)) {
				$field->set('textformatters', array('TextformatterEntities'));
			}
		}
	}

	/**
	 * Return the fields required to configure an instance of FieldtypeText
	 * 
	 * @param Field $field
	 * @return InputfieldWrapper
	 *
	 */
	public function ___getConfigInputfields(Field $field) {
		$inputfields = parent::___getConfigInputfields($field);
		if(!$this->allowTextFormatters()) return $inputfields;
		
		$modules = $this->wire()->modules;
		$textformatters = $modules->findByPrefix('Textformatter');
		
		if(!count($textformatters)) return $inputfields;
		
		/** @var InputfieldFieldset $fieldset */
		$fieldset = $modules->get('InputfieldFieldset');
		$fieldset->attr('name', '_text_fieldset');
		$fieldset->label = $this->_('Text settings');
		$fieldset->icon = 'text-width';
		$inputfields->add($fieldset);
		
		/** @var InputfieldAsmSelect $f */
		$f = $modules->get('InputfieldAsmSelect'); 
		$f->attr('name', 'textformatters'); 
		$f->label = $this->_('Text formatters');

		foreach($textformatters as $moduleName) {
			$info = $modules->getModuleInfo($moduleName);
			$f->addOption($moduleName, "$info[title]"); 
		}

		$value = $field->get('textformatters');
		if(!is_array($value)) $value = array();
		$f->val($value);
		$f->description = 
			$this->_('If you want to apply any automatic formatting to the field when it is prepared for output, select one or more text formatters here.') . ' ' . 
			$this->_('If you select more than one, drag them into the order they should be applied.') . ' ' . 
			sprintf($this->_('Find more in the [text formatter modules directory](%s).'), 'https://processwire.com/modules/category/textformatter/');
		$f->notes = 
			$this->_('For plain text fields that will not contain HTML or markup, we recommend selecting the **HTML Entity Encoder** option above.');
		$fieldset->add($f);
		
		if(!in_array('TextformatterEntities', $value) && $this->allowTextformatters() && !$this->wire()->input->is('post')) {
			// warn user when HTML is allowed and we aren’t sure they intend to allow it
			$allowHTML = false;
			if(wireInstanceOf($field->type, 'FieldtypeTextarea')) {
				$inputType = $field->get('inputfieldClass');
				$htmlInputTypes = array('InputfieldTinyMCE', 'InputfieldCKEditor'); 
				$contentType = $field->get('contentType');
				$allowHTML = in_array($inputType, $htmlInputTypes) || $contentType >= FieldtypeTextarea::contentTypeHTML;
			}
			if(!$allowHTML) {
				$htmlValue = '<p>HTML</p>';
				foreach($value as $textformatter) {
					/** @var Textformatter $textformatter */
					$textformatter = $modules->get($textformatter);
					if($textformatter) $textformatter->format($htmlValue);
				}
				if(strpos($htmlValue, '<') !== false) $this->warning(
					$this->_('This field currently allows Markup/HTML in formatted output.') . ' ' . 
					$this->_('If you do not intend to allow Markup/HTML, please select the “HTML Entity Encoder” for “Text formatters”.')
				);
			}
		}

		if($field->type->className() === 'FieldtypeText') {
			/** @var InputfieldSelect $f */
			$defaultLabel = $this->_('Default'); 
			$f = $modules->get('InputfieldRadios');
			$f->attr('name', 'inputfieldClass');
			$f->label = $this->_('Input module');
			$f->description = $this->_('Save after changing this as it may affect what settings are available on the “Input” tab.'); 
			$f->addOption('', $this->_('Text') . " [span.detail] - $defaultLabel [/span]");
			foreach($modules->findByPrefix('Inputfield', 2) as $moduleName => $moduleInfo) {
				if($moduleName === 'InputfieldText') continue;
				if(stripos($moduleName, 'textarea') !== false) continue;
				if(!wireInstanceOf($moduleName, 'InputfieldHasTextValue')) continue;
				$f->addOption($moduleName, "$moduleInfo[title] [span.detail] - $moduleInfo[summary] [/span]"); 
			}
			$f->val((string) $field->get('inputfieldClass')); 
			$f->collapsed = Inputfield::collapsedBlank;
			$fieldset->add($f);
		}

		return $inputfields; 
	}

	/**
	 * Convert an array of exported data to a format that will be understood internally
	 *
	 * @param Field $field
	 * @param array $data
	 * @return array Data as given and modified as needed. Also included is $data[errors], an associative array
	 *	indexed by property name containing errors that occurred during import of config data.
	 *
	 */
	public function ___importConfigData(Field $field, array $data) {
		if(isset($data['textformatters']) && is_array($data['textformatters'])) {
			$errors = array();
			foreach($data['textformatters'] as $className) {
				if(!$this->wire()->modules->isInstalled($className)) {
					$errors[] = "Requires module '$className' to be installed";
				}
			}
			if(count($errors)) $data['errors']['textformatters'] = implode(" \n", $errors); 
		}
		return $data;
	}
	
	/**
	 * Import value
	 *
	 * @param Page $page
	 * @param Field $field
	 * @param array|int|object|string $value
	 * @param array $options
	 * @return array|string
	 *
	 */
	public function ___importValue(Page $page, Field $field, $value, array $options = array()) {
		if(is_array($value) && isset($value['default']) && !$this->wire()->languages) {
			// multi-language value to non-multi-language site, use only default language
			$value = $value['default'];
		}
		$value = parent::___importValue($page, $field, $value, $options);
		return $value;
	}

}

