<?php namespace ProcessWire;

/**
 * An Inputfield for handling file uploads
 *
 * @property string $extensions Allowed file extensions, space separated
 * @property int $maxFiles Maximum number of files allowed
 * @property int $maxFilesize Maximum file size
 * @property bool $useTags Whether or not tags are enabled
 * @property string $tagsList Predefined tags
 * @property bool|int $unzip Whether or not unzip is enabled
 * @property bool|int $overwrite Whether or not overwrite mode is enabled
 * @property int $descriptionRows Number of rows for description field (default=1, 0=disable)
 * @property string $destinationPath Destination path for uploaded file
 * @property string $itemClass Class name(s) for each file item (default=InputfieldFileItem ui-widget ui-widget-content)
 * @property bool|int $noUpload Set to true or 1 to disable uploading to this field
 * @property bool|int $noLang Set to true or 1 to disable multi-language descriptions
 * @property bool|int $noAjax Set to true or 1 to disable ajax uploading
 * @property int $uploadOnlyMode Set to true or 1 to disable existing file list display, or 2 to also prevent file from having 'temp' status.
 * @property bool|int $noCollapseItem Set to true to disable collapsed items (like for LanguageTranslator tool or other things that add tools to files)
 * @property bool|int $noShortName Set to true to disable shortened filenames in output
 * @property bool|int $noCustomButton Set to true to disable use of the styled <input type='file'>
 *
 * @method string renderItem($pagefile, $id, $n)
 * @method string renderList($value)
 * @method string renderUpload($value)
 * @method void fileAdded(Pagefile $pagefile)
 * @method array extractMetadata(Pagefile $pagefile, array $metadata = array())
 * @method void processInputAddFile($filename)
 * @method void processInputDeleteFile(Pagefile $pagefile)
 * @method bool processInputFile(WireInputData $input, Pagefile $pagefile, $n)
 *
 */
class InputfieldFile extends Inputfield implements InputfieldItemList, InputfieldHasSortableValue {

	public static function getModuleInfo() {
		return array(
			'title' => __('Files', __FILE__), // Module Title
			'summary' => __('One or more file uploads (sortable)', __FILE__), // Module Summary
			'version' => 125,
			'permanent' => true, 
			);
	}
	
	/**
	 * Cache of responses we'll be sending on ajax requests
	 *
	 */
	protected $ajaxResponses = array();

	/**
	 * Was a file replaced? 
	 *
	 */
	protected $singleFileReplacement = false;

	/**
	 * Saved instanceof WireUpload in case API retrieval is needed (see getWireUpload() method)
	 *
	 */
	protected $wireUpload = null;

	/**
	 * Set to the current Pagefile item when doing iteration
	 * 
	 * @var Pagefile|null
	 * 
	 */
	protected $currentItem = null;

	/**
	 * True when field should behave in an upload only mode
	 * 
	 * @var bool|int
	 * 
	 */
	protected $uploadOnlyMode = 0;

	/**
	 * This is true when we are only rendering the value rather than the inputs
	 * 
	 * @var bool
	 * 
	 */
	protected $renderValueMode = false;

	/**
	 * True when in ajax mode
	 * 
	 * @var bool
	 * 
	 */
	protected $isAjax = false;

	/**
	 * Admin theme specific settings
	 * 
	 * @var array
	 * 
	 */
	protected $themeSettings = array();

	/**
	 * Commonly used text labels, translated, indexed by label name
	 * 
	 * @var array
	 * 
	 */
	protected $labels = array();

	/**
	 * Initialize the InputfieldFile
	 *
	 */
	public function init() {
		parent::init();

		// note: these two fields originate from FieldtypeFile. 
		// Initializing them here ensures this Inputfield has the values set automatically.
		$this->set('extensions', '');
		$this->set('maxFiles', 0); 
		$this->set('maxFilesize', 0); 
		$this->set('useTags', 0);
		$this->set('tagsList', ''); 

		// native to this Inputfield
		$this->set('unzip', 0); 
		$this->set('overwrite', 0); 
		$this->set('descriptionRows', 1); 
		$this->set('destinationPath', ''); 
		$this->set('itemClass', 'InputfieldFileItem ui-widget ui-widget-content'); 
		$this->set('noUpload', 0); // set to 1 to disable uploading to this field
		$this->set('noLang', 0); 
		$this->set('noAjax', 0); // disable ajax uploading
		$this->set('noCollapseItem', 0);
		$this->set('noShortName', 0);
		$this->set('noCustomButton', false);
		$this->attr('type', 'file'); 
		
		$this->labels = array(
			'description' => $this->_('Description'), 
			'tags' => $this->_('Tags'),
			'drag-drop' => $this->_('drag and drop files in here'), 
			'delete' => $this->_('Delete'),
			'choose-file' => $this->_('Choose File'),
			'choose-files' => $this->_('Choose Files'),
		);
		
		$this->isAjax = $this->wire('input')->get('InputfieldFileAjax') 
			|| $this->wire('input')->get('reloadInputfieldAjax')
			|| $this->wire('input')->get('renderInputfieldAjax');

		$this->setMaxFilesize(trim(ini_get('post_max_size'))); 
		$this->uploadOnlyMode = (int) $this->wire('input')->get('uploadOnlyMode');
		$this->addClass('InputfieldItemList', 'wrapClass');
		$this->addClass('InputfieldHasFileList', 'wrapClass');

		$themeDefaults = array(
			'error' => "<span class='ui-state-error-text'>{out}</span>",
		);
		$themeSettings = $this->wire('config')->InputfieldFile;
		$this->themeSettings = is_array($themeSettings) ? array_merge($themeDefaults, $themeSettings) : $themeDefaults;
	}
	
