<?php
/**
 * Akeeba Engine
 *
 * @package   akeebaengine
 * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program. If not, see
 * <https://www.gnu.org/licenses/>.
 */

namespace Akeeba\Engine\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use DirectoryIterator;
use LogicException;

/**
 * Unified engine parameters helper class. Deals with scripting, GUI configuration elements and information on engine
 * parts (filters, dump engines, scan engines, archivers, installers).
 */
class EngineParameters
{
	/**
	 * Holds the parsed scripting.json contents
	 *
	 * @var array
	 */
	public $scripting = null;

	/**
	 * The currently active scripting type
	 *
	 * @var string
	 */
	protected $activeType = null;

	/**
	 * Holds the known paths holding JSON definitions of engines, installers and configuration gui elements
	 *
	 * @var  array
	 */
	protected $enginePartPaths = [];

	/**
	 * Cache of the engines known to this object
	 *
	 * @var array
	 */
	protected $engine_list = [];

	/**
	 * Cache of the GUI configuration elements known to this object
	 *
	 * @var array
	 */
	protected $gui_list = [];

	/**
	 * Cache of the installers known to this object
	 *
	 * @var array
	 */
	protected $installer_list = [];

	/**
	 * Append a path to the end of the paths list for a specific section
	 *
	 * @param   string  $path     Absolute filesystem path to add
	 * @param   string  $section  The section to add it to (gui, engine, installer, filters)
	 *
	 * @return  void
	 */
	public function addPath(string $path, string $section = 'gui')
	{
		$path = Factory::getFilesystemTools()->TranslateWinPath($path);

		// If the array is empty, populate with the defaults
		if (!array_key_exists($section, $this->enginePartPaths))
		{
			$this->getEnginePartPaths($section);
		}

		// If the path doesn't already exist, add it
		if (!in_array($path, $this->enginePartPaths[$section]))
		{
			$this->enginePartPaths[$section][] = $path;
		}
	}

	/**
	 * Returns an array with domain keys and domain class names for the current
	 * backup type. The idea is that shifting this array walks through the backup
	 * process. When the array is empty, the backup is done.
	 *
	 * Each element of the array is an array with two keys: domain and class.
	 *
	 * @return  array
	 */
	public function getDomainChain(): array
	{
		$configuration = Factory::getConfiguration();
		$script        = $configuration->get('akeeba.basic.backup_type', 'full');

		$scripting = $this->loadScripting();
		$domains   = $scripting['domains'];
		$keys      = $scripting['scripts'][$script]['chain'];

		$result = [];

		foreach ($keys as $domain_key)
		{
			$result[] = [
				'domain' => $domains[$domain_key]['domain'],
				'class'  => $domains[$domain_key]['class'],
			];
		}

		return $result;
	}

	/**
	 * Get the paths for a specific section
	 *
	 * @param   string  $section  The section to get the path list for (engine, installer, gui, filter)
	 *
	 * @return  array
	 */
	public function getEnginePartPaths(string $section = 'gui')
	{
		// Create the key if it's not already present
		if (!array_key_exists($section, $this->enginePartPaths))
		{
			$this->enginePartPaths[$section] = [];
		}

		if (!empty($this->enginePartPaths[$section]))
		{
			return $this->enginePartPaths[$section];
		}

		// Add the defaults if the list is empty
		switch ($section)
		{
			case 'engine':
				$this->enginePartPaths[$section] = [
					Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot()),
				];
				break;

			case 'installer':
				$this->enginePartPaths[$section] = [
					Factory::getFilesystemTools()->TranslateWinPath(Platform::getInstance()->get_installer_images_path()),
				];
				break;

			case 'gui':
				// Add core GUI definitions
				$this->enginePartPaths[$section] = [
					Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Core'),
				];

				// Add platform GUI definition files
				$platform_paths = Platform::getInstance()->getPlatformDirectories();

				foreach ($platform_paths as $p)
				{
					$this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Config');

					$pro = defined('AKEEBA_PRO') && AKEEBA_PRO;
					$pro = defined('AKEEBABACKUP_PRO') ? (AKEEBABACKUP_PRO ? true : false) : $pro;

					if ($pro)
					{
						$this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Config/Pro');
					}
				}
				break;

			case 'filter':
				$this->enginePartPaths[$section] = [
					Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Platform/Filter/Stack'),
					Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Filter/Stack'),
				];

				$platform_paths = Platform::getInstance()->getPlatformDirectories();

				foreach ($platform_paths as $p)
				{
					$this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Filter/Stack');
				}

				break;

			default:
				throw new LogicException(sprintf('Can not get paths for engine section ‘%s’. No section by this name is known to Akeeba Engine.', $section));
		}

