<?php
/**
 * @package     Joomla.Plugin
 * @subpackage  Fields.Subform
 *
 * @copyright   (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;
use Joomla\Component\Fields\Administrator\Plugin\FieldsPlugin;

/**
 * Fields subform Plugin
 *
 * @since  4.0.0
 */
class PlgFieldsSubform extends FieldsPlugin
{
	/**
	 * Two-dimensional array to hold to do a fast in-memory caching of rendered
	 * subfield values.
	 *
	 * @var array
	 *
	 * @since 4.0.0
	 */
	protected $renderCache = array();

	/**
	 * Array to do a fast in-memory caching of all custom field items.
	 *
	 * @var array
	 *
	 * @since 4.0.0
	 */
	protected static $customFieldsCache = null;

	/**
	 * Handles the onContentPrepareForm event. Adds form definitions to relevant forms.
	 *
	 * @param   Form          $form  The form to manipulate
	 * @param   array|object  $data  The data of the form
	 *
	 * @return  void
	 *
	 * @since  4.0.0
	 */
	public function onContentPrepareForm(Form $form, $data)
	{
		// Get the path to our own form definition (basically ./params/subform.xml)
		$path = $this->getFormPath($form, $data);

		if ($path === null)
		{
			return;
		}

		// Ensure it is an object
		$formData = (object) $data;

		// Now load our own form definition into a DOMDocument, because we want to manipulate it
		$xml = new DOMDocument;
		$xml->load($path);

		// Prepare a DOMXPath object
		$xmlxpath = new DOMXPath($xml);

		/**
		 * Get all fields of type "subfields" in our own XML
		 *
		 * @var $valuefields \DOMNodeList
		 */
		$valuefields = $xmlxpath->evaluate('//field[@type="subfields"]');

		// If we haven't found it, something is wrong
		if (!$valuefields || $valuefields->length != 1)
		{
			return;
		}

		// Now iterate over those fields and manipulate them, set its parameter `context` to our context
		foreach ($valuefields as $valuefield)
		{
			$valuefield->setAttribute('context', $formData->context);
		}

		// When this is not a new instance (editing an existing instance)
		if (isset($formData->id) && $formData->id > 0)
		{
			// Don't allow the 'repeat' attribute to be edited
			foreach ($xmlxpath->evaluate('//field[@name="repeat"]') as $field)
			{
				$field->setAttribute('readonly', '1');
			}
		}

		// And now load our manipulated form definition into the JForm
		$form->load($xml->saveXML(), true, '/form/*');
	}

	/**
	 * Manipulates the $field->value before the field is being passed to
	 * onCustomFieldsPrepareField.
	 *
	 * @param   string     $context  The context
	 * @param   object     $item     The item
	 * @param   \stdClass  $field    The field
	 *
	 * @return  void
	 *
	 * @since 4.0.0
	 */
	public function onCustomFieldsBeforePrepareField($context, $item, $field)
	{
		// Check if the field should be processed by us
		if (!$this->isTypeSupported($field->type))
		{
			return;
		}

		$decoded_value = json_decode($field->value, true);

		if (!$decoded_value || !is_array($decoded_value))
		{
			return;
		}

		$field->value = $decoded_value;
	}