	public function get($key) {
		if($key === 'renderValueMode') return $this->renderValueMode;
		if($key === 'singleFileReplacement') return $this->singleFileReplacement;
		if($key === 'descriptionFieldLabel') return $this->labels['description'];
		if($key === 'tagsFieldLabel') return $this->labels['tags'];
		if($key === 'deleteLabel') return $this->labels['delete'];
		if($key === 'themeSettings') return $this->themeSettings;
		return parent::get($key);
	}
	
	public function set($key, $value) {
		if($key == 'maxFilesize') return $this->setMaxFilesize($value);
		return parent::set($key, $value); 
	}

	/**
	 * Set the max file size in bytes or use string like "30m", "2g" "500k"
	 * 
	 * @param int|string $filesize
	 * @return $this
	 * 
	 */
	public function setMaxFilesize($filesize) {
		$max = $this->strToBytes($filesize);
		$phpMax = $this->strToBytes(ini_get('upload_max_filesize'));
		if($phpMax < $max) $max = $phpMax;	
		$this->maxFilesize = $max; 
		return $this;
	}

	/**
	 * Convert string like "32M" to bytes (integer)
	 * 
	 * @param string|int $filesize
	 * @return int
	 * 
	 */
	protected function strToBytes($filesize) {
		if(ctype_digit("$filesize")) {
			$bytes = (int) $filesize;
		} else {
			$filesize = rtrim($filesize, 'bB'); // convert mb=>m, gb=>g, kb=>k
			$last = strtolower(substr($filesize, -1));
			if(ctype_alpha($last)) $filesize = rtrim($filesize, $last);
			$filesize = (int) $filesize;
			if($last == 'g') {
				$bytes = (($filesize * 1024) * 1024) * 1024;
			} else if($last == 'm') {
				$bytes = ($filesize * 1024) * 1024;
			} else if($last == 'k') {
				$bytes = $filesize * 1024;
			} else if($filesize > 0) {
				$bytes = $filesize;
			} else {
				$bytes = (5 * 1024) * 1024;
			}
		}
		return $bytes; 
	}

	/**
	 * Per Inputfield interface, returns true when this field is empty
	 *
	 */
	public function isEmpty() {
		return !wireCount($this->value);
	}

	/**
	 * Set an attribute
	 * 
	 * @param array|string $key
	 * @param array|int|string $value
	 * @return Inputfield|InputfieldFile
	 * 
	 */
	public function setAttribute($key, $value) {
		if($key == 'value') {
			if($value instanceof Pagefile) {
				// if given a Pagefile rather than a Pagefiles, use the Pagefiles instead
				$value = $value->pagefiles; 
			}
		}
		return parent::setAttribute($key, $value);
	}
	
	/**
	 * Check to ensure that the containing form as an 'enctype' attr needed for uploading files
	 *
	 */
	protected function checkFormEnctype() {
		$parent = $this->parent;
		while($parent) {
			if($parent->attr('method') == 'post') {
				if(!$parent->attr('enctype')) $parent->attr('enctype', 'multipart/form-data');
				break;
			}
			$parent = $parent->parent; 
		}
	}

	/**
	 * Set the parent of this Inputfield
	 *
	 * @param InputfieldWrapper $parent
	 * @return $this
	 *
	 */
	public function setParent(InputfieldWrapper $parent) {
		parent::setParent($parent); 
		$this->checkFormEnctype();
		return $this;
	}

	/**
	 * Get the unique 'id' attribute for the given Pagefile
	 * 
	 * @param Pagefile $pagefile
	 * @return string
	 * 
	 */
	protected function pagefileId(Pagefile $pagefile) {
		return $this->name . "_" . $pagefile->hash; 
	}

