<?php namespace ProcessWire;

/**
 * ProcessWire Page Auto Completion select widget
 *
 * This Inputfield connects the jQuery UI Autocomplete widget with the ProcessWire ProcessPageSearch AJAX API.
 *
 * ProcessWire 3.x, Copyright 2016 by Ryan Cramer
 * https://processwire.com
 * 
 * @property int $parent_id Limit results to this parent, or if combined with findPagesSelector, the search is performed as $pages->get($parent_id)->find() rather than $pages->find().
 * @property int $template_id Limit results to pages using this template.
 * @property array $template_ids Limit results to pages using this templates (alternate to the single template_id). 
 * @property string $labelFieldName Field to display in the results. (default=title)
 * @property string $labelFieldFormat Format string to display in the results, overrides labelFieldName when used (default=blank).
 * @property string $searchFields Field(s) to search for text. Separate multiple by a space. (default=title)
 * @property string $operator Selector operator to use in performing the search (default: %=)
 * @property string $findPagesSelector Optional selector to use for finding pages (default=blank)
 * @property int $maxSelectedItems Maximum number of items that may be selected (0=unlimited, default).
 * @property bool $useList Whether or not to use a separate selected list. If false specified, selected item will be populated directly to the input. (default=true)
 * @property bool $allowAnyValue Allow any value to stay in the input, even if not selectable? (default=false)
 * @property string $disableChars Autocomplete won't be triggered if input contains any of the characters in this string. (default=blank)
 * @property bool $useAndWords When true, each word will be isolated to a separate searchFields=searchValue, which enables it to duplicate ~= operator behavior while using a %= operator (default=false).
 * @property int $lang_id Force use of this language for results
 * 
 * @method string renderList()
 * @method string renderListItem($label, $value, $class = '')
 *
 */
class InputfieldPageAutocomplete extends Inputfield implements InputfieldHasArrayValue, InputfieldHasSortableValue {

	public static function getModuleInfo() {
		return array(
			'title' => __('Page Auto Complete', __FILE__), // Module Title
			'summary' => __('Multiple Page selection using auto completion and sorting capability. Intended for use as an input field for Page reference fields.', __FILE__), // Module Summary
			'version' => 112,
			);
	}

	/**
	 * Initialize variables used for the autocompletion
	 *
	 */
	public function init() {
		parent::init();

		// limit results to this parent, or if combined with a 'findPagesSelector', 
		// the search is performed as $pages->get($parent_id)->find() rather than $pages->find()
		$this->set('parent_id', 0); 

		// limit results to pages using this template
		$this->set('template_id', 0);
		$this->set('template_ids', array()); 

		// field to display in the results
		$this->set('labelFieldName', 'title'); 
		
		// format string to display in the results (instead of labelFieldName, when used)
		$this->set('labelFieldFormat', '');

		// field(s) to search for text, separate multiple by a space
		$this->set('searchFields', 'title'); 

		// operator to use in performing the search
		$this->set('operator', '%='); 

		// optional selector to use for all other properties
		$this->set('findPagesSelector', ''); 
	
		// maximum number of items that may be selected
		$this->set('maxSelectedItems', 0); 
	
		// whether or not to use the selection list
		// if not used, selected value will be populated directly to input
		$this->set('useList', true); 
	
		// allow a non-selectable value to stay in the autocomplete input? 
		// useful for situations where autocomplete might be used for collection of other values
		// like when used with ProcessPageEditLink
		$this->set('allowAnyValue', false);
	
		// autocomplete won't be triggered if input contains any characters present in this string
		$this->set('disableChars', ''); 
	
		// when true, each word will be isolated to a separate searchFields=searchValue, which 
		// enables it to duplicate ~= operator behavior while using a %= operator. 
		$this->set('useAndWords', false); 
	
		// Force Language when applicable
		$this->set('lang_id', 0);
	
		// Whether to allow unpublished pages (null=not set, 0|false=no, 1|true=yes)
		$this->set('allowUnpub', null);
	}

