<?php
/**
 * @package     FOF
 * @copyright   2010-2017 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Form;

use FOF30\Container\Container;
use FOF30\Form\Header\HeaderBase;
use FOF30\Model\DataModel;
use FOF30\View\DataView\DataViewInterface;
use JFactory;
use JForm;
use Joomla\Registry\Registry;
use JText;
use SimpleXMLElement;

defined('_JEXEC') or die;

/**
 * Form is an extension to JForm which support not only edit views but also
 * browse (record list) and read (single record display) views based on XML
 * forms.
 *
 * @package  FrameworkOnFramework
 * @since    2.0
 */
class Form extends JForm
{
	/**
	 * The model attached to this view
	 *
	 * @var DataModel
	 */
	protected $model;

	/**
	 * The view used to render this form
	 *
	 * @var DataViewInterface
	 */
	protected $view;

	/**
	 * The Container this form belongs to
	 *
	 * @var \FOF30\Container\Container
	 */
	protected $container;

	/**
	 * Map of entity objects for re-use.
	 * Prototypes for all fields and rules are here.
	 *
	 * Array's structure:
	 * <code>
	 * entities:
	 * {ENTITY_NAME}:
	 * {KEY}: {OBJECT}
	 * </code>
	 *
	 * @var    array
	 */
	protected $entities = array();

	/**
	 * Method to instantiate the form object.
	 *
	 * @param   Container $container The component Container where this form belongs to
	 * @param   string    $name      The name of the form.
	 * @param   array     $options   An array of form options.
	 */
	public function __construct(Container $container, $name, array $options = array())
	{
		parent::__construct($name, $options);

		$this->container = $container;
	}

	/**
	 * Returns the value of an attribute of the form itself
	 *
	 * @param   string $attribute The name of the attribute
	 * @param   mixed  $default   Optional default value to return
	 *
	 * @return  mixed
	 *
	 * @since 2.0
	 */
	public function getAttribute($attribute, $default = null)
	{
		$value = $this->xml->attributes()->$attribute;

		if (is_null($value))
		{
			return $default;
		}
		else
		{
			return (string)$value;
		}
	}

	/**
	 * Loads the CSS files defined in the form, based on its cssfiles attribute
	 *
	 * @return  void
	 *
	 * @since 2.0
	 */
	public function loadCSSFiles()
	{
		// Support for CSS files
		$cssfiles = $this->getAttribute('cssfiles');

		if (!empty($cssfiles))
		{
			$cssfiles = explode(',', $cssfiles);

			foreach ($cssfiles as $cssfile)
			{
				$this->getView()->addCssFile(trim($cssfile));
			}
		}

		// Support for LESS files
		$lessfiles = $this->getAttribute('lessfiles');

		if (!empty($lessfiles))
		{
			$lessfiles = explode(',', $lessfiles);

			foreach ($lessfiles as $def)
			{
				$parts = explode('||', $def, 2);
				$lessfile = $parts[0];
				$alt = (count($parts) > 1) ? trim($parts[1]) : null;
				$this->getView()->addLess(trim($lessfile), $alt);
			}
		}
	}

	/**
	 * Loads the Javascript files defined in the form, based on its jsfiles attribute
	 *
	 * @return  void
	 *
	 * @since 2.0
	 */
	public function loadJSFiles()
	{
		$jsfiles = $this->getAttribute('jsfiles');

		if (empty($jsfiles))
		{
			return;
		}

		$jsfiles = explode(',', $jsfiles);

		foreach ($jsfiles as $jsfile)
		{
			$this->getView()->addJavascriptFile(trim($jsfile));
		}
	}

	/**
	 * Returns a reference to the protected $data object, allowing direct
	 * access to and manipulation of the form's data.
	 *
	 * @return   \JRegistry|Registry  The form's data registry
	 *
	 * @since 2.0
	 */
	public function &getData()
	{
		return $this->data;
	}