	/**
	 * Render a description input for the given Pagefile
	 * 
	 * @param Pagefile $pagefile
	 * @param string $id 
	 * @param int $n
	 * @return string
	 * 
	 */
	protected function renderItemDescriptionField(Pagefile $pagefile, $id, $n) {
	
		if($n) {}
		$out = '';
		$tabs = '';
		static $hasLangTabs = null;
		static $langTabSettings = array();

		if($this->renderValueMode) {
			if($this->wire('languages')) {
				$description = $pagefile->description($this->wire('user')->language);
			} else {
				$description = $pagefile->description;
			}
			if(strlen($description)) $description = 
				"<div class='InputfieldFileDescription detail'>" . $this->wire('sanitizer')->entities1($description) . "</div>";
			return $description;
		}
		
		if($this->descriptionRows > 0) {

			$userLanguage = $this->wire('user')->language;
			$languages = $this->noLang ? null : $this->wire('languages');
			$defaultDescriptionFieldLabel = $this->wire('sanitizer')->entities1($this->labels['description']);

			if(!$userLanguage || !$languages || $languages->count() < 2) {
				$numLanguages = 0;
				$languages = array(null);
			} else {
				$numLanguages = $languages->count();
				if(is_null($hasLangTabs)) {
					$hasLangTabs = $this->wire('modules')->isInstalled('LanguageTabs');
					if($hasLangTabs) {
						/** @var LanguageTabs $languageTabs */
						$languageTabs = $this->wire('modules')->getModule('LanguageTabs');
						$langTabSettings = $languageTabs->getSettings();
					}
				}
			}

			foreach($languages as $language) {

				$descriptionFieldName = "description_$id";
				$descriptionFieldLabel = $defaultDescriptionFieldLabel;
				$labelClass = "detail";
				$attrStr = '';

				if($language) {
					$tabField = empty($langTabSettings['tabField']) ? 'title' : $langTabSettings['tabField'];
					$descriptionFieldLabel = (string) $language->getUnformatted($tabField);
					if(empty($descriptionFieldLabel)) $descriptionFieldLabel = $language->get('name');
					$descriptionFieldLabel = $this->wire('sanitizer')->entities($descriptionFieldLabel);
					if(!$language->isDefault()) $descriptionFieldName = "description{$language->id}_$id";
					$labelClass .= ' LanguageSupportLabel';
					if(!$languages->editable($language)) {
						$labelClass .= ' LanguageNotEditable';
						$descriptionFieldLabel = "<s>$descriptionFieldLabel</s>";
					}
					$tabID = "langTab_{$id}__$language";
					$aClass = "langTab$language";
					if(!empty($langTabSettings['aClass'])) $aClass .= " " . $langTabSettings['aClass'];
					$tabs .= "<li><a data-lang='$language' class='$aClass' href='#$tabID'>$descriptionFieldLabel</a></li>";
					$out .= "<div class='InputfieldFileDescription LanguageSupport' data-language='$language' id='$tabID'>"; // open wrapper
				} else {
					$out .= "<div class='InputfieldFileDescription'>"; // open wrapper
					$attrStr = "placeholder='$descriptionFieldLabel&hellip;'";
					$labelClass = 'detail pw-hidden';
				}
				
				$attrStr = "name='$descriptionFieldName' id='$descriptionFieldName' $attrStr";
				
				$out .= "<label for='$descriptionFieldName' class='$labelClass'>$descriptionFieldLabel</label>";

				$description = $this->wire('sanitizer')->entities($pagefile->description($language));

				if($this->descriptionRows > 1) {
					$out .= "<textarea $attrStr rows='$this->descriptionRows'>$description</textarea>";
				} else {
					$out .= "<input type='text' $attrStr value='$description' />";
				}

				$out .= "</div>"; // close wrapper
			}
			
			if($numLanguages && $hasLangTabs) {
				$ulClass = empty($langTabSettings['ulClass']) ? '' : " class='$langTabSettings[ulClass]'";
				$ulAttr = empty($langTabSettings['ulAttrs']) ? '' : " $langTabSettings[ulAttrs]";
				$out = "<div class='hasLangTabs langTabsContainer'><div class='langTabs'><ul $ulAttr$ulClass>$tabs</ul>$out</div></div>";
				if($this->isAjax) $out .= "<script>setupLanguageTabs($('#wrap_" . $this->attr('id') . "'));</script>";
			}

		}

		if($this->useTags) $out .= $this->renderItemTagsField($pagefile, $id, $n); 

		return $out;
	}

	/**
	 * Render the tags input for the given Pagefile
	 * 
	 * @param Pagefile $pagefile
	 * @param string $id
	 * @param int $n
	 * @return string
	 * 
	 */
	protected function renderItemTagsField(Pagefile $pagefile, $id, $n) {

		if($n) {}
		$tagsLabel = $this->wire('sanitizer')->entities($this->labels['tags']) . '&hellip;';
		$tagsStr = $this->wire('sanitizer')->entities($pagefile->tags);
		$tagsAttr = '';

		if($this->useTags >= FieldtypeFile::useTagsPredefined) {
			// select predefined
			$tagsClass = 'InputfieldFileTagsSelect';
			$tagsAttr = "data-cfgname='InputfieldFileTags_{$this->hasField->name}' ";

		} else {
			// text input
			$tagsClass = 'InputfieldFileTagsInput';
		}

		$out = 
			"<div class='InputfieldFileTags'>" . 
				"<label for='tags_$id' class='detail pw-hidden'>$tagsLabel</label>" . 
				"<input type='text' name='tags_$id' id='tags_$id' value='$tagsStr' " . 
					"placeholder='$tagsLabel' class='$tagsClass' $tagsAttr/>" . 
			"</div>";
		
		return $out;
	}