	/**
	 * Render a selected list item 
	 * 
	 * @param string $label
	 * @param string $value
	 * @param string $class
	 * @return string
	 *
	 */
	protected function ___renderListItem($label, $value, $class = '') {
		if($class) $class = " $class";
		if(strpos($label, '&') !== false) $label = $this->wire('sanitizer')->unentities($label);
		$label = $this->wire('sanitizer')->entities($label);
		$out = 	
			"<li class='ui-state-default$class'>" . 
			"<i class='fa fa-sort fa-fw'></i> " . 
			"<span class='itemValue'>$value</span>" . 
			"<span class='itemLabel'>$label</span> " . 
			"<a class='itemRemove' href='#'><i class='fa fa-trash'></i></a>" . 
			"</li>";
		return $out; 
	}

	/**
	 * Render the selected items list
	 * 
	 * @return string
	 *
	 */
	protected function ___renderList() { 

		$out = "<ol id='{$this->id}_items' data-id='{$this->id}' data-name='{$this->name}'>" . 
			$this->renderListItem("Label", "1", "itemTemplate"); 
		
		foreach($this->value as $page_id) {
			if(!$page_id) continue; 
			$page = $this->pages->get((int) $page_id); 
			if(!$page || !$page->id) continue; 
			$value = $this->labelFieldFormat ? $page->getText($this->labelFieldFormat, true, false) : $page->get($this->labelFieldName); 
			$value = strip_tags($value);
			if(!strlen($value)) $value = $page->get('title|name');
			$out .= $this->renderListItem($value, $page->id); 
		}

		$out .= "</ol>";
		return $out; 
	}

	/**
	 * Render the autocompletion widget
	 *
	 */
	public function ___render() {

		if($this->maxSelectedItems == 1) $this->useList = false;
		$out = $this->useList ? $this->renderList() : '';
		$value = implode(',', $this->value); 
		$url = $this->getAjaxUrl();

		// convert our list of search fields to a CSV string for use in the ProcessPageSearch query
		$searchField = '';
		$searchFields = str_replace(array(',', '|'), ' ', $this->searchFields);
		foreach(explode(' ', $searchFields) as $key => $name) {
			$name = trim($name); 
			// @esrch pr#994 --
			if(strpos($name, '.')) {
				list($name, $subname) = explode('.', $name);
				$name = $this->wire('sanitizer')->fieldName($name);
				$subname = $this->wire('sanitizer')->fieldName($subname);
				if($name && $subname) $name .= "-$subname";
				// -- pr#994
			} else {
				$name = $this->wire('sanitizer')->fieldName($name);
			}
			if($name) $searchField .= ($searchField ? ',' : '') . $name; 
		}

		if(!$searchField) $searchField = 'title';
		$addNote = $this->_('Hit enter to add as new item'); 
		
		$labelField = $this->labelFieldFormat ? "autocomplete_$this->name" : $this->labelFieldName; 
		$operator = $this->operator; 
		$id = $this->id; 
		$max = (int) $this->maxSelectedItems; 
		$class = $this->useList ? 'has_list' : 'no_list';
		if($this->useAndWords) $class .= " and_words";
		if($this->allowAnyValue) $class .= " allow_any";
		
		$disableChars = $this->disableChars; 
		if($disableChars) {
			$hasDoubleQuote = strpos($disableChars, '"') !== false; 
			$hasSingleQuote = strpos($disableChars, "'") !== false; 
			if($hasDoubleQuote && $hasSingleQuote) {
				$this->error("disableChars cannot have both double and single quotes"); 
				$disableChars = str_replace('"', '', $disableChars); 
			}
			if($hasSingleQuote) $disableChars = "data-disablechars=\"$disableChars\" ";
				else $disableChars = "data-disablechars='$disableChars' ";
		}
		
		$attrs =
			"data-max='$max' " . 
			"data-url='$url' " . 
			"data-label='" . $this->wire('sanitizer')->entities($labelField) . "' " . 
			"data-search='$searchField' " . 
			"data-operator='$operator'";
		
		$textValue = '';
		$remove = '';
		if(!$this->useList) { 
			if(count($this->value)) {
				$item = $this->wire('pages')->getById($this->value)->first();
				if($item && $item->id) {
					$textValue = $this->labelFieldFormat ? $item->getText($this->labelFieldFormat, true, false) : $item->get($labelField);
					if(!strlen($textValue)) $textValue = $item->get('title|name');
					$textValue = strip_tags($textValue);
					if(strpos($textValue, '&') !== false) $textValue = $this->wire('sanitizer')->unentities($textValue);
					$textValue = $this->wire('sanitizer')->entities($textValue);
				}
			}
			$remove = "<i class='fa fa-fw fa-times-circle InputfieldPageAutocompleteRemove'></i>";
		}
		
		$addingLabel = $this->wire('sanitizer')->entities1($this->_('New item:'));
		
		$out .= <<< _OUT

		<p>
		<input type='hidden' name='{$this->name}[]' id='$id' class='InputfieldPageAutocompleteData' value='$value' $attrs/>
		<input type='text' data-parent-input='$id' id='{$id}_input' class='$class' value='$textValue' $disableChars/>
		<i class='fa fa-fw fa-angle-double-right InputfieldPageAutocompleteStatus'></i>
		$remove
		<span class='notes InputfieldPageAutocompleteNote' data-adding='$addingLabel'><br />$addNote</span>
		</p>
		
_OUT;
	
		/*
		<script>
		$(document).ready(function() { 
			InputfieldPageAutocomplete.init('$id', '$url', '$labelField', '$searchField', '$operator');
			console.log('initAutocomplete: $id');
		}); 
		</script>
		*/
		return $out; 
	}

