<?php namespace ProcessWire;

/**
 * ProcessWire File Fieldtype
 *
 * Field that stores one or more files with optional description. 
 *
 * For documentation about the fields used in this class, please see:  
 * /wire/core/Fieldtype.php
 * /wire/core/FieldtypeMulti.php
 * 
 * ProcessWire 3.x, Copyright 2017 by Ryan Cramer
 * https://processwire.com
 * 
 * @method string formatValueString(Page $page, Field $field, $value)
 *
 */

class FieldtypeFile extends FieldtypeMulti {
	
	public static function getModuleInfo() {
		return array(
			'title' => __('Files', __FILE__),
			'version' => 105,
			'summary' => __('Field that stores one or more files', __FILE__),
			'permanent' => true,
		);
	}

	/**
	 * outputFormat: Automatic (single item or null when max files set to 1, array of items otherwise)
	 * 
	 */
	const outputFormatAuto = 0;

	/**
	 * outputFormat: Array of items
	 * 
	 */
	const outputFormatArray = 1;

	/**
	 * outputFormat: Single item or null when empty
	 * 
	 */
	const outputFormatSingle = 2;

	/**
	 * outputFormat: String that renders the item
	 * 
	 */
	const outputFormatString = 30;

	/**
	 * File schema is configured to support tags (flag)
	 *
	 */
	const fileSchemaTags = 1; 

	/**
	 * File schema is configured to support 'created' date (flag)
	 *
	 */
	const fileSchemaDate = 2;

	/**
	 * File schema is configured to support 'filedata' encoded data (flag)
	 *
	 */
	const fileSchemaFiledata = 4;

	/**
	 * Flag for useTags: tags off/disabled
	 * 
	 */
	const useTagsOff = 0;

	/**
	 * Flag for useTags: normal text input tags
	 * 
	 */
	const useTagsNormal = 1;

	/**
	 * Flag for useTags: predefined tags
	 * 
	 */
	const useTagsPredefined = 8;

	/**
	 * Default class for Inputfield object used, auto-generated at construct
	 * 
	 * @var string
	 * 
	 */
	protected $defaultInputfieldClass = '';

	/**
	 * Construct
	 * 
	 */
	public function __construct() {
		$this->defaultInputfieldClass = str_replace('Fieldtype', 'Inputfield', $this->className);
	}

	/**
	 * Get the Inputfield module to handle input for this Fieldtype
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @return Inputfield
	 * 
	 */
	public function getInputfield(Page $page, Field $field) {

		$inputfield = null;
		$inputfieldClass = $field->get('inputfieldClass');
		if($inputfieldClass) $inputfield = $this->modules->get($inputfieldClass); 
		if(!$inputfield) $inputfield = $this->modules->get($this->defaultInputfieldClass); 
		$inputfield->class = $this->className();

		$this->setupHooks($page, $field, $inputfield); 
	
		if(!$field->get('extensions')) {
			if($page->id && $page->template->fieldgroup->hasField($field)) {
				// message that appears on page being edited with this field
				$this->error(sprintf($this->_('Field "%s" is not yet ready to use and needs to be configured.'), $field->name)); 
			} else if(!count($_POST)) {
				// message that appears during configuration, but suppressed during post to prevent it from appearing after save
				$this->message(
					$this->_('Settings have not yet been committed.') . "<br /><small>" . 
					$this->_('Please review the settings on this page and save once more (even if you do not change anything) to confirm you accept them.') . "</small>", 
					Notice::allowMarkup); 
			}
		}

		return $inputfield;
	}

	public function ___getCompatibleFieldtypes(Field $field) {
		$fieldtypes = $this->wire(new Fieldtypes());
		foreach($this->wire('fieldtypes') as $fieldtype) {
			if($fieldtype instanceof FieldtypeFile) $fieldtypes->add($fieldtype); 
		}
		return $fieldtypes; 
	}

