<?php namespace ProcessWire;

/**
 * ProcessWire Field Editing Process
 *
 * Add, Edit, and Remove Fields
 * 
 * For more details about how Process modules work, please see: 
 * /wire/core/Process.php 
 * 
 * ProcessWire 3.x, Copyright 2016 by Ryan Cramer
 * https://processwire.com
 * 
 * @property bool $listAfterSave
 *
 * @method string execute()
 * @method string executeAdd()
 * @method string executeEdit()
 * @method void executeSave()
 * @method string executeChangeType()
 * @method void executeSaveChangeType()
 * @method void saveContext()
 * @method bool allowFieldInTemplate(Field $field, Template $template)
 * @method InputfieldForm getListFilterForm()
 * @method MarkupAdminDataTable getListTable(array|Fields $fields)
 * @method array getListTableRow(Field $field)
 * @method InputfieldForm buildEditForm()
 * @method buildEditFormContext(InputfieldForm $form)
 * @method buildEditFormCustom(InputfieldForm $form)
 * @method InputfieldWrapper buildEditFormDelete()
 * @method InputfieldWrapper buildEditFormBasics()
 * @method InputfieldWrapper buildEditFormInfo()
 * @method InputfieldWrapper buildEditFormAdvanced()
 * 
 * @method void fieldAdded(Field $field)
 * @method void fieldSaved(Field $field)
 * @method void fieldDeleted(Field $field)
 * @method void fieldChangedType(Field $field)
 * @method void fieldChangedContext(Field $field, Fieldgroup $fieldgroup, array $diffContextArray);
 * 
 * @todo Update/improve tags entry
 * 
 */

class ProcessField extends Process implements ConfigurableModule {

	public static function getModuleInfo() {
		return array(
			'title' => __('Fields', __FILE__),
			'summary' => __('Edit individual fields that hold page data', __FILE__),
			'version' => 112,
			'permanent' => true, 
			'permission' => 'field-admin', // add this permission if you want this Process available for roles other than Superuser
			'icon' => 'cube', 
			'useNavJSON' => true,
			'addFlag' => Modules::flagsNoUserConfig
			);
	}

	/**
	 * @var InputfieldForm|null $form
	 * 
	 */
	protected $form = null;

	/**
	 * @var Field|null $field
	 * 
	 */
	protected $field;

	/**
	 * @var int $id
	 * 
	 */
	protected $id;

	/**
	 * @var array $moduleInfo
	 * 
	 */
	protected $moduleInfo = array();

	/**
	 * @var array $labels
	 * 
	 */
	protected $labels = array();

	/**
	 * Optional context fieldgroup
	 * 
	 * When populated, we are editing the field only in the context of this fieldgroup/template. 
	 * 
	 * @var Fieldgroup|null $fieldgroup
	 *
	 */
	protected $fieldgroup = null;

	/**
	 * Optional namespace for context fieldgroup
	 * 
	 * @var string
	 * 
	 */
	protected $contextNamespace = '';

	/**
	 * Label that indicates what the context is (whether fieldgroup or namespace)
	 * 
	 * Provided via get/post var $context_label
	 * 
	 * @var string
	 * 
	 */
	protected $contextLabel = '';

	/**
	 * Data thats been overridden by all fieldgroups, indexed by fieldgroup ID with array value indexed by overridden field keys
	 * 
	 * @var array $fieldgroupData
	 *
	 */
	protected $fieldgroupData = array();

	/**
	 * Init the module
	 *
	 */
	public function init() {
		if($this->input->urlSegment1 == 'edit') $this->modules->get("JqueryWireTabs");
		$this->moduleInfo = self::getModuleInfo();
		$this->headline($this->moduleInfo['title']); 
		$this->labels = array(
			'save' => $this->_('Save'), // Save button label
			'import' => $this->_('Import'), 
			'export' => $this->_('Export'),
			'ok' => $this->_('Ok')
			);
		
		if($this->input->post->id) $this->id = (int) $this->input->post->id;
			else $this->id = $this->input->get->id ? (int) $this->input->get->id : 0;

		if($this->id) $this->field = $this->fields->get($this->id);
		if(!$this->field) $this->field = new Field();

		return parent::init();
	}

	/**
	 * Render JSON map of all fields (old method, but should be kept for backwards compatibility)
	 * 
	 * @return string JSON
	 *
	 */
	public function renderListJSON() {
		$a = array();
		$showAll = $this->wire('input')->get('all');
		foreach($this->wire('fields') as $field) {
			if(!$showAll && ($field->flags & Field::flagSystem) && $field->name != 'title') continue; 
			$a[] = array(
				'id' => $field->id,
				'name' => $field->name,
				'flags' => $field->flags, 
				);
		}
		header("Content-Type: application/json"); 
		return json_encode($a); 
	}

	/**
	 * Output JSON list of navigation items for this (intended to for ajax use)
	 *
	 * For 2.5+ admin theme navigation
	 * 
	 * @param array $options
	 * @return string|array
	 *
	 */
	public function ___executeNavJSON(array $options = array()) {
		$fieldsArray = array();
		$showAll = $this->wire('config')->advanced; 
		foreach($this->wire('fields') as $field) {
			if(!$showAll && ($field->flags & Field::flagSystem) && $field->name != 'title') continue;
			$fieldsArray[] = $field; 
		}
		$options['items'] = $fieldsArray;
		$options['itemLabel'] = 'name';
		return parent::___executeNavJSON($options);
	}

	/**
	 * Renders filtering options when viewing a list of Fields
	 *
	 * @return InputfieldForm
	 * 
	 */
	public function ___getListFilterForm() {

		$showAllLabel = $this->_('Show All'); 

		/** @var InputfieldForm $form */
		$form = $this->modules->get("InputfieldForm"); 
		$form->attr('id', 'field_filter_form'); 
		$form->attr('method', 'get'); 
		$form->attr('action', './'); 

		/** @var InputfieldFieldset $fieldset */
		$fieldset = $this->modules->get("InputfieldFieldset"); 
		$fieldset->attr('id', 'template_filters');
		$fieldset->entityEncodeLabel = false; 
		$fieldset->label = $this->_("Filters"); // Field list filters fieldset label
		$fieldset->icon = 'filter';
		$fieldset->collapsed = Inputfield::collapsedYes;
		$form->add($fieldset);

		/** @var InputfieldSelect $field */
		$field = $this->modules->get("InputfieldSelect"); 
		$field->attr('id+name', 'templates_id'); 
		$field->addOption('', $showAllLabel); 
		foreach($this->templates as $template) {
			$name = $template->name; 
			if($template->flags & Template::flagSystem) $name .= "*";
			$field->addOption($template->id, $name); 
		}
		$this->session->ProcessFieldListTemplatesID = (int) $this->input->get->templates_id; 
		$field->label = $this->_('Filter by Template');
		$field->description = $this->_("When selected, only the fields from a specific template will be shown. Built-in fields are also shown when filtering by template. Asterisk (*) indicates system templates."); // Filter by template description
		$value = (int) $this->session->ProcessFieldListTemplatesID; 
		$field->attr('value', $value); 
		if($value && $template = $this->templates->get($value)) {
			$form->description = sprintf($this->_('Showing fields from template: %s'), $template);
			$this->wire('processHeadline', $this->_('Fields by Template')); // Page headline when filtering by template
			$fieldset->collapsed = Inputfield::collapsedNo;
		} else {
			$template = null;
			$field->collapsed = Inputfield::collapsedYes; 
		}
		$fieldset->add($field); 

		// ----------------------------------------------------------------

		/** @var InputfieldSelect $field */
		$field = $this->modules->get("InputfieldSelect"); 
		$field->attr('id+name', 'fieldtype'); 
		$field->addOption('', $showAllLabel); 
		foreach($this->fieldtypes as $fieldtype) {
			$field->addOption($fieldtype->name, $fieldtype->longName);
		}
		$this->session->set('ProcessFieldListFieldtype', $this->sanitizer->name($this->input->get('fieldtype'))); 
		$field->label = $this->_('Filter by Field Type');
		$field->description = $this->_('When specified, only fields of the selected type will be shown. Built-in fields are also shown when filtering by field type.'); // Filter by fieldtype description
		$value = $this->session->get('ProcessFieldListFieldtype'); 
		$field->attr('value', $value); 
		if($value && $fieldtype = $this->fieldtypes->get($value)) {
			$form->description = sprintf($this->_('Showing fields of type: %s'), $fieldtype->longName);
			$fieldset->collapsed = Inputfield::collapsedNo;
		} else {
			$field->collapsed = Inputfield::collapsedYes; 
		}
		$fieldset->add($field); 

		// ----------------------------------------------------------------

		if(is_null($template) && !$this->session->ProcessFieldListFieldtype) {
			/** @var InputfieldCheckbox $field */
			$field = $this->modules->get("InputfieldCheckbox"); 
			$field->attr('id+name', 'show_system'); 
			$field->label = $this->_('Show built-in fields?');
			$field->description = $this->_("When checked, built-in fields will also be shown. These include system fields and permanent fields. System fields are required by the system and cannot be deleted or have their name changed. Permanent fields are those that cannot be removed from a template. These fields are used internally by ProcessWire."); // Show built-in fields description
			$field->value = 1; 
			$field->collapsed = Inputfield::collapsedYes; 
			$this->session->ProcessFieldListShowSystem = (int) $this->input->get->show_system; 
			if($this->session->ProcessFieldListShowSystem) {
				$field->attr('checked', 'checked');
				$field->collapsed = Inputfield::collapsedNo;
				$fieldset->collapsed = Inputfield::collapsedNo;
				$form->description = $this->_("Showing all fields, including built-in system and permament fields."); 
			}
			$fieldset->add($field); 
		} else {
			$this->session->ProcessFieldListShowSystem = 1; 
		}
		
		return $form;
	}

	/**
	 * Render a list of current fields
	 * 
	 * @return string
	 *
	 */
	public function ___execute() {
		if($this->wire('config')->ajax) return $this->renderListJSON();

		$out = $this->getListFilterForm()->render() . "\n<div id='ProcessFieldList'>\n";
		$fieldsByTag = array();
		$untaggedLabel = $this->_('Untagged');
		$hasFilters = $this->session->ProcessFieldListTemplatesID || $this->session->ProcessFieldListFieldtype;
		$collapsedTags = array();
		$caseTags = array(); // indexed by lowercase version of tag


		if(!$hasFilters) foreach($this->fields as $field) {
			if($this->session->ProcessFieldListShowSystem) {
				if($field->flags & Field::flagSystem || $field->flags & Field::flagPermanent) 
					$field->tags .= " " . $this->_x('Built-In', 'tag'); // Tag applied to the group of built-in/system fields
			}
			if(empty($field->tags)) {
				$tag = strtolower($untaggedLabel);
				if(!isset($fieldsByTag[$tag])) $fieldsByTag[$tag] = array();
				$fieldsByTag[$tag][$field->name] = $field;
				$caseTags[$tag] = $untaggedLabel;
				continue; 
			}
			$tags = explode(' ', trim($field->tags));
			foreach($tags as $tag) {
				if(empty($tag)) continue; 
				$caseTag = ltrim($tag, '-');
				$tag = strtolower($tag);
				if(substr($tag, 0, 1) == '-') {
					$tag = ltrim($tag, '-');
					$collapsedTags[] = $tag;
				}
				if(!isset($fieldsByTag[$tag])) $fieldsByTag[$tag] = array();
				$fieldsByTag[$tag][$field->name] = $field;	
				if(!isset($caseTags[$tag])) $caseTags[$tag] = $caseTag; 
			}
		}

		$tagCnt = count($fieldsByTag);
		if($tagCnt > 1) {
			$form = $this->wire(new InputfieldWrapper());
			ksort($fieldsByTag);
			foreach($fieldsByTag as $tag => $fields) {
				ksort($fields);
				$f = $this->modules->get('InputfieldMarkup');
				$f->entityEncodeLabel = false;
				$f->label = $caseTags[$tag];
				$f->icon = 'tags';
				$f->value = $this->getListTable($fields)->render();
				if(in_array($tag, $collapsedTags)) $f->collapsed = Inputfield::collapsedYes; 
				$form->add($f);
			}
			$out .= $form->render();
		} else {
			$out .= $this->getListTable($this->fields)->render();
		}

		$out .= "\n</div><!--/#ProcessFieldList-->";

		/** @var InputfieldButton $button */
		$button = $this->modules->get('InputfieldButton');
		$button->id = 'add_field_button'; 
		$button->href = "./add"; 
		$button->value = $this->_x('Add New Field', 'list button'); 
		$button->icon = 'plus-circle';
		$button->showInHeader();
		$out .= $button->render();
	
		$button = $this->modules->get('InputfieldButton');
		$button->id = 'import_button';
		$button->href = "./import/";
		$button->value = $this->labels['import'];
		$button->icon = 'paste';
		$button->setSecondary();
		$out .= $button->render();
		
		$button = $this->modules->get('InputfieldButton');
		$button->id = 'export_button'; 
		$button->href = "./export/";
		$button->value = $this->labels['export'];
		$button->icon = 'copy';
		$button->setSecondary();
		$out .= $button->render();

		if($this->input->nosave) {
			$this->session->remove('ProcessFieldListTemplatesID');
			$this->session->remove('ProcessFieldListFieldtype');
			$this->session->remove('ProcessFieldListShowSystem');
		}

		return $out;
	}