	/**
	 * Convert the CSV string provided in the $input to an array of ints needed for this fieldtype
	 * 
	 * @param WireInputData $input
	 * @return $this
	 *
	 */
	public function ___processInput(WireInputData $input) {
		
		parent::___processInput($input);
		
		$value = $this->attr('value');
		if(is_array($value)) $value = reset($value);
		$value = trim($value);
		
		if(strpos($value, ",") !== false) $value = explode(",", $value);
			else if($value) $value = array($value);
			else $value = array();
		
		foreach($value as $k => $v) {
			if(empty($v)) {
				unset($value[$k]);
			} else {
				$value[$k] = (int) $v;
			}
		}
		
		$this->attr('value', $value);
		
		return $this;
	}

	/**
	 * Get the AJAX search URL that will be queried (minus the actual term)
	 *
	 * This URL is focused on using the AJAX API from ProcessPageSearch
	 *
	 */
	protected function getAjaxUrl() {

		$selector = $this->findPagesSelector; 	

		if($this->parent_id) {
			if($selector) {
				// if a selector was specified, AND a parent, then we'll use the parent as a root
				$selector .= ",has_parent={$this->parent_id}";
			} else {
				// otherwise matches must be direct children of the parent
				$selector = "parent_id={$this->parent_id}"; 
			}
		}

		if(count($this->template_ids)) {
			$selector .= ",templates_id=" . implode('|', $this->template_ids);
		} else if($this->template_id) {
			$selector .= ",templates_id={$this->template_id}";
		}
		
		if($this->lang_id) {
			$selector .= ",lang_id=" . (int) $this->lang_id;
		}
		
		$allowUnpub = $this->getSetting('allowUnpub');
		if(!is_null($allowUnpub) && !$allowUnpub) {
			$selector .= ",status<" . Page::statusUnpublished;
		}

		// allow for full site matches
		if(!strlen($selector)) $selector = "id>0";

		// match no more than 50, unless selector specifies it's own limit
		if(strpos($selector, 'limit=') === false) $selector .= ",limit=50";

		// replace non-escaped commas with ampersands
		$selector = preg_replace('/(?<!\\\\),\s*/', '&', $selector); 
		
		if(strpos($selector, '.')) {
			// replace things like children.count with children-count since "." is not allowed in URL var names
			$selector = preg_replace('/(^|&)([_a-zA-Z0-9]+)\.([_a-zA-Z0-9]+)=/', '$1$2-$3=', $selector); 
		}

		// specify what label field we want to retrieve
		if($this->labelFieldFormat) {
			$name = "autocomplete_" . $this->attr('name');
			$this->wire('modules')->get('ProcessPageSearch')->setDisplayFormat($name, $this->labelFieldFormat, true);
			$selector .= "&format_name=$name";
		}
		$selector .= "&get=" . $this->labelFieldName;
		
		return $this->config->urls->admin . "page/search/for?" . $selector;
	}