	/**
	 * Method to load the form description from an XML file.
	 *
	 * The reset option works on a group basis. If the XML file references
	 * groups that have already been created they will be replaced with the
	 * fields in the new XML file unless the $reset parameter has been set
	 * to false.
	 *
	 * @param   string  $file   The filesystem path of an XML file.
	 * @param   bool    $reset  Flag to toggle whether form fields should be replaced if a field
	 *                          already exists with the same group/name.
	 * @param   bool    $xpath  An optional xpath to search for the fields.
	 *
	 * @return  boolean  True on success, false otherwise.
	 */
	public function loadFile($file, $reset = true, $xpath = false)
	{
		// Check to see if the path is an absolute path.
		if (!is_file($file))
		{
			return false;
		}

		// Attempt to load the XML file.
		$xml = simplexml_load_file($file);

		return $this->load($xml, $reset, $xpath);
	}

	/**
	 * Attaches a DataModel to this form
	 *
	 * @param   DataModel &$model The model to attach to the form
	 *
	 * @return  void
	 */
	public function setModel(DataModel &$model)
	{
		$this->model = $model;
	}

	/**
	 * Returns the DataModel attached to this form
	 *
	 * @return DataModel
	 */
	public function &getModel()
	{
		return $this->model;
	}

	/**
	 * Attaches a DataViewInterface to this form
	 *
	 * @param   DataViewInterface &$view The view to attach to the form
	 *
	 * @return  void
	 */
	public function setView(DataViewInterface &$view)
	{
		$this->view = $view;
	}

	/**
	 * Returns the DataViewInterface attached to this form
	 *
	 * @return DataViewInterface
	 */
	public function &getView()
	{
		return $this->view;
	}

	/**
	 * Method to get an array of FormHeader objects in the headerset.
	 *
	 * @return  array  The array of HeaderInterface objects in the headerset.
	 *
	 * @since   2.0
	 */
	public function getHeaderset()
	{
		$fields = array();

		$elements = $this->findHeadersByGroup();

		// If no field elements were found return empty.

		if (empty($elements))
		{
			return $fields;
		}

		// Build the result array from the found field elements.

		/** @var \SimpleXMLElement $element */
		foreach ($elements as $element)
		{
			// Get the field groups for the element.
			$attrs = $element->xpath('ancestor::headerset[@name]/@name');
			$groups = array_map('strval', $attrs ? $attrs : array());
			$group = implode('.', $groups);

			// If the field is successfully loaded add it to the result array.
			/** @var HeaderBase $field */
			if ($field = $this->loadHeader($element, $group))
			{
				$fields[$field->id] = $field;
			}
		}

		return $fields;
	}

	/**
	 * Method to get an array of <header /> elements from the form XML document which are
	 * in a control group by name.
	 *
	 * @param   mixed   $group    The optional dot-separated form group path on which to find the fields.
	 *                            Null will return all fields. False will return fields not in a group.
	 * @param   boolean $nested   True to also include fields in nested groups that are inside of the
	 *                            group for which to find fields.
	 *
	 * @return  \SimpleXMLElement|bool  Boolean false on error or array of SimpleXMLElement objects.
	 *
	 * @since   2.0
	 */
	protected function &findHeadersByGroup($group = null, $nested = false)
	{
		$false = false;
		$fields = array();

		// Make sure there is a valid JForm XML document.
		if (!($this->xml instanceof \SimpleXMLElement))
		{
			return $false;
		}

		// Get only fields in a specific group?
		if ($group)
		{
			// Get the fields elements for a given group.
			$elements = &$this->findHeader($group);

			// Get all of the field elements for the fields elements.
			/** @var \SimpleXMLElement $element */
			foreach ($elements as $element)
			{
				// If there are field elements add them to the return result.
				if ($tmp = $element->xpath('descendant::header'))
				{
					// If we also want fields in nested groups then just merge the arrays.
					if ($nested)
					{
						$fields = array_merge($fields, $tmp);
					}

					// If we want to exclude nested groups then we need to check each field.
					else
					{
						$groupNames = explode('.', $group);

						foreach ($tmp as $field)
						{
							// Get the names of the groups that the field is in.
							$attrs = $field->xpath('ancestor::headers[@name]/@name');
							$names = array_map('strval', $attrs ? $attrs : array());

							// If the field is in the specific group then add it to the return list.
							if ($names == (array)$groupNames)
							{
								$fields = array_merge($fields, array($field));
							}
						}
					}
				}
			}
		}
		elseif ($group === false)
		{
			// Get only field elements not in a group.
			$fields = $this->xml->xpath('descendant::headers[not(@name)]/header | descendant::headers[not(@name)]/headerset/header ');
		}
		else
		{
			// Get an array of all the <header /> elements.
			$fields = $this->xml->xpath('//header');
		}

		return $fields;
	}

