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

namespace Joomla\Component\Contenthistory\Administrator\Helper;

\defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Filesystem\Path;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Table\ContentHistory;
use Joomla\CMS\Table\ContentType;
use Joomla\CMS\Table\Table;
use Joomla\Database\ParameterType;

/**
 * Categories helper.
 *
 * @since  3.2
 */
class ContenthistoryHelper
{
	/**
	 * Method to put all field names, including nested ones, in a single array for easy lookup.
	 *
	 * @param   \stdClass  $object  Standard class object that may contain one level of nested objects.
	 *
	 * @return  array  Associative array of all field names, including ones in a nested object.
	 *
	 * @since   3.2
	 */
	public static function createObjectArray($object)
	{
		$result = array();

		if ($object === null)
		{
			return $result;
		}

		foreach ($object as $name => $value)
		{
			$result[$name] = $value;

			if (is_object($value))
			{
				foreach ($value as $subName => $subValue)
				{
					$result[$subName] = $subValue;
				}
			}
		}

		return $result;
	}

	/**
	 * Method to decode JSON-encoded fields in a standard object. Used to unpack JSON strings in the content history data column.
	 *
	 * @param   string  $jsonString  JSON String to convert to an object.
	 *
	 * @return  \stdClass  Object with any JSON-encoded fields unpacked.
	 *
	 * @since   3.2
	 */
	public static function decodeFields($jsonString)
	{
		$object = json_decode($jsonString);

		if (is_object($object))
		{
			foreach ($object as $name => $value)
			{
				if ($subObject = json_decode($value))
				{
					$object->$name = $subObject;
				}
			}
		}

		return $object;
	}

	/**
	 * Method to get field labels for the fields in the JSON-encoded object.
	 * First we see if we can find translatable labels for the fields in the object.
	 * We translate any we can find and return an array in the format object->name => label.
	 *
	 * @param   \stdClass    $object      Standard class object in the format name->value.
	 * @param   ContentType  $typesTable  Table object with content history options.
	 *
	 * @return  \stdClass  Contains two associative arrays.
	 *                    $formValues->labels in the format name => label (for example, 'id' => 'Article ID').
	 *                    $formValues->values in the format name => value (for example, 'state' => 'Published'.
	 *                    This translates the text from the selected option in the form.
	 *
	 * @since   3.2
	 */
	public static function getFormValues($object, ContentType $typesTable)
	{
		$labels = array();
		$values = array();
		$expandedObjectArray = static::createObjectArray($object);
		static::loadLanguageFiles($typesTable->type_alias);

		if ($formFile = static::getFormFile($typesTable))
		{
			if ($xml = simplexml_load_file($formFile))
			{
				// Now we need to get all of the labels from the form
				$fieldArray = $xml->xpath('//field');
				$fieldArray = array_merge($fieldArray, $xml->xpath('//fields'));

				foreach ($fieldArray as $field)
				{
					if ($label = (string) $field->attributes()->label)
					{
						$labels[(string) $field->attributes()->name] = Text::_($label);
					}
				}

				// Get values for any list type fields
				$listFieldArray = $xml->xpath('//field[@type="list" or @type="radio"]');

				foreach ($listFieldArray as $field)
				{
					$name = (string) $field->attributes()->name;

					if (isset($expandedObjectArray[$name]))
					{
						$optionFieldArray = $field->xpath('option[@value="' . $expandedObjectArray[$name] . '"]');

						$valueText = null;

						if (is_array($optionFieldArray) && count($optionFieldArray))
						{
							$valueText = trim((string) $optionFieldArray[0]);
						}

						$values[(string) $field->attributes()->name] = Text::_($valueText);
					}
				}
			}
		}

		$result = new \stdClass;
		$result->labels = $labels;
		$result->values = $values;

		return $result;
	}

	/**
	 * Method to get the XML form file for this component. Used to get translated field names for history preview.
	 *
	 * @param   ContentType  $typesTable  Table object with content history options.
	 *
	 * @return  mixed  \JModel object if successful, false if no model found.
	 *
	 * @since   3.2
	 */
	public static function getFormFile(ContentType $typesTable)
	{
		// First, see if we have a file name in the $typesTable
		$options = json_decode($typesTable->content_history_options);

		if (is_object($options) && isset($options->formFile) && File::exists(JPATH_ROOT . '/' . $options->formFile))
		{
			$result = JPATH_ROOT . '/' . $options->formFile;
		}
		else
		{
			$aliasArray = explode('.', $typesTable->type_alias);
			$component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0];
			$path  = Folder::makeSafe(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/');
			array_shift($aliasArray);
			$file = File::makeSafe(implode('.', $aliasArray) . '.xml');
			$result = File::exists($path . $file) ? $path . $file : false;
		}

		return $result;
	}

	/**
	 * Method to query the database using values from lookup objects.
	 *
	 * @param   \stdClass  $lookup  The std object with the values needed to do the query.
	 * @param   mixed      $value   The value used to find the matching title or name. Typically the id.
	 *
	 * @return  mixed  Value from database (for example, name or title) on success, false on failure.
	 *
	 * @since   3.2
	 */
	public static function getLookupValue($lookup, $value)
	{
		$result = false;

		if (isset($lookup->sourceColumn) && isset($lookup->targetTable) && isset($lookup->targetColumn) && isset($lookup->displayColumn))
		{
			$db    = Factory::getDbo();
			$value = (int) $value;
			$query = $db->getQuery(true);
			$query->select($db->quoteName($lookup->displayColumn))
				->from($db->quoteName($lookup->targetTable))
				->where($db->quoteName($lookup->targetColumn) . ' = :value')
				->bind(':value', $value, ParameterType::INTEGER);
			$db->setQuery($query);

			try
			{
				$result = $db->loadResult();
			}
			catch (\Exception $e)
			{
				// Ignore any errors and just return false
				return false;
			}
		}

		return $result;
	}

