<?php
/**
 * Blocksify_Icons — server-side icon SVG resolver.
 *
 * Replaces the previous "save full SVG markup into post_content" pattern.
 * Authors now save a sparse bag — `{source: 'lucide', name: 'circle',
 * strokeWidth: 2}` — and this class resolves it to SVG markup at render
 * time using bundled Lucide chunks in `includes/data/lucide/{a-z}.php`.
 *
 * Render dispatch order (in `render()`):
 *   1. Bag carries `svg` field → echo verbatim. Covers legacy posts +
 *      `custom-svg` paste + inline-svg `custom-upload`. Backward-compat
 *      stays load-bearing forever — five lines, zero cost.
 *   2. Bag's `source` matches a registered library → call its `builder`.
 *      Lucide ships built-in; Pro libraries register via the
 *      `blocksify.icon.sources` PHP-side filter.
 *   3. Otherwise → '' (empty string, graceful no-op).
 *
 * Filter — `blocksify.icon.sources` (PHP mirrors the JS picker filter
 * of the same name; each runtime contributes its own field surface to
 * the shared source-id key):
 *
 *   add_filter( 'blocksify.icon.sources', function( $sources ) {
 *       $sources['heroicons-outline'] = array(
 *           'label'   => __( 'Heroicons (outline)', 'blocksify-pro' ),
 *           'builder' => 'pro_heroicons_outline_build_svg',  // ($bag) => svg
 *           'tier'    => 'pro',
 *       );
 *       return $sources;
 *   } );
 *
 * Lucide chunk storage: 25 alphabetical files (no 'y' in current
 * upstream) holding pre-rendered SVG strings with `stroke-width="2"`
 * baked. Chunks load lazily by first letter, statically cached for the
 * request. Custom stroke pays one `str_replace` per render.
 *
 * @package Blocksify
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class Blocksify_Icons {

	/** Per-request cache of loaded Lucide chunk arrays, keyed by first-letter. */
	private static $lucide_chunks = array();

	/** Resolved library registry — filter applied once per request. */
	private static $libraries = null;

	/**
	 * Render any icon bag to SVG markup. Pure — no I/O beyond the
	 * (lazy) chunk require + filter application.
	 *
	 * @param mixed $icon Icon bag (array) or anything else.
	 * @return string SVG markup, or '' when the bag has no resolvable icon.
	 */
	public static function render( $icon ) {
		if ( ! is_array( $icon ) ) {
			return '';
		}
		if ( isset( $icon['svg'] ) && is_string( $icon['svg'] ) && '' !== $icon['svg'] ) {
			return $icon['svg'];
		}
		$source = isset( $icon['source'] ) ? (string) $icon['source'] : '';
		if ( '' === $source ) {
			return '';
		}
		$libraries = self::libraries();
		if ( ! isset( $libraries[ $source ]['builder'] ) ) {
			return '';
		}
		$builder = $libraries[ $source ]['builder'];
		if ( ! is_callable( $builder ) ) {
			return '';
		}
		return (string) call_user_func( $builder, $icon );
	}

	/**
	 * Direct-resolve shortcut. Equivalent to
	 * `render(['source'=>$source,'name'=>$name,...$extra])` without the
	 * bag-wrapping ceremony.
	 *
	 * @param string $source Source id.
	 * @param string $name   Icon name.
	 * @param array  $extra  Optional library-specific knobs (strokeWidth, …).
	 * @return string SVG markup or ''.
	 */
	public static function resolve( $source, $name, $extra = array() ) {
		return self::render(
			array_merge(
				is_array( $extra ) ? $extra : array(),
				array( 'source' => (string) $source, 'name' => (string) $name )
			)
		);
	}

	/**
	 * Lucide-specific shortcut for callers with a name in hand (paging
	 * defaults, Pro libraries that extend Lucide, …). Looks up the
	 * pre-rendered SVG in the appropriate alphabetical chunk, optionally
	 * swapping stroke-width from the baked default of 2.
	 *
	 * @param string     $name        Lucide icon name (e.g. 'circle-user-round').
	 * @param float|null $strokeWidth Optional override; null or 2 = baked default (no str_replace).
	 * @return string SVG markup or ''.
	 */
	public static function lucide_svg( $name, $strokeWidth = null ) {
		if ( ! is_string( $name ) || '' === $name ) {
			return '';
		}
		$first = $name[0];
		if ( $first < 'a' || $first > 'z' ) {
			return '';
		}
		if ( ! isset( self::$lucide_chunks[ $first ] ) ) {
			$path = BLOCKSIFY_PATH . 'includes/data/lucide/' . $first . '.php';
			self::$lucide_chunks[ $first ] = file_exists( $path ) ? require $path : array();
		}
		if ( ! isset( self::$lucide_chunks[ $first ][ $name ] ) ) {
			return '';
		}
		$svg = self::$lucide_chunks[ $first ][ $name ];
		if ( null === $strokeWidth ) {
			return $svg;
		}
		$sw = (float) $strokeWidth;
		if ( $sw <= 0 || 2.0 === $sw ) {
			return $svg;
		}
		return str_replace(
			' stroke-width="2">',
			' stroke-width="' . self::format_stroke( $sw ) . '">',
			$svg
		);
	}

	/**
	 * Builder for the Lucide library entry — thin wrapper that pulls
	 * `name` + `strokeWidth` out of the bag and dispatches to
	 * `lucide_svg`.
	 *
	 * @param array $bag Icon bag.
	 * @return string SVG markup or ''.
	 */
	public static function build_lucide_svg( $bag ) {
		$name = isset( $bag['name'] ) ? (string) $bag['name'] : '';
		$sw   = isset( $bag['strokeWidth'] ) ? $bag['strokeWidth'] : null;
		return self::lucide_svg( $name, $sw );
	}

	/**
	 * Lazy-init + cache the library registry. Lucide is the default
	 * Free entry; Pro / 3rd-party plugins extend via the filter.
	 *
	 * @return array<string, array>
	 */
	private static function libraries() {
		if ( null === self::$libraries ) {
			$defaults = array(
				'lucide' => array(
					'label'   => __( 'Lucide', 'blocksify' ),
					'builder' => array( __CLASS__, 'build_lucide_svg' ),
					'tier'    => 'free',
				),
			);
			$libraries       = apply_filters( 'blocksify.icon.sources', $defaults );
			self::$libraries = is_array( $libraries ) ? $libraries : $defaults;
		}
		return self::$libraries;
	}

	/**
	 * Format a stroke-width float for embedding in the SVG attribute.
	 * Integer values drop the trailing zero (`2.0` → `2`) for byte
	 * parity with the JS twin's `Number(strokeWidth) || 2` output.
	 */
	private static function format_stroke( $sw ) {
		if ( (float) (int) $sw === $sw ) {
			return (string) (int) $sw;
		}
		return rtrim( rtrim( sprintf( '%.4F', $sw ), '0' ), '.' );
	}
}