	/**
	 * Method to get a header field represented as a HeaderInterface object.
	 *
	 * @param   string $name  The name of the header field.
	 * @param   string $group The optional dot-separated form group path on which to find the field.
	 * @param   mixed  $value The optional value to use as the default for the field. (DEPRECATED)
	 *
	 * @return  HeaderInterface|bool  The HeaderInterface object for the field or boolean false on error.
	 *
	 * @since   2.0
	 */
	public function getHeader($name, $group = null, $value = null)
	{
		// Make sure there is a valid Form XML document.
		if (!($this->xml instanceof \SimpleXMLElement))
		{
			return false;
		}

		// Attempt to find the field by name and group.
		$element = $this->findHeader($name, $group);

		// If the field element was not found return false.
		if (!$element)
		{
			return false;
		}

		return $this->loadHeader($element, $group);
	}

	/**
	 * Method to get a header field represented as an XML element object.
	 *
	 * @param   string $name  The name of the form field.
	 * @param   string $group The optional dot-separated form group path on which to find the field.
	 *
	 * @return  mixed  The XML element object for the field or boolean false on error.
	 *
	 * @since   2.0
	 */
	protected function findHeader($name, $group = null)
	{
		$element = false;
		$fields = array();

		// Make sure there is a valid JForm XML document.
		if (!($this->xml instanceof \SimpleXMLElement))
		{
			return false;
		}

		// Let's get the appropriate field element based on the method arguments.
		if ($group)
		{
			// Get the fields elements for a given group.
			$elements = &$this->findGroup($group);

			// Get all of the field elements with the correct name for the fields elements.
			/** @var \SimpleXMLElement $element */
			foreach ($elements as $element)
			{
				// If there are matching field elements add them to the fields array.
				if ($tmp = $element->xpath('descendant::header[@name="' . $name . '"]'))
				{
					$fields = array_merge($fields, $tmp);
				}
			}

			// Make sure something was found.
			if (!$fields)
			{
				return false;
			}

			// Use the first correct match in the given group.
			$groupNames = explode('.', $group);

			/** @var \SimpleXMLElement $field */
			foreach ($fields as &$field)
			{
				// Get the group names as strings for ancestor fields elements.
				$attrs = $field->xpath('ancestor::headerfields[@name]/@name');
				$names = array_map('strval', $attrs ? $attrs : array());

				// If the field is in the exact group use it and break out of the loop.
				if ($names == (array)$groupNames)
				{
					$element = &$field;
					break;
				}
			}
		}
		else
		{
			// Get an array of fields with the correct name.
			$fields = $this->xml->xpath('//header[@name="' . $name . '"]');

			// Make sure something was found.
			if (!$fields)
			{
				return false;
			}

			// Search through the fields for the right one.
			foreach ($fields as &$field)
			{
				// If we find an ancestor fields element with a group name then it isn't what we want.
				if ($field->xpath('ancestor::headerfields[@name]'))
				{
					continue;
				}

				// Found it!
				else
				{
					$element = &$field;
					break;
				}
			}
		}

		return $element;
	}

	/**
	 * Method to load, setup and return a HeaderInterface object based on field data.
	 *
	 * @param   string $element The XML element object representation of the form field.
	 * @param   string $group   The optional dot-separated form group path on which to find the field.
	 *
	 * @return  HeaderInterface|bool  The HeaderInterface object for the field or boolean false on error.
	 *
	 * @since   2.0
	 */
	protected function loadHeader($element, $group = null)
	{
		// Make sure there is a valid SimpleXMLElement.
		if (!($element instanceof \SimpleXMLElement))
		{
			return false;
		}

		// Get the field type.
		$type = $element['type'] ? (string)$element['type'] : 'field';

		// Load the JFormField object for the field.
		$field = $this->loadHeaderType($type);

		// If the object could not be loaded, get a text field object.
		if ($field === false)
		{
			$field = $this->loadHeaderType('field');
		}

		// Setup the HeaderInterface object.
		$field->setForm($this);

		if ($field->setup($element, $group))
		{
			return $field;
		}
		else
		{
			return false;
		}
	}

