<?php namespace ProcessWire;

/**
 * ProcessWire Fieldgroups
 *
 * #pw-summary Maintains collections of Fieldgroup object instances and represents the `$fieldgroups` API variable.
 * #pw-body For full details on all methods available in a Fieldgroup, be sure to also see the `WireArray` class.
 * #pw-var $fieldgroups
 * 
 * ProcessWire 3.x, Copyright 2016 by Ryan Cramer
 * https://processwire.com
 * 
 * @method int saveContext(Fieldgroup $fieldgroup)
 * @method array getExportData(Fieldgroup $fieldgroup)
 * @method array setImportData(Fieldgroup $fieldgroup, array $data)
 *
 *
 */

class Fieldgroups extends WireSaveableItemsLookup {

	/**
	 * Instances of FieldgroupsArray
	 * 
	 * @var FieldgroupsArray
	 *
	 */
	protected $fieldgroupsArray; 
	
	public function init() {
		$this->fieldgroupsArray = $this->wire(new FieldgroupsArray());
		$this->load($this->fieldgroupsArray);
	}

	/**
	 * Get the DatabaseQuerySelect to perform the load operation of items
	 *
	 * @param Selectors|string|null $selectors Selectors or a selector string to find, or NULL to load all. 
	 * @return DatabaseQuerySelect
	 *
	 */
	protected function getLoadQuery($selectors = null) {
		$query = parent::getLoadQuery($selectors); 
		$lookupTable = $this->wire('database')->escapeTable($this->getLookupTable()); 
		$query->select("$lookupTable.data"); // QA
		return $query; 
	}

	/**
	 * Load all the Fieldgroups from the database
	 *
	 * The loading is delegated to WireSaveableItems.
	 * After loaded, we check for any 'global' fields and add them to the Fieldgroup, if not already there.
	 *
	 * @param WireArray $items
	 * @param Selectors|string|null $selectors Selectors or a selector string to find, or NULL to load all. 
	 * @return WireArray Returns the same type as specified in the getAll() method.
	 *
	 */
	protected function ___load(WireArray $items, $selectors = null) {
		$items = parent::___load($items, $selectors); 	
		return $items; 
	}

	/**
	 * Per WireSaveableItems interface, return all available Fieldgroup instances
	 * 
	 * @return FieldgroupsArray
	 *
	 */
	public function getAll() {
		return $this->fieldgroupsArray;
	}

	/**
	 * Per WireSaveableItems interface, create a blank instance of a Fieldgroup
	 * 
	 * @return Fieldgroup
	 *
	 */
	public function makeBlankItem() {
		return $this->wire(new Fieldgroup()); 
	}

	/**
	 * Per WireSaveableItems interface, return the name of the table that Fieldgroup instances are stored in
	 * 
	 * @return string
	 *
	 */
	public function getTable() {
		return 'fieldgroups';
	}

	/**
	 * Per WireSaveableItemsLookup interface, return the name of the table that Fields are linked to Fieldgroups 
	 * 
	 * @return string
	 *
	 */
	public function getLookupTable() {
		return 'fieldgroups_fields';
	}

	/**
	 * Get the number of templates using the given fieldgroup. 
	 *
	 * Primarily used to determine if the Fieldgroup is deleteable. 
	 *
	 * @param Fieldgroup $fieldgroup
	 * @return int
	 *
	 */
	public function getNumTemplates(Fieldgroup $fieldgroup) {
		return count($this->getTemplates($fieldgroup)); 
	}

	/**
	 * Given a Fieldgroup, return a TemplatesArray of all templates using the Fieldgroup
	 *
	 * @param Fieldgroup $fieldgroup
	 * @return TemplatesArray
	 *
	 */
	public function getTemplates(Fieldgroup $fieldgroup) {
		$templates = $this->wire(new TemplatesArray());
		foreach($this->wire('templates') as $tpl) {
			if($tpl->fieldgroup->id == $fieldgroup->id) $templates->add($tpl); 
		}
		return $templates; 
	}

	/**
	 * Save the Fieldgroup to DB
	 *
	 * If fields were removed from the Fieldgroup, then track them down and remove them from the associated field_* tables
	 *
	 * @param Saveable $item Fieldgroup to save
	 * @return bool True on success, false on failure
	 * @throws WireException
	 *
	 */
	public function ___save(Saveable $item) {

		$database = $this->wire('database');
		
		/** @var Fieldgroup $item */

		if($item->id && $item->removedFields) {

			foreach($this->wire('templates') as $template) {
				if($template->fieldgroup->id !== $item->id) continue; 
				foreach($item->removedFields as $field) {
					// make sure the field is valid to delete from this template
					$error = $this->isFieldNotRemoveable($field, $item, $template);
					if($error !== false) throw new WireException("$error Save of fieldgroup changes aborted.");
					if($field->type) $field->type->deleteTemplateField($template, $field); 
					$item->finishRemove($field); 
				}
			}

			$item->resetRemovedFields();
		}

		$contextData = array();
		if($item->id) { 
			// save context data
			$query = $database->prepare("SELECT fields_id, data FROM fieldgroups_fields WHERE fieldgroups_id=:item_id"); 
			$query->bindValue(":item_id", (int) $item->id, \PDO::PARAM_INT); 
			$query->execute();
			/** @noinspection PhpAssignmentInConditionInspection */
			while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
				$contextData[$row['fields_id']] = $row['data'];
			}
			$query->closeCursor();
		}

