<?php namespace ProcessWire;

/**
 * ProcessWire FieldtypeFieldsetOpen and InputfieldFieldsetOpen
 *
 * Provides a Fieldtype and Inputfield for opening a Fieldset. 
 *
 * For documentation about the fields used in this class, please see:  
 * /wire/core/Fieldtype.php
 * 
 * ProcessWire 3.x, Copyright 2016 by Ryan Cramer
 * https://processwire.com
 *
 *
 */

class InputfieldFieldsetOpen extends InputfieldWrapper { } 

class FieldtypeFieldsetOpen extends Fieldtype {

	/**
	 * Appended to the name of a 'Close' version of a FieldsetOpen
	 *
	 */
	const fieldsetCloseIdentifier = '_END';

	public static function getModuleInfo() {
		return array(
			'title' => 'Fieldset (Open)',
			'version' => 101,
			'summary' => 'Open a fieldset to group fields. Should be followed by a Fieldset (Close) after one or more fields.',
			'permanent' => true, 
			);
	}

	/**
	 * Setup hooks to minotor when Fields are added and deleted
	 *
	 * But only do it once so that we don't have the same hooks setup when there are multiple Fieldsets
	 *
	 */
	public function init() {
		if($this->process == 'ProcessField' || $this->process = 'ProcessTemplate') {
			$this->addProcessHooks();
		}
	}

	/**
	 * Add hooks to ProcessField or ProcessTemplate, called only when applicable
	 * 
	 */
	protected function addProcessHooks() {
		$this->addHook('ProcessField::fieldAdded', $this, 'hookFieldAdded');
		$this->addHook('ProcessField::fieldDeleted', $this, 'hookFieldDeleted');
		$this->addHook('ProcessField::allowFieldInTemplate', $this, 'hookAllowFieldInTemplate');
		$this->addHook('ProcessTemplate::fieldAdded', $this, 'hookTemplateFieldAdded');
		$this->addHook('ProcessTemplate::fieldRemoved', $this, 'hookTemplateFieldRemoved'); 
	}

	public function sanitizeValue(Page $page, Field $field, $value) {
		return null;
	}

	public function getInputfield(Page $page, Field $field) {
		$inputfield = $this->wire(new InputfieldFieldsetOpen());
		$inputfield->class = $this->className() . ' InputfieldFieldset';
		return $inputfield; 
	}

	public function savePageField(Page $page, Field $field) {
		return true; 
	}

	public function ___getConfigInputfields(Field $field) {
		$inputfields = parent::___getConfigInputfields($field);
		return $inputfields; 
	}

	public function ___getConfigAdvancedInputfields(Field $field) {
		$inputfields = parent::___getConfigAdvancedInputfields($field); 
		// these really aren't applicable with fieldsets, so remove them
		$inputfields->remove($inputfields->get('autojoin')); 
		$inputfields->remove($inputfields->get('global')); 
		return $inputfields; 
	}

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

	public function loadPageField(Page $page, Field $field) {
		return '';
	}

	public function getLoadQuery(Field $field, DatabaseQuerySelect $query) {
		return $query;
	}
	
	/**
	 * Is given field a fieldset opener?
	 * 
	 * For hooks to share in determining if this is a field they want to operate on
	 * 
	 * @param Field $field
	 * @return bool
	 *
	 */
	protected function isFieldset(Field $field) {
		return $field->type instanceof FieldtypeFieldsetOpen && !($field->type instanceof FieldtypeFieldsetClose);
	}

	/**
	 * Check that Fieldgroup has matching open/close fieldsets and in correct order
	 * 
	 * @param Fieldgroup $fieldgroup
	 * @return bool
	 * @since 3.0.164
	 * 
	 */
	public function checkFieldgroupFieldsets(Fieldgroup $fieldgroup) {
		
		list($openers, $closers, $isChanged) = array(array(), array(), false);

		foreach($fieldgroup as $field) {
			/** @var Fieldtype $fieldtype */
			if(!$field->type instanceof FieldtypeFieldsetOpen) continue;
			if($field->type instanceof FieldtypeFieldsetClose) {
				if(!strpos($field->name, self::fieldsetCloseIdentifier)) continue;
				$name = substr($field->name, 0, -1 * strlen(self::fieldsetCloseIdentifier));
				$closers[$name] = $field;
			} else if(!isset($closers[$field->name])) {
				// ensure opener comes before closer, otherwise only closer remains
				$openers[$field->name] = $field;
			}
		}

		if(count($openers) === count($closers)) return true;

		foreach($openers as $name => $opener) {
			/** @var Field $opener */
			if(isset($closers[$name])) continue;
			$closer = $this->getFieldsetCloseField($opener);
			if($closer) {
				$fieldgroup->insertAfter($closer, $opener);
			} else {
				$fieldgroup->remove($opener);
			}
			$isChanged = true;
		}
		
		foreach($closers as $name => $closer) {
			/** @var Field $closer */
			if(isset($openers[$name])) continue;
			$opener = $this->getFieldsetOpenField($closer); 
			if($opener) {
				$fieldgroup->insertBefore($opener, $closer); 
			} else {
				$fieldgroup->remove($closer);
			}
			$isChanged = true;
		}
		
		return $isChanged;
	}