	/**
	 * Method to remove a header from the form definition.
	 *
	 * @param   string  $name   The name of the form field for which remove.
	 * @param   string  $group  The optional dot-separated form group path on which to find the field.
	 *
	 * @return  boolean  True on success, false otherwise.
	 *
	 * @throws  \UnexpectedValueException
	 */
	public function removeHeader($name, $group = null)
	{
		// Make sure there is a valid JForm XML document.
		if (!($this->xml instanceof SimpleXMLElement))
		{
			throw new \UnexpectedValueException(sprintf('%s::getFieldAttribute `xml` is not an instance of SimpleXMLElement', get_class($this)));
		}

		// Find the form field element from the definition.
		$element = $this->findHeader($name, $group);

		// If the element exists remove it from the form definition.
		if ($element instanceof SimpleXMLElement)
		{
			$dom = dom_import_simplexml($element);
			$dom->parentNode->removeChild($dom);

			return true;
		}

		return false;
	}

	/**
	 * Proxy for {@link Helper::loadFieldType()}.
	 *
	 * @param   string  $type The field type.
	 * @param   boolean $new  Flag to toggle whether we should get a new instance of the object.
	 *
	 * @return  FieldInterface|bool  FieldInterface object on success, false otherwise.
	 *
	 * @since   2.0
	 */
	protected function loadFieldType($type, $new = true)
	{
		return $this->loadType('field', $type, $new);
	}

	/**
	 * Proxy for {@link Helper::loadHeaderType()}.
	 *
	 * @param   string  $type The field type.
	 * @param   boolean $new  Flag to toggle whether we should get a new instance of the object.
	 *
	 * @return  HeaderInterface|bool  HeaderInterface object on success, false otherwise.
	 *
	 * @since   2.0
	 */
	protected function loadHeaderType($type, $new = true)
	{
		return $this->loadType('header', $type, $new);
	}

	/**
	 * Proxy for {@link Helper::loadRuleType()}.
	 *
	 * @param   string  $type The rule type.
	 * @param   boolean $new  Flag to toggle whether we should get a new instance of the object.
	 *
	 * @return  \JFormRule|bool  JFormRule object on success, false otherwise.
	 *
	 * @see     Helper::loadRuleType()
	 * @since   2.0
	 */
	protected function loadRuleType($type, $new = true)
	{
		return $this->loadType('rule', $type, $new);
	}

	/**
	 * Method to load a form entity object given a type.
	 * Each type is loaded only once and then used as a prototype for other objects of same type.
	 * Please, use this method only with those entities which support types (forms don't support them).
	 *
	 * @param   string  $entity The entity.
	 * @param   string  $type   The entity type.
	 * @param   boolean $new    Flag to toggle whether we should get a new instance of the object.
	 *
	 * @return  mixed Entity object on success, false otherwise.
	 */
	protected function loadType($entity, $type, $new = true)
	{
		// Reference to an array with current entity's type instances
		$types = &$this->entities[$entity];

		// Return an entity object if it already exists and we don't need a new one.
		if (isset($types[$type]) && $new === false)
		{
			return $types[$type];
		}

		$class = $this->loadClass($entity, $type);

		if ($class !== false)
		{
			// Instantiate a new type object.
			$types[$type] = new $class;

			return $types[$type];
		}
		else
		{
			return false;
		}
	}

