<?php
/**
 * Trait for processors of any type that replaces values by a placeholder.
 *
 * @package Ealab\Deployer
 */

namespace EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\ItemValueHandler;

use EaLab\WP_Deployer\vendor\Ealab\Deployer\Exception\IgnoredTransmutationException;

/**
 * Trait for processors of any type that replaces values by a placeholder.
 *
 * Processors must have a static getId() method to be used with this trait.
 */
trait PlaceholderableTrait {
	/**
	 * Applies the given callback on the placeholders of the given value.
	 *
	 * Use this to replace the placeholders by their actual values.
	 *
	 * @param string|mixed  $value The value to process. String is expected.
	 * @param callable      $apply_callback The callback to apply on the placeholders. It must accept an array
	 *     of preg_match_all matches and return the replacement value as string.
	 * @param callable|null $post_apply_callback Optional callback to apply on the result after the placeholders
	 *     have been processed.
	 *
	 * @return mixed The processed value.
	 *
	 * @throws IgnoredTransmutationException If the value is not a string or does not contain any placeholder.
	 */
	public static function applyOnPlaceholders( $value, callable $apply_callback, ?callable $post_apply_callback = null ) {
		if ( ! is_string( $value ) ) {
			// The value is not a string. We only work on strings.
			// Use the transmutation value handler data walker pattern to point on the real value to process.
			throw new IgnoredTransmutationException();
		}

		$processor_placeholder_prefix = static::getProcessorPlaceholderPrefix();

		if ( false === strpos( $value, $processor_placeholder_prefix ) ) {
			// The placeholder is not in the value.
			throw new IgnoredTransmutationException();
		}

		// Escape the placeholder prefix for the regex.
		$processor_placeholder_prefix = preg_quote( $processor_placeholder_prefix, '/' );

		// The first pass is to find the potentially valid placeholders.
		preg_match_all( '/(' . $processor_placeholder_prefix . '(:(\d+):([^]]*))?]+)/', $value, $matches );

		// Get the placeholders lengths.
		$known_lengths = array();

		foreach ( $matches[3] as $length ) {
			if ( is_numeric( $length ) ) {
				$known_lengths[ $length ] = (int) $length;
				continue;
			}

			// The length is not set. It means that it's a placeholder without sub-identifier.
			if ( ! in_array( null, $known_lengths, true ) ) {
				$known_lengths['no_sub_id'] = null;
			}
		}

		foreach ( $known_lengths as $sub_identifier_length ) {
			// The second pass is to apply the callback to the placeholders.
			// It checks the expected length of the sub-identifier (if any).
			if ( null === $sub_identifier_length ) {
				// No sub-identifier.
				$pattern = '/(' . $processor_placeholder_prefix . '(())])/';
			} elseif ( 0 === $sub_identifier_length ) {
				// Zero-length sub-identifier.
				$pattern = '/(' . $processor_placeholder_prefix . '(:0:())])/';
			} else {
				// Sub-identifier of a specific length.
				// Split the sub-identifier length quantifier to work around the preg_replace_callback limitation
				// on the quantifier that triggers "Compilation failed: number too big in {} quantifier".
				$sub_identifier_length_splits = array();

				$sub_identifier_length_tmp = $sub_identifier_length;

				while ( $sub_identifier_length_tmp > 0 ) {
					// 65535 is the maximum value found on a PHP 7.4 installation.
					// Update this value if it is too high or too low (maybe make it configurable).
					$sub_identifier_length_splits[] = min( $sub_identifier_length_tmp, 65535 );

					$sub_identifier_length_tmp -= 65535;
				}

				$pattern = '/(' . $processor_placeholder_prefix . '(:' . $sub_identifier_length . ':(';

				foreach ( $sub_identifier_length_splits as $sub_identifier_length_split ) {
					$pattern .= '.{' . $sub_identifier_length_split . '}';
				}

				$pattern .= '))?]+)/';
			}

			$value = preg_replace_callback( $pattern, $apply_callback, $value );
		}

		if ( null !== $post_apply_callback ) {
			// This callback is useful to apply additional processing on the result.
			// E.g. fixing the value type to int or string for values that
			// contain a single local ID.
			$value = $post_apply_callback( $value );
		}

		return $value;
	}

	/**
	 * Gets the placeholder of the processor.
	 *
	 * This is useful to act as a placeholder in value replacements.
	 * The fact that it's partly a hash limits the risk of collision with
	 * real content or another plugin using the same placeholder.
	 *
	 * @param string $sub_identifier An optional sub-identifier if the processor needs multiple placeholders.
	 *
	 * @return string The processor hash.
	 */
	protected static function getProcessorPlaceholder( string $sub_identifier = '' ): string {
		$placeholder = static::getProcessorPlaceholderPrefix();

		if ( ! empty( $sub_identifier ) ) {
			// Add the length of the sub-identifier to avoid bad delimiter detections.
			$placeholder .= ':' . strlen( $sub_identifier ) . ':' . $sub_identifier;
		}

		return $placeholder . ']';
	}

	/**
	 * Gets the prefix of the processor placeholder.
	 *
	 * @return string The processor placeholder prefix.
	 */
	protected static function getProcessorPlaceholderPrefix(): string {
		return '[' . static::getId() . '.' . substr( sha1( static::class ), 0, 8 );
	}

	/**
	 * Checks if the given value is a placeholder for this processor.
	 *
	 * It supports sub-identifiers.
	 *
	 * TODO: this could be enhanced to a parser that would either return the first processor placeholder found,
	 * or apply a callback to each processor placeholder found.
	 *
	 * @param string $value The value to compare.
	 *
	 * @return bool|string True if the value is a placeholder for this processor without sub-identifier,
	 *   the sub-identifier string if there is one, false otherwise.
	 */
	public static function isProcessorPlaceholder( string $value ) {
		$processor_placeholder_prefix = static::getProcessorPlaceholderPrefix();

		if ( 0 !== strpos( $value, $processor_placeholder_prefix ) ) {
			return false;
		}

		$value = substr( $value, strlen( $processor_placeholder_prefix ) );

		if ( ':' !== substr( $value, 0, 1 ) ) {
			// No sub-identifier. The remaining character must be the closing bracket.
			return ']' === $value;
		}

		// Get rid of the colon.
		$value = substr( $value, 1 );

		// Sub-identifier.
		// Get the length of the sub-identifier.
		$sub_identifier_parts = explode( ':', $value, 2 );

		if ( 2 !== count( $sub_identifier_parts ) ) {
			return false;
		}

		if ( ']' !== substr( $sub_identifier_parts[1], -1 ) ) {
			// The closing bracket must be at the end.
			return false;
		}

		$sub_identifier = substr( $sub_identifier_parts[1], 0, -1 );

		$sub_identifier_expected_length = $sub_identifier_parts[0];

		if ( filter_var( $sub_identifier_expected_length, FILTER_VALIDATE_INT ) === false ) {
			// The sub-identifier length must be an integer.
			return false;
		}

		$sub_identifier_expected_length = (int) $sub_identifier_expected_length;

		if ( strlen( $sub_identifier ) !== $sub_identifier_expected_length ) {
			// The sub-identifier length must match the specified length of the sub-identifier.
			return false;
		}

		// Return the sub identifier, or simply true if the sub identifier is empty.
		return '' === $sub_identifier ? true : $sub_identifier;
	}
}
