<?php
/**
 * Bundled SVG icon library.
 *
 * @package MenuVerse
 */

namespace WPXero\MenuVerse\Frontend;

defined( 'ABSPATH' ) || exit;

/**
 * Reads the bundled icon manifest and renders icons as inline SVG.
 *
 * Icons are described as geometry, not as markup, so the same definitions
 * drive the PHP renderer and the React picker without either side handling raw
 * HTML. Nothing is fetched: there is no icon font and no remote request.
 */
final class Icon_Library {

	/**
	 * Parsed manifest cache.
	 *
	 * @var array|null
	 */
	private static $manifest = null;

	/**
	 * Registers hooks.
	 *
	 * @return void
	 */
	public function boot() {
		add_filter( 'menuverse_sanitize_format_icon', array( __CLASS__, 'sanitize_icon' ), 10, 2 );
	}

	/**
	 * Rejects icon slugs that are not in the bundled library.
	 *
	 * @param string $value  Sanitized slug.
	 * @param array  $schema Schema definition for the value.
	 * @return string
	 */
	public static function sanitize_icon( $value, $schema ) {
		if ( '' === $value || self::has( $value ) ) {
			return $value;
		}

		return isset( $schema['default'] ) ? (string) $schema['default'] : '';
	}

	/**
	 * Returns the parsed manifest.
	 *
	 * @return array
	 */
	private static function manifest() {
		if ( null !== self::$manifest ) {
			return self::$manifest;
		}

		$file = MENUVERSE_PATH . 'assets/icons/manifest.json';
		$data = is_readable( $file ) ? wp_json_file_decode( $file, array( 'associative' => true ) ) : null;

		self::$manifest = is_array( $data ) ? $data : array(
			'_meta' => array(),
			'icons' => array(),
		);

		return self::$manifest;
	}

	/**
	 * Returns every icon definition keyed by slug.
	 *
	 * @return array
	 */
	public static function all() {
		$manifest = self::manifest();

		/**
		 * Filters the bundled icon definitions.
		 *
		 * @since 1.0.0
		 *
		 * @param array $icons Icon definitions keyed by slug.
		 */
		return apply_filters( 'menuverse_icons', isset( $manifest['icons'] ) ? $manifest['icons'] : array() );
	}

	/**
	 * Returns the icons offered in the picker, excluding interface glyphs.
	 *
	 * @return array
	 */
	public static function selectable() {
		return array_filter(
			self::all(),
			static function ( $icon ) {
				return empty( $icon['ui'] );
			}
		);
	}

	/**
	 * Determines whether a slug exists in the library.
	 *
	 * @param string $slug Icon slug.
	 * @return bool
	 */
	public static function has( $slug ) {
		$icons = self::all();

		return isset( $icons[ $slug ] );
	}

	/**
	 * Returns the translated label for an icon.
	 *
	 * @param string $slug Icon slug.
	 * @return string
	 */
	public static function label( $slug ) {
		$icons = self::all();

		return isset( $icons[ $slug ]['label'] ) ? (string) $icons[ $slug ]['label'] : $slug;
	}

	/**
	 * Renders an icon as inline SVG.
	 *
	 * The result is safe to echo: every value is either a number cast from the
	 * manifest or an escaped attribute, never user input.
	 *
	 * @param string $slug    Icon slug.
	 * @param string $classes Optional class attribute.
	 * @param int    $size    Rendered pixel size.
	 * @return string
	 */
	public static function get( $slug, $classes = '', $size = 24 ) {
		$icons = self::all();

		if ( ! isset( $icons[ $slug ]['shapes'] ) ) {
			return '';
		}

		$shapes = '';

		foreach ( (array) $icons[ $slug ]['shapes'] as $shape ) {
			$shapes .= self::shape( (array) $shape );
		}

		if ( '' === $shapes ) {
			return '';
		}

		return sprintf(
			'<svg class="%1$s" viewBox="0 0 24 24" width="%2$d" height="%2$d" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">%3$s</svg>',
			esc_attr( $classes ),
			(int) $size,
			$shapes
		);
	}

	/**
	 * Renders one shape from the manifest.
	 *
	 * @param array $shape Shape definition.
	 * @return string
	 */
	private static function shape( array $shape ) {
		$type   = isset( $shape['t'] ) ? $shape['t'] : '';
		$filled = ! empty( $shape['filled'] );
		$paint  = $filled ? ' fill="currentColor" stroke="none"' : '';

		switch ( $type ) {
			case 'path':
				if ( empty( $shape['d'] ) || ! self::is_safe_path( $shape['d'] ) ) {
					return '';
				}

				return sprintf( '<path d="%s"%s/>', esc_attr( $shape['d'] ), $paint );

			case 'circle':
				return sprintf(
					'<circle cx="%s" cy="%s" r="%s"%s/>',
					esc_attr( (float) $shape['cx'] ),
					esc_attr( (float) $shape['cy'] ),
					esc_attr( (float) $shape['r'] ),
					$paint
				);

			case 'rect':
				return sprintf(
					'<rect x="%s" y="%s" width="%s" height="%s" rx="%s"%s/>',
					esc_attr( (float) $shape['x'] ),
					esc_attr( (float) $shape['y'] ),
					esc_attr( (float) $shape['width'] ),
					esc_attr( (float) $shape['height'] ),
					esc_attr( isset( $shape['rx'] ) ? (float) $shape['rx'] : 0 ),
					$paint
				);

			default:
				return '';
		}
	}

	/**
	 * Restricts path data to SVG path syntax.
	 *
	 * The manifest ships with the plugin, but a filter can add icons, so the
	 * geometry is validated rather than trusted.
	 *
	 * @param string $data Path data.
	 * @return bool
	 */
	private static function is_safe_path( $data ) {
		return (bool) preg_match( '/^[MmLlHhVvCcSsQqTtAaZz0-9\s.,\-]+$/', (string) $data );
	}
}