	/**
	 * Get a basename for the file, possibly shortened, suitable for display in InputfieldFileList
	 * 
	 * @param Pagefile $pagefile
	 * @param int $maxLength
	 * @return string
	 * 
	 */
	public function getDisplayBasename(Pagefile $pagefile, $maxLength = 25) {
		$displayName = $pagefile->basename;
		if($this->noShortName) return $displayName;
		if(strlen($displayName) > $maxLength) {
			$ext = ".$pagefile->ext";
			$maxLength -= (strlen($ext) + 1);
			$displayName = basename($displayName, $ext);
			$displayName = substr($displayName, 0, $maxLength);
			$displayName .= "&hellip;" . ltrim($ext, '.');
		}
		return $displayName; 	
	}

	/**
	 * Render markup for a file item
	 * 
	 * @param Pagefile $pagefile
	 * @param string $id
	 * @param int $n
	 * @return string
	 * 
	 */
	protected function ___renderItem($pagefile, $id, $n) {
	
		$displayName = $this->getDisplayBasename($pagefile);
		$deleteLabel = $this->labels['delete'];
	
		$out = 
			"<p class='InputfieldFileInfo InputfieldItemHeader ui-state-default ui-widget-header'>" . 
			wireIconMarkupFile($pagefile->basename, "fa-fw HideIfEmpty") . '&nbsp;' . 
			"<a class='InputfieldFileName' title='$pagefile->basename' target='_blank' href='{$pagefile->url}'>$displayName</a> " . 
			"<span class='InputfieldFileStats'>" . str_replace(' ', '&nbsp;', $pagefile->filesizeStr) . "</span> ";
		
		if(!$this->renderValueMode) $out .=
			"<label class='InputfieldFileDelete'>" . 
				"<input type='checkbox' name='delete_$id' value='1' title='$deleteLabel' />" . 
				"<i class='fa fa-fw fa-trash'></i></label>";
		
		$out .= 
			"</p>" . 
			"<div class='InputfieldFileData description ui-widget-content'>" . 
			$this->renderItemDescriptionField($pagefile, $id, $n);
		
		if(!$this->renderValueMode) $out .= 
			"<input class='InputfieldFileSort' type='text' name='sort_$id' value='$n' />";
		
		$out .= 
			"</div>";

		return $out; 
	}

	protected function renderItemWrap($out) {
		// note: using currentItem rather than a new argument since there are now a few modules extending
		// this one and if they implement their own calls to this method or version of this method then 
		// they will get strict notices from php if we add a new argument here. 
		$item = $this->currentItem; 
		$id = $item && !$this->renderValueMode ? " id='file_$item->hash'" : "";
		return "<li$id class='{$this->itemClass}'>$out</li>";
	}
	
	protected function renderListReady($value) {
		if(!$this->renderValueMode) {
			// if just rendering the files list (as opposed to saving it), delete any temp files that may have accumulated
			if(!$this->overwrite && !count($_POST) && !$this->isAjax && !$this->uploadOnlyMode) {
				// don't delete files when in render single field or fields mode
				if(!$this->wire('input')->get('field') && !$this->wire('input')->get('fields')) {
					if($value instanceof Pagefiles) $value->deleteAllTemp();
				}
			}
		}
	}

	protected function ___renderList($value) {

		if(!$value) return '';
		$out = '';
		$n = 0; 
	
		$this->renderListReady($value);

		if(!$this->uploadOnlyMode && WireArray::iterable($value)) {
			foreach($value as $k => $pagefile) {
				$id = $this->pagefileId($pagefile);
				$this->currentItem = $pagefile;
				$out .= $this->renderItemWrap($this->renderItem($pagefile, $id, $n++));
			}
		}

		$class = 'InputfieldFileList ui-helper-clearfix';
		if($this->overwrite && !$this->renderValueMode) $class .= " InputfieldFileOverwrite";
		if($out) $out = "<ul class='$class'>$out</ul>";
		
		return $out; 
	}