	/**
	 * Load a class for one of the form's entities of a particular type.
	 * Currently, it makes sense to use this method for the "field" and "rule" entities
	 * (but you can support more entities in your subclass).
	 *
	 * @param   string $entity One of the form entities (field, header or rule).
	 * @param   string $type   Type of an entity.
	 *
	 * @return  mixed  Class name on success or false otherwise.
	 *
	 * @since   2.0
	 */
	public function loadClass($entity, $type)
	{
		// Get the prefixes for namespaced classes (FOF3 way)
		$namespacedPrefixes = array(
			$this->container->getNamespacePrefix(),
			'FOF30\\',
		);

		// Get the prefixes for non-namespaced classes (FOF2 and Joomla! way)
		$plainPrefixes = array('J');

		// If the type is given as prefix.type add the custom type into the two prefix arrays
		if (strpos($type, '.'))
		{
			list($prefix, $type) = explode('.', $type);

			array_unshift($plainPrefixes, $prefix);
			array_unshift($namespacedPrefixes, $prefix);
		}

		// First try to find the namespaced class
		foreach ($namespacedPrefixes as $prefix)
		{
			$class = rtrim($prefix, '\\') . '\\Form\\' . ucfirst($entity) . '\\' . ucfirst($type);

			if (class_exists($class, true))
			{
				return $class;
			}
		}

		// TODO The rest of the code is legacy and will be removed in a future version

		// Then try to find the non-namespaced class
		$classes = array();

		foreach ($plainPrefixes as $prefix)
		{
			$class = \JString::ucfirst($prefix, '_') . 'Form' . \JString::ucfirst($entity, '_') . \JString::ucfirst($type, '_');

			if (class_exists($class, true))
			{
				return $class;
			}

			$classes[] = $class;
		}

		// Get the field search path array.
		$reflector = new \ReflectionClass('\\JFormHelper');
		$addPathMethod = $reflector->getMethod('addPath');
		$addPathMethod->setAccessible(true);
		$paths = $addPathMethod->invoke(null, $entity);

		// If the type is complex, add the base type to the paths.
		if ($pos = strpos($type, '_'))
		{
			// Add the complex type prefix to the paths.
			for ($i = 0, $n = count($paths); $i < $n; $i++)
			{
				// Derive the new path.
				$path = $paths[$i] . '/' . strtolower(substr($type, 0, $pos));

				// If the path does not exist, add it.
				if (!in_array($path, $paths))
				{
					$paths[] = $path;
				}
			}

			// Break off the end of the complex type.
			$type = substr($type, $pos + 1);
		}

		// Try to find the class file.
		$type = strtolower($type) . '.php';

		foreach ($paths as $path)
		{
			if ($file = \JPath::find($path, $type))
			{
				require_once $file;

				foreach ($classes as $class)
				{
					if (class_exists($class, false))
					{
						return $class;
					}
				}
			}
		}

		return false;
	}

	/**
	 * WARNING: THIS IS IGNORED IN FOF3!
	 *
	 * @param   string  $new  IGNORED!
	 *
	 * @return  void
	 *
	 * @deprecated 3.0
	 */
	public static function addFieldPath($new = null)
	{
		if ($new) {}; // Prevents phpStorm from freaking out about the unused $new parameter...

		if (class_exists('JLog'))
		{
			\JLog::add(__CLASS__ . '::' . __METHOD__ . '() is deprecated since FOF 3.0 and should not be used.', \JLog::WARNING, 'deprecated');
		}
	}

	/**
	 * WARNING: THIS IS IGNORED IN FOF3!
	 *
	 * @param   string  $new  IGNORED!
	 *
	 * @return  void
	 *
	 * @deprecated 3.0
	 */
	public static function addHeaderPath($new = null)
	{
		if ($new) {}; // Prevents phpStorm from freaking out about the unused $new parameter...

		if (class_exists('JLog'))
		{
			\JLog::add(__CLASS__ . '::' . __METHOD__ . '() is deprecated since FOF 3.0 and should not be used.', \JLog::WARNING, 'deprecated');
		}
	}

	/**
	 * WARNING: THIS IS IGNORED IN FOF3!
	 *
	 * @param   string  $new  IGNORED!
	 *
	 * @return  void
	 *
	 * @deprecated 3.0
	 */
	public static function addFormPath($new = null)
	{
		if ($new) {}; // Prevents phpStorm from freaking out about the unused $new parameter...

		if (class_exists('JLog'))
		{
			\JLog::add(__CLASS__ . '::' . __METHOD__ . '() is deprecated since FOF 3.0 and should not be used.', \JLog::WARNING, 'deprecated');
		}
	}

	/**
	 * WARNING: THIS IS IGNORED IN FOF3!
	 *
	 * @param   string  $new  IGNORED!
	 *
	 * @return  void
	 *
	 * @deprecated 3.0
	 */
	public static function addRulePath($new = null)
	{
		if ($new) {}; // Prevents phpStorm from freaking out about the unused $new parameter...

		if (class_exists('JLog'))
		{
			\JLog::add(__CLASS__ . '::' . __METHOD__ . '() is deprecated since FOF 3.0 and should not be used.', \JLog::WARNING, 'deprecated');
		}
	}