	/**
	 * Setup any necessary hooks for this Inputfield, intended to be called from getInputfield() method
	 *
	 * We're going to hook into the inputfield to set the upload destination path.
	 * This is because the destination path may be determined by events that occur
	 * between the time this method is executed, and the time the upload is saved.
	 * An example is the page files draft path vs. the published path.
	 *
	 * Make sure that any Fieldtype's descended from this one call the setupHooks method in their getInputfield()
	 * method. 
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param Inputfield $inputfield
	 *
	 */
	protected function setupHooks(Page $page, Field $field, Inputfield $inputfield) {

		$options = array(
			'page' => $page, 
			'field' => $field, 
			);

		$inputfield->addHookBefore('processInput', $this, 'hookProcessInput', $options); 
	}

	/**
	 * Hook into the InputfieldFile's processInput method to set the upload destination path
	 *
	 * This hook originates with the setupHooks method above. 
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookProcessInput($event) {
		/** @var InputfieldFile $inputfield */
		$inputfield = $event->object; 	
		/** @var Page $page */
		$page = $event->options['page']; 
		/** @var Field $field */
		$field = $event->options['field']; 
		$pagefiles = $page->get($field->name); 
		$inputfield->destinationPath = $pagefiles->path();
	}

	/**
	 * Given a raw value (value as stored in DB), return the value as it would appear in a Page object
 	 *
	 * @param Page $page
	 * @param Field $field
	 * @param string|int|array $value
	 * @return string|int|array|object $value
	 *
	 */
	public function ___wakeupValue(Page $page, Field $field, $value) {

		if($value instanceof Pagefiles) return $value; 
		$pagefiles = $this->getBlankValue($page, $field); 
		if(empty($value)) return $pagefiles; 
	
		if(!is_array($value) || array_key_exists('data', $value)) $value = array($value); 
		foreach($value as $v) {
			if(empty($v['data'])) continue; 
			$pagefile = $this->getBlankPagefile($pagefiles, $v['data']); 
			$pagefile->description(true, $v['description']); 
			if(isset($v['modified'])) $pagefile->modified = $v['modified'];
			if(isset($v['created'])) $pagefile->created = $v['created'];
			if(isset($v['tags'])) $pagefile->tags = $v['tags'];
			if(!empty($v['filedata'])) $pagefile->filedata = json_decode($v['filedata'], true);
			$pagefile->setTrackChanges(true); 
			$pagefiles->add($pagefile); 
		}
	
		$pagefiles->resetTrackChanges(true); 
		return $pagefiles;  
	}

	/**
	 * Given an 'awake' value, as set by wakeupValue, convert the value back to a basic type for storage in DB. 
	 *              
	 * @param Page $page
	 * @param Field $field
	 * @param string|int|array|object $value
	 * @return string|int
	 *
	 */
	public function ___sleepValue(Page $page, Field $field, $value) {

		$sleepValue = array();
		if(!$value instanceof Pagefiles) return $sleepValue; 
	
		foreach($value as $pagefile) {
			$item = array(
				'data' => $pagefile->basename, 
				'description' => $pagefile->description(true), 
				); 
			
			$fileSchema = $field->get('fileSchema');
	
			if($fileSchema & self::fileSchemaDate) { 
				$item['modified'] = date('Y-m-d H:i:s', $pagefile->modified);
				$item['created'] = date('Y-m-d H:i:s', $pagefile->created);
			}
	
			if($fileSchema & self::fileSchemaTags) {
				$item['tags'] = $pagefile->tags;
			}
			
			if($fileSchema & self::fileSchemaFiledata) {
				$item['filedata'] = null; 
				$filedata = $pagefile->filedata;
				if(!empty($filedata)) $item['filedata'] = json_encode($filedata); 
			}
	
			$sleepValue[] = $item;
		}
		return $sleepValue;
	}

	public function ___exportValue(Page $page, Field $field, $value, array $options = array()) {
		
		$pagefiles = $value; 
		$value = $this->sleepValue($page, $field, $value); 
		$exportValue = array();
		$defaults = array(
			'noJSON' => false, // no JSON for exported file descriptions or other properties (use array instead)
		);
		if(!isset($options['FieldtypeFile'])) $options['FieldtypeFile'] = array();
		$options['FieldtypeFile'] = array_merge($defaults, $options['FieldtypeFile']); 
		
		foreach($value as $k => $v) {
			/** @var Pagefile $pagefile */
			$pagefile = $pagefiles->get($v['data']); 
			
			$a = array(
				'url' => $pagefile->httpUrl(),
				'size' => $pagefile->filesize(), 
			); 
			
			if(!empty($options['system'])) {
				unset($v['created'], $v['modified']);
				$exportKey = $v['data'];
			} else {
				$a['name'] = $v['data'];
				$exportKey = $k;
			}
			
			unset($v['data']); 
	
			if($options['FieldtypeFile']['noJSON']) {
				// export version 2 for exported description uses array value for multi-language, rather than JSON string
				if(!isset($v['description'])) $v['description'] = '';
				$v['description'] = $this->exportDescription($v['description']);
			}
			
			$exportValue[$exportKey] = array_merge($a, $v);
		}
		
		return $exportValue; 	
	}

	/**
	 * Export description value to array (multi-language, indexed by lang name) or string (non-multi-language)
	 * 
	 * @param string|array $value
	 * @return array|string
	 * 
	 */
	protected function exportDescription($value) {
		
		/** @var Languages $languages */
		$languages = $this->wire('languages');
		
		if(is_string($value)) {
			if(strpos($value, '[') !== 0 && strpos($value, '{') !== 0) return $value;
			$a = json_decode($value, true);
			if(!is_array($a)) return $value;
		} else if(is_array($value)) {
			$a = $value;
		} else {
			$a = array();
		}
	
		if(!$languages) {
			$value = count($a) ? (string) reset($a) : ''; // return first/default
			return $value; 
		}

		$description = array(); 
		
		// ensure value present for every language, even if blank
		foreach($languages as $language) {
			$description[$language->name] = '';
		}
		
		foreach($a as $langKey => $langVal) {
			if(ctype_digit("$langKey")) $langKey = (int) $langKey;
			if(empty($langKey)) {
				$langKey = 'default';
			} else {
				$langKey = $languages->get($langKey)->name;
				if(empty($langKey)) continue;
			}
			$description[$langKey] = $langVal;
		}
		
		return $description;
	}

	public function getBlankValue(Page $page, Field $field) {
		$pagefiles = $this->wire(new Pagefiles($page));
		$pagefiles->setField($field); 
		$pagefiles->setTrackChanges(true); 
		return $pagefiles; 
	}

	/**
 	 * Returns a blank Pagefile instance, which may be another type of Pagefile (i.e. a Pageimage)
	 *
	 * This method ensures that the correct type of items are populated by wakeupValue()
	 *
	 * @param Pagefiles $pagefiles
	 * @param string $filename
	 * @return Pagefile 
	 *
	 */
	protected function getBlankPagefile(Pagefiles $pagefiles, $filename) {
		return $this->wire(new Pagefile($pagefiles, $filename)); 
	}

	public function sanitizeValue(Page $page, Field $field, $value) {
		if($value instanceof Pagefiles) return $value; 
		$pagefiles = $page->getUnformatted($field->name); 
		if(!$value) return $pagefiles; 
		if($value instanceof Pagefile) return $pagefiles->add($value); 
		if(!is_array($value)) $value = array($value); 
		foreach($value as $file) $pagefiles->add($file); 
		return $pagefiles; 
	}

	/**
	 * Perform output formatting on the value delivered to the API
	 *
	 * Entity encode the file's description field. 
	 * 
	 * If the maxFiles setting is 1, then we format the value to dereference as single Pagefile rather than a
	 * PagefilesArray
	 *
	 * This method is only used when $page->outputFormatting is true. 
	 * 
	 * @param Page $page
	 * @param Field $field
	 * @param Pagefiles $value
	 * @return Pagefiles|Pagefile
	 *
	 */
	public function ___formatValue(Page $page, Field $field, $value) {

		if(!$value instanceof Pagefiles) return $value; 
		
		/** @var Pagefiles $value */

		foreach($value as $pagefile) {
			if($pagefile->isTemp()) $value->removeQuietly($pagefile); 
		}
		
		$textformatters = $field->get('textformatters'); 
		if(!is_array($textformatters)) $textformatters = array();
		if($field->get('entityEncode') && !count($textformatters)) $textformatters[] = 'TextformatterEntities';
		$useTags = (int) $field->get('useTags'); 
	
		foreach($textformatters as $name) {
			$textformatter = $this->wire('modules')->get($name); 
			if(!$textformatter) continue;
			foreach($value as $v) {
				if($v->formatted()) continue; 
				$description = $v->description;
				$textformatter->formatValue($page, $field, $description);	
				$v->description = $description; 
				if($useTags) {
					$tags = $v->tags; 
					$textformatter->formatValue($page, $field, $tags); 
					$v->tags = $tags; 
				}
			}
		}
		
		foreach($value as $v) {
			$v->formatted = true;
		}

		/*
		if($field->entityEncode) { 
			foreach($value as $k => $v) {
				if($v->formatted()) continue; 
				$v->description = htmlspecialchars($v->description, ENT_QUOTES, "UTF-8"); 
				$v->tags = htmlspecialchars($v->tags, ENT_QUOTES, "UTF-8"); 
				$v->formatted = true; 
			}
		}
		*/
	
		$count = count($value); 
		$isEmpty = $count == 0;
		$maxFiles = (int) $field->get('maxFiles'); 
		if($maxFiles && $count > $maxFiles) $value = $value->slice(0, $maxFiles); 
		
		switch((int) $field->get('outputFormat')) {
			
			case self::outputFormatArray:
				// we are already in this format so don't need to do anything
				break;
			
			case self::outputFormatSingle:
				$value = $isEmpty ? null : $value->first();
				break;
			
			case self::outputFormatString:
				$value = $this->formatValueString($page, $field, $value); 
				break;
			
			default: // outputFormatAuto
				if($maxFiles == 1) { 
					$value = $isEmpty ? null : $value->first();
				}
		}
		
		if($isEmpty && $field->get('defaultValuePage')) {
			$defaultPage = $this->wire('pages')->get((int) $field->get('defaultValuePage')); 
			if($defaultPage->id && $defaultPage->id != $page->id) {
				$value = $defaultPage->get($field->name);	
			}
		}
		
		return $value; 
	}
	
	protected function ___formatValueString(Page $page, Field $field, $value) {
		if($page) {} // ignore
		$out = '';
		$outputString = $field->get('outputString');
		if(empty($outputString)) $outputString = '{url}';
		preg_match_all('/\{([_a-zA-Z0-9]+)\}/', $outputString, $matches);
		foreach($value as $item) {
			$s = $outputString;
			foreach($matches[1] as $key => $match) {
				$v = $item->$match;
				$s = str_replace($matches[0][$key], $v, $s);
			}
			$out .= $s;
		}
		return $out; 
	}

	public function getMatchQuery($query, $table, $subfield, $operator, $value) {

		if(in_array($operator, array("*=", "~=", "%=", "^=", "$="))) {
			// fulltext match filename or description
			/** @var DatabaseQuerySelectFulltext $ft */
			$ft = $this->wire(new DatabaseQuerySelectFulltext($query)); 
			$ft->match($table, $subfield, $operator, $value); 

		} else {
			$query = parent::getMatchQuery($query, $table, $subfield, $operator, $value); 
		}
		return $query; 
	}

	public function ___getSelectorInfo(Field $field, array $data = array()) {
		$info = parent::___getSelectorInfo($field, $data); 
		$info['subfields']['data']['label'] = $this->_('filename'); 
		return $info;
	}

	public function getDatabaseSchema(Field $field) {

		$database = $this->wire('database');
		$schema = parent::getDatabaseSchema($field);
		$table = $database->escapeTable($field->table); 
		$maxLen = $database->getMaxIndexLength();

		$schema['data'] = "varchar($maxLen) NOT NULL";
		$schema['description'] = "text NOT NULL";
		$schema['modified'] = "datetime"; 
		$schema['created'] = "datetime";
		$schema['filedata'] = "mediumtext"; 
		$schema['keys']['description'] = 'FULLTEXT KEY description (description)';
		$schema['keys']['filedata'] = 'FULLTEXT KEY filedata (filedata)'; 
		$schema['keys']['modified'] = 'index (modified)'; 
		$schema['keys']['created'] = 'index (created)';
		
		if($field->id && !($field->get('fileSchema') & self::fileSchemaFiledata)) {
			// permanently add new 'filedata' column to schema
			$addFiledata = false;

			try {
				$query = $database->prepare("SHOW COLUMNS FROM `$table` WHERE Field='filedata'");
				$query->execute();
				$numRows = (int) $query->rowCount();
				$query->closeCursor();

				if($numRows) {
					$field->set('fileSchema', $field->get('fileSchema') | self::fileSchemaFiledata);
					$field->save();
				} else {
					$addFiledata = true;
				}
			} catch(\Exception $e) {
				// intentionally blank
			}

			if($addFiledata) try {
				$database->exec("ALTER TABLE `{$table}` ADD `filedata` $schema[filedata]");
				$database->exec("ALTER TABLE `{$table}` ADD " . $schema['keys']['filedata']);
				$field->set('fileSchema', $field->get('fileSchema') | self::fileSchemaFiledata);
				$field->save();
				$this->message("Added filedata to DB schema for '{$field->name}'", Notice::log);

			} catch(\Exception $e) {

				$query = $database->prepare("SHOW COLUMNS FROM `$table` WHERE Field='filedata'");
				$query->execute();
				$numRows = (int) $query->rowCount();
				$query->closeCursor();

				if($numRows) {
					$field->set('fileSchema', $field->get('fileSchema') | self::fileSchemaFiledata);
					$field->save();
				} else {
					$this->error("Error adding created/modified to '{$field->name}' schema", Notice::log);
					unset($schema['filedata'], $schema['keys']['filedata']); 
				}
			}
		}

		if($field->id && !($field->get('fileSchema') & self::fileSchemaDate)) {
			// permanently add new 'modified' and 'created' column to file schema
			$addDates = false;

			try {
				$query = $database->prepare("SHOW COLUMNS FROM `$table` WHERE Field='modified'"); 
				$query->execute();
				$numRows = (int) $query->rowCount();
				$query->closeCursor();
				
				if($numRows) {
					$field->set('fileSchema', $field->get('fileSchema') | self::fileSchemaDate); 
					$field->save();
				} else {
					$addDates = true; 
				}
			} catch(\Exception $e) {
				// intentionally blank
			}

			if($addDates) try {
				$database->exec("ALTER TABLE `{$table}` ADD `modified` $schema[modified]");
				$database->exec("ALTER TABLE `{$table}` ADD `created` $schema[created]");
				$database->exec("ALTER TABLE `{$table}` ADD " . $schema['keys']['modified']); 
				$database->exec("ALTER TABLE `{$table}` ADD " . $schema['keys']['created']); 
				$field->set('fileSchema', $field->get('fileSchema') | self::fileSchemaDate); 
				$field->save();
				$date = date('Y-m-d H:i:s'); 
				$query = $database->prepare("UPDATE `$table` SET created=:created, modified=:modified"); 
				$query->bindValue(":created", $date);
				$query->bindValue(":modified", $date); 
				$query->execute();
				$this->message("Added created/modified to DB schema for '{$field->name}'", Notice::log); 
				
			} catch(\Exception $e) {
				
				$query = $database->prepare("SHOW COLUMNS FROM `$table` WHERE Field='modified'"); 
				$query->execute();
				$numRows = (int) $query->rowCount();
				$query->closeCursor();
				
				if($numRows) {
					$field->set('fileSchema', $field->get('fileSchema') | self::fileSchemaDate); 
					$field->save();
				} else {
					$this->error("Error adding created/modified to '{$field->name}' schema", Notice::log); 
				}
			
			}
		}

		$tagsAction = null; // null=no change; 1=add tags, 0=remove tags
		$schemaTags = 'varchar(250) NOT NULL'; 
		$schemaTagsIndex = 'FULLTEXT KEY tags (tags)'; 

		if($field->get('useTags') && !($field->get('fileSchema') & self::fileSchemaTags)) {
			$tagsAction = 'add';
		} else if(!$field->get('useTags') && ($field->get('fileSchema') & self::fileSchemaTags)) {
			$tagsAction = 'remove';
		}

		if($tagsAction === 'add') {
			// add tags field
			try {
				$query = $database->prepare("SHOW COLUMNS FROM `$table` WHERE Field='tags'"); 
				$query->execute();
				$numRows = (int) $query->rowCount();
				$query->closeCursor();
			} catch(\Exception $e) {
				// probably in a clone, we can ignore and skip over any further changes
				$numRows = 1; 
			}

			if(!$numRows) try {
				$database->exec("ALTER TABLE `{$table}` ADD tags $schemaTags");
				$database->exec("ALTER TABLE `{$table}` ADD $schemaTagsIndex"); 
				$field->set('fileSchema', $field->get('fileSchema') | self::fileSchemaTags); 
				$field->save();
				$this->message("Added tags to DB schema for '{$field->name}'", Notice::log); 
			} catch(\Exception $e) {
				$this->error("Error adding tags to '{$field->name}' schema", Notice::log); 
			}

		} else if($tagsAction === 'remove') {
			// remove tags field
			try {
				$database->exec("ALTER TABLE `{$table}` DROP INDEX tags"); 
				$database->exec("ALTER TABLE `{$table}` DROP tags"); 
				$field->set('fileSchema', $field->get('fileSchema') & ~self::fileSchemaTags);
				$field->save();
				$this->message("Dropped tags from DB schema for '{$field->name}'", Notice::log); 
			} catch(\Exception $e) {
				$this->error("Error dropping tags from '{$field->name}' schema", Notice::log); 
			}
		}

		if($field->get('fileSchema') & self::fileSchemaTags) { 
			$schema['tags'] = $schemaTags;
			$schema['keys']['tags'] = $schemaTagsIndex;
		}

		return $schema;
	}


	public function ___deletePageField(Page $page, Field $field) {

		// if($this->config->debug) $this->message("deletePageField, Page:$page, Field:$field"); 

		if($pagefiles = $page->get($field->name)) {

			$dvpID = $field->get('defaultValuePage'); 
			
			if($dvpID && $dvpID != $page->id && $pagefiles->page->id != $page->id) {
				// pagefiles is a default/fallback value from another page and should not be deleted
				
			} else if($pagefiles instanceof Pagefiles) {
				$pagefiles->deleteAll(); 

			} else if($pagefiles instanceof Pagefile) {
				$pagefiles->unlink();

			} else if($page->hasField($field) && $this->config->debug) {
				$this->error("Not Pagefiles or Pagefile"); 
			}

		} else if($page->hasField($field) && $this->config->debug) {
			$this->error("Unable to retreive $page.{$field->name}"); 			
		}

		parent::___deletePageField($page, $field); 

		return true; 
		
	}

	public function ___deleteField(Field $field) {
		// delete files not necessary since deletePageField would have been called for all instances before this could be called
		return parent::___deleteField($field); 
	}

	/**
	 * Default list of file extensions supported by this field, and used as the default by getConfigInputfields()
	 * method. 
	 *
	 * Subclasses can override with their own string of file extensions
	 *
	 */
	protected function getDefaultFileExtensions() {
		return "pdf doc docx xls xlsx gif jpg jpeg png";
	}

	/**
	 * Disable autojoin for files
	 * 
	 * @param Field $field
	 * @param DatabaseQuerySelect $query
	 * @return DatabaseQuerySelect
	 *
	 */
	public function getLoadQueryAutojoin(Field $field, DatabaseQuerySelect $query) {
		return null;
	}  


	public function ___getConfigInputfields(Field $field) {

		$inputfields = parent::___getConfigInputfields($field);

		// extensions
		/** @var InputfieldTextarea $f */
		$f = $this->modules->get('InputfieldTextarea');
		$f->attr('name', 'extensions'); 
		$f->attr('value', $field->get('extensions') ? $field->get('extensions') : $this->getDefaultFileExtensions()); 
		$f->attr('rows', 3); 
		$f->label = $this->_('Valid File Extensions');
		$f->description = $this->_('Enter all file extensions allowed by this upload field. Separate each extension by a space. No periods or commas. This field is not case sensitive.'); // Valid file extensions description
		$inputfields->append($f);

		// max files
		/** @var InputfieldInteger $f */
		$f = $this->modules->get('InputfieldInteger');
		$f->attr('name', 'maxFiles'); 
		$f->attr('value', (int) $field->get('maxFiles')); 
		$f->attr('size', 4);
		$f->label = $this->_('Maximum files allowed');
		$f->description = $this->_('0=No limit'); 
		$f->collapsed = Inputfield::collapsedBlank;
		$inputfields->append($f); 
	
		// output format
		/** @var InputfieldRadios $f */
		$f = $this->modules->get('InputfieldRadios'); 
		$f->attr('name', 'outputFormat'); 
		$f->label = $this->_('Formatted value'); 
		$f->description = $this->_('Select the type of value you want this field to provide when accessed from the API on the front-end of your site.'); 
		$f->notes = $this->_('When output formatting is off, the value is always an array (WireArray).'); 
		$f->addOption(self::outputFormatAuto, $this->_('Automatic (single item or null when max files set to 1, array of items otherwise)')); 
		$f->addOption(self::outputFormatArray, $this->_('Array of items'));
		$f->addOption(self::outputFormatSingle, $this->_('Single item (null if empty)')); 
		$f->addOption(self::outputFormatString, $this->_('Rendered string of text (that you provide)')); 
		$f->attr('value', (int) $field->get('outputFormat')); 
		$f->collapsed = Inputfield::collapsedBlank;
		$inputfields->add($f);
	
		// output string
		/** @var InputfieldText $f */
		$f = $this->modules->get('InputfieldText');
		$f->attr('name', 'outputString');
		$f->label = $this->_('Rendered string of text');
		$f->attr('placeholder', "i.e. <a href='{url}'>{description}</a>");
		$f->attr('value', $field->get('outputString') ? $field->get('outputString') : '');
		$f->description = $this->_('Provide the rendered string of text you want to output as the value of this field. If the field contains multiple items, this string will be rendered multiple times. If the field contains no items, a blank string will be used.');
		$f->notes = $this->_('You may use any of the following tags:') . ' {url}, {description}, {tags}';
		$f->showIf = "outputFormat=" . self::outputFormatString;
		$inputfields->add($f);

		// default value page
		/** @var InputfieldPageListSelect $f */
		$f = $this->modules->get('InputfieldPageListSelect'); 
		$f->attr('name', 'defaultValuePage'); 
		$f->label = $this->_('Default value (when empty)'); 
		$f->description = $this->_('Optionally select a page that will contain the default value (in this same field). You may wish to create a page specifically for this purpose.'); 
		$f->attr('value', (int) $field->get('defaultValuePage')); 
		$f->collapsed = $field->get('defaultValuePage') ? Inputfield::collapsedNo : Inputfield::collapsedYes; 
		$inputfields->add($f); 
	
		// textformatters
		/** @var InputfieldAsmSelect $f */
		$f = $this->modules->get('InputfieldAsmSelect');
		$f->setAttribute('name', 'textformatters');
		$f->label = $this->_('Text formatters (for file descriptions)');
		$f->description = $this->_('Select one or more text formatters (and their order) that will be applied to the file description when output formatting is active. The HTML Entity Encoder is recommended as a minimum.'); 

		foreach($this->wire('modules') as $module) {
			$className = $module->className();
			if(strpos($className, 'Textformatter') !== 0) continue; 
			$info = $this->wire('modules')->getModuleInfo($module); 
			$f->addOption($className, "$info[title]");
		}
		
		if(!is_array($field->get('textformatters'))) {
			$field->set('textformatters', $field->get('entityEncode') ? array('TextformatterEntities') : array());
		}
		$f->attr('value', $field->get('textformatters')); 
		$inputfields->add($f); 

		// entity encode (deprecated)
		/** @var InputfieldHidden $f */
		$f = $this->modules->get("InputfieldHidden");
		$f->attr('name', 'entityEncode');
		$f->attr('value', '');
		if($field->get('entityEncode')) $f->attr('checked', 'checked');
		$inputfields->append($f);
		$field->set('entityEncode', null);

		// use tags
		/** @var InputfieldRadios $f */
		$f = $this->modules->get("InputfieldRadios"); 
		$f->attr('name', 'useTags');
		$f->label = $this->_('Use Tags?');
		$f->description = $this->_('When enabled, the field will also contain an option for tags in addition to the description.'); // Use tags description
		$f->icon = 'tags';
		$predefinedLabel = $this->_('User selects from list of predefined tags');
		$f->addOption(self::useTagsOff, $this->_('Tags disabled'));
		$f->addOption(self::useTagsNormal, $this->_('User enters tags by text input'));
		$f->addOption(self::useTagsPredefined, $predefinedLabel);
		$f->addOption(self::useTagsNormal | self::useTagsPredefined, $predefinedLabel . ' + ' . $this->_('can input their own')); 
		$f->attr('value', (int) $field->get('useTags')); 
		if(!$f->attr('value')) $f->collapsed = Inputfield::collapsedYes;
		$inputfields->append($f); 
	
		/** @var InputfieldTextarea $f */
		$f = $this->modules->get('InputfieldText'); 
		$f->attr('name', 'tagsList'); 
		$f->label = $this->_('Predefined tags');
		$f->description = $this->_('Enter tags separated by a space. Tags may contain letters, digits, underscores or hyphens.');
		$f->icon = 'tags';
		$f->attr('value', $field->get('tagsList'));
		$f->showIf = 'useTags>1';
		$inputfields->append($f); 

		// inputfield class
		/** @var InputfieldSelect $f */
		$f = $this->modules->get('InputfieldSelect');
		$f->attr('name', 'inputfieldClass');
		$f->label = $this->_('Inputfield Type');
		$f->description = $this->_('The type of field that will be used to collect input.');
		$f->notes = $this->_('Change this only if instructed to do so by 3rd party Inputfield module instructions.');
		$f->required = true;
		$baseClass = $this->defaultInputfieldClass;
		
		foreach($this->wire('modules')->findByPrefix('Inputfield') as $fm) {
			if($baseClass == 'InputfieldFile' && strpos($fm, 'InputfieldImage') === 0) continue;
			if("$fm" == $baseClass || is_subclass_of(__NAMESPACE__ . "\\$fm", __NAMESPACE__ . "\\$baseClass")) {
				$f->addOption("$fm", str_replace("Inputfield", '', "$fm"));
			}
		}
		
		$inputfieldClass = $field->get('inputfieldClass');
		$f->attr('value', $inputfieldClass ? $inputfieldClass : $this->defaultInputfieldClass);
		$f->collapsed = $inputfieldClass && $inputfieldClass != $this->defaultInputfieldClass ? Inputfield::collapsedNo : Inputfield::collapsedYes; 
		
		$inputfields->append($f);
		
		return $inputfields;
	}



}

