<?php
/**
 * Handles the transmutation of shortcodes references.
 *
 * @package Ealab\Deployer
 */

namespace Ealab\Deployer\Tracking\ItemValueHandler\ValueProcessor\TransmutationHandler;

use Dakwamine\Component\RootDependencyDefinition;
use Ealab\Deployer\Exception\IgnoredTransmutationException;
use Ealab\Deployer\Tracking\Config\ConfigCollector;
use Ealab\Deployer\Tracking\Config\TransmutationHandlerShortcodeReferences\TransmutationHandlerShortcodeReferencesConfigProviderManager;
use Ealab\Deployer\Tracking\ItemValueHandler\ItemValueHandlerBase;
use Ealab\Deployer\Tracking\TrackingManager;

/**
 * Handles the transmutation of shortcodes references.
 */
class ShortcodeReferencesTransmutationHandler extends TransmutationHandlerBase {
	/**
	 * The config collector.
	 *
	 * @var ConfigCollector|null
	 */
	private ?ConfigCollector $config_collector = null;

	/**
	 * The config provider manager for this handler.
	 *
	 * @var TransmutationHandlerShortcodeReferencesConfigProviderManager|null
	 */
	private ?TransmutationHandlerShortcodeReferencesConfigProviderManager $config_provider_manager = null;

	/**
	 * Tracking manager.
	 *
	 * @var TrackingManager|null
	 */
	private ?TrackingManager $tracking_manager = null;