	/**
	 * Renders this fields value by rendering all sub fields and joining all those rendered sub fields together.
	 *
	 * @param   string     $context  The context
	 * @param   object     $item     The item
	 * @param   \stdClass  $field    The field
	 *
	 * @return  string
	 *
	 * @since 4.0.0
	 */
	public function onCustomFieldsPrepareField($context, $item, $field)
	{
		// Check if the field should be processed by us
		if (!$this->isTypeSupported($field->type))
		{
			return;
		}

		// If we don't have any subfields (or values for them), nothing to do.
		if (!is_array($field->value) || count($field->value) < 1)
		{
			return;
		}

		// Get the field params
		$field_params = $this->getParamsFromField($field);

		/**
		 * Placeholder to hold all rows (if this field is repeatable).
		 * Each array entry is another array representing a row, containing all of the sub fields that
		 * are valid for this row and their raw and rendered values.
		 */
		$subform_rows = array();

		// Create an array with entries being subfields forms, and if not repeatable, containing only one element.
		$rows = $field->value;

		if ($field_params->get('repeat', '1') == '0')
		{
			$rows = array($field->value);
		}

		// Iterate over each row of the data
		foreach ($rows as $row)
		{
			// Holds all sub fields of this row, incl. their raw and rendered value
			$row_subfields = array();

			// For each row, iterate over all the subfields
			foreach ($this->getSubfieldsFromField($field) as $subfield)
			{
				// Fill value (and rawvalue) if we have data for this subfield in the current row, otherwise set them to empty
				$subfield->rawvalue = $subfield->value = isset($row[$subfield->name]) ? $row[$subfield->name] : '';

				// Do we want to render the value of this field, and is the value non-empty?
				if ($subfield->value !== '' && $subfield->render_values == '1')
				{
					/**
					 * Construct the cache-key for our renderCache. It is important that the cache key
					 * is as unique as possible to avoid false duplicates (e.g. type and rawvalue is not
					 * enough for the cache key, because type 'list' and value '1' can have different
					 * rendered values, depending on the list items), but it also must be as general as possible
					 * to not cause too many unneeded rendering processes (e.g. the type 'text' will always be
					 * rendered the same when it has the same rawvalue).
					 */
					$renderCache_key = serialize(
						array(
							$subfield->type,
							$subfield->id,
							$subfield->rawvalue,
						)
					);

					// Let's see if we have a fast in-memory result for this
					if (isset($this->renderCache[$renderCache_key]))
					{
						$subfield->value = $this->renderCache[$renderCache_key];
					}
					else
					{
						// Render this virtual subfield
						$subfield->value = Factory::getApplication()->triggerEvent(
							'onCustomFieldsPrepareField',
							array($context, $item, $subfield)
						);
						$this->renderCache[$renderCache_key] = $subfield->value;
					}
				}

				// Flatten the value if it is an array (list, checkboxes, etc.) [independent of render_values]
				if (is_array($subfield->value))
				{
					$subfield->value = implode(' ', $subfield->value);
				}

				// Store the subfield (incl. its raw and rendered value) into this rows sub fields
				$row_subfields[$subfield->fieldname] = $subfield;
			}

			// Store all the sub fields of this row
			$subform_rows[] = $row_subfields;
		}

		// Store all the rows and their corresponding sub fields in $field->subform_rows
		$field->subform_rows = $subform_rows;

		// Call our parent to combine all those together for the final $field->value
		return parent::onCustomFieldsPrepareField($context, $item, $field);
	}