	protected function ___renderUpload($value) {
		if($value) {}
		if($this->noUpload || $this->renderValueMode) return '';

		// enables user to choose more than one file
		if($this->maxFiles != 1) $this->setAttribute('multiple', 'multiple'); 

		$attrs = $this->getAttributes();
		unset($attrs['value']); 
		if(substr($attrs['name'], -1) != ']') $attrs['name'] .= '[]';

		$extensions = $this->extensions;
		if($this->unzip && !$this->maxFiles) $extensions .= ' zip';
		$formatExtensions = $this->formatExtensions($extensions);
		$chooseLabel = $this->labels['choose-file'];
		$dragDropLabel = $this->labels['drag-drop'];
		$attrStr = $this->getAttributesString($attrs);

		$out =
			"<div " .
				"data-maxfilesize='$this->maxFilesize' " .
				"data-extensions='$extensions' " .
				"data-fieldname='$attrs[name]' " .
				"class='InputfieldFileUpload'>
				";
	
		if($this->getSetting('noCustomButton')) {
			$out .= "<input $attrStr>";
			
		} else {
			$out .= "
					<div class='InputMask ui-button ui-state-default'>
						<span class='ui-button-text'>
							<i class='fa fa-fw fa-folder-open-o'></i>$chooseLabel
						</span>
						<input $attrStr>
					</div>
					";
		}
		
		$out .= "  		
				<span class='InputfieldFileValidExtensions detail'>$formatExtensions</span>
				<input type='hidden' class='InputfieldFileMaxFiles' value='$this->maxFiles' />
			";
		
		if(!$this->noAjax) $out .= "
				<span class='AjaxUploadDropHere description'>
					<span>
						<i class='fa fa-cloud-upload'></i>&nbsp;$dragDropLabel
					</span>
				</span>
			";	
		
		$out .= "</div>"; // .InputfieldFileUpload

		return $out; 
	}

	public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
		$this->addClass('InputfieldNoFocus', 'wrapClass');
		
		if($this->useTags) {
			$this->wire('modules')->get('JqueryUI')->use('selectize');
			$this->addClass('InputfieldFileHasTags', 'wrapClass');
			if($this->useTags >= FieldtypeFile::useTagsPredefined && $this->hasField) {
				// predefined tags
				$config = $this->wire('config');
				$fieldName = $this->hasField->name;
				$jsName = "InputfieldFileTags_$fieldName";
				$allowUserTags = $this->useTags & FieldtypeFile::useTagsNormal;
				$data = $config->js($jsName);
				if(!is_array($data)) $data = array();
				if(empty($data['tags'])) {
					$tags = array();
					foreach(explode(' ', (string) $this->get('tagsList')) as $tag) {
						$tag = trim($tag);
						if(!strlen($tag)) continue;
						$tags[strtolower($tag)] = $tag;
					}
					if($allowUserTags) {
						$pagefiles = $this->val();
						if($pagefiles instanceof Pagefiles) {
							$_tags = $pagefiles->tags(true);
							if(count($_tags)) $tags = array_merge($tags, $_tags);
						}
					}
					$data['tags'] = array_values($tags);
					$data['allowUserTags'] = $allowUserTags;
					$config->js($jsName, $data); 
				}
				$this->wrapAttr('data-configName', $jsName);
			} else {
				// regular tags text input
			}
		}
		