		return $this->enginePartPaths[$section];
	}

	/**
	 * Returns a hash list of Akeeba engines and their data. Each entry has the engine name as key and contains two
	 * arrays, under the 'information' and 'parameters' keys.
	 *
	 * @param   string  $engine_type  The engine type to return information for
	 *
	 * @return  array
	 */
	public function getEnginesList(string $engine_type): array
	{
		$engine_type = ucfirst($engine_type);

		// Try to serve cached data first
		if (isset($this->engine_list[$engine_type]))
		{
			return $this->engine_list[$engine_type];
		}

		// Find absolute path to normal and plugins directories
		$temp      = $this->getEnginePartPaths('engine');
		$path_list = [];

		foreach ($temp as $path)
		{
			$path_list[] = $path . '/' . $engine_type;
		}

		// Initialize the array where we store our data
		$this->engine_list[$engine_type] = [];

		// Loop for the paths where engines can be found
		foreach ($path_list as $path)
		{
			if (!@is_dir($path))
			{
				continue;
			}

			if (!@is_readable($path))
			{
				continue;
			}

			$di = new DirectoryIterator($path);

			/** @var DirectoryIterator $file */
			foreach ($di as $file)
			{
				if (!$file->isFile())
				{
					continue;
				}

				if ($file->getExtension() !== 'json')
				{
					continue;
				}

				$bare_name = ucfirst($file->getBasename('.json'));

				// Some hosts copy .json and .php files, renaming them (ie foobar.1.php)
				// We need to exclude them, otherwise we'll get a fatal error for declaring the same class twice
				if (preg_match('/[^A-Za-z0-9]/', $bare_name))
				{
					continue;
				}

				$information = [];
				$parameters  = [];

				$this->parseEngineJSON($file->getRealPath(), $information, $parameters);

				$this->engine_list[$engine_type][lcfirst($bare_name)] = [
					'information' => $information,
					'parameters'  => $parameters,
				];
			}
		}

		return $this->engine_list[$engine_type];
	}

	/**
	 * Parses the GUI JSON files and returns an array of groups and their data
	 *
	 * @return  array
	 */
	public function getGUIGroups(): array
	{
		// Try to serve cached data first
		if (!empty($this->gui_list) && is_array($this->gui_list))
		{
			if (count($this->gui_list) > 0)
			{
				return $this->gui_list;
			}
		}

		// Find absolute path to normal and plugins directories
		$path_list = $this->getEnginePartPaths('gui');

		// Initialize the array where we store our data
		$this->gui_list = [];

		// Loop for the paths where engines can be found
		foreach ($path_list as $path)
		{
			if (!@is_dir($path))
			{
				continue;
			}

			if (!@is_readable($path))
			{
				continue;
			}

			$allJSONFiles = [];
			$di           = new DirectoryIterator($path);

			/** @var DirectoryIterator $file */
			foreach ($di as $file)
			{
				if (!$file->isFile())
				{
					continue;
				}

				// PHP 5.3.5 and earlier do not support getExtension
				if ($file->getExtension() !== 'json')
				{
					continue;
				}

				$allJSONFiles[] = $file->getRealPath();
			}

			if (empty($allJSONFiles))
			{
				continue;
			}

			// Sort GUI files alphabetically
			asort($allJSONFiles);

			// Include each GUI def file
			foreach ($allJSONFiles as $filename)
			{
				$information = [];
				$parameters  = [];

				$this->parseInterfaceJSON($filename, $information, $parameters);

				// This effectively skips non-GUI JSONs (e.g. the scripting JSON)
				if (!empty($information['description']))
				{
					if (!isset($information['merge']))
					{
						$information['merge'] = 0;
					}

					$group_name = substr(basename($filename), 0, -5);

					$def = [
						'information' => $information,
						'parameters'  => $parameters,
					];

					if (!$information['merge'] || !isset($this->gui_list[$group_name]))
					{
						$this->gui_list[$group_name] = $def;
					}
					else
					{
						$this->gui_list[$group_name]['information'] = array_merge($this->gui_list[$group_name]['information'], $def['information']);
						$this->gui_list[$group_name]['parameters']  = array_merge($this->gui_list[$group_name]['parameters'], $def['parameters']);
					}
				}
			}
		}

		ksort($this->gui_list);

		// Push stack filter settings to the 03.filters section
		$path_list = $this->getEnginePartPaths('filter');

		// Loop for the paths where optional filters can be found
		foreach ($path_list as $path)
		{
			if (!@is_dir($path))
			{
				continue;
			}

			if (!@is_readable($path))
			{
				continue;
			}

			// Store JSON names in temp array because we'll sort based on filename (GUI order IS IMPORTANT!!)
			$allJSONFiles = [];

			$di = new DirectoryIterator($path);

			/** @var DirectoryIterator $file */
			foreach ($di as $file)
			{
				if (!$file->isFile())
				{
					continue;
				}

				// PHP 5.3.5 and earlier do not support getExtension
				if ($file->getExtension() !== 'json')
				{
					continue;
				}

				$allJSONFiles[] = $file->getRealPath();
			}

			if (empty($allJSONFiles))
			{
				continue;
			}

			// Sort filter files alphabetically
			asort($allJSONFiles);

			// Include each filter def file
			foreach ($allJSONFiles as $filename)
			{
				$information = [];
				$parameters  = [];

				$this->parseInterfaceJSON($filename, $information, $parameters);

				if (!array_key_exists('03.filters', $this->gui_list))
				{
					$this->gui_list['03.filters'] = ['parameters' => []];
				}

				if (!array_key_exists('parameters', $this->gui_list['03.filters']))
				{
					$this->gui_list['03.filters']['parameters'] = [];
				}

				if (!is_array($parameters))
				{
					$parameters = [];
				}

				$this->gui_list['03.filters']['parameters'] = array_merge($this->gui_list['03.filters']['parameters'], $parameters);
			}
		}

		/**
		 * Parse showon attributes
		 *
		 * The GUI list array format is like this:
		 * ```
		 * [
		 *    '01.sectionName' => [
		 *       'information' => [...],
		 *       'parameters' => [
		 *           'some.parameter.name' => [
		 *               'title' => 'something',
		 *               ...
		 *               'showon' => 'some.other.parameter.name:1'
		 *           ],
		 *           ...
		 *       ]
		 *    ],
		 *    ...
		 * ]
		 * ```
		 *
		 * We need to convert the shown of the parameters to JSON data which can be used by the `showon` JavaScript
		 * code. The assumption only made here is that all parameter keys are turned into `INPUT` elements with an
		 * attribute `name="var[some.parameter.name]"`. This is how the GUI code in all backup applications our company
		 * makes works.
		 *
		 * For backup applications which do not have showon support (they lack the JavaScript code) this does not matter
		 * and neither does it break anything. All parameters are shown all the time like we have been doing for years.
		 */
		$this->gui_list = array_map(
			function (array $section): array {
				foreach ($section['parameters'] as $paramName => &$paramDef)
				{
					if (isset($paramDef['showon']))
					{
						// Parse showon
						$paramDef['showon'] = $this->parseShowOnConditions($paramDef['showon']);
					}
				}

				return $section;
			},
			$this->gui_list
		);

		return $this->gui_list;
	}

	/**
	 * Parses the installer JSON files and returns an array of installers and their data
	 *
	 * @param   boolean  $forDisplay  If true only returns the information relevant for displaying the GUI
	 *
	 * @return  array
	 */
	public function getInstallerList(bool $forDisplay = false): array
	{
		// Try to serve cached data first
		if (!empty($this->installer_list) && is_array($this->installer_list))
		{
			if (count($this->installer_list) > 0)
			{
				return $this->installer_list;
			}
		}

		// Find absolute path to normal and plugins directories
		$path_list = [
			Platform::getInstance()->get_installer_images_path(),
		];

		// Initialize the array where we store our data
		$this->installer_list = [];

		// Loop for the paths where engines can be found
		foreach ($path_list as $path)
		{
			if (!@is_dir($path))
			{
				continue;
			}

			if (!@is_readable($path))
			{
				continue;
			}

			$di = new DirectoryIterator($path);

			/** @var DirectoryIterator $file */
			foreach ($di as $file)
			{
				if (!$file->isFile())
				{
					continue;
				}

				// PHP 5.3.5 and earlier do not support getExtension
				if ($file->getExtension() !== 'json')
				{
					continue;
				}

				$rawData = file_get_contents($file->getRealPath());
				$data    = empty($rawData) ? [] : json_decode($rawData, true);

				if ($forDisplay)
				{
					$innerData = reset($data);

					if (array_key_exists('listinoptions', $innerData))
					{
						if ($innerData['listinoptions'] == 0)
						{
							continue;
						}
					}
				}

				foreach ($data as $key => $values)
				{
					$this->installer_list[$key] = [];

					foreach ($values as $key2 => $value)
					{
						$this->installer_list[$key][$key2] = $value;
					}
				}
			}
		}

		return $this->installer_list;
	}

	/**
	 * Returns the JSON representation of the GUI definition and the associated values
	 *
	 * @return   string
	 */
	public function getJsonGuiDefinition(): string
	{
		// Initialize the array which will be converted to JSON representation
		$json_array = [
			'engines'    => [],
			'installers' => [],
			'gui'        => [],
		];

		// Get a reference to the configuration
		$configuration = Factory::getConfiguration();

		// Get data for all engines
		$engine_types = [
			'archiver',
			'dump',
			'scan',
			'writer',
			'postproc',
		];

		foreach ($engine_types as $type)
		{
			$engines = $this->getEnginesList($type);

			$tempArray    = [];
			$engineTitles = [];

			foreach ($engines as $engine_name => $engine_data)
			{
				// Translate information
				foreach ($engine_data['information'] as $key => $value)
				{
					switch ($key)
					{
						case 'title':
						case 'content':
						case 'description':
							$value = Platform::getInstance()->translate($value);
							break;
					}

					$tempArray[$engine_name]['information'][$key] = $value;

					if ($key == 'title')
					{
						$engineTitles[$engine_name] = $value;
					}
				}

				// Process parameters
				$parameters = [];

				foreach ($engine_data['parameters'] as $param_key => $param)
				{
					$param['default'] = $configuration->get($param_key, $param['default'], false);

					foreach ($param as $option_key => $option_value)
					{
						// Translate title, description, enumkeys
						switch ($option_key)
						{
							case 'title':
							case 'description':
							case 'content':
							case 'labelempty':
							case 'labelnotempty':
								$param[$option_key] = Platform::getInstance()->translate($option_value);
								break;

							case 'enumkeys':
								$enumkeys = explode('|', $option_value);
								$new_keys = [];
								foreach ($enumkeys as $old_key)
								{
									$new_keys[] = Platform::getInstance()->translate($old_key);
								}
								$param[$option_key] = implode('|', $new_keys);
								break;

							case 'showon':
								$param[$option_key] = $this->parseShowOnConditions($param[$option_key]);

							default:
						}
					}

					$parameters[$param_key] = $param;
				}

				// Add processed parameters
				$tempArray[$engine_name]['parameters'] = $parameters;
			}

			asort($engineTitles);

			foreach ($engineTitles as $engineName => $title)
			{
				$json_array['engines'][$type][$engineName] = $tempArray[$engineName];
			}
		}

		// Get data for GUI elements
		$json_array['gui'] = [];
		$groupdefs         = $this->getGUIGroups();

		foreach ($groupdefs as $groupKey => $definition)
		{
			$group_name = '';

			if (isset($definition['information']) && isset($definition['information']['description']))
			{
				$group_name = Platform::getInstance()->translate($definition['information']['description']);
			}

			// Skip no-name groups
			if (empty($group_name))
			{
				continue;
			}

			$parameters = [];

			foreach ($definition['parameters'] as $param_key => $param)
			{
				$param['default'] = $configuration->get($param_key, $param['default'], false);

				foreach ($param as $option_key => $option_value)
				{
					// Translate title, description, enumkeys
					switch ($option_key)
					{
						case 'title':
						case 'description':
						case 'content':
							$param[$option_key] = Platform::getInstance()->translate($option_value);
							break;

						case 'enumkeys':
							$enumkeys = explode('|', $option_value);
							$new_keys = [];
							foreach ($enumkeys as $old_key)
							{
								$new_keys[] = Platform::getInstance()->translate($old_key);
							}
							$param[$option_key] = implode('|', $new_keys);
							break;

						default:
					}
				}
				$parameters[$param_key] = $param;
			}
			$json_array['gui'][$group_name] = $parameters;
		}

		// Get data for the installers
		$json_array['installers'] = $this->getInstallerList(true);

		uasort($json_array['installers'], function ($a, $b) {
			if ($a['name'] == $b['name'])
			{
				return 0;
			}

			return ($a['name'] < $b['name']) ? -1 : 1;
		});

		$json = json_encode($json_array);

		return $json;
	}

	/**
	 * Returns a volatile scripting parameter for the active backup type
	 *
	 * @param   string  $key      The relative key, e.g. core.createarchive
	 * @param   mixed   $default  Default value
	 *
	 * @return  mixed  The scripting parameter's value
	 */
	public function getScriptingParameter(string $key, $default = null)
	{
		$configuration = Factory::getConfiguration();

		if (is_null($this->activeType))
		{
			$this->activeType = $configuration->get('akeeba.basic.backup_type', 'full');
		}

		return $configuration->get('volatile.scripting.' . $this->activeType . '.' . $key, $default);
	}

	/**
	 * Imports the volatile scripting parameters to the registry
	 *
	 * @return  void
	 */
	public function importScriptingToRegistry(): void
	{
		$scripting     = $this->loadScripting();
		$configuration = Factory::getConfiguration();
		$configuration->mergeArray($scripting['data'], false);
	}

	/**
	 * Loads the scripting.json and returns an array with the domains, the scripts and the raw data
	 *
	 * @return  array  The parsed scripting.json. Array keys: domains, scripts, data
	 */
	public function loadScripting(?string $jsonPath = ''): ?array
	{
		if (!empty($this->scripting))
		{
			return $this->scripting;
		}

		$this->scripting = [];
		$jsonPath        = $jsonPath ?: Factory::getAkeebaRoot() . '/Core/scripting.json';

		if (!@file_exists($jsonPath))
		{
			return $this->scripting;
		}

		$rawData          = file_get_contents($jsonPath);
		$rawScriptingData = empty($rawData) ? [] : json_decode($rawData, true);
		$domain_keys      = explode('|', $rawScriptingData['volatile.akeebaengine.domains']);
		$domains          = [];

		foreach ($domain_keys as $key)
		{
			$record        = [
				'domain' => $rawScriptingData['volatile.domain.' . $key . '.domain'],
				'class'  => $rawScriptingData['volatile.domain.' . $key . '.class'],
				'text'   => $rawScriptingData['volatile.domain.' . $key . '.text'],
			];
			$domains[$key] = $record;
		}

		$script_keys = explode('|', $rawScriptingData['volatile.akeebaengine.scripts']);
		$scripts     = [];

		foreach ($script_keys as $key)
		{
			$record        = [
				'chain' => explode('|', $rawScriptingData['volatile.scripting.' . $key . '.chain']),
				'text'  => $rawScriptingData['volatile.scripting.' . $key . '.text'],
			];
			$scripts[$key] = $record;
		}

		$this->scripting = [
			'domains' => $domains,
			'scripts' => $scripts,
			'data'    => $rawScriptingData,
		];

		return $this->scripting;
	}

	/**
	 * Parses an engine JSON file returning two arrays, one with the general information
	 * of that engine and one with its configuration variables' definitions
	 *
	 * @param   string  $jsonPath     Absolute path to engine JSON file
	 * @param   array   $information  [out] The engine information hash array
	 * @param   array   $parameters   [out] The parameters hash array
	 *
	 * @return  bool  True if the file was loaded
	 */
	public function parseEngineJSON(string $jsonPath, array &$information, array &$parameters): bool
	{
		if (!file_exists($jsonPath))
		{
			return false;
		}

		$information = [
			'title'       => '',
			'description' => '',
		];

		$parameters = [];

		$rawData  = file_get_contents($jsonPath);
		$jsonData = empty($rawData) ? [] : json_decode($rawData, true);

		foreach ($jsonData ?? [] as $section => $data)
		{
			if (is_array($data))
			{
				if ($section == '_information')
				{
					// Parse information
					foreach ($data as $key => $value)
					{
						$information[$key] = $value;
					}
				}
				elseif (substr($section, 0, 1) != '_')
				{
					// Parse parameters
					$newparam = [
						'title'       => '',
						'description' => '',
						'type'        => 'string',
						'default'     => '',
					];

					foreach ($data as $key => $value)
					{
						$newparam[$key] = $value;
					}
					$parameters[$section] = $newparam;
				}
			}
		}

		return true;
	}

	/**
	 * Parses a graphical interface JSON file returning two arrays, one with the general
	 * information of that configuration section and one with its configuration variables'
	 * definitions.
	 *
	 * @param   string  $jsonPath     Absolute path to engine JSON file
	 * @param   array   $information  [out] The GUI information hash array
	 * @param   array   $parameters   [out] The parameters hash array
	 *
	 * @return bool True if the file was loaded
	 */
	public function parseInterfaceJSON(string $jsonPath, array &$information, array &$parameters): bool
	{
		if (!file_exists($jsonPath))
		{
			return false;
		}

		$information = [
			'description' => '',
		];

		$parameters = [];
		$rawData    = file_get_contents($jsonPath);
		$jsonData   = empty($rawData) ? [] : json_decode($rawData, true);

		foreach ($jsonData as $section => $data)
		{
			if (is_array($data))
			{
				if ($section == '_group')
				{
					// Parse information
					foreach ($data as $key => $value)
					{
						$information[$key] = $value;
					}

					continue;
				}

				if (substr($section, 0, 1) != '_')
				{
					// Parse parameters
					$newparam = [
						'title'       => '',
						'description' => '',
						'type'        => 'string',
						'default'     => '',
						'protected'   => 0,
					];

					foreach ($data as $key => $value)
					{
						$newparam[$key] = $value;
					}

					$parameters[$section] = $newparam;
				}
			}
		}

		return true;
	}

	/**
	 * Add a path to the beginning of the paths list for a specific section
	 *
	 * @param   string  $path     Absolute filesystem path to add
	 * @param   string  $section  The section to add it to (gui, engine, installer, filters)
	 *
	 * @return  void
	 */
	public function prependPath(string $path, string $section = 'gui'): void
	{
		$path = Factory::getFilesystemTools()->TranslateWinPath($path);

		// If the array is empty, populate with the defaults
		if (!array_key_exists($section, $this->enginePartPaths))
		{
			$this->getEnginePartPaths($section);
		}

		// If the path doesn't already exist, add it
		if (!in_array($path, $this->enginePartPaths[$section]))
		{
			array_unshift($this->enginePartPaths[$section], $path);
		}
	}

	/**
	 * Parse the `showon` conditions text into an instructions array for the ShowOn JavaScript
	 *
	 * @param   string|null  $showOn     The `showon` conditions text
	 * @param   string|null  $arrayName  The array all of our parameters are members of, default 'var'.
	 *
	 * @return  array  The ShowOn JavaScript instructions
	 *
	 * @since   9.3.1
	 */
	private function parseShowOnConditions(?string $showOn, ?string $arrayName = 'var'): array
	{
		if (empty($showOn))
		{
			return [];
		}

		$showOnData  = [];
		$showOnParts = preg_split('#(\[AND\]|\[OR\])#', $showOn, -1, PREG_SPLIT_DELIM_CAPTURE);
		$op          = '';

		foreach ($showOnParts as $showOnPart)
		{
			if (in_array($showOnPart, ['[AND]', '[OR]']))
			{
				$op = trim($showOnPart, '[]');

				continue;
			}

			$compareEqual     = strpos($showOnPart, '!:') === false;
			$showOnPartBlocks = explode(($compareEqual ? ':' : '!:'), $showOnPart, 2);

			$field = $arrayName
				? sprintf("%s[%s]", $arrayName, $showOnPartBlocks[0])
				: $showOnPartBlocks[0];

			$showOnData[] = [
				'field'  => $field,
				'values' => explode(',', $showOnPartBlocks[1]),
				'sign'   => $compareEqual === true ? '=' : '!=',
				'op'     => $op,
			];

			$op = '';
		}

		return $showOnData;
	}
}
