<?php
/**
 * Dynamic macro registration for classes (ported pattern).
 *
 * @package PDFDraft
 */

namespace PDFDraft\Core\Support\Traits;

use BadMethodCallException;
use Closure;
use ReflectionClass;
use ReflectionMethod;

trait Macroable {
	/**
	 * The registered string macros.
	 *
	 * @var array
	 */
	protected static $macros = array();

	/**
	 * Register a custom macro.
	 *
	 * @param string          $name  Macro name.
	 * @param object|callable $macro Callable or invokable macro.
	 * @return void
	 */
	public static function macro( $name, $macro ) {
		static::$macros[ $name ] = $macro;
	}

	/**
	 * Mix another object into the class.
	 *
	 * @param object $mixin   Object whose methods are copied as macros.
	 * @param bool   $replace When true, overwrite existing macros with the same name.
	 * @return void
	 *
	 * @throws \ReflectionException If the mixin cannot be reflected.
	 */
	public static function mixin( $mixin, $replace = true ) {
		$methods = ( new ReflectionClass( $mixin ) )->getMethods(
			ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
		);
		foreach ( $methods as $method ) {
			if ( $replace || ! static::hasMacro( $method->name ) ) {
				$method->setAccessible( true );
				static::macro( $method->name, $method->invoke( $mixin ) );
			}
		}
	}

	/**
	 * Checks if macro is registered.
	 *
	 * @param string $name Macro name.
	 * @return bool
	 */
	public static function hasMacro( $name ) {
		return isset( static::$macros[ $name ] );
	}

	/**
	 * Flush the existing macros.
	 *
	 * @return void
	 */
	public static function flushMacros() {
		static::$macros = array();
	}

	/**
	 * Dynamically handle calls to the class.
	 *
	 * @param string $method     Method name.
	 * @param array  $parameters Arguments passed to the macro.
	 * @return mixed
	 *
	 * @throws \BadMethodCallException When the macro is not registered.
	 */
	public static function __callStatic( $method, $parameters ) {
		if ( ! static::hasMacro( $method ) ) {
			// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not HTML output.
			throw new BadMethodCallException(
				sprintf(
					'Method %s::%s does not exist.',
					static::class,
					$method
				)
			);
			// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}
		$macro = static::$macros[ $method ];
		if ( $macro instanceof Closure ) {
			$macro = $macro->bindTo( null, static::class );
		}
		return $macro( ...$parameters );
	}

	/**
	 * Dynamically handle calls to the class.
	 *
	 * @param string $method     Method name.
	 * @param array  $parameters Arguments passed to the macro.
	 * @return mixed
	 *
	 * @throws \BadMethodCallException When the macro is not registered.
	 */
	public function __call( $method, $parameters ) {
		if ( ! static::hasMacro( $method ) ) {
			// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not HTML output.
			throw new BadMethodCallException(
				sprintf(
					'Method %s::%s does not exist.',
					static::class,
					$method
				)
			);
			// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}
		$macro = static::$macros[ $method ];
		if ( $macro instanceof Closure ) {
			$macro = $macro->bindTo( $this, static::class );
		}
		return $macro( ...$parameters );
	}
}