	/**
	 * Returns a DOMElement which is the child of $parent and represents
	 * the form XML definition for this field.
	 *
	 * @param   \stdClass   $field   The field
	 * @param   DOMElement  $parent  The original parent element
	 * @param   Form        $form    The form
	 *
	 * @return  \DOMElement
	 *
	 * @since 4.0.0
	 */
	public function onCustomFieldsPrepareDom($field, DOMElement $parent, Form $form)
	{
		// Call the onCustomFieldsPrepareDom method on FieldsPlugin
		$parent_field = parent::onCustomFieldsPrepareDom($field, $parent, $form);

		if (!$parent_field)
		{
			return $parent_field;
		}

		// Override the fieldname attribute of the subform - this is being used to index the rows
		$parent_field->setAttribute('fieldname', 'row');

		// If the user configured this subform instance as required
		if ($field->required)
		{
			// Then we need to have at least one row
			$parent_field->setAttribute('min', '1');
		}

		// Get the configured parameters for this field
		$field_params = $this->getParamsFromField($field);

		// If this fields should be repeatable, set some attributes on the subform element
		if ($field_params->get('repeat', '1') == '1')
		{
			$parent_field->setAttribute('multiple', 'true');
			$parent_field->setAttribute('layout', 'joomla.form.field.subform.repeatable-table');
		}

		// Create a child 'form' DOMElement under the field[type=subform] element.
		$parent_fieldset = $parent_field->appendChild(new DOMElement('form'));
		$parent_fieldset->setAttribute('hidden', 'true');
		$parent_fieldset->setAttribute('name', ($field->name . '_modal'));

		if ($field_params->get('max_rows'))
		{
			$parent_field->setAttribute('max', $field_params->get('max_rows'));
		}

		// If this field should be repeatable, set some attributes on the modal
		if ($field_params->get('repeat', '1') == '1')
		{
			$parent_fieldset->setAttribute('repeat', 'true');
		}

		// Get the configured sub fields for this field
		$subfields = $this->getSubfieldsFromField($field);

		// If we have 5 or more of them, use the `repeatable` layout instead of the `repeatable-table`
		if (count($subfields) >= 5)
		{
			$parent_field->setAttribute('layout', 'joomla.form.field.subform.repeatable');
		}

		// Iterate over the sub fields to call prepareDom on each of those sub-fields
		foreach ($subfields as $subfield)
		{
			// Let the relevant plugins do their work and insert the correct
			// DOMElement's into our $parent_fieldset.
			Factory::getApplication()->triggerEvent(
				'onCustomFieldsPrepareDom',
				array($subfield, $parent_fieldset, $form)
			);
		}

		return $parent_field;
	}

	/**
	 * Returns an array of all options configured for this field.
	 *
	 * @param   \stdClass  $field  The field
	 *
	 * @return  \stdClass[]
	 *
	 * @since 4.0.0
	 */
	protected function getOptionsFromField(\stdClass $field)
	{
		$result = array();

		// Fetch the options from the plugin
		$params = $this->getParamsFromField($field);

		foreach ($params->get('options', array()) as $option)
		{
			$result[] = (object) $option;
		}

		return $result;
	}

	/**
	 * Returns the configured params for a given field.
	 *
	 * @param   \stdClass  $field  The field
	 *
	 * @return  \Joomla\Registry\Registry
	 *
	 * @since 4.0.0
	 */
	protected function getParamsFromField(\stdClass $field)
	{
		$params = (clone $this->params);

		if (isset($field->fieldparams) && is_object($field->fieldparams))
		{
			$params->merge($field->fieldparams);
		}

		return $params;
	}

	/**
	 * Returns an array of all subfields for a given field. This will always return a bare clone
	 * of a sub field, so manipulating it is safe.
	 *
	 * @param   \stdClass  $field  The field
	 *
	 * @return  \stdClass[]
	 *
	 * @since 4.0.0
	 */
	protected function getSubfieldsFromField(\stdClass $field)
	{
		if (static::$customFieldsCache === null)
		{
			// Prepare our cache
			static::$customFieldsCache = array();

			// Get all custom field instances
			$customFields = FieldsHelper::getFields('', null, false, null, true);

			foreach ($customFields as $customField)
			{
				// Store each custom field instance in our cache with its id as key
				static::$customFieldsCache[$customField->id] = $customField;
			}
		}

		$result = array();

		// Iterate over all configured options for this field
		foreach ($this->getOptionsFromField($field) as $option)
		{
			// Check whether the wanted sub field really is an existing custom field
			if (!isset(static::$customFieldsCache[$option->customfield]))
			{
				continue;
			}

			// Get a clone of the sub field, so we and the caller can do some manipulation with it.
			$cur_field = (clone static::$customFieldsCache[$option->customfield]);

			// Manipulate it and add our custom configuration to it
			$cur_field->render_values = $option->render_values;

			/**
			 * Set the name of the sub field to its id so that the values in the database are being saved
			 * based on the id of the sub fields, not on their name. Actually we do not need the name of
			 * the sub fields to render them, but just to make sure we have the name when we need it, we
			 * store it as `fieldname`.
			 */
			$cur_field->fieldname = $cur_field->name;
			$cur_field->name = 'field' . $cur_field->id;

			// And add it to our result
			$result[] = $cur_field;
		}

		return $result;
	}
}