	/**
	 * Method to remove fields from the object based on values entered in the #__content_types table.
	 *
	 * @param   \stdClass    $object     Object to be passed to view layout file.
	 * @param   ContentType  $typeTable  Table object with content history options.
	 *
	 * @return  \stdClass  object with hidden fields removed.
	 *
	 * @since   3.2
	 */
	public static function hideFields($object, ContentType $typeTable)
	{
		if ($options = json_decode($typeTable->content_history_options))
		{
			if (isset($options->hideFields) && is_array($options->hideFields))
			{
				foreach ($options->hideFields as $field)
				{
					unset($object->$field);
				}
			}
		}

		return $object;
	}

	/**
	 * Method to load the language files for the component whose history is being viewed.
	 *
	 * @param   string  $typeAlias  The type alias, for example 'com_content.article'.
	 *
	 * @return  void
	 *
	 * @since   3.2
	 */
	public static function loadLanguageFiles($typeAlias)
	{
		$aliasArray = explode('.', $typeAlias);

		if (is_array($aliasArray) && count($aliasArray) == 2)
		{
			$component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0];
			$lang = Factory::getLanguage();

			/**
			 * Loading language file from the administrator/language directory then
			 * loading language file from the administrator/components/extension/language directory
			 */
			$lang->load($component, JPATH_ADMINISTRATOR)
			|| $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component));

			// Force loading of backend global language file
			$lang->load('joomla', Path::clean(JPATH_ADMINISTRATOR));
		}
	}

	/**
	 * Method to create object to pass to the layout. Format is as follows:
	 * field is std object with name, value.
	 *
	 * Value can be a std object with name, value pairs.
	 *
	 * @param   \stdClass  $object      The std object from the JSON string. Can be nested 1 level deep.
	 * @param   \stdClass  $formValues  Standard class of label and value in an associative array.
	 *
	 * @return  \stdClass  Object with translated labels where available
	 *
	 * @since   3.2
	 */
	public static function mergeLabels($object, $formValues)
	{
		$result = new \stdClass;

		if ($object === null)
		{
			return $result;
		}

		$labelsArray = $formValues->labels;
		$valuesArray = $formValues->values;

		foreach ($object as $name => $value)
		{
			$result->$name = new \stdClass;
			$result->$name->name = $name;
			$result->$name->value = $valuesArray[$name] ?? $value;
			$result->$name->label = $labelsArray[$name] ?? $name;

			if (is_object($value))
			{
				$subObject = new \stdClass;

				foreach ($value as $subName => $subValue)
				{
					$subObject->$subName = new \stdClass;
					$subObject->$subName->name = $subName;
					$subObject->$subName->value = $valuesArray[$subName] ?? $subValue;
					$subObject->$subName->label = $labelsArray[$subName] ?? $subName;
					$result->$name->value = $subObject;
				}
			}
		}

		return $result;
	}

	/**
	 * Method to prepare the object for the preview and compare views.
	 *
	 * @param   ContentHistory  $table  Table object loaded with data.
	 *
	 * @return  \stdClass  Object ready for the views.
	 *
	 * @since   3.2
	 */
	public static function prepareData(ContentHistory $table)
	{
		$object = static::decodeFields($table->version_data);
		$typesTable = Table::getInstance('ContentType', 'Joomla\\CMS\\Table\\');
		$typeAlias = explode('.', $table->item_id);
		array_pop($typeAlias);
		$typesTable->load(array('type_alias' => implode('.', $typeAlias)));
		$formValues = static::getFormValues($object, $typesTable);
		$object = static::mergeLabels($object, $formValues);
		$object = static::hideFields($object, $typesTable);
		$object = static::processLookupFields($object, $typesTable);

		return $object;
	}

	/**
	 * Method to process any lookup values found in the content_history_options column for this table.
	 * This allows category title and user name to be displayed instead of the id column.
	 *
	 * @param   \stdClass    $object      The std object from the JSON string. Can be nested 1 level deep.
	 * @param   ContentType  $typesTable  Table object loaded with data.
	 *
	 * @return  \stdClass  Object with lookup values inserted.
	 *
	 * @since   3.2
	 */
	public static function processLookupFields($object, ContentType $typesTable)
	{
		if ($options = json_decode($typesTable->content_history_options))
		{
			if (isset($options->displayLookup) && is_array($options->displayLookup))
			{
				foreach ($options->displayLookup as $lookup)
				{
					$sourceColumn = $lookup->sourceColumn ?? false;
					$sourceValue = $object->$sourceColumn->value ?? false;

					if ($sourceColumn && $sourceValue && ($lookupValue = static::getLookupValue($lookup, $sourceValue)))
					{
						$object->$sourceColumn->value = $lookupValue;
					}
				}
			}
		}

		return $object;
	}
}