		$result = parent::___save($item); 

		if(count($contextData)) {
			// restore context data
			foreach($contextData as $fields_id => $data) {
				$fieldgroups_id = (int) $item->id; 
				$fields_id = (int) $fields_id; 
				$query = $database->prepare("UPDATE fieldgroups_fields SET data=:data WHERE fieldgroups_id=:fieldgroups_id AND fields_id=:fields_id"); // QA
				$query->bindValue(":data", $data, \PDO::PARAM_STR); 
				$query->bindValue(":fieldgroups_id", $fieldgroups_id, \PDO::PARAM_INT);
				$query->bindValue(":fields_id", $fields_id, \PDO::PARAM_INT); 
				$query->execute();
			}
		}

		return $result;
	}

	/**
	 * Delete the given fieldgroup from the database
	 *
	 * Also deletes the references in fieldgroups_fields table
	 *
	 * @param Saveable|Fieldgroup $item
	 * @return bool
	 * @throws WireException
	 *
	 */
	public function ___delete(Saveable $item) {

		$templates = array();
		foreach($this->wire('templates') as $template) {
			if($template->fieldgroup->id == $item->id) $templates[] = $template->name; 
		}

		if(count($templates)) {
			throw new WireException(
				"Can't delete fieldgroup '{$item->name}' because it is in use by template(s): " . 
				implode(', ', $templates)
			); 
		}

		return parent::___delete($item); 
	}

	/**
	 * Delete the entries in fieldgroups_fields for the given Field
	 *
	 * @param Field $field
	 * @return bool
	 *
	 */
	public function deleteField(Field $field) {
		$database = $this->wire('database'); 
		$query = $database->prepare("DELETE FROM fieldgroups_fields WHERE fields_id=:fields_id"); // QA
		$query->bindValue(":fields_id", $field->id, \PDO::PARAM_INT);
		$result = $query->execute();
		return $result;
	}

	/**
	 * Create and return a cloned copy of this item
	 *
	 * If the new item uses a 'name' field, it will contain a number at the end to make it unique
	 *
	 * @param Saveable $item Item to clone
	 * @param string $name
	 * @return bool|Saveable $item Returns the new clone on success, or false on failure
	 * @return Saveable|Fieldgroup
	 *
	 */
	public function ___clone(Saveable $item, $name = '') {
		return parent::___clone($item, $name);
		// @TODO clone the field context data
		/*
		$id = $item->id; 
		$item = parent::___clone($item);
		if(!$item) return false;
		return $item; 	
		*/
	}

	/**
	 * Save contexts for all fields in the given fieldgroup 
	 * 
	 * @param Fieldgroup $fieldgroup
	 * @return int Number of field contexts saved
	 * 
	 */
	public function ___saveContext(Fieldgroup $fieldgroup) {
		$contexts = $fieldgroup->getFieldContextArray();
		$numSaved = 0;
		foreach($contexts as $fieldID => $context) {
			$field = $fieldgroup->getFieldContext((int) $fieldID); 
			if(!$field) continue;
			if($this->wire('fields')->saveFieldgroupContext($field, $fieldgroup)) $numSaved++;
		}
		return $numSaved; 
	}
	
	/**
	 * Export config data for the given fieldgroup
	 * 
	 * @param Fieldgroup $fieldgroup
	 * @return array
	 *
	 */
	public function ___getExportData(Fieldgroup $fieldgroup) {
		$data = $fieldgroup->getTableData();
		$fields = array();
		$contexts = array();
		foreach($fieldgroup as $field) {
			$fields[] = $field->name;
			$fieldContexts = $fieldgroup->getFieldContextArray();
			if(isset($fieldContexts[$field->id])) {
				$contexts[$field->name] = $fieldContexts[$field->id];
			} else {
				$contexts[$field->name] = array();
			}
		}
		$data['fields'] = $fields;
		$data['contexts'] = $contexts;
		return $data;
	}

	/**
	 * Given an export data array, import it back to the class and return what happened
	 *
	 * Changes are not committed until the item is saved
	 *
	 * @param Fieldgroup $fieldgroup
	 * @param array $data
	 * @return array Returns array(
	 * 	[property_name] => array(
	 * 		'old' => 'old value',	// old value, always a string
	 * 		'new' => 'new value',	// new value, always a string
	 * 		'error' => 'error message or blank if no error'
	 * 	)
	 * @throws WireException if given invalid data
	 *
	 */
	public function ___setImportData(Fieldgroup $fieldgroup, array $data) {

		$return = array(
			'fields' => array(
				'old' => '',
				'new' => '',
				'error' => array()
			),
			'contexts' => array(
				'old' => '',
				'new' => '',
				'error' => array()
			),
		);

		$fieldgroup->setTrackChanges(true);
		$fieldgroup->errors("clear");
		$_data = $this->getExportData($fieldgroup);
		$rmFields = array();

		if(isset($data['fields'])) {
			// field data
			$old = "\n" . implode("\n", $_data['fields']) . "\n";
			$new = "\n" . implode("\n", $data['fields']) . "\n";

			if($old !== $new) {

				$return['fields']['old'] = $old;
				$return['fields']['new'] = $new;

				// figure out which fields should be removed
				foreach($fieldgroup as $field) {
					$fieldNames[$field->name] = $field->name;
					if(!in_array($field->name, $data['fields'])) {
						$fieldgroup->remove($field);
						$label = "-$field->name";
						$return['fields']['new'] .= $label . "\n";;
						$rmFields[] = $field->name;
					}
				}

				// figure out which fields should be added
				foreach($data['fields'] as $name) {
					$field = $this->wire('fields')->get($name);
					if(in_array($name, $rmFields)) continue;
					if(!$field) {
						$error = sprintf($this->_('Unable to find field: %s'), $name);
						$return['fields']['error'][] = $error;
						$label = str_replace("\n$name\n", "\n?$name\n", $return['fields']['new']);
						$return['fields']['new'] = $label;
						continue;
					}
					if(!$fieldgroup->hasField($field)) {
						$label = str_replace("\n$field->name\n", "\n+$field->name\n", $return['fields']['new']);
						$return['fields']['new'] = $label;
						$fieldgroup->add($field);
					} else {
						$field = $fieldgroup->getField($field->name, true); // in context
						$fieldgroup->add($field);
						$label = str_replace("\n$field->name\n", "\n$field->name\n", $return['fields']['new']);
						$return['fields']['new'] = $label;
					}
				}

			}

			$return['fields']['new'] = trim($return['fields']['new']);
			$return['fields']['old'] = trim($return['fields']['old']);
		}

		if(isset($data['contexts'])) {
			// context data
			foreach($data['contexts'] as $key => $value) {
				// remove items where they are both empty
				if(empty($value) && empty($_data['contexts'][$key])) {
					unset($data['contexts'][$key], $_data['contexts'][$key]);
				}
			}

			foreach($_data['contexts'] as $key => $value) {
				// remove items where they are both empty
				if(empty($value) && empty($data['contexts'][$key])) {
					unset($data['contexts'][$key], $_data['contexts'][$key]);
				}
			}

			$old = wireEncodeJSON($_data['contexts'], true, true);
			$new = wireEncodeJSON($data['contexts'], true, true);

			if($old !== $new) {

				$return['contexts']['old'] = trim($old);
				$return['contexts']['new'] = trim($new);

				foreach($data['contexts'] as $name => $context) {
					$field = $fieldgroup->getField($name, true); // in context
					if(!$field) {
						if(!empty($context)) $return['contexts']['error'][] = sprintf($this->_('Unable to find field to set field context: %s'), $name);
						continue;
					}
					$id = $field->id;
					$fieldContexts = $fieldgroup->getFieldContextArray();
					if(isset($fieldContexts[$id]) || !empty($context)) {
						$fieldgroup->setFieldContextArray($id, $context); 
						$fieldgroup->trackChange('fieldContexts');
					}
				}
			}
		}

		// other data
		foreach($data as $key => $value) {
			if($key == 'fields' || $key == 'contexts') continue;
			$old = isset($_data[$key]) ? $_data[$key] : null;
			if(is_array($old)) $old = wireEncodeJSON($old, true, false);
			$new = is_array($value) ? wireEncodeJSON($value, true, false) : $value;
			if($old == $new) continue;
			$fieldgroup->set($key, $value);
			$error = (string) $fieldgroup->errors("first clear");
			$return[$key] = array(
				'old' => $old,
				'new' => $value,
				'error' => $error,
			);
		}
		
		if(count($rmFields)) {
			$return['fields']['error'][] = sprintf($this->_('Warning, all data in these field(s) will be permanently deleted (please confirm): %s'), implode(', ', $rmFields));
		}

		$fieldgroup->errors('clear');

		return $return;
	}

	/**
	 * Is the given Field not allowed to be removed from given Template?
	 *
	 * #pw-internal
	 *
	 * @param Field $field
	 * @param Template $template
	 * @param Fieldgroup $fieldgroup
	 * @return bool|string Returns error message string if not removeable or boolean false if it is removeable
	 *
	 */
	public function isFieldNotRemoveable(Field $field, Fieldgroup $fieldgroup, Template $template = null) {
		
		if(is_null($template)) $template = $this->wire('templates')->get($fieldgroup->name);

		if(($field->flags & Field::flagGlobal) && (!$template || !$template->noGlobal)) {
			if($template && $template->getConnectedField()) {
				// if template has a 1-1 relationship with a field, noGlobal is not enforced
				return false;
			} else {
				return
					"Field '$field' may not be removed from fieldgroup '$fieldgroup->name' " .
					"because it is globally required (Field::flagGlobal).";
			}
		}

		if($field->flags & Field::flagPermanent) {
			return 
				"Field '$field' may not be removed from fieldgroup '$fieldgroup->name' " . 
				"because it is permanent (Field::flagPermanent).";
		}

		return false;
	}

}