	/**
	 * Entrypoint for recursive shortcodes processing for export.
	 *
	 * @param string|mixed $value The value to process. Usually a string.
	 * @param callable     $tag_callback The callback to apply on the found shortcodes.
	 *
	 * @return string|mixed The processed value. If the value could not be processed, the original value is returned.
	 */
	private function applyOnShortcodes( $value, callable $tag_callback ) {
		if ( ! is_string( $value ) ) {
			return $value;
		}

		/**
		 * The callback for the preg_replace_callback function.
		 *
		 * It is used to find the nested content, then rebuild the shortcode with the nested content,
		 * and finally process the current shortcode.
		 * It attaches the tag callback to the main preg_replace_callback function
		 * to allow processing of the shortcode tags.
		 *
		 * @param array $m The matches from preg_replace_callback. Contains the 7 elements given by
		 *     the get_shortcode_regex function. See getShortcodeRegex for more details.
		 *
		 * @return string The processed content that will be used by preg_replace_callback as replacement.
		 */
		$preg_callback = function ( array $m ) use ( $tag_callback ) {
			// Dev note: like the `do_shortcode_tag` function, we allow the use of [[foo]] syntax for escaping a tag.
			if ( '[' === $m[1] && ']' === $m[6] ) {
				return $m[0];
			}

			$tag = $m[2];

			// Optional content.
			$content = $m[5] ?? null;

			// Check if the shortcode has a closing tag at the end.
			$closing_tag = '[/' . $tag . ']';

			if ( empty( $content ) && substr( $m[0], -strlen( $tag ) - 3 ) !== $closing_tag ) {
				$closing_tag = '';
			}

			if ( ! empty( $m[5] ) ) {
				// Process the nested content.
				$m[5] = $this->applyOnShortcodes( $m[5], $tag_callback );
			}

			// Rebuild the shortcode with the nested content.
			$m[0] = '[' . $m[1] . $m[2] . $m[3] . $m[4] . $m[6] . ']' . $m[5] . $closing_tag;

			// Finally process the current shortcode.
			return $tag_callback( $m[0] );
		};

		$replaced_value = preg_replace_callback( $this->getShortcodeRegex(), $preg_callback, $value );

		if ( ! is_string( $replaced_value ) ) {
			// The preg_replace_callback function failed.
			return $value;
		}

		return $replaced_value;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getDependencyDefinitions(): array {
		$definitions = parent::getDependencyDefinitions();

		$definitions[] = new RootDependencyDefinition( ConfigCollector::class, $this->config_collector );
		$definitions[] = new RootDependencyDefinition( TrackingManager::class, $this->tracking_manager );
		$definitions[] = new RootDependencyDefinition( TransmutationHandlerShortcodeReferencesConfigProviderManager::class, $this->config_provider_manager );

		return $definitions;
	}

	/**
	 * {@inheritDoc}
	 */
	public static function getId(): string {
		return 'shortcode_references';
	}

	/**
	 * {@inheritDoc}
	 */
	static public function getPublicName(): string {
		return _dpgettext( 'deployer-core', 'Transmutation handler public name', 'References in shortcode' );
	}

	/**
	 * Convenient wrapper to get the regex for shortcodes.
	 *
	 * @param array|null $specific_tags The list of specific tags to match.
	 *
	 * @return string The regex, with the slashes.
	 *     Using preg_match_all or preg_replace_callback, the data exposed by preg_*
	 *     is an array containing 7 keys about the found shortcodes, in the following order:
	 *     - 0: The full shortcode
	 *     - 1: An extra [ character (if the shortcode should be left unaltered)
	 *     - 2: The shortcode name
	 *     - 3: The shortcode argument list
	 *     - 4: The self-closing /
	 *     - 5: The content of the shortcode (may contain other shortcodes)
	 *     - 6: An extra ] character (if the shortcode should be left unaltered)
	 */
	private function getShortcodeRegex( ?array $specific_tags = null ): string {
		// This will return a regex that matches shortcodes.
		// Dev note: if specific tags need to be tested for debugging, replace or add the tags in the array.
		return '/' . get_shortcode_regex( $specific_tags ) . '/';
	}

	/**
	 * {@inheritDoc}
	 */
	public function toExport( $value, ?array $options = array() ) {
		if ( ! is_string( $value ) ) {
			throw new IgnoredTransmutationException();
		}

		// Collect the configuration of this handler.
		$shortcode_attr_mapping = $this->config_collector->collect( $this->config_provider_manager, 'transmutation_handler__shortcode_references' );

		// Callback that will perform the transmutation.
		// Dev note: this could be made configurable.
		$tag_callback = function ( $transmutable ) use ( $options, $shortcode_attr_mapping ) {
			$replaced = preg_replace_callback(
				$this->getShortcodeRegex(),
				function ( array $m ) use ( $options, $shortcode_attr_mapping ) {
					$tag = $m[2];

					if ( ! array_key_exists( $tag, $shortcode_attr_mapping ) ) {
						// This shortcode has no attributes to transmute.
						return $m[0];
					}

					$found_attributes = shortcode_parse_atts( $m[3] );

					foreach ( $found_attributes as $attribute_key => &$attribute_value ) {
						$tracker_id = $shortcode_attr_mapping[ $tag ][ $attribute_key ]['tracker'] ?? null;

						if ( ! $tracker_id ) {
							// This attribute is not to be transmuted.
							continue;
						}

						// Transmute this attribute value.
						$tracker = $this->tracking_manager->getItemTrackerByTrackerId( $tracker_id );

						if ( ! $tracker ) {
							// This tracker does not exist. Leave this attribute value as is.
							continue;
						}

						$known_uuid = $tracker->getItemUuidByLocalId( $attribute_value, true );

						if ( ! $known_uuid ) {
							// This local ID is not known in the tracker. Leave this attribute value as is.
							continue;
						}

						$trackable_item = $tracker->loadTrackableItem( $known_uuid );

						if ( ! $trackable_item ) {
							// This known UUID is not linked to a valid item. Leave this attribute value as is.
							continue;
						}

						// Add the dependency to the TrackableItem holder.
						$calling_item_value_handler = $options['_item_value_handler'] ?? null;

						if ( $calling_item_value_handler instanceof ItemValueHandlerBase ) {
							$calling_item_value_handler->addItemDependency( $trackable_item );
						}

						// Finally encapsulate the attribute value in a placeholder.
						$attribute_value = static::getProcessorPlaceholder( $known_uuid );
					}

					// Unset the reference to the attribute value.
					unset( $attribute_value );

					// Rebuild the shortcode attributes.
					$rebuilt_attributes = array();

					foreach ( $found_attributes as $attribute_key => $attribute_value ) {
						// Try to detect the quote style and spaces used for this attribute in the original data.
						$valid_detection = preg_match( '/\s*' . $attribute_key . '\s*=\s*[\'"]/', $m[3], $attribute_start );

						if ( $valid_detection ) {
							// Detect the quote style.
							$uses_single_quote = preg_match( '/\s' . $attribute_key . '\s*=\s*\'/', $m[3] );

							$rebuilt_attributes[] = reset( $attribute_start ) . $attribute_value . ( $uses_single_quote ? '\'' : '"' );
						} else {
							// Backup implementation: just use double quotes.
							// Dev note: this implementation is not perfect because it gets rid of the original spaces
							// between the attributes and the spaces around the equal sign, and assumes that the
							// commonly used double quotes are in effect in the original data. But it should work in
							// most cases because those spaces should not be significant in the context of shortcode
							// attributes, and double quotes are mainly used.
							$rebuilt_attributes[] = ' ' . $attribute_key . '="' . $attribute_value . '"';
						}
					}

					// Prepend and append any spaces that were in the original shortcode.
					// First, retrieve the spaces at the end of the attributes $m[3].
					// This implementation may not be perfect, but it should work in most cases.
					$ending_spaces_char_count = strlen( $m[3] ) - strlen( rtrim( $m[3] ) );
					$ending_spaces            = $ending_spaces_char_count ? substr( $m[3], -$ending_spaces_char_count ) : '';

					$final_rebuilt_attributes = implode( '', $rebuilt_attributes ) . $ending_spaces;

					// Rebuild the shortcode.
					// Optional content.
					$content = $m[5] ?? null;

					// Check if the shortcode has a closing tag at the end.
					$closing_tag = '[/' . $tag . ']';

					if ( empty( $content ) && substr( $m[0], -strlen( $tag ) - 3 ) !== $closing_tag ) {
						$closing_tag = '';
					}

					return '[' . $m[1] . $tag . $final_rebuilt_attributes . $m[4] . $m[6] . ']' . $m[5] . $closing_tag;
				},
				$transmutable
			);

			if ( is_string( $replaced ) ) {
				$transmutable = $replaced;
			}

			return $transmutable;
		};

		return $this->applyOnShortcodes( $value, $tag_callback );
	}

	/**
	 * {@inheritdoc}
	 */
	public function toImportFinalize( $value, ?array $options = array() ) {
		return static::applyOnPlaceholders(
			$value,
			function ( array $regex_match ) {
				$item_full_uuid = $regex_match[3];

				// Get the local ID from the full UUID.
				$tracker = $this->tracking_manager->getItemTrackerByItemFullUuid( $item_full_uuid );

				if ( ! $tracker ) {
					// This tracker does not exist. Replace by the 0 reference as an attempt to not break things.
					return '0';
				}

				$local_id = $tracker->getLocalIdFromUuid( $item_full_uuid );

				if ( ! $local_id ) {
					// This UUID is not known in the tracker. This should not happen.
					// Replace by the 0 reference as an attempt to not break things.
					return '0';
				}

				return $local_id;
			}
		);
	}

	/**
	 * {@inheritdoc}
	 */
	public function toImportPresave( $value, ?array $options = array() ) {
		return static::applyOnPlaceholders(
			$value,
			function ( array $regex_match ) {
				// Return a 0 reference.
				return '0';
			}
		);
	}
}