	/**
	 * Get a reference to the form's Container
	 *
	 * @return Container
	 */
	public function &getContainer()
	{
		return $this->container;
	}

	/**
	 * Set the form's Container
	 *
	 * @param Container $container
	 */
	public function setContainer($container)
	{
		$this->container = $container;
	}

	/**
	 * Method to bind data to the form.
	 *
	 * @param   mixed  $data  An array or object of data to bind to the form.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   11.1
	 */
	public function bind($data)
	{
		$this->data = class_exists('JRegistry') ? new \JRegistry() : new Registry();

		if (is_object($data) && ($data instanceof DataModel))
		{
			$maxDepth = (int) $this->getAttribute('relation_depth', '1');

			return parent::bind($this->modelToBindSource($data, $maxDepth));
		}

		return parent::bind($data);
	}

	/**
	 * Method to bind data to the form for the group level.
	 *
	 * @param   string  $group  The dot-separated form group path on which to bind the data.
	 * @param   mixed   $data   An array or object of data to bind to the form for the group level.
	 *
	 * @return  void
	 *
	 * @since   11.1
	 */
	protected function bindLevel($group, $data)
	{
		if (is_object($data) && ($data instanceof DataModel))
		{
			parent::bindLevel($group, $this->modelToBindSource($data));

			return;
		}

		parent::bindLevel($group, $data);
	}

	/**
	 * Method to load, setup and return a JFormField object based on field data.
	 *
	 * @param   string  $element  The XML element object representation of the form field.
	 * @param   string  $group    The optional dot-separated form group path on which to find the field.
	 * @param   mixed   $value    The optional value to use as the default for the field.
	 *
	 * @return  mixed  The JFormField object for the field or boolean false on error.
	 *
	 * @since   11.1
	 */
	protected function loadField($element, $group = null, $value = null)
	{
		// Make sure there is a valid SimpleXMLElement.
		if (!($element instanceof SimpleXMLElement))
		{
			return false;
		}

		// Get the field type.
		$type = $element['type'] ? (string) $element['type'] : 'text';

		// Load the JFormField object for the field.
		$field = $this->loadFieldType($type);

		// If the object could not be loaded, get a text field object.
		if ($field === false)
		{
			$field = $this->loadFieldType('text');
		}

		/*
		 * Get the value for the form field if not set.
		 * Default to the translated version of the 'default' attribute
		 * if 'translate_default' attribute if set to 'true' or '1'
		 * else the value of the 'default' attribute for the field.
		 */
		if ($value === null)
		{
			$default = (string) $element['default'];

			if (($translate = $element['translate_default']) && ((string) $translate == 'true' || (string) $translate == '1'))
			{
				$lang = JFactory::getLanguage();

				if ($lang->hasKey($default))
				{
					$debug = $lang->setDebug(false);
					$default = JText::_($default);
					$lang->setDebug($debug);
				}
				else
				{
					$default = JText::_($default);
				}
			}

			$getValueFrom = (isset($element['name_from'])) ? (string) $element['name_from'] : (string) $element['name'];

			$value = $this->getValue($getValueFrom, $group, $default);
		}

		// Setup the JFormField object.
		$field->setForm($this);

		if ($field->setup($element, $value, $group))
		{
			return $field;
		}
		else
		{
			return false;
		}
	}