	/**
	 * Get the table that lists fields
	 * 
	 * @param array|Fields $fields
	 * @return MarkupAdminDataTable
	 * 
	 */
	protected function ___getListTable($fields) {

		/** @var MarkupAdminDataTable $table */
		$table = $this->modules->get("MarkupAdminDataTable");

		$headerRow = array(
			$this->_x('Name', 'list thead'), 
			$this->_x('Label', 'list thead'), 
			$this->_x('Type', 'list thead'), 
			$this->_x('Templates', 'list thead quantity')
			);

		$table->headerRow($headerRow);
		$table->setEncodeEntities(false);
		$numRows = 0;

		foreach($fields as $field) {
			$row = $this->getListTableRow($field);
			if(!empty($row)) {
				$table->row($row); 
				$numRows++;
			}
		}

		if(!$numRows) $table->row(array($this->_("No fields matched your filter"))); 

		return $table;
	}

	/**
	 * Get a table row (array) for placement in getListTable()
	 * 
	 * @param Field $field
	 * @return array
	 * 
	 */
	protected function ___getListTableRow(Field $field) {

		$flagDefs = array(
			'Autojoin' => array(
				'icon' => 'fa fa-sign-in',
				'label' => $this->_x('autojoin', 'list notes')
				),
			'Global' => array(
				'icon' => 'fa fa-globe',
				'label' => $this->_x('global', 'list notes')
				),
			'System' => array(
				'icon' => 'fa fa-puzzle-piece',
				'label' => $this->_x('system', 'list notes')
				),
			'Permanent' => array(
				'icon' => 'fa fa-building-o',
				'label' => $this->_x('permanent', 'list notes')
				),
			'Required' => array(
				'icon' => 'fa fa-asterisk', 
				'label' => $this->_x('required', 'list notes')
				),
			'Dependency' => array(
				'icon' => 'fa fa-question-circle', 
				'label' => $this->_x('show if...', 'list notes')
				),
			'Access' => array(
				'icon' => 'fa fa-key', 
				'label' => $this->_x('access', 'list notes')
				),
			);

		$numTemplates = 0; 
		$flags = array();
		$builtIn = false;
		
		$templatesID = $this->session->ProcessFieldListTemplatesID; 
		if($templatesID && $template = $this->templates->get($templatesID)) {
			if(!$template->fieldgroup->has($field)) return array();
		}
		if($fieldtype = $this->session->ProcessFieldListFieldtype) {
			if($field->type != $fieldtype) return array();
		}

		if($field->flags & Field::flagAutojoin) $flags[] = 'Autojoin';
		if($field->flags & Field::flagGlobal) $flags[] = 'Global';
		if($field->flags & Field::flagAccess) $flags[] = 'Access';

		if($field->flags & Field::flagSystem) {
			$flags[] = 'System';
			$builtIn = true;
		}

		if($field->flags & Field::flagPermanent) {
			$flags[] = 'Permanent';
			$builtIn = true;
		}
		
		if($field->showIf) $flags[] = 'Dependency';
		if($field->required) $flags[] = 'Required';

		if($builtIn && !$templatesID && $field->name != 'title') {
			if(!$this->session->ProcessFieldListShowSystem) return array();
		}

		foreach($field->getFieldgroups() as $fieldgroup) {
			$numTemplates += $fieldgroup->numTemplates();
		}
		$numTemplatesLink = "../template/?filter_field={$field->name}&nosave=1";

		$flagsOut = '';
		foreach($flags as $flagName) {
			$flag = $flagDefs[$flagName];
			$icon = "<i class='$flag[icon]'></i>";
			$flagsOut .= "<a href='#' class='fieldFlag fieldFlag$flagName tooltip' title='$flag[label]'>$icon</span></a>";
		}

		$icon = $field->icon ? "<i class='fa fa-" . str_replace(array('icon-', 'fa-'), '', $this->wire('sanitizer')->name($field->icon)) . "'></i> " : '';

		return array(
			$field->name => "edit?id={$field->id}",
			$icon . $this->sanitizer->entities($field->getLabel()), 
			$this->sanitizer->entities($field->type->shortName),
			"$flagsOut<a href='$numTemplatesLink'>$numTemplates</a>"
			);
	}

	/**
	 * Add a new field
	 *
	 */
	public function ___executeAdd() {

		// unrelated shortcut feature: double check that all field tables actually exist
		// and re-create them in instances where they don't (like if a bunch of fields
	 	// were migrated over in an SQL dump of the "fields" table or something). 
		$this->wire("fields")->checkFieldTables(); 

		return $this->executeEdit(); 
	}


	/**
	 * Edit an existing Field
	 *
	 */
	public function ___executeEdit() {
		
		if(is_null($this->form)) $this->buildEditForm();
		$this->breadcrumb('./', $this->moduleInfo['title']);
		
		if($this->field->id) {
			$headline = sprintf($this->_x('Edit Field: %s', 'edit headline'), $this->field->name); // Headline when editing a field
		} else {
			$headline = $this->_x('Add New Field', 'add headline'); // Headline when adding a field
		}
		
		if($this->fieldgroup) {
			$this->breadcrumb("./edit?id=" . $this->field->id, $this->field->name);
			if($this->contextLabel) {
				$headline .= ' (' . sprintf($this->_('when used with: %s'), $this->contextLabel) . ')';
			} else {
				$headline .= ' (' . sprintf($this->_('when used with template: %s'), $this->fieldgroup->name) . ')';
			}
		}

		$this->headline($headline);
		$this->browserTitle($headline);
		//$this->identifyContextChanges();

		if($this->field->id && !$this->fieldgroup && $this->session->get($this, 'optimize') == $this->field->id) {
			// this is outside of buildEditForm intentionally, in case anything has hooked buildEditForm and adding properties to it
			$alert = $this->buildEditFormAlert();	
			if(!is_null($alert)) {
				$this->form->action .= '#alert';
				$f = $this->form->getChildByName('submit_save_field');
				if($f) {
					$this->form->insertBefore($alert, $f);
				} else {
					$this->form->add($alert);
				}
			}
		}
		
		$out = $this->form->render(); 
		$out .= $this->renderContextSelect();

		return $out;
	}

	/**
	 * Get an array of all template IDs (integers) that don't have the given field
	 * 
	 * @param Field $field
	 * @return array
	 * 
	 */
	protected function getTemplatesWithoutField(Field $field) {
		$templatesWithoutField = array();
		foreach($this->wire('templates') as $template) {
			if($template->fieldgroup->hasField($field)) continue;
			$templatesWithoutField[] = (int) $template->id;
		}
		return $templatesWithoutField;
	}
	
	protected function buildEditFormAlert() {

		$this->field->type->getDatabaseSchema($this->field); // may add to trackGets, so we include it (i.e. FieldtypeFile and fileSchema)
		$gets = $this->field->trackGets();
		$xkeys = array();
		$numRows = 0; 
		$checkLabel = $this->_('Check field reported:') . ' ';
		
		if(is_array($gets)) {
			foreach($this->field->data as $key => $value) {
				if(in_array($key, $gets)) continue;
				if($this->form->getChildByName($key)) continue; // confrim there isn't a field with the name
				$xkeys[] = $key;
			}
		}

		foreach($this->wire('fields') as $field) {
			
			$table = $field->getTable();
			
			$sql = 	"SELECT * FROM $table " .
					"LEFT JOIN pages ON $table.pages_id=pages.id " .
					"WHERE pages.id IS NULL ";
			
			$templatesWithoutField = $this->getTemplatesWithoutField($field);
			if(count($templatesWithoutField)) {
				$sql .= "OR pages.templates_id IN(" . implode(',', $templatesWithoutField) . ")";
			}

			$query = $this->database->prepare($sql);
			
			try {
				$query->execute();
				$cnt = $query->rowCount();
				if($cnt > 0) {
					if($field->name == $this->field->name) {
						$message = $checkLabel . sprintf($this->_('%d orphaned table rows found'), $cnt); 
						$numRows = $cnt; 
						$n = 0; 
						while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
							$message .= "\n<small>pages_id: $row[pages_id], data: " . $this->sanitizer->entities($row['data']) . "</small>";
							if(++$n >= 100) break;
						}
						if($n >= 100) $message .= "\n" . $this->_('...and so on...');
					} else {
						$message = sprintf($this->_('Please run "Actions > Check field data" on field %s'), "<a href='./edit?id=$field->id#info'>$field->name</a>");
					}
					$this->error(nl2br($message), Notice::allowMarkup); 
				}
			} catch(\Exception $e) {
			}
		}
		
		if(!count($xkeys) && !$numRows) {
			$this->message($checkLabel . sprintf($this->_('No issues found for field %s'), $this->field->name)); 
			$this->session->remove($this, 'optimize'); 
			return null;
		}
		
		$form = $this->wire(new InputfieldWrapper());
		$form->attr('class', 'WireTab');
		$form->attr('id', 'alert');
		$form->attr('title', $this->_x('Alert', 'tab'));
		
		if(count($xkeys)) {
			$f = $this->wire('modules')->get('InputfieldCheckboxes'); 
			$f->attr('name', '_remove_keys'); 
			$f->label = $this->_('Unknown Properties'); 
			$f->description = $this->_('The following properties were found with this field with zero accesses during configuration. Sometimes this can indicate that the properties are no longer in use. Check the box next to each property you want to remove. If you are not sure, there is no harm in just leaving them there.'); 
			$f->icon = 'exclamation-triangle';
			foreach($xkeys as $key) $f->addOption($key); 
			$this->wire('session')->set($this, '_remove_keys', $xkeys); 
			$form->add($f); 
			$this->error($checkLabel . $this->_('Potential unknown properties found in this field. Please see the "Alert" tab.')); 
		}
	