		return parent::renderReady($parent, $renderValueMode); 
	}

	public function ___render() {
		if(!$this->extensions) $this->error($this->_('No file extensions are defined for this field.')); 
		$numItems = wireCount($this->value);
		if($this->allowCollapsedItems()) $this->addClass('InputfieldItemListCollapse', 'wrapClass');
		if($numItems == 0) {
			$this->addClass('InputfieldFileEmpty', 'wrapClass');
		} else if($numItems == 1) {
			$this->addClass('InputfieldFileSingle', 'wrapClass');
		} else {
			$this->addClass('InputfieldFileMultiple', 'wrapClass');
		}
		return $this->renderList($this->value) . $this->renderUpload($this->value);
	}
	
	public function ___renderValue() {
		$this->renderValueMode = true; 
		$out = $this->render();
		$this->renderValueMode = false;
		return $out; 
	}

	protected function ___fileAdded(Pagefile $pagefile) {
		if($this->noUpload) return;
		
		$isValid = $this->wire('sanitizer')->validateFile($pagefile->filename(), array(
			'pagefile' => $pagefile
		));
		
		if($isValid === false) {
			$errors = $this->wire('sanitizer')->errors('clear array');
			throw new WireException(
				$this->_('File failed validation') . 
				(count($errors) ? ": " . implode(', ', $errors) : "")
			);
		} else if($isValid === null) {
			// there was no validator available for this file type
		}

		$message = $this->_('Added file:') . " {$pagefile->basename}"; // Label that precedes an added filename

		if($this->isAjax && !$this->noAjax) {
			$n = count($this->value); 
			if($n) $n--; // for sorting
			$this->currentItem = $pagefile; 
			$markup = $this->fileAddedGetMarkup($pagefile, $n);
			$this->ajaxResponse(false, $message, $pagefile->url, $pagefile->filesize(), $markup); 
		} else {
			$this->message($message); 
		}
	}
	
	protected function fileAddedGetMarkup(Pagefile $pagefile, $n) {
		return $this->renderItemWrap($this->renderItem($pagefile, $this->pagefileId($pagefile), $n));	
	}

	/**
	 * Given a Pagefile return array of meta data pulled from it
	 * 
	 * @param Pagefile $pagefile
	 * @param array $metadata Existing metadata, if applicable
	 * @return array Associative array of meta data (i.e. description and tags)
	 * 
	 */
	protected function ___extractMetadata(Pagefile $pagefile, array $metadata = array()) {
		
		$metadata['description'] = $pagefile->description;
		
		/** @var Languages $languages */
		$languages = $this->wire('languages');
		if($languages && !$this->noLang) {
			foreach($languages as $language) {
				if($language->isDefault()) continue;
				$metadata["description$language->id"] = $pagefile->description($language);
			}
		}
		
		$metadata['tags'] = $pagefile->tags;
		$filedata = $pagefile->filedata();
		if(count($filedata)) $metadata['filedata'] = $filedata;
		
		return $metadata;
	}

	/**
	 * @param string $filename
	 * @throws WireException
	 * 
	 */
	protected function ___processInputAddFile($filename) {

		$total = count($this->value); 
		$metadata = array();
		$rm = null;

		if($this->maxFiles > 1 && $total >= $this->maxFiles) return; 
		
		// allow replacement of file if maxFiles is 1
		if($this->maxFiles == 1 && $total) {
			$pagefile = $this->value->first();
			$metadata = $this->extractMetadata($pagefile, $metadata);
			$rm = true; 
			if($filename == $pagefile->basename) {
				// use overwrite mode rather than replace mode when single file and same filename
				if($this->overwrite) $rm = false;
			}
			if($rm) {
				if($this->overwrite) $this->processInputDeleteFile($pagefile);
				$this->singleFileReplacement = true; 
			}
		} 
	
		if($this->overwrite) {
			$pagefile = $this->value->get($filename); 
			clearstatcache();
			if($pagefile) {
				// already have a file of the same name
				if($pagefile instanceof Pageimage) $pagefile->removeVariations(); 
				$metadata = $this->extractMetadata($pagefile, $metadata);
			} else {
				// we don't have a file with the same name as the one that was uploaded
				// file must be in another files field on the same page, that could be problematic
				$ul = $this->getWireUpload();
				// see if any files were overwritten that weren't part of our field
				// if so, we need to restore them and issue an error
				$err = false;
				foreach($ul->getOverwrittenFiles() as $bakFile => $newFile) {
					if(basename($newFile) != $filename) continue; 
					unlink($newFile); 	
					rename($bakFile, $newFile); // restore
					$ul->error(sprintf($this->_('Refused file %s because it is already on the file system and owned by a different field.'), $filename)); 
					$err = true; 
				}
				if($err) return;
			}
		}

		$this->value->add($filename); 
		/** @var Pagefile $item */
		$item = $this->value->last();
		
		try {
			foreach($metadata as $key => $val) {
				if($val) $item->$key = $val;
			}
			// items saved in ajax or uploadOnly mode are temporary till saved in non-ajax/non-uploadOnly
			if($this->isAjax && !$this->overwrite) {
				if($this->wire('input')->get('InputfieldFileAjax') !== 'noTemp') {
					$item->isTemp(true);
				}
			}
			$this->fileAdded($item); 
		} catch(\Exception $e) {
			$item->unlink();
			$this->value->remove($item); 
			throw new WireException($e->getMessage()); 
		}
	}

	protected function ___processInputDeleteFile(Pagefile $pagefile) {
		$fileLabel = $this->wire('config')->debug ? $pagefile->url() : $pagefile->name;
		$this->message($this->_("Deleted file:") . " $fileLabel"); // Label that precedes a deleted filename
		$this->value->delete($pagefile); 
		$this->trackChange('value');
	}

	protected function ___processInputFile(WireInputData $input, Pagefile $pagefile, $n) {
		
		$id = $this->name . '_' . $pagefile->hash;
	
		if($this->uploadOnlyMode) {
			// skip files that aren't present as just uploaded
			$key = "sort_$id";
			if($input->$key === null) return false;
		}
		
		$changed = false; 
	
		// replace (currently only used by InputfieldImage)
		$key = "replace_$id";
		$replace = $input->$key;
		if($replace) {
			if(strpos($replace, '?') !== false) {
				list($replace, $unused) = explode('?', $replace);
				if($unused) {}
			}
			$replaceFile = $this->value->getFile($replace);
			if($replaceFile && $replaceFile instanceof Pagefile) {
				$this->processInputDeleteFile($replaceFile);
				if(strtolower($pagefile->ext()) == strtolower($replaceFile->ext())) {
					$this->value->rename($pagefile, $replaceFile->name);
				}
				$changed = true; 
			}
		}
	
		// rename (currently only used by InputfieldImage)
		$key = "rename_$id";
		$rename = $input->$key;
		if(strlen($rename) && $rename != $pagefile->basename(false)) {
			$name = $pagefile->basename();
			$rename .= "." . $pagefile->ext();
			// cleanBasename($basename, $originalize = false, $allowDots = true, $translate = false) 
			$rename = $pagefile->pagefiles->cleanBasename($rename, true, true, true);
			$message = sprintf($this->_('Renamed file "%1$s" to "%2$s"'), $name, $rename);
			if($pagefile->rename($rename) !== false) {
				$this->message($message);
				$changed = true; 
			} else {
				$this->warning($this->_('Failed') . " - $message"); 
			}
		}
	
		// description and tags
		$languages = $this->noLang ? null : $this->wire('languages');
		$keys = $languages ? array('tags') : array('description', 'tags'); 

		foreach($keys as $key) { 
			if(isset($input[$key . '_' . $id])) { 
				$value = $input[$key . '_' . $id]; 
				if(is_array($value)) $value = implode(' ', $value);
				$value = trim($value); 
				if($value != $pagefile->$key) {
					$pagefile->$key = $value; 
					$changed = true; 
				}
			}
		}

		// multi-language descriptions
		if($languages) foreach($languages as $language) {
			if(!$languages->editable($language)) continue; 
			$key = $language->isDefault() ? "description_$id" : "description{$language->id}_$id";
			if(!isset($input[$key])) continue; 
			$value = trim($input[$key]); 
			if($value != $pagefile->description($language)) {
				$pagefile->description($language, $value); 
				$changed = true; 
			}
		}
	
		if($this->uploadOnlyMode) {
			if($this->uploadOnlyMode === 2) {
				$sort = 0; // ensures an isTemp(false) call occurs below
			} else {
				$sort = null;
			}
			$changed = true;
		} else {
			$key = "sort_$id";
			$sort = $input->$key;
			if($sort !== null) {
				$sort = (int) $sort; 
				$pagefile->sort = $sort;
				if($n !== $sort) $changed = true;
			}
		}

		if(isset($input['delete_' . $id])) {
			$this->processInputDeleteFile($pagefile); 
			$changed = true; 
			
		} else if(!$this->isAjax && !$this->overwrite && $pagefile->isTemp() && $sort !== null) {
			// if page saved with temporary items when not ajax, those temporary items become non-temp
			$pagefile->isTemp(false);
			if($this->maxFiles == 1) while(count($this->value) > 1) {
				$item = $this->value->first();
				$this->value->remove($item);
			}
			$changed = true;
		}

		return $changed; 
	}

	public function ___processInput(WireInputData $input) {
		
		if(is_null($this->value)) $this->value = $this->wire(new Pagefiles($this->wire('page')));
		if(!$this->destinationPath) $this->destinationPath = $this->value->path();
		
		if(!$this->destinationPath || !is_dir($this->destinationPath)) {
			return $this->error($this->_("destinationPath is empty or does not exist"));
		}
		if(!is_writable($this->destinationPath)) {
			return $this->error($this->_("destinationPath is not writable"));
		}

		$changed = false; 
		$total = count($this->value); 

		if(!$this->noUpload) { 

			if($this->maxFiles <= 1 || $total < $this->maxFiles) { 

				$ul = $this->getWireUpload();
				$ul->setName($this->attr('name')); 
				$ul->setDestinationPath($this->destinationPath); 
				$ul->setOverwrite($this->overwrite); 
				$ul->setAllowAjax($this->noAjax ? false : true);
				if($this->maxFilesize) $ul->setMaxFileSize($this->maxFilesize); 

				if($this->maxFiles == 1) {
					$ul->setMaxFiles(1); 

				} else if($this->maxFiles) {
					$maxFiles = $this->maxFiles - $total; 
					$ul->setMaxFiles($maxFiles); 

				} else if($this->unzip) { 
					$ul->setExtractArchives(true); 
				}

				$ul->setValidExtensions(explode(' ', trim($this->extensions))); 

				foreach($ul->execute() as $filename) {
					$this->processInputAddFile($filename); 
					$changed = true; 
				}

				if($this->isAjax && !$this->noAjax) foreach($ul->getErrors() as $error) { 
					$this->ajaxResponse(true, $error); 
				}

			} else if($this->maxFiles) {
				// over the limit
				$this->ajaxResponse(true, $this->_("Max file upload limit reached")); 
			}
		}

		$n = 0; 

		foreach($this->value as $pagefile) {
			if($this->processInputFile($input, $pagefile, $n)) $changed = true; 
			$n++; 
		}

		if($changed) {
			$this->value->sort('sort'); 
			$this->trackChange('value'); 
		}

		if(count($this->ajaxResponses) && $this->isAjax) {
			echo $this->renderAjaxResponse();
		}

		return $this; 
	}

	/**
	 * Render JSON response to AJAX request
	 * 
	 * @return string
	 * 
	 */
	protected function renderAjaxResponse() {
		if($this->wire('input')->get('ckeupload')) {
			// https://docs.ckeditor.com/ckeditor4/docs/#!/guide/dev_file_upload
			$a = $this->ajaxResponses[0];
			$response = array(
				'uploaded' => $a['error'] ? 0 : 1,
				'fileName' => basename($a['file']),
				'url' => $a['file'],
				'ajaxResponse' => $a, // for InputfieldImage.js
			);
			if($a['error']) {
				$response['error'] = array(
					'message' => $a['message']
				);
			}
			return json_encode($response);
		} else {
			return json_encode($this->ajaxResponses);
		}
	}

	/**
	 * Send an ajax response
	 *
	 * @param bool $error Whether it was successful
	 * @param string $message Message you want to return
	 * @param string $file Full path and filename or blank if not applicable
	 * @param string $size 
	 * @param string $markup
	 *
	 */
	protected function ajaxResponse($error, $message, $file = '', $size = '', $markup = '') {
		$response = array(
			'error' => $error, 
			'message' => $message, 
			'file' => $file,
			'size' => $size,
			'markup' => $markup, 
			'replace' => $this->singleFileReplacement,
			'overwrite' => $this->overwrite
			);

		$this->ajaxResponses[] = $response; 
	}

	/**
	 * Return the current WireUpload instance or create a new one if not yet created
	 *
	 * @return WireUpload
	 *
	 */
	public function getWireUpload() {
		if(is_null($this->wireUpload)) $this->wireUpload = $this->wire(new WireUpload($this->attr('name'))); 
		return $this->wireUpload; 
	}

	/**
	 * Template method: allow items to be collapsed?
	 *
	 * @return bool
	 *
	 */
	protected function allowCollapsedItems() {
		return $this->descriptionRows == 0 && !$this->useTags && !$this->noCollapseItem;
	}

	/**
	 * Format list of file extensions for output with upload field
	 *
	 * @param string $extensions
	 * @return string
	 *
	 */
	protected function formatExtensions($extensions) {
		return $this->wire('sanitizer')->entities(str_replace(' ', ', ', trim($extensions)));
	}

	/**
	 * Configuration settings for InputfieldFile
	 * 
	 * @return InputfieldWrapper
	 * 
	 */
	public function ___getConfigInputfields() {
		$inputfields = parent::___getConfigInputfields();
	
		/** @var InputfieldCheckbox $f */
		$f = $this->modules->get("InputfieldCheckbox"); 
		$f->attr('name', 'unzip'); 
		$f->attr('value', 1); 
		$f->setAttribute('checked', $this->unzip ? 'checked' : ''); 
		$f->label = $this->_('Decompress ZIP files?');
		$f->description = $this->_("If checked, ZIP archives will be decompressed and all valid files added as uploads (if supported by the hosting environment). Max files must be set to 0 (no max) in order for ZIP uploads to be functional."); // Decompress ZIP files description
		$f->collapsed = Inputfield::collapsedBlank;
		$inputfields->append($f);
		
		$f = $this->modules->get("InputfieldCheckbox");
		$f->attr('name', 'overwrite');
		$f->label = $this->_('Overwrite existing files?');
		$f->description = $this->_('If checked, a file uploaded with the same name as an existing file will replace the existing file (description and tags will remain). If not checked, uploaded filenames will be renamed to be unique.'); // Overwrite description
		$f->notes = $this->_('Please note that when this option is enabled, AJAX-uploaded files are saved with the page immediately at upload, rather than when you click "save". As a result, you may wish to leave this option unchecked unless you have a specific need for it.'); // Overwrite notes
		if($this->overwrite) $f->attr('checked', 'checked');
		$f->collapsed = Inputfield::collapsedBlank;
		$inputfields->append($f);

		$f = $this->modules->get("InputfieldInteger"); 
		$f->attr('name', 'descriptionRows'); 
		$f->attr('value', $this->descriptionRows !== null ? (int) $this->descriptionRows : 1); 
		//$f->minValue = 0; 
		//$f->maxValue = 30; 
		$f->label = $this->_('Number of rows for description field?');
		$f->description = $this->_("Enter the number of rows available for the file description field, or enter 0 to not have a description field."); // Number of rows description
		$inputfields->append($f); 
		
		if($this->wire('languages') && $this->descriptionRows >= 1) {
			$f = $this->modules->get("InputfieldCheckbox"); 
			$f->attr('name', 'noLang'); 
			$f->attr('value', 1); 
			$f->setAttribute('checked', $this->noLang ? 'checked' : ''); 
			$f->label = $this->_('Disable multi-language descriptions?');
			$f->description = $this->_('By default, descriptions are multi-language when you have Language Support installed. If you want to disable multi-language descriptions, check this box.'); // Disable multi-language description
			$inputfields->append($f); 
		}
		
		return $inputfields; 	
	}



}