	/**
	 * Method to get a form field represented as an XML element object.
	 *
	 * @param   string  $name   The name of the form field.
	 * @param   string  $group  The optional dot-separated form group path on which to find the field.
	 *
	 * @return  mixed  The XML element object for the field or boolean false on error.
	 *
	 * @since   11.1
	 */
	protected function findField($name, $group = null)
	{
		$element = false;
		$fields = array();

		// Make sure there is a valid JForm XML document.
		if (!($this->xml instanceof SimpleXMLElement))
		{
			return false;
		}

		// Let's get the appropriate field element based on the method arguments.
		if ($group)
		{
			// Get the fields elements for a given group.
			$elements = &$this->findGroup($group);

			// Get all of the field elements with the correct name for the fields elements.
			/** @var SimpleXMLElement $element */
			foreach ($elements as $element)
			{
				// If there are matching field elements add them to the fields array.
				if ($tmp = $element->xpath('descendant::field[@name="' . $name . '"]'))
				{
					$fields = array_merge($fields, $tmp);
				}
				elseif ($tmp = $element->xpath('descendant::field[@name_from="' . $name . '"]'))
				{
					$fields = array_merge($fields, $tmp);
				}
			}

			// Make sure something was found.
			if (!$fields)
			{
				return false;
			}

			// Use the first correct match in the given group.
			$groupNames = explode('.', $group);

			/** @var SimpleXMLElement $field */
			foreach ($fields as &$field)
			{
				// Get the group names as strings for ancestor fields elements.
				$attrs = $field->xpath('ancestor::fields[@name]/@name');
				$names = array_map('strval', $attrs ? $attrs : array());

				// If the field is in the exact group use it and break out of the loop.
				if ($names == (array) $groupNames)
				{
					$element = &$field;
					break;
				}
			}
		}
		else
		{
			// Get an array of fields with the correct name.
			$fields = $this->xml->xpath('//field[@name="' . $name . '"]');

			if (!$fields)
			{
				$fields = array();
			}

			$fieldsNameFrom = $this->xml->xpath('//field[@name_from="' . $name . '"]');

			if ($fieldsNameFrom)
			{
				$fields = array_merge($fields, $fieldsNameFrom);
			}

			// Make sure something was found.
			if (empty($fields))
			{
				return false;
			}

			// Search through the fields for the right one.
			foreach ($fields as &$field)
			{
				// If we find an ancestor fields element with a group name then it isn't what we want.
				if ($field->xpath('ancestor::fields[@name]'))
				{
					continue;
				}

				// Found it!
				else
				{
					$element = &$field;
					break;
				}
			}
		}

		return $element;
	}

	/**
	 * Converts a DataModel into data suitable for use with the form. The difference to the Model's getData() method is
	 * that we process hasOne and belongsTo relations. This is a recursive function which will be called at most
	 * $maxLevel deep. You can set this in the form XML file, in the relation_depth attribute.
	 *
	 * The $modelsProcessed array which is passed in successive recursions lets us prevent pointless Inception-style
	 * recursions, e.g. Model A is related to Model B is related to Model C is related to Model A. You clearly don't
	 * care to see a.b.c.a.b in the results. You just want a.b.c. Obviously c is indirectly related to a because that's
	 * where you began the recursion anyway.
	 *
	 * @param   DataModel  $model            The item to dump its contents into an array
	 * @param   int        $maxLevel         Maximum nesting level of relations to process. Default: 1.
	 * @param   array      $modelsProcessed  Array of the fully qualified model class names already processed.
	 *
	 * @return  array
	 * @throws  DataModel\Relation\Exception\RelationNotFound
	 */
	protected function modelToBindSource(DataModel $model, $maxLevel = 1, $modelsProcessed = array())
	{
		$maxLevel--;

		$data = $model->toArray();

		$relations = $model->getRelations()->getRelationNames();
		$relationTypes = $model->getRelations()->getRelationTypes();
		$relationTypes = array_map(function ($x) {
			return ltrim($x, '\\');
		}, $relationTypes);
		$relationTypes = array_flip($relationTypes);

		if (is_array($relations) && count($relations) && ($maxLevel >= 0))
		{
			foreach ($relations as $relationName)
			{
				$rel = $model->getRelations()->getRelation($relationName);
				$class = get_class($rel);

				if (!isset($relationTypes[$class]))
				{
					continue;
				}

				if (!in_array($relationTypes[$class], array('hasOne', 'belongsTo')))
				{
					continue;
				}

				/** @var DataModel $relData */
				$relData = $model->$relationName;

				if (!($relData instanceof DataModel))
				{
					continue;
				}

				$modelType = get_class($relData);

				if (in_array($modelType, $modelsProcessed))
				{
					continue;
				}

				$modelsProcessed[] = $modelType;

				$relDataArray = $this->modelToBindSource($relData, $maxLevel, $modelsProcessed);

				if (!is_array($relDataArray) || empty($relDataArray))
				{
					continue;
				}

				foreach ($relDataArray as $k => $v)
				{
					$data[$relationName . '.' . $k] = $v;
				}
			}
		}

		return $data;
	}


}