	/**
	 * Get the Field that closes/terminates the given Fieldset field
	 * 
	 * @param Field $field
	 * @param bool $createIfNotExists Create it if it doesn't already exist?
	 * @return null|Field of type FieldtypeFieldsetClose
	 * 
	 */
	public function getFieldsetCloseField(Field $field, $createIfNotExists = false) {
		
		if(!$this->isFieldset($field)) return null;
	
		$name = $field->name . self::fieldsetCloseIdentifier;
		$closer = $this->wire('fields')->get($name); 
		
		if(!$closer) {
			$closeFieldID = (int) $field->get('closeFieldID');
			if($closeFieldID) {
				$closer = $this->wire('fields')->get($closeFieldID); 
			}
		}
		
		if(!$closer && $createIfNotExists) {
			$closer = $this->wire(new Field());
			$closer->type = $this->wire(new FieldtypeFieldsetClose());
			$closer->name = $name;
			$closer->label = "Close an open fieldset";
			$closer->description = 
				"This field was added automatically to accompany fieldset '$field'.  " . 
				"It should be placed in your template/fieldgroup wherever you want the fieldset to end.";
			$closer->set('openFieldID', $field->id); 
			$closer->save();
		} 
		
		if($closer && !$field->get('closeFieldID')) {
			$field->set('closeFieldID', $closer->id); 
			$field->save();
		}
		
		return $closer; 
	}
	
	/**
	 * Get the Field that opens the given FieldsetClose field
	 *
	 * @param Field $field
	 * @return null|Field of type FieldtypeFieldsetOpen, if found
	 * @since 3.0.164
	 *
	 */
	public function getFieldsetOpenField(Field $field) {

		if(!$field->type instanceof FieldtypeFieldsetClose) return null;
		if(!strpos($field->name, self::fieldsetCloseIdentifier)) return null;

		$name = substr($field->name, 0, -1 * strlen(self::fieldsetCloseIdentifier));
		$opener = $this->wire()->fields->get($name);

		if($opener) return $opener;

		foreach($this->wire()->fields as $f) {
			if(!$f->type instanceof FieldtypeFieldsetOpen) continue;
			$closeFieldID = (int) $f->get('closeFieldID');
			if($closeFieldID != $field->id) continue;
			$opener = $f;
			break;
		}

		return $opener;
	}

	/**
	 * Hook executed when field is added via ProcessField
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookFieldAdded($event) {
		$field = $event->arguments[0]; 
		if(!$this->isFieldset($field)) return; 
		$closer = $this->getFieldsetCloseField($field, true); 
		$this->message("Also added field '$closer', to accompany '$field'"); 
	}

	/**
	 * Hook executed when field is deleted via ProcessField
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookFieldDeleted($event) {

		$field = $event->arguments[0]; 
		if(!$this->isFieldset($field)) return; 

		$closer = $this->getFieldsetCloseField($field); 
		
		if($closer) {
			$this->wire('fields')->delete($closer);
			$this->message("Delete also issued for field '$closer'");
		}
	}

	/**
	 * Hook executed when field is added to a template via ProcessTemplate
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookTemplateFieldAdded($event) {
		/** @var Field $field */
		/** @var Template $template */
		list($field, $template) = $event->arguments; 
		if(!$this->isFieldset($field)) return; 
		$closer = $this->getFieldsetCloseField($field); 
		if(!$closer) return; 
		if(!$template->fieldgroup->hasField($closer)) {
			$template->fieldgroup->add($closer);
			$this->message("Also added field '$closer', which should be placed where you want to close fieldset '$field'");
		}
	}

	/**
	 * Hook executed when field is removed from a template via ProcessTemplate
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookTemplateFieldRemoved($event) {
		list($field, $template) = $event->arguments; 
		if(!$this->isFieldset($field)) return; 
		$closer = $this->getFieldsetCloseField($field); 
		if(!$closer) return; 
		$template->fieldgroup->remove($closer); 
		$this->message("Also removed '$closer', which accompanies '$field'"); 
	}

	/**
	 * Hook to ProcessField::allowFieldInTemplate
	 * 
	 * @param HookEvent $event
	 * 
	 */
	public function hookAllowFieldInTemplate(HookEvent $event) { 
		/** @var Field $field */
		// $field = $event->arguments(0);	
		/** @var Template $template */
		// $template = $event->arguments(1);
	}

	/**
	 * Hook called by Fields::save() after a field using this type has been renamed
	 *
	 * Note that PW already takes care of renaming the field_[name] table.
	 * Most Fieldtypes don't need to do anything here, but this exists just in case.
	 *
	 * #pw-internal
	 *
	 * @param Field $field
	 * @param string $prevName Previous name (current name can be found in $field->name)
	 *
	 */
	public function ___renamedField(Field $field, $prevName) {
		// avoid ending up in infinite loop, since FieldtypeFieldsetClose extends this
		if($this instanceof FieldtypeFieldsetClose) return;
		// rename the _END field to match this one
		$fields = $this->wire()->fields;
		$oldName = $prevName . self::fieldsetCloseIdentifier;
		$newName = $field->name . self::fieldsetCloseIdentifier;
		$closer = $this->getFieldsetCloseField($field); 
		if(!$closer) $closer = $fields->get($oldName);
		if($closer && $closer->name != $newName) {
			$closer->name = $newName;
			$fields->save($closer);
		}
		parent::___renamedField($field, $prevName); 
	}
}