		if($numRows) {
			$f = $this->wire('modules')->get('InputfieldCheckbox'); 
			$f->attr('name', '_remove_rows');
			$f->label = sprintf($this->_('Remove %d orphaned table rows?'), $numRows);
			$f->description = $this->_('We found rows of data in the table for this field that do not match up with any page, or match pages that do not have this field.');
			$f->icon = 'exclamation-triangle';
			$f->attr('value', $this->field->id); 
			$f->autocheck = false;
			$form->add($f); 
		}
		
		return $form; 
	}

	/**
	 * Add a '*' symbol to the labels of any fields that are overriding the defaults
	 * 
	 * @deprecated
	 *
	 */
	protected function identifyContextChanges() {

		if(!$this->fieldgroup) return;

		$fieldOriginal = $this->wire('fields')->get($this->field->id);
		$context = $this->fieldgroup->getFieldContextArray($this->field->id, $this->contextNamespace); 
		$languages = $this->wire('languages');

		foreach($context as $key => $value) {

			$languageID = 0;
			if($languages) foreach($languages as $language) {
				if(strpos($key, (string) $language->id) && preg_match('/(.+?)' . $language->id . '$/', $key, $matches)) {
					$key = $matches[1];					
					$languageID = $language->id; 
					break;
				}
			} 

			if($key == 'label') $key = 'field_label'; // for retrieving inputfield

			$inputfield = $this->form->getChildByName($key);
			if(!$inputfield) continue; 

			if($key == 'field_label') $key = 'label'; // convert back
			if($languageID) $key .= $languageID; 

			if($value == $fieldOriginal->$key) continue; 
			$inputfield->label .= ' *';
		}
	}

	/**
	 * Render a select box where the user can choose another fieldgroup context
	 *
	 */
	protected function renderContextSelect() {

		if(!$this->field->id) return '';
		if($this->fieldgroup && $this->wire('input')->get('modal')) {
			return "<input type='hidden' name='fieldgroup_id' value='{$this->fieldgroup->id}' />";
		}
		
		$fieldgroups = $this->field->getFieldgroups();
		if(!count($fieldgroups)) return '';
		$contextLabel = $this->_('Override by template');

		$out = 	
			"<div id='fieldgroupContext'>" . 
			"<select id='fieldgroupContextSelect' name='fieldgroup_id'>" . 
			"<option value=''>$contextLabel</option>"; // . $this->_x('None (default)', 'context select') . "</option>";

		foreach($fieldgroups->sort('name') as $fieldgroup) {
			$selected = $this->fieldgroup && $this->fieldgroup->id == $fieldgroup->id ? " selected='selected'" : '';
			$out .= "<option$selected value='{$fieldgroup->id}'>{$fieldgroup->name}</option>";
		}

		$out .= 
			"</select>" . 
			"</div>";

		/*
		if($this->fieldgroup) $out .= 
			'<p class="detail">' . 
			sprintf($this->_('*Indicates that field is currently overriding a default value. Settings that you specify on this screen override the default settings only when used with the "%s" template.'), $this->fieldgroup->name) . 
			'</p>';
		*/
	
		return $out; 	
	}

	/**
	 * Build the Field Edit form
	 * 
	 * @return InputfieldForm
	 * @throws WireException
	 *
	 */
	protected function ___buildEditForm() {

		// optional context fieldgroup
		if($this->input->post->fieldgroup_id) $fieldgroup_id = (int) $this->input->post->fieldgroup_id; 
			else if($this->input->get->fieldgroup_id && !count($_POST)) $fieldgroup_id = (int) $this->input->get->fieldgroup_id; 
			else $fieldgroup_id = 0;
		
		if($fieldgroup_id) {
			// optional namespace for context fieldgroup
			if($this->input->post('_context_namespace')) $contextNamespace = $this->input->post('_context_namespace');
				else if($this->input->get('context_namespace') && !count($_POST)) $contextNamespace = $this->input->get('context_namespace');
				else $contextNamespace = '';
			if($contextNamespace) {
				$contextNamespace = $this->wire('sanitizer')->fieldName($contextNamespace);
				$this->contextNamespace = $contextNamespace;
			}	
		}
		if($this->input->post('_context_label')) $contextLabel = $this->input->post('_context_label');
			else if($this->input->get('context_label')) $contextLabel = $this->input->get('context_label');
			else $contextLabel = '';
		if(strlen($contextLabel)) {
			$contextLabel = $this->wire('sanitizer')->entities($contextLabel);
			$this->contextLabel = $contextLabel;
		}
		
		if($this->field->id && $this->session->get($this, 'optimize') == $this->field->id) {
			// keep track of what gets retrieved from each field
			foreach($this->wire('fields') as $field) $field->trackGets(true);
		}

		/** @var InputfieldForm $form */
		$form = $this->modules->get('InputfieldForm');
		$form->attr('id+name', 'ProcessFieldEdit'); 
		$form->attr('action', 'save'); 
		$form->attr('method', 'post'); 
		$form->addClass('InputfieldFormFocusFirst');
		$this->form = $form;

		if($fieldgroup_id && $this->field->id) {
			$this->fieldgroup = $this->wire('fieldgroups')->get($fieldgroup_id); 
			if(!$this->fieldgroup) throw new WireException("Invalid fieldgroup"); 
			if(!$this->fieldgroup->has($this->field)) throw new WireException("Fieldgroup '{$this->fieldgroup->name}' does not have field '{$this->field->name}'"); 
			$this->field = $this->fieldgroup->getFieldContext($this->field->id, $this->contextNamespace);  // get the field in context of the fieldgroup
		}

		$form->add($this->buildEditFormBasics());

		if($this->field->id) { 
			$accessTab = $this->buildEditFormAccess();
			if($this->field->type) {
				$this->buildEditFormCustom($form);
				if(!$this->fieldgroup) {
					if($accessTab) $form->add($accessTab);
					$form->add($this->buildEditFormAdvanced());
				}
			}
			if(!$this->fieldgroup) {
				$info = $this->buildEditFormInfo();
				if(count($info)) $form->add($info);
				$this->buildEditFormContext($form);
				$form->add($this->buildEditFormDelete());
			} else {
				if($accessTab) $form->add($accessTab);
				$this->buildEditFormContext($form);
				// $this->buildEditFormContextFieldgroup($form); 
			}
		}

		/** @var InputfieldHidden $field */
		$field = $this->modules->get('InputfieldHidden');
		$field->attr('name', 'id'); 
		$field->attr('value', $this->field->id); 
		$form->add($field); 

		if($this->fieldgroup) {
			$field = $this->modules->get('InputfieldHidden');
			$field->attr('name', 'fieldgroup_id'); 
			$field->attr('value', $this->fieldgroup->id); 
			$form->add($field);
			if($this->contextNamespace) {
				$field = $this->modules->get('InputfieldHidden');
				$field->attr('name', '_context_namespace');
				$field->attr('value', $this->contextNamespace);
				$form->add($field);
			}
			if($this->contextLabel) {
				$field = $this->modules->get('InputfieldHidden');
				$field->attr('name', '_context_label');
				$field->attr('value', $this->contextLabel);
				$form->add($field);
			}
		}
	
		/** @var InputfieldSubmit $field */
		$field = $this->modules->get('InputfieldSubmit');
		$field->attr('value', $this->labels['save']); 
		$field->attr('name', 'submit_save_field'); 
		$field->showInHeader();
		$form->add($field); 

		if($this->wire('input')->get('process_template')) {
			// ProcessTemplate has loaded the field editor in a modal window
			// so we add a cancel button that asmSelect will recognize for it's modal
			/** @var InputfieldButton $field */
			$field = $this->modules->get('InputfieldButton');
			$field->attr('id+name', 'modal_cancel_button'); 
			$field->attr('value', $this->_x('Cancel', 'button'));
			$field->setSecondary();
			$form->append($field);
		
			// contains the asm list item status, populated by JS
			/** @var InputfieldHidden $field */
			$field = $this->modules->get('InputfieldHidden');
			$field->attr('id+name', 'asmListItemStatus'); 
			$field->attr('class', 'asmListItemStatus');
			$field->attr('data-tpl', "<span class='ui-priority-secondary'>" . str_replace('Fieldtype', '', $this->field->type) . "</span> %"); // % gets replaced with live percent
			$field->attr('value', '');
			$form->append($field); 
		}
		

		return $form; 
	}

	/**
	 * Build field/template context edit
	 * 
	 * @param InputfieldForm $form
	 * 
	 */
	protected function ___buildEditFormContext($form) {
		
		$allChanges = array();
		$fieldgroups = $this->fieldgroup ? array($this->fieldgroup) : $this->wire('fieldgroups'); 
		
		foreach($fieldgroups as $fieldgroup) {
			/** @var Fieldgroup $fieldgroup */
			/** @var Field $field */
			$field = $fieldgroup->getField($this->field->name); 
			if(!$field) continue; 
			if(!$fieldgroup->hasFieldContext($field->name, $this->contextNamespace)) continue; 	
			$allChanges[$fieldgroup->name] = $this->getContextChanges($form, $field, $fieldgroup);
		}
		
		if(!count($allChanges) && !$this->wire('config')->advanced) return;
		
		$tab = $this->wire(new InputfieldWrapper());
		$tab->attr('title', $this->_('Overrides'));
		$tab->attr('class', 'WireTab');
		$form->add($tab); 
		
		$f = $this->wire('modules')->get('InputfieldMarkup');
		$f->description = $this->_('The following settings are overridden for this field by the indicated template(s). Check the box to the right of any row to remove the setting override (restoring the original value).');
		$f->icon = 'exchange';
		if($this->fieldgroup) {
			if($this->contextNamespace) {
				$f->label = sprintf($this->_('Setting overrides for field namespace: %s'), $this->contextNamespace);
			} else {
				$f->label = sprintf($this->_('Setting overrides for template: %s'), $this->fieldgroup->name);
			}
		} else {
			$f->label = $this->_('Setting overrides by template'); 
			$f->notes = $this->_('To edit an override setting or override other settings, edit any template and click the field name in the fields list.');
		}
		$tab->add($f);

		if(count($allChanges)) {
			/** @var MarkupAdminDataTable $table */
			$table = $this->wire('modules')->get('MarkupAdminDataTable');
			$table->setEncodeEntities(false);
			$table->setSortable(false);
			$header = array();
			if(!$this->fieldgroup) $header[] = $this->_x('Template', 'context-thead');
			$header[] = $this->_x('Setting', 'context-thead');
			$header[] = $this->_x('Original', 'context-thead');
			$header[] = $this->_x('Override', 'context-thead');
			$header[] = "<i class='fa fa-fw fa-lg fa-trash override-select-all'></i>";
			$table->headerRow($header);

			$sanitizer = $this->wire('sanitizer');
			$lastFieldgroupName = '';
			$this->wire('modules')->get('JqueryUI')->use('modal');

			foreach($allChanges as $fieldgroupName => $changes) {
				$fieldgroup = $this->wire('fieldgroups')->get($fieldgroupName);
				foreach($changes as $key => $change) {
					$a = $fieldgroupName;
					// $a = "<a class='pw-modal' data-buttons='#Inputfield_submit_save_field' data-autoclose='1' " . 
					//	"href='./edit?id={$this->field->id}&fieldgroup_id=$fieldgroup->id'>$fieldgroupName</a>";
					$fieldgroupLabel = $fieldgroupName == $lastFieldgroupName ? '' : $a;
					$row = array();
					if(!$this->fieldgroup) $row[] = $fieldgroupLabel;
					$row[] = $sanitizer->entities($change['label']);
					$row[] = "<del>" . $sanitizer->entities($change['originalValue']) . "</del>";
					$row[] = $sanitizer->entities($change['value']);
					$row[] = "<input type='checkbox' name='_remove_context[]' value='$fieldgroup->id:$key' />";
					$options = $fieldgroupLabel ? array('separator' => true) : array();
					$table->row($row, $options);
					$lastFieldgroupName = $fieldgroupName;
				}
			}

			$f->value = $table->render();
		} else {
			$f->value = '<p>' . $this->_('There are currently no settings being overridden by template.') . '</p>';
			$f->collapsed = Inputfield::collapsedYes;
		}
	
		// allow the following configuration only for advanced mode
		if(!$this->wire('config')->advanced) return;
		
		/** @var InputfieldCheckboxes $field */
		$field = $this->modules->get('InputfieldCheckboxes');
		$field->attr('name', 'allowContexts');
		$field->label = $this->_('Allowed overrides');
		$field->icon = 'sliders';
		$field->description =
			$this->_('Checked settings will appear as configuration options when editing this field within the context of a particular template.') . ' ' .
			$this->_('**Warning:** doing this for settings beyond those specified by the module author may not always work, or may cause problems.');
		$field->table = true;
		$field->thead =
			$this->_('Label') . '|' .
			$this->_('Name') . '|' .
			$this->_('Type');

		$tabNames = array(
			'fieldtypeConfig' => $this->_('Details:'),
			'inputfieldConfig' => $this->_('Input:')
		);
		$exclusions = array(
			'collapsed',
			'showIf',
			'required',
			'requiredIf',
			'columnWidth',
			'visibility',
			'themeOffset',
			'themeBorder',
			'themeColor',
		);

		$fieldtypeNames = $this->field->type->getConfigAllowContext($this->field);
		if(!is_array($fieldtypeNames)) $fieldtypeNames = array();
		$dummyPage = $this->wire('pages')->get("/"); // only using this to satisfy param requirement 
		$inputfieldNames = $this->field->getInputfield($dummyPage)->getConfigAllowContext($this->field);
		if(!is_array($inputfieldNames)) $inputfieldNames = array();
		$alwaysSelected = array_merge($fieldtypeNames, $inputfieldNames);
		$allowContexts = $this->field->get('allowContexts');
		$alwaysSelected = array_diff($alwaysSelected, $allowContexts);
		$qty = 0;

		foreach($tabNames as $tabName => $tabLabel) {
			/** @var InputfieldWrapper $tabInputfield */
			$tabInputfield = $form->getChildByName($tabName);
			if(!$tabInputfield) continue;
			foreach($tabInputfield->getAll() as $f) {
				$name = $f->name;
				if(strpos($name, '_') === 0) continue;
				if(in_array($name, $exclusions)) continue;
				if($f instanceof InputfieldWrapper || $f instanceof InputfieldMarkup || $f instanceof InputfieldHidden) continue;
				$typeName = str_replace('Inputfield', '', $f->className());
				$label = str_replace('|', ' ', "$tabLabel $f->label") .
					"| [span.detail] $name [/span] " .
					"| [span.detail] $typeName [/span]";
				if(in_array($name, $alwaysSelected)) {
					$field->addOption($name, $label, array('checked' => 'checked', 'disabled' => 'disabled'));
					$allowContexts[] = $name;
				} else {
					$field->addOption($name, $label);
					$qty++;
				}
			}
		}
		
		$field->attr('value', $allowContexts);
		if($qty) $tab->append($field);
	}

	/*
	protected function ___buildEditFormContextFieldgroup($form) {
		if(!$this->fieldgroup) return;
		$changes = $this->getContextChanges($form, $this->field, $this->fieldgroup);
		
		$tab = new InputfieldWrapper();
		$tab->attr('title', $this->_('Overrides'));
		$tab->attr('class', 'WireTab');
		$form->add($tab); 
		
		$f = $this->wire('modules')->get('InputfieldCheckboxes');
		$f->label = $this->_('Remove context overrides');
		$f->table = true; 
		$f->thead = 
			$this->_x('Remove setting', 'context-thead') . '|' .
			$this->_x('Original', 'context-thead') . '|' . 
			$this->_x('Override', 'context-thead'); 
		$f->description = sprintf($this->_('The following settings are overriding the original field settings when this field is used in the context of the "%s" template.'), $this->fieldgroup->name); // Context description
		$f->description .= ' ' . $this->_('To remove any of the overridden values (and restore the original field value) check the box next to each setting you want to remove/restore.'); // Context description 2
		$tab->add($f);
		
		foreach($changes as $key => $change) {
			$f->addOption($key, "$change[label]|$change[originalValue]|$change[value]");
		}
	}
	*/

	/**
	 * Add Fieldtype and Inputfield custom fields to the form 
	 * 
	 * @param InputfieldForm $form
	 *
	 */
	protected function ___buildEditFormCustom($form) {
		
		$customFields = $this->field->getConfigInputfields();
		$tab = null;
		
		foreach($customFields as $field) {
			// skip over wrappers if they don't have fields in them 
			if($field instanceof InputfieldWrapper && !count($field->children)) continue; 
			//if(!$this->fieldgroup) $field->attr('class', 'WireTab');
			$field->attr('class', 'WireTab');
			$field->head = '';
			if($field->name == 'inputfieldConfig') $tab = $field;
			$form->add($field); 
		}
		
		if($tab) $this->buildEditFormFrontEdit($tab);
	}

	/**
	 * Build the front-end editor settings
	 * 
	 * @param InputfieldWrapper $tab
	 * 
	 */
	protected function buildEditFormFrontEdit($tab) {
		
		if(!$this->field) return;
		$cls = __NAMESPACE__ . "\\FieldtypeFieldsetOpen";
		if($this->field->type instanceof $cls) return;
		
		$fieldset = $this->wire('modules')->get('InputfieldFieldset');
		$fieldset->label = $this->_('Front-end editing');
		$fieldset->icon = 'edit';
		$fieldset->collapsed = Inputfield::collapsedYes;
		$tab->add($fieldset);
		
		if(!$this->wire('modules')->isInstalled('PageFrontEdit')) {
			$f = $this->wire('modules')->get('InputfieldMarkup');
			$fieldset->add($f);
			$button = $this->wire('modules')->get('InputfieldButton');
			$button->href = $this->wire('config')->urls->admin . "module/installConfirm?name=PageFrontEdit";
			$button->value = $this->_('Install');
			$button->icon = 'sign-in';
			$button->target = '_blank';
			$f->description = $this->_('Please install the front-end page editor module to enable front-end editing for fields. After installing, reload and return here for more options.');
			$f->value = $button->render();
			return;
		}
		
		$file = $this->wire('config')->paths->PageFrontEdit . 'PageFrontEditConfig.php';
		/** @noinspection PhpIncludeInspection */
		include_once($file);
		$moduleConfig = $this->wire(new PageFrontEditConfig());
		$moduleConfig->fieldHelpInputfields($fieldset, $this->field); 
	}

	/**
	 * Add a delete tab to the form
	 * 
	 * @return InputfieldWrapper
	 *
	 */
	protected function ___buildEditFormDelete() {

		$deleteLabel = $this->_('Delete field');

		/** @var InputfieldWrapper $form */
		$form = $this->wire(new InputfieldWrapper());
		$form->attr('id', 'delete');
		$form->attr('class', 'WireTab');
		//$form->head = $deleteLabel;
		$form->attr('title', $this->_x('Delete', 'tab')); 

		/** @var InputfieldCheckbox $field */
		$field = $this->modules->get('InputfieldCheckbox');
		$field->label = $deleteLabel;
		$field->icon = 'times-circle';
		$field->attr('id+name', "delete"); 
		$field->attr('value', $this->field->id); 

		if($this->field->id && $this->field->numFieldgroups() == 0) {
			$field->description = $this->_("This field is not in use and is safe to delete.");
		} else { 
			$field->attr('disabled', 'disabled'); 
			$field->description = $this->_("This field may not be deleted because it is in use by one or more templates."); 
		}

		$form->add($field);

		return $form;
	}

	/**
	 * Basic field configuration options: name, type, label, description
	 * 
	 * @return InputfieldWrapper
	 *
	 */
	protected function ___buildEditFormBasics() {

		/** @var InputfieldWrapper $form */
		$form = $this->wire(new InputfieldWrapper());
		$form->attr('id', 'basics');
		$form->attr('class', 'WireTab');
		$form->attr('title', $this->_x('Basics', 'tab')); 

		if($this->fieldgroup) { 	
			// ok

		} else {
			/** @var InputfieldName $field */
			$field = $this->modules->get('InputfieldName');
			$field->attr('value', $this->field->name); 
			$field->description = $this->_("Use only ASCII letters (a-z A-Z), numbers (0-9) or underscores.");
			$form->add($field); 

			/** @var InputfieldSelect $field */
			$field = $this->modules->get('InputfieldSelect');
			$field->label = $this->_x('Type', 'select label'); // Label for field type select
			$field->attr('name', 'type'); 
			$field->required = true; 
			if($this->field->type) $field->attr('value', $this->field->type->name); 
				else $field->addOption('', ''); 

			if(!$this->field->id) $field->description = $this->_("After selecting your field type and saving, you may be presented with additional configuration options specific to the field type you selected."); // Note that appears when adding new field

			if($this->field->type) $fieldtypes = $this->field->type->getCompatibleFieldtypes($this->field);
				else $fieldtypes = $this->fieldtypes; 

			if($fieldtypes && count($fieldtypes)) {
				$fieldtypeLabels = array();
				foreach($fieldtypes as $fieldtype) {
					/** @var Fieldtype $fieldtype */
					$label = $fieldtype->longName;
					if(isset($fieldtypeLabels[$label])) $label .= " ($fieldtype->name)";
					$fieldtypeLabels[$label] = $fieldtype;
				}
				ksort($fieldtypeLabels);
				$advanced = $this->config->advanced;
				foreach($fieldtypeLabels as $label => $fieldtype) {
					if(!$advanced && $fieldtype->isAdvanced() && $this->field->name != 'title' 
						&& $field->value != $fieldtype->className()) continue; 
					$field->addOption($fieldtype->name, $label); 
				}
			} else {
				$field->addOption($this->field->type->name, $this->field->type->longName); 
			}
			
			if(!$this->field->id) {
				$names = array();
				foreach($this->wire('fields') as $f) {
					if(($f->flags & Field::flagSystem) && !$this->wire('config')->advanced) continue;
					if(strpos($f->name, '_END')) continue;
					$names['_' . $f->name] = $f->name;
				}
				$field->addOption($this->_('Clone existing field'), $names);
			}

			$form->add($field); 
		}

		/** @var Languages|null $languages */
		$languages = $this->wire('languages');
		$languageFields = array();

		/** @var InputfieldText $field */
		$field = $this->modules->get('InputfieldText');
		$field->attr('id+name', 'field_label'); 
		$field->label = $this->_x('Label', 'text input'); // Label for 'field label' text input
		$field->attr('value', $this->field->label); 
		$field->class .= ' asmListItemDesc'; // for modal to populate parent asmSelect desc field with this value (recognized by jquery.asmselect.js)
		$field->description = $this->_("This is the label that appears above the entry field. If left blank, the name will be used instead."); // Description for 'field label'
		$form->add($field);
		$languageFields[] = $field;

		/** @var InputfieldTextarea $field */
		$field = $this->modules->get('InputfieldTextarea');
		$field->label = $this->_x('Description', 'textarea input'); // Label for the 'field description' textarea input
		$field->attr('name', 'description'); 
		$field->attr('value', $this->field->description); 
		$field->attr('rows', 3); 
		$field->description = $this->_("Additional information describing this field and/or instructions on how to enter the content."); // Description for 'field description'
		$field->collapsed = Inputfield::collapsedBlank;
		$form->add($field);
		$languageFields[] = $field;
		
		$field = $this->modules->get('InputfieldTextarea');
		$field->label = $this->_x('Notes', 'textarea input'); // Label for the 'field description' textarea input
		$field->attr('name', 'notes');
		$field->attr('value', $this->field->notes);
		$field->attr('rows', 3);
		$field->description = $this->_("Usage notes or additional information that appears beneath the input."); // Description for 'field notes'
		$field->notes = $this->_('Notes appear here and look like this.');
		$field->collapsed = Inputfield::collapsedBlank;
		$form->add($field);
		$languageFields[] = $field;

		if($languages) foreach($languageFields as $field) {
			$field->useLanguages = true; 
			$name = $field->name; 
			if($name == 'field_label') $name = 'label';
			foreach($languages as $language) {
				if($language->isDefault) continue; 
				$field->set("value{$language->id}", $this->field->get("$name{$language->id}")); 
			}
		}

		return $form;
	}

	/**
	 * Build the 'Info' field shown in the Field Edit form
	 * 
	 * @return InputfieldWrapper
	 *
	 */
	protected function ___buildEditFormInfo() {

		/** @var InputfieldWrapper $form */
		$form = $this->wire(new InputfieldWrapper());
		$form->attr('class', 'WireTab');
		$form->attr('id', 'info');
		$form->attr('title', $this->_x('Actions', 'tab'));
	
		/** @var Templates $allTemplates */
		$allTemplates = $this->wire('templates');
		$inTemplates = array();
		
		foreach($allTemplates as $template) {
			if($template->fieldgroup->hasField($this->field)) $inTemplates[$template->id] = $template;
		}

		/** @var InputfieldCheckboxes $field */
		$field = $this->modules->get('InputfieldCheckboxes');
		$field->attr('name', 'send_templates'); 	
		$field->label = $this->_('Add or remove field from templates'); 
		if(count($inTemplates)) {
			$field->description = $this->_('This field is in use on the checked templates below.') . ' ';
		} else {
			$field->description = $this->_('This field is not currently in use on any templates.') . ' ';
		}
		$multi = $this->field->type instanceof FieldtypeMulti; 
		$field->description .= $this->_('You may quickly add or remove this field from templates by checking or unchecking the relevant boxes and clicking save.'); // Description for template usage
		$field->notes .= $this->_('You will be asked to confirm additions and removals on the next screen after you save.'); 
		$field->table = true;
		$field->icon = 'check-square';
		$field->thead = 
			$this->_x('Name', 'usage-table-th') . '|' . 
			$this->_x('Label', 'usage-table-th') . '|' . 
			$this->_x('Populated pages', 'usage-table-th') . 
			($multi ? '|' . $this->_x('Rows of data', 'usage-table-th') : ''); 
		$numOmitted = 0;
		foreach($allTemplates as $template) {
			$label = $template->name;
			$longLabel = str_replace('|', ' ', $template->getLabel());
			$numRows = '';
			$numPages = '';
			if(isset($inTemplates[$template->id])) {
				$numPages = $this->wire('fields')->getNumPages($this->field, array('template' => $template));
				$numRows = $multi ? $this->wire('fields')->getNumRows($this->field, array('template' => $template)) : null; 
				$longLabel = "**[$longLabel](../template/edit?id=$template->id)**";
				$label = "**$label**";
			} else if($template->flags & Template::flagSystem) {
				if(!$this->wire('config')->advanced) {
					$numOmitted++;
					continue;
				}
			}
			$label = "$label|$longLabel|$numPages" . ($multi ? "|$numRows" : "");
			$field->addOption($template->id, $label); 
		}
		$field->attr('value', array_keys($inTemplates)); 
		$field->set('_valuePrevious', $field->attr('value')); 
		if($numOmitted) $field->notes .= ' ' . sprintf($this->_('%d system templates are not shown because advanced mode is off.'), $numOmitted); 
		$form->add($field);
		
		$field = $this->wire('modules')->get('InputfieldHidden');
		$field->attr('id+name', '_send_templates_changed'); 
		$field->attr('value', ''); 
		$form->add($field); 
	
		// --------------------------	
	
		/** @var InputfieldText $field */
		$field = $this->modules->get("InputfieldText");
		$field->attr('id+name', 'clone_field');
		$field->attr('value', '');
		$field->label = $this->_('Duplicate/clone this field?');
		$field->icon = 'copy';
		$field->description = $this->_('To clone this field, enter the name of the new field you wish to create.'); // Description for clone field
		$field->notes = $this->_('Note that you will be editing your cloned copy after submitting this form.'); 
		$field->collapsed = Inputfield::collapsedYes;
		$form->append($field);
		
		// --------------------------	
	
		/** @var InputfieldCheckbox $field */
		$field = $this->modules->get('InputfieldCheckbox');
		$field->attr('name', '_optimize');
		$field->label = $this->_('Check field data');
		$field->icon = 'medkit';
		$field->description = $this->_('Check the field for unused data or other possible optimizations. If any issues are found, you will have the opportunity to correct them from the Alert tab after saving.');
		$field->collapsed = Inputfield::collapsedBlank;
		$form->add($field);

		return $form; 
	}

	/**
	 * Build the "Access" tab for the field edit form, or null if not supported for Field
	 * 
	 * @return InputfieldWrapper|null
	 * 
	 */
	protected function buildEditFormAccess() {
		
		if($this->field->type instanceof FieldtypeFieldsetOpen) return null;

		/** @var Config $config */
		$config = $this->wire('config');

		/** @var InputfieldWrapper $tab */
		$tab = $this->wire(new InputfieldWrapper());
		$tab->attr('class', 'WireTab');
		$tab->attr('id', 'access');
		$tab->attr('title', $this->_x('Access', 'tab'));

		/** @var InputfieldRadios $f */
		$f = $this->wire('modules')->get('InputfieldRadios');
		$f->attr('name', 'useRoles');
		$f->label = $this->_('Do you want to manage access control for this field?');
		$f->description = $this->_('When enabled, you can limit view and edit access to this field by user role.');
		$f->icon = 'key';
		$f->addOption(1, $this->_x('Yes', 'access'));
		$f->addOption(0, $this->_x('No', 'access'));
		$f->attr('value', (int) $this->field->useRoles);
		$f->optionColumns = 1;
		$tab->add($f);

		/** @var InputfieldMarkup $f */
		$f = $this->modules->get("InputfieldMarkup");
		$f->attr('id', '_roles_editor');
		$f->showIf = 'useRoles=1';
		$f->label = $this->_('What roles can view and/or edit this field?');
		$f->description = $this->_('Users must also have page view or edit access on the page where the field exists before these permissions are applicable.'); // Access control description
		$f->notes = $this->_('When no boxes are checked, only superuser can view/edit the contents of the field.');

		/** @var MarkupAdminDataTable $table */
		$table = $this->modules->get("MarkupAdminDataTable");
		$table->setEncodeEntities(false);
		$table->headerRow(array(
			$this->_x('Role', 'access-thead'),
			$this->_x('View', 'access-thead'),
			$this->_x('Edit', 'access-thead'),
		));
		$checked = " checked='checked'";
		$disabled = " disabled='disabled'";
		$viewRoles = $this->field->viewRoles;
		$editRoles = $this->field->editRoles;
		$guestRole = $this->wire('roles')->get($config->guestUserRolePageID);
		$guestHasView = in_array($guestRole->id, $viewRoles);

		foreach($this->wire('roles') as $role) {
			/** @var Role $role */
			if($role->id == $config->superUserRolePageID) continue;
			$label = $role->name;
			$editable = $role->hasPermission('page-edit');
			if($role->id == $guestRole->id) $label .= ' <span class="detail">' . $this->_('(everyone)') . "</span>";
			$table->row(array(
				$label,
				("<input type='checkbox' id='viewRoles_$role->id' class='viewRoles' name='viewRoles[]' value='$role->id' " .
					(in_array($role->id, $viewRoles) || $guestHasView ? $checked : '') . " />"),
				("<input type='checkbox' id='editRoles_$role->id' class='editRoles' name='editRoles[]'  value='$role->id' " .
					(in_array($role->id, $editRoles) ? $checked : '') . " " . ($editable ? '' : $disabled) . " />")
			));
		}

		$f->value = $table->render();
		$tab->add($f);

		/** @var InputfieldCheckboxes $f */
		$f = $this->wire('modules')->get('InputfieldCheckboxes');
		$f->attr('name', '_accessFlags');
		$f->label = $this->_('Access toggles');
		$f->addOption(Field::flagAccessEditor, $this->_('Show field in page editor if viewable but not editable (user can see but not change)'));
		$f->addOption(Field::flagAccessAPI, $this->_('Make field value accessible from API even if not viewable (see below)*'));
		$f->showIf = "useRoles=1";
		$value = array();
		if($this->field->flags & Field::flagAccessAPI) $value[] = Field::flagAccessAPI;
		if($this->field->flags & Field::flagAccessEditor) $value[] = Field::flagAccessEditor;
		$f->attr('value', $value);
		$f->notes = '*' . sprintf($this->_('When a field is not viewable (on the front-end with output formatting on), the $page->%s value will be a blank version of the expected type, which prevents the value from being shown.'), $this->field->name) .
			' ' . $this->_('Check the box above to bypass this behavior, making the value always API accessible. This will leave you to do any access checking.') .
			' ' . $this->_('You can check view access from the API for this field like this:') .
			sprintf("\n\n`if(\$page->viewable('%s')) { ... }`", $this->field->name);
		$f->collapsed = Inputfield::collapsedBlank;
		$tab->add($f);

		return $tab;
	}


	/**
	 * Build the 'Advanced' field shown in the Field Edit form
	 * 
	 * @return InputfieldWrapper
	 *
	 */
	protected function ___buildEditFormAdvanced() {

		if($this->field->type) {
			$form = $this->field->type->getConfigAdvancedInputfields($this->field); 
		} else {
			$form = $this->wire(new InputfieldWrapper());
		} 
		
		/** @var InputfieldWrapper $form */
		$form->attr('id', 'advanced');
		$form->attr('class', 'WireTab');
		//$form->head = $this->_('Advanced options'); // Section header for 'Advanced'
		$form->attr('title', $this->_x('Advanced', 'tab')); 

		/** @var InputfieldIcon $field */
		$field = $this->modules->get("InputfieldIcon");
		$field->attr('name', 'icon');
		$field->attr('value', $this->field->icon);
		$field->icon = 'puzzle-piece';
		$field->label = $this->_('Icon');
		$field->description = $this->_('If you want to associate an icon with the field, select an icon below. Click the "Show all icons" link for visual selection.'); // Description for field tags
		//$field->collapsed = Inputfield::collapsedBlank;
		$form->prepend($field); 

		/** @var InputfieldText $field */
		$field = $this->modules->get("InputfieldText"); 
		$field->attr('name', 'tags'); 
		$field->attr('value', $this->field->tags); 
		$field->icon = 'tags';
		$field->label = $this->_('Tags');
		$field->description = $this->_('If you want to visually group this field with others in the fields list, enter a one-word tag. Enter the same tag on other fields you want to group with. To specify multiple tags, separate each with a space. Use of tags may be worthwhile if your site has a large number of fields.'); // Description for field tags
		$field->notes = 
			$this->_('Each tag must be one word (hyphenation is okay).') . " " . 
			$this->_('To make a tag collapsed in the fields list, prepend a hyphen to it, like this: -hello');
		$form->prepend($field); 
		
		return $form;
	}

	/**
	 * Save the results of a Field Edit
	 *
	 */
	public function ___executeSave() {

		$this->buildEditForm();

		if(!$this->input->post->submit_save_field) {
			$this->session->redirect("./"); 
		}

		if($this->fieldgroup) {
			$this->saveContext();
			return '';
		}
		
		$isNew = !$this->field->id;

		if($this->input->post->delete && $this->input->post->delete == $this->field->id && $this->field->numFieldgroups() == 0) {
			$this->session->CSRF->validate();
			$this->session->message($this->_('Deleted field') . " - {$this->field->name}"); // Message after deleting a field, followed by field name
			$this->fields->delete($this->field); 
			$this->fieldDeleted($this->field); 
			$this->session->redirect("./"); 
			return ''; 
		}

		if($isNew) {
			// when adding new field, make sure name is valid
			$name = $this->wire('input')->post('name');
			$name = $this->wire('sanitizer')->fieldName($name);
			if(!$this->isAllowedName($name)) return $this->wire('session')->redirect('./add');
			$type = $this->wire('input')->post('type');
			// check for clone option on create
			if(strpos($type, '_') === 0) {
				// clone existing field option is existing field name with "_" prepended
				$cloneFieldName = $this->wire('sanitizer')->fieldName(substr($type, 1));
				$cloneField = $this->wire('fields')->get($cloneFieldName);
				if($cloneField) {
					$this->field = $this->wire('fields')->clone($cloneField, $name);
					if($this->field) {
						// cloned
					} else {
						throw new WireException("Error cloning $cloneFieldName");
					}
				}
			}
		}

		try {
			$this->form->processInput($this->input->post); 
			$this->saveInputfields($this->form); 
		} catch(\Exception $e) {
			$this->error($e->getMessage()); 
		}
		
		$errors = array();

		if(!$this->field->name) {
			$errors[] = $this->_("Field name is required"); 
			
		} else if(!$this->field->type) {
			$errors[] = $this->_("Field type is required"); 
			
		} else if($isNew) {
			try { 
				$this->field->save();
				$this->session->message($this->_('Added Field') . " - {$this->field->name}"); 
				$this->fieldAdded($this->field); 
			} catch(\Exception $e) {
				$this->error($e->getMessage()); 
				if(!$this->field->id) $this->session->redirect("./"); 
			}
			$redirectURL = "edit?id={$this->field->id}";
			if(!$this->wire('input')->get('modal')) $redirectURL .= "#fieldtypeConfig";
			$this->session->redirect($redirectURL);
				
		} else {
			
			$removeKeys = $this->wire('input')->post('_remove_keys');
			if($removeKeys && count($removeKeys)) {
				$_removeKeys = $this->wire('session')->get($this, '_remove_keys');
				foreach($removeKeys as $xkey) {
					if(!in_array($xkey, $_removeKeys)) continue; // validate via session
					$this->field->remove($xkey);
					$this->message(sprintf($this->_('Removed unused property: %s'), $xkey));
				}
			}
			
			if($this->input->post('_remove_rows') == $this->field->id) {
				$table = $this->field->getTable();
				$sql = "DELETE $table FROM $table LEFT JOIN pages ON $table.pages_id=pages.id WHERE pages.id IS NULL ";
				$templatesWithoutField = $this->getTemplatesWithoutField($this->field);
				if(count($templatesWithoutField)) {
					$sql .= "OR pages.templates_id IN(" . implode(',', $templatesWithoutField) . ")";
				}
				$query = $this->database->prepare($sql);
				$query->execute();
				$cnt = $query->rowCount();
				if($cnt) $this->message($this->field->name . ": " . sprintf($this->_('Deleted %d orphaned rows'), $cnt));
			}

			$this->session->remove($this, 'optimize');
			if($this->input->post('_optimize')) $this->session->set($this, 'optimize', $this->field->id); 
		
			try { 
				$this->field->save();
				$this->message($this->_('Saved Field') . " - {$this->field->name}"); 
				$this->saveRemoveOverrides();
				$this->fieldSaved($this->field); 
				$select = $this->form->get("type"); 
				if($this->field->type->className() != $select->value) {
					$this->session->redirect("changeType?id={$this->field->id}&type={$select->value}"); 
				}	
			} catch(\Exception $e) {
				$this->error($e->getMessage()); 
			}
			
			$cloneField = $this->form->get('clone_field');
			$cloneName = $cloneField ? $cloneField->attr('value') : ''; 
			if($cloneName && $this->isAllowedName($cloneName)) {
				$clone = $this->fields->clone($this->field);
				if($clone && $clone->id) {
					$clone->name = $cloneName; 
					if($clone->label) $clone->label = $clone->label . ' ' . $this->_('(copy)'); 
					try {
						$this->fields->save($clone);
						$this->message($this->_('Cloned Field') . " - {$this->field->name} => {$clone->name}");
						if(!count($errors)) {
							if($clone->type instanceof FieldtypeFieldsetOpen) $clone->set('closeFieldID', null);
							$this->fieldAdded($clone);
							$this->wire('session')->message($this->_('You are now editing the field you cloned.'));
							$this->wire('session')->redirect("./edit?id=$clone->id#basics");
						}
					} catch(\Exception $e) {
						$errors[] = $e->getMessage(); 
					}
					// $this->listAfterSave = true;
				} else {
					$errors[] = ($this->_("Error creating clone of this field") . " - {$this->field->name}");
				}
			}
			
			if(count($errors)) {
				foreach($errors as $error) {
					$this->error($error);
				}
				if($isNew) return $this->executeAdd();
			}

			if($this->wire('input')->post('_send_templates_changed') == 'changed') {
				$sendTemplates = $this->form->get('send_templates');
				$value = $sendTemplates->attr('value');
				$valuePrevious = $sendTemplates->_valuePrevious;
				if($value != $valuePrevious) {
					$templateIDs = array();
					foreach($value as $templateID) {
						if(in_array($templateID, $valuePrevious)) continue;
						$templateIDs[] = (int) $templateID; // add
						// $this->message("Add to template " . $this->wire('templates')->get($templateID)); 
					}
					foreach($valuePrevious as $templateID) {
						if(in_array($templateID, $value)) continue;
						$templateIDs[] = $templateID * -1;  // remove
						// $this->message("Remove from template " . $this->wire('templates')->get($templateID)); 
					}
					if(count($templateIDs)) {
						$this->wire('session')->redirect("./send-templates?id={$this->field->id}&templates=" . implode(',', $templateIDs));
					}
				}
			}
		}
	
		if($this->listAfterSave) {
			$this->session->redirect("./");
		} else {
			$url = "edit?id={$this->field->id}";
			if($this->wire('input')->post("_optimize")) $url .= '#alert';
			$this->session->redirect($url);
		}
		
		return '';
	}

	/**
	 * Is the given field name allowed to use for a new or cloned field
	 * 
	 * @param string $name
	 * @return bool
	 * 
	 */
	protected function isAllowedName($name) {
		$_name = $name;
		$name = $this->wire('sanitizer')->fieldName($name);
		$allowed = false;
		$okay = array('files', 'datetime'); // field names that are okay, even if they collide with something else
		$error = '';
		if(empty($name)) {
			$error = $this->_('Field name is empty'); 
		} else if($name !== $_name) {
			$error = $this->_('Field names may only contain ASCII letters, digits or underscore.');
		} else if($this->wire('fields')->get($name)) {
			$error = sprintf($this->_('Field name "%s" is already in use'), $name);
		} else if(($this->wire($name) || $this->wire('fields')->isNative($name)) && !in_array($name, $okay)) {
			$error = sprintf($this->_('Field name "%s" is a reserved word'), $name);
		} else if(preg_match('/^(_|\d)/', $name)) {
			$error = $this->_('Field names may not begin with "_" or digits');
		} else if($this->wire('languages') && $this->wire('languages')->get($name)->id) {
			$error = $this->_('Field name may not be the same as a Language name');
		} else {
			$allowed = true; 
		}
		if($error) {
			if($this->form && $field = $this->form->getChildByName('name')) {
				$field->error($error); 
			} else {
				$this->error($error); 
			}
		}
		return $allowed; 
	}

	/**
	 * Save field in the context of a fieldgroup only
	 *
	 */
	protected function ___saveContext() {
		try {
			$oldContextArray = $this->fieldgroup->getFieldContextArray($this->field->id); 
			$this->form->processInput($this->input->post); 
			$this->saveInputfields($this->form); 
			$this->fields->saveFieldgroupContext($this->field, $this->fieldgroup, $this->contextNamespace); 
			$this->saveRemoveOverrides();
			$newContextArray = $this->fieldgroup->getFieldContextArray($this->field->id); 
			$diffContextArray = array_diff_assoc($newContextArray, $oldContextArray); 
			if(count($diffContextArray)) {
				$this->fieldChangedContext($this->field, $this->fieldgroup, $diffContextArray); 
			}
		} catch(\Exception $e) {
			$this->error($e->getMessage()); 
		}
		$this->session->redirect("edit" . 
			"?id={$this->field->id}" . 
			"&fieldgroup_id={$this->fieldgroup->id}" . 
			"&context_namespace=$this->contextNamespace" . 
			"&context_label=$this->contextLabel"
		); 
	}

	/**
	 * Save the results of a Field Edit, field by field
	 * 
	 * @param InputfieldWrapper $wrapper
	 *
	 */
	protected function saveInputfields(InputfieldWrapper $wrapper) {

		$languages = $this->wire('languages');
	
		foreach($wrapper->children() as $inputfield) {

			if($inputfield instanceof InputfieldWrapper && count($inputfield->children())) {
				$this->saveInputfields($inputfield); 
				continue; 
			}

			$name = $inputfield->name; 
			$value = $inputfield->value; 

			if(!$name || $inputfield instanceof InputfieldSubmit || $inputfield instanceof InputfieldMarkup) continue; 

			// see /core/Fieldtype.php for the inputfields that initiate the autojoin and global flags
			if($name == 'autojoin') {
				if(!$this->input->post->autojoin) $this->field->flags = $this->field->flags & ~Field::flagAutojoin; 
					else $this->field->flags = $this->field->flags | Field::flagAutojoin;
				continue; 

			} else if($name == 'global') {
				if(!$this->input->post->global) $this->field->flags = $this->field->flags & ~Field::flagGlobal; 
					else $this->field->flags = $this->field->flags | Field::flagGlobal;
				continue; 
			} else if($name == 'system' && $this->config->advanced) {
				if(!$this->input->post->system) {
					$this->field->flags = $this->field->flags | Field::flagSystemOverride;
					$this->field->flags = $this->field->flags & ~Field::flagSystem;
				} else {
					$this->field->flags = $this->field->flags | Field::flagSystem;
				}
				continue; 
			} else if($name == 'permanent' && $this->config->advanced) {
				if(!$this->input->post->permanent) $this->field->flags = $this->field->flags & ~Field::flagPermanent; 
					else $this->field->flags = $this->field->flags | Field::flagPermanent;
				continue; 
			}

			if($name == 'type' && $this->field->id) continue; // skip this change for existing fields
			if($name == 'type' && strpos($value, '_') === 0) continue; // skip clone type
			if($name == 'delete') continue; 
			if($name == 'fieldgroup_id') continue; 
			if($name == 'field_label') $name = 'label';
			if($name == 'id' && $this->field->id) continue;
			if($name == 'send_templates') continue;
			if($this->field->send_templates) unset($this->field->send_templates); // value was previously getting stored
			if($name == 'useRoles') $value = (bool) ((int) $value);
		
			// if adding new field or existing name has changed, check that its an allowed name
			if($name == 'name' && (!$this->field->id || $value !== $this->field->name)) {
				if(!$this->isAllowedName($value)) continue; 
			}

			$this->field->set($name, $value); 

			// account for languages, if used
			if($languages && $inputfield->getSetting('useLanguages')) {
				foreach($languages as $language) {
					$value = $inputfield->get("value" . $language->id);
					$this->field->set($name . $language->id, $value); 
				}
			}
		}
		
		if($this->field->useRoles) {
			// access control active for this field
			$this->field->viewRoles = $this->wire('input')->post('viewRoles');
			$this->field->editRoles = $this->wire('input')->post('editRoles');
			$accessFlags = $this->wire('input')->post('_accessFlags');
			if(!is_array($accessFlags)) $accessFlags = array();
			$flags = $this->field->flags;
			if(in_array(Field::flagAccessAPI, $accessFlags)) {
				$flags = $flags | Field::flagAccessAPI;
			} else {
				$flags = $flags & ~Field::flagAccessAPI;
			}
			if(in_array(Field::flagAccessEditor, $accessFlags)) {
				$flags = $flags | Field::flagAccessEditor;
			}  else {
				$flags = $flags & ~Field::flagAccessEditor;
			}
			if($flags != $this->field->flags) $this->field->flags = $flags;
		}
	}

	/**
	 * Saves the submitted checkboxes from the "Overrides" tab
	 * 
	 */
	protected function saveRemoveOverrides() {
		$removeContext = $this->wire('input')->post('_remove_context');
		if(empty($removeContext)) return;
		$contextArrays = array();
		$fieldgroups = array();
		foreach($removeContext as $value) {
			// FYI: "<input type='checkbox' name='_remove_context[]' value='$fieldgroup->id:$key' />"
			list($fieldgroupID, $property) = explode(':', $value);
			if(isset($fieldgroups[$fieldgroupID])) {
				$fieldgroup = $fieldgroups[$fieldgroupID];
			} else {
				$fieldgroup = $this->wire('fieldgroups')->get((int) $fieldgroupID);
				if(!$fieldgroup) continue;
				$fieldgroups[$fieldgroup->id] = $fieldgroup;
			}
			if(isset($contextArrays[$fieldgroup->id])) {
				$contextArray = $contextArrays[$fieldgroup->id];
			} else {
				$contextArray = $fieldgroup->getFieldContextArray($this->field->id);
			}
			if(strpos($property, 'flagsAdd-') === 0 || strpos($property, 'flagsDel-') === 0) {
				list($flagType, $flag) = explode('-', $property);
				$flag = (int) $flag;
				if(isset($contextArray[$flagType])) $contextArray[$flagType] = $contextArray[$flagType] & ~$flag;
			} else {
				unset($contextArray[$property]);
			}
			$this->message($this->_('Removed context override') . " (template=$fieldgroup->name, property=$property)");
			$contextArrays[$fieldgroup->id] = $contextArray;
		}
		foreach($contextArrays as $fieldgroupID => $contextArray) {
			$fieldgroup = $fieldgroups[$fieldgroupID];
			$fieldgroup->setFieldContextArray($this->field->id, $contextArray);
			$fieldgroup->saveContext();
		}
	}


	/**	
	 * Executed when a field type change is requested and provides an informative confirmation form
	 * 
	 * @return string
	 *
	 */
	public function ___executeChangeType() {

		$this->buildEditForm();

		$this->wire('processHeadline', sprintf($this->_('Change type for field: %s'), $this->field->name)); // Page headline when changing type
		$this->wire('breadcrumbs')->add(new Breadcrumb('./', 'Fields'))->add(new Breadcrumb("./edit?id={$this->field->id}", $this->field->name)); 

		if(!$this->input->get->type) $this->session->redirect('./'); 
		$newType = $this->wire('sanitizer')->name($this->input->get->type); 
		$newType = $this->wire('fieldtypes')->get($newType); 
		if(!$newType) $this->session->redirect('./'); 

		/** @var InputfieldForm $form */
		$form = $this->modules->get("InputfieldForm"); 
		$form->attr('method', 'post');
		$form->attr('action', 'saveChangeType'); 
		$form->head = sprintf($this->_('Change field type from "%1$s" to "%2$s"'), $this->field->type->longName, $newType->longName);  
		$form->description = $this->_("Please note that changing the field type alters the database schema. If the new fieldtype is not compatible with the old, or if it contains a significantly different schema, it is possible for data loss to occur. As a result, you are advised to backup the database before completing a field type change."); // Change field type description

		/** @var InputfieldCheckbox $f */
		$f = $this->modules->get("InputfieldCheckbox"); 
		$f->attr('name', 'confirm_type'); 
		$f->attr('value', $newType->className()); 
		$f->label = $this->_("Confirm field type change");
		$f->description = $this->_("If you are sure you want to change the field type, check the box below and submit this form."); // Confirm change description
		$form->append($f);
		
		$f = $this->modules->get("InputfieldCheckbox");
		$f->attr('name', 'keep_settings');
		$f->label = $this->_('Keep field settings?'); 
		$f->description = $this->_('Check this box to retain all the custom settings for this field (from the Details and Input tabs). This is desirable if the new field type has the same or similar configuration properties to the old field type. However, it can also result in unnecessary or redundant configuration data taking up space in the field. You can always analyze this later from: Advanced > Check field data.'); // Keep field settings description
		$f->attr('checked', 'checked'); 
		$f->showIf = 'confirm_type=' . $newType->className();
		$form->append($f); 

		/** @var InputfieldHidden $f */
		$f = $this->modules->get("InputfieldHidden"); 	
		$f->attr('name', 'id'); 
		$f->attr('value', $this->field->id); 
		$form->append($f); 	

		/** @var InputfieldSubmit $field */
		$field = $this->modules->get('InputfieldSubmit');
		$field->attr('name', 'submit_change_field_type'); 
		$form->append($field); 
	
		return $form->render();	
	}

	/**
	 * Save a changed field type
	 *
	 */
	public function ___executeSaveChangeType() {
		$this->buildEditForm();

		if(!$this->field || !$this->input->post->confirm_type) {
			$this->message($this->_("Field type change aborted")); 
			$this->session->redirect('./'); 
		}

		$type = $this->wire('sanitizer')->name($this->input->post->confirm_type); 
		if($type = $this->fieldtypes->get($type)) {
			$this->session->CSRF->validate();
			$this->message($this->_("Field type changed")); 
			$this->wire('fields')->addHookBefore('changeFieldtype', $this, 'hookFieldsChangeFieldtype'); 
			$this->field->type = $type; 
			$this->field->save();
			$this->fieldChangedType($this->field); 
		}
	
		$this->session->redirect("edit?id={$this->field->id}"); 	
	}
	
	public function hookFieldsChangeFieldtype(HookEvent $event) {
		// FYI: $field = $event->arguments(0); $keepSettings = $event->arguments(1); 
		if($this->input->post('keep_settings')) {
			$event->arguments(1, true); 
		}
	}


	/**
	 * Execute import 
	 * 
	 * @return string
	 * @throws WireException if given invalid import data
	 *
	 */
	public function ___executeImport() {
		
		$this->wire('processHeadline', $this->labels['import']);
		$this->wire('breadcrumbs')->add(new Breadcrumb('../', $this->moduleInfo['title']));
		
		require(dirname(__FILE__) . '/ProcessFieldExportImport.php');
		
		/** @var ProcessFieldExportImport $o */
		$o = $this->wire(new ProcessFieldExportImport());
		/** @var InputfieldForm $form */
		$form = $o->buildImport();
		
		return $form->render();
	}

	/**
	 * Execute export
	 * 
	 * @return string
	 *
	 */
	public function ___executeExport() {
		
		$this->wire('processHeadline', $this->labels['export']);
		$this->wire('breadcrumbs')->add(new Breadcrumb('../', $this->moduleInfo['title'])); 
		
		require(dirname(__FILE__) . '/ProcessFieldExportImport.php'); 
		/** @var ProcessFieldExportImport $o */
		$o = $this->wire(new ProcessFieldExportImport());
		/** @var InputfieldForm $form */
		$form = $o->buildExport();
		
		return $form->render();
	}

	/**
	 * Execute send field to template(s)
	 *
	 * @return string
	 * @throws WireException
	 *
	 */
	public function ___executeSendTemplates() {

		if(!$this->field || !$this->field->id) throw new WireException('No field specified');
		$templateIDs = $this->wire('input')->get('templates');
		if(!strlen($templateIDs)) throw new WireException("No templates specified"); 
		$templateIDs = explode(',', $templateIDs);
		
		$form = $this->wire('modules')->get('InputfieldForm'); 
		$form->attr('action', "./send-templates-save?id={$this->field->id}");
		
		$fields = array($this->field); 
		if($this->field->type instanceof FieldtypeFieldsetOpen) {
			$fieldsetClose = $this->wire('fields')->get($this->field->name . '_END'); 
			if($fieldsetClose) $fields[] = $fieldsetClose;
		}

		foreach($templateIDs as $templateID) {
			
			$templateID = (int) $templateID; 
			if(!$templateID) continue; 
			$template = $this->wire('templates')->get(abs($templateID)); 
			if(!$template) continue; 
			
			if($templateID < 0) {
				// remove from template
				
				$f = $this->wire('modules')->get('InputfieldCheckbox'); 
				$f->attr('name', "remove_{$this->field->id}_template_$template->id"); 
				$f->attr('value', $this->field->id); 
				$f->icon = 'minus-circle';
				$f->label = sprintf($this->_('Remove field "%1$s" from template "%2$s"'), $this->field->name, $template->name);
				$f->label2 = sprintf($this->_('Confirm removal of field "%1$s" from template "%2$s"'), $this->field->name, $template->name); 
				$numRows = $this->wire('fields')->getNumRows($this->field, array('template' => $template));
				if($numRows) {
					$f->description = $this->_('WARNING: This will result in data associated with the field being permanently deleted.') . ' '; // Warning for field deletion
					$f->notes = sprintf($this->_n('This will also delete %d row of data.', 'This will also delete %d rows of data.', $numRows), $numRows);  // Notes for field deletion
					if($numRows > 100) $f->notes .= ' ' . $this->_('After you submit, be patient as it may take some time for this operation to complete.'); // Additional notes for large quantity field deletion
				} else {
					$f->description = '';
					$f->notes = $this->_('There do not appear to be any rows of data associated with this field/template combination');
				}
				$f->description .= $this->_('Please check the box to confirm.');
				$form->add($f); 

			} else {
				// add to template

				foreach($fields as $field) {
					
					$f = $this->wire('modules')->get('InputfieldSelect');
					$f->attr('name', "add_{$field->id}_template_$templateID");
					$f->label = sprintf($this->_('Template: %s'), $template->name);
					$f->label = sprintf($this->_('Add field "%1$s" to template "%2$s"'), $field->name, $template->name);
					$f->description = sprintf($this->_('Where do you want to add "%s?"'), $field->name);
					$f->icon = 'plus-circle';
					$beforeLabel = $this->_('Before %s');
					$afterLabel = $this->_('After %s');
					$value = 0;
					if(!count($template->fieldgroup)) {
						$f->addOption('-', $this->_('Add as first field'));
						$value = '-';
					}
					$f->addOption(0, $this->_('Do not add'));

					foreach($template->fieldgroup as $field) { // overwrite of $field is OK
						if(strpos((string) $field->type, 'FieldtypeFieldset') === 0) continue;
						$f->addOption($field->name, array(
							"-$field->id" => sprintf($beforeLabel, $field->name),
							"$field->id"  => sprintf($afterLabel, $field->name)
						));
						$value = "$field->id";
					}

					$f->attr('value', $value);
					$form->add($f); 
				}
			}
		}
		
		$f = $this->wire('modules')->get('InputfieldSubmit');
		$form->add($f); 
		
		$this->headline($this->_('Add or remove from template(s)')); 
		$this->breadcrumb("./edit?id={$this->field->id}", $this->field->name); 
		$out = "<h2>" . sprintf($this->_('Please review and adjust or confirm your changes'), $this->field->name) . "</h2>";
		$out .= $form->render();
		
		return $out; 
	}

	/**
	 * Process the form from executeSendTemplates and redirect back to editor
	 *
	 */
	public function ___executeSendTemplatesSave() {
		
		if(!$this->field || !$this->field->id) throw new WireException('No field specified');
		$this->wire('session')->CSRF->validate();
	
		$isFieldset = false;
		$fields = array($this->field);
		if($this->field->type instanceof FieldtypeFieldsetOpen) {
			$fieldsetClose = $this->wire('fields')->get($this->field->name . '_END');
			if($fieldsetClose) {
				$fields[] = $fieldsetClose;
				$isFieldset = true; 
			}
		}
		
		// first handle additions, since it's possible for removals to take a long time or remote chance of timeout
		foreach($this->wire('templates') as $template) {
	
			foreach($fields as $key => $field) {
					
				// usually just 1 element in $fields, except when $field is a FieldtypeFieldsetOpen in which case 
				// there is also the corresponding FieldtypeFieldsetClose
				$isFieldsetEnd = $isFieldset && $key > 0;
				
				$addFieldID = $this->wire('input')->post("add_{$field->id}_template_$template->id");
				if(!$addFieldID) continue;
				if(!$this->allowFieldInTemplate($field, $template)) continue;
				if($isFieldsetEnd && $template->fieldgroup->hasField($field)) continue;
				if($addFieldID === '-' && !$isFieldsetEnd) {
					// add as first field
					$template->fieldgroup->add($field);
					continue;
				}
				
				$before = substr($addFieldID, 0, 1) == '-';
				$addFieldID = (int) ltrim($addFieldID, '-');
				$fieldNames = array();

				foreach($template->fieldgroup as $f) {
					$fieldName = $f->name;
					$fieldNames[$fieldName] = $fieldName;
					
					if($f->id != $addFieldID) continue;
					
					if($isFieldsetEnd && !isset($fieldNames[$fields[0]->name])) {
						// force to insert after opening fieldset
						$before = false;
						$addFieldID = $fields[0]->id;
						continue;
					}

					if($before) {
						$template->fieldgroup->insertBefore($field, $f);
						$this->message(sprintf($this->_('Added field "%1$s" to template "%2$s" before "%3$s"'),
							$field->name, $template->name, $f->name));
					} else {
						$template->fieldgroup->insertAfter($field, $f);
						$this->message(sprintf($this->_('Added field "%1$s" to template "%2$s" after "%3$s"'),
							$field->name, $template->name, $f->name));
					}
				}
			}
			$template->fieldgroup->save();
		}
		
		// next handle removals
		foreach($this->wire('templates') as $template) {
			$removeFieldID = (int) $this->wire('input')->post("remove_{$this->field->id}_template_$template->id");
			if($removeFieldID && $removeFieldID === (int) $this->field->id) {
				$template->fieldgroup->remove($this->field);
				if($this->field->type instanceof FieldtypeFieldsetOpen) {
					$fieldsetClose = $this->wire('fields')->get($this->field->name . '_END'); 
					if($fieldsetClose) $template->fieldgroup->remove($fieldsetClose); 
				}
				try {
					$template->fieldgroup->save();
					$this->message(sprintf($this->_('Removed field "%1$s" from template "%2$s"'), $this->field->name, $template->name));
				} catch(\Exception $e) {
					$this->error(
						sprintf($this->_('Error removing "%1$s" from template "%2$s"'), $this->field->name, $template->name) . " - " . 
							$e->getMessage(), 
						Notice::log
					); 
				}
			}
		}
		
		$this->wire('session')->redirect("./edit?id={$this->field->id}"); 
	}

	/**
	 * Return array of human readable changes as a result of field context
	 *
	 * @param InputfieldWrapper $form
	 * @param Field $field
	 * @param Fieldgroup $fieldgroup
	 * @return array
	 *
	 */
	protected function getContextChanges(InputfieldWrapper $form, Field $field, Fieldgroup $fieldgroup) {

		if(!$fieldgroup) return array();

		$contextArray = $fieldgroup->getFieldContextArray($field->id, $this->contextNamespace);
		if(!count($contextArray)) return array();

		$fieldOriginal = $this->wire('fields')->get($field->id);
		$changes = array();

		$labels = array(
			'on' => $this->_('On'),
			'off' => $this->_('Off'),
			'all' => $this->_('All'), 
			'blank' => $this->_('[blank]'),
			'collapsed' => $this->_('Field presentation/visibility'),
			'viewRoles' => $this->_('Roles allowed to view this field'),
			'editRoles' => $this->_('Roles allowed to edit this field'),
			'flagsAddAccess' => $this->_('Enable access control'),
			'flagsDelAccess' => $this->_('Remove access control'),
			'flagsAddAccessAPI' => $this->_('Make field value API accessible even when not viewable'),
			'flagsDelAccessAPI' => $this->_('Make field value NOT API accessible when not viewable'),
			'flagsAddAccessEditor' => $this->_('Show in page editor even if not editable'),
			'flagsDelAccessEditor' => $this->_('Hide in page editor when not editable'),
		);

		$flagsAddLookup = array(
			Field::flagAccess => 'flagsAddAccess',
			Field::flagAccessAPI =>'flagsAddAccessAPI',
			Field::flagAccessEditor =>'flagsAddAccessEditor',
		);
		$flagsDelLookup = array(
			Field::flagAccess => 'flagsDelAccess',
			Field::flagAccessAPI => 'flagsDelAccessAPI',
			Field::flagAccessEditor =>'flagsDelAccessEditor',
		);

		foreach($contextArray as $key => $value) {
			$name = $key;
			$formFieldName = $key;
			if($formFieldName == 'label') $formFieldName = 'field_label';
			$formField = $form->getChildByName($formFieldName);
			$originalValue = $fieldOriginal->$key;
			if($formField) {
				if($formField instanceof InputfieldSelect) {
					$options = $formField->getOptions();
					if(is_array($value)) foreach($value as $k => $v) {
						if(isset($options[$v])) $value[$k] = $options[$v];
					} else if(isset($options[$value])) {
						$value = $options[$value];
					}
					if(is_array($originalValue)) foreach($originalValue as $k => $v) {
						if(isset($options[$v])) $originalValue[$k] = $options[$v];
					} else if(isset($options[$originalValue])) {
						$originalValue = $options[$originalValue];
					}
				} else if($formField instanceof InputfieldCheckbox) {
					$value = $value ? $labels['on'] : $labels['off'];
					$originalValue = $originalValue ? $labels['on'] : $labels['off'];
				}
			}
			if(is_array($value) && ($key == 'viewRoles' || $key == 'editRoles')) {
				foreach($value as $k => $v) {
					$v = $this->wire('roles')->get((int) $v);
					$roleName = $v->name == 'guest' ? $labels['all'] : $v->name;
					if($v) $value[$k] = $roleName;
				}
			}
			if(is_array($originalValue) && ($key == 'viewRoles' || $key == 'editRoles')) {
				foreach($originalValue as $k => $v) {
					$v = $this->wire('roles')->get((int) $v);
					$roleName = $v->name == 'guest' ? $labels['all'] : $v->name;
					if($v) $originalValue[$k] = $roleName;
				}
			}
			$valueStr = $value;
			$originalValueStr = $originalValue;
			if(is_array($originalValueStr)) $originalValueStr = implode(', ', $originalValueStr);
			if(is_array($valueStr)) $valueStr = implode(', ', $valueStr);
			if($key == 'flagsAdd' || $key == 'flagsDel') {
				$originalValueStr = ((int) $originalValue) ? $labels['on'] : $labels['off'];
				$valueStr = ((int) $value) ? $labels['on'] : $labels['off'];
			}
			$originalValueStr = str_replace('|', ' ', $originalValueStr);
			$valueStr = str_replace('|', ' ', $valueStr);
			$change = array(
				"name" => $name, 
				"value" => $valueStr,
				"originalValue" => $originalValueStr
			);

			if(isset($labels[$key])) {
				$label = $labels[$key];
			} else if($formField) {
				$label = $formField->label;
			} else if($key == 'flagsAdd') {
				foreach($flagsAddLookup as $flag => $lookupKey) {
					if($value & $flag) {
						$change["label"] = $labels[$lookupKey];
						$changes["flagsAdd-$flag"] = $change;
					}
				}
				continue;
			} else if($key == 'flagsDel') {
				foreach($flagsDelLookup as $flag => $lookupKey) {
					if($value & $flag) {
						$change["label"] = $labels[$lookupKey];
						$changes["flagsDel-$flag"] = $change;
					}
				}
				continue;
			} else if($this->wire('languages') && preg_match('/^(label|description|notes)(\d+)$/', $key, $matches)) {
				$label = ucfirst($matches[1]) . ' (' . $this->wire('languages')->get((int) $matches[2])->get('title|name') . ')';
			} else {
				$label = $key;
			}
			$change["label"] = $label;
			$changes[$key] = $change;
		}
		return $changes;
	}


	/**
	 * Build a form allowing configuration of this Module
	 * 
	 * @param array $data
	 * @return InputfieldWrapper
	 *
	 */
	public function getModuleConfigInputfields(array $data) {

		$fields = $this->wire(new InputfieldWrapper());
		$modules = $this->wire('modules');

		$field = $modules->get("InputfieldCheckbox");
		$field->attr('name', 'listAfterSave');
		$field->attr('value', 1); 
		$field->attr('checked', empty($data['listAfterSave']) ? '' : 'checked'); 
		$field->label = $this->_("Return to fields list after saving a field?");
		$field->description = $this->_("By default, you will remain in the fields editor after saving a field. If you want to instead return to the fields list, check this box.");
		$fields->append($field);

		return $fields;
	}

	/**
	 * Return the current field being edited, or NULL if it hasn't been set
	 *
	 * $this->field is set by buildEditForm
	 *
	 * @return Field|null
	 *
	 */
	public function getField() {
		return $this->field; 
	}

	/**
	 * For hooks to listen to when a new field is added
	 * 
	 * @param Field $field
	 *
	 */
	public function ___fieldAdded(Field $field) { }

	/**
	 * For hooks to listen to when any field is saved
	 * 
	 * @param Field $field
	 *
	 */
	public function ___fieldSaved(Field $field) { }

	/**
	 * For hooks to listen to when a field is deleted
	 * 
	 * @param Field $field
	 *
	 */
	public function ___fieldDeleted(Field $field) { }

	/**
	 * For hooks to listen to when a field type changes
	 * 
	 * @param Field $field
	 *
	 */
	public function ___fieldChangedType(Field $field) { }

	/**
	 * Hook called when field context is changed and saved
	 * 
	 * @param Field $field
	 * @param Fieldgroup $fieldgroup
	 * @param array $changes Indexed by property name
	 * 
	 */
	public function ___fieldChangedContext(Field $field, Fieldgroup $fieldgroup, array $changes) { }

	/**
	 * For hooks to modify if they want to specific prevent a field from being added to a template from here
	 * 
	 * @param Field $field
	 * @param Template $template
	 * @return bool
	 *
	 */
	public function ___allowFieldInTemplate(Field $field, Template $template) {
		if($field && $template) {}
		return true; 
	}

}