	/**
	 * Install the autocomplete module
	 *
	 * Make sure we're in InputfieldPage's list of valid page selection widgets
	 *
	 */
	public function ___install() {
		$data = $this->wire('modules')->getModuleConfigData('InputfieldPage'); 	
		$data['inputfieldClasses'][] = $this->className();
		$this->wire('modules')->saveModuleConfigData('InputfieldPage', $data); 
	}

	/**
	 * Uninstall the autocomplete module
	 *
	 * Remove from InputfieldPage's list of page selection widgets
	 *
	 */
	public function ___uninstall() {
		$data = $this->wire('modules')->getModuleConfigData('InputfieldPage'); 	
		foreach($data['inputfieldClasses'] as $key => $value) {
			if($value == $this->className()) unset($data['inputfieldClasses'][$key]); 
		}
		$this->wire('modules')->saveModuleConfigData('InputfieldPage', $data); 
	}

	/**
	 * Provide configuration options for modifying the behavior when paired with InputfieldPage
	 *
	 */
	public function ___getConfigInputfields() {
		$inputfields = parent::___getConfigInputfields();

		$recommended = $this->_('Recommended');
		/** @var InputfieldRadios $field */
		$field = $this->modules->get('InputfieldRadios');
		$field->setAttribute('name', 'operator');
		$field->label = $this->_('Autocomplete search operator');
		$field->description = $this->_("The search operator that is used in the API when performing autocomplete matches.");
		$field->notes = $this->_("If you aren't sure what you want here, leave it set at the default: *="); 
		$field->required = false;
		$field->addOption('*=', '`*= ` ' . $this->_("Contains phrase or partial word (using fulltext index) - Recommended")); 
		$field->addOption('%=', '`%= ` ' . $this->_("Contains phrase or partial word (using LIKE)") . " - $recommended");
		$field->addOption('~=', '`~= ` ' . $this->_("Contains all the [full] words, in any order")); 
		$field->addOption('^=', '`^= ` ' . $this->_("Starts with word/phrase")); 
		$field->addOption('$=', '`$= ` ' . $this->_("Ends with word/phrase")); 
		$field->addOption('=', '` = ` ' . $this->_("Equals [exact]")); 
		$field->attr('value', $this->operator);
		$field->collapsed = Inputfield::collapsedNo; 
		$inputfields->add($field);

		/** @var InputfieldText $field */
		$field = $this->modules->get('InputfieldText'); 
		$field->attr('name', 'searchFields'); 
		$field->label = $this->_('Fields to query for autocomplete');
		$field->description = $this->_('Enter the names of the fields that should have their text queried for autocomplete matches.');
		$field->description .= ' ' . $this->_('Typically this would just be the title field, but you may add others by separating each with a space.'); 
		$field->description .= ' ' . $this->_('Note that this is different from the “label field” because you are specifying what fields will be searched, not what fields will be shown.');
		$field->collapsed = Inputfield::collapsedNo; 
		$field->attr('value', $this->searchFields);
		$notes = $this->_('Indexed text fields include:');
		foreach($this->wire('fields') as $f) {
			if(!$f->type instanceof FieldtypeText) continue; 
			$notes .= ' ' . $f->name . ','; 
		}
		$field->notes = rtrim($notes, ','); 
		$inputfields->add($field); 

		return $inputfields; 
	}
}
