<?php
/**
 * Blocksify_Attrs — pure utility class for attribute-key derivation +
 * cascade-source probing + meaningful-value test + CSS color sanitize.
 *
 * Owned by NO trait. Decoupled from `Blocksify_CSS_Generator` (the
 * orchestrator) so trait classes can call these without depending on
 * the orchestrator. The orchestrator → trait dependency direction stays
 * one-way: orchestrator dispatches to traits, traits stay leaf nodes.
 *
 * Migrated out of `Blocksify_CSS_Generator` in Phase 5 cleanup pass —
 * generator's identical methods are now thin proxies that delegate here
 * (kept for backward compat with any third-party Pro code that calls
 * the generator's static methods directly).
 *
 * @package Blocksify
 */

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

class Blocksify_Attrs {

	/**
	 * Per-device attr key. Desktop returns base name; tablet/mobile
	 * append capitalized suffix.
	 *
	 *   bg_attr_key( 'padding', 'tablet' )  → 'paddingTablet'
	 *   bg_attr_key( 'padding', 'desktop' ) → 'padding'
	 *
	 * @param string $base   Base attr name (no suffix).
	 * @param string $device 'desktop' | 'tablet' | 'mobile'.
	 * @return string
	 */
	public static function key( $base, $device ) {
		if ( 'desktop' === $device ) {
			return $base;
		}
		return $base . ucfirst( $device );
	}

	/**
	 * Hover-layer attr key. Single layer per attr — no per-device hover
	 * variants (FOUNDATION §6.4).
	 *
	 * @param string $base
	 * @return string
	 */
	public static function hover_key( $base ) {
		return $base . 'Hover';
	}

	/**
	 * Part-prefixed attr key. Combines the part-prefix transformation
	 * (FOUNDATION §7.10 / SPEC-trait-parts) with the device suffix.
	 *
	 *   part_key( '',          'bg', 'tablet' ) → 'bgTablet'
	 *   part_key( 'tab',       'bg', 'tablet' ) → 'tabBgTablet'
	 *   part_key( 'tabActive', 'bg', 'desktop') → 'tabActiveBg'
	 *
	 * Empty `$part_prefix` preserves the root-attr behavior of `key()` —
	 * existing callers that drop in `$part_prefix = ''` see no change.
	 *
	 * @param string $part_prefix Part name (e.g. 'tab', 'tabActive') or '' for root.
	 * @param string $base        Base attr name (e.g. 'bg', 'textColor').
	 * @param string $device      'desktop' | 'tablet' | 'mobile'.
	 * @return string
	 */
	public static function part_key( $part_prefix, $base, $device ) {
		$name = '' === $part_prefix ? $base : $part_prefix . ucfirst( $base );
		return self::key( $name, $device );
	}

	/**
	 * Part-prefixed hover-layer attr key.
	 *
	 *   part_hover_key( '',    'bg' ) → 'bgHover'
	 *   part_hover_key( 'tab', 'bg' ) → 'tabBgHover'
	 *
	 * @param string $part_prefix Part name or '' for root.
	 * @param string $base        Base attr name.
	 * @return string
	 */
	public static function part_hover_key( $part_prefix, $base ) {
		$name = '' === $part_prefix ? $base : $part_prefix . ucfirst( $base );
		return self::hover_key( $name );
	}

	/**
	 * Walk the cascade from `$device` up to find a meaningful upstream
	 * value (mirrors the JS `isResponsiveOverride` cascade source check).
	 * Tablet looks at Desktop. Mobile looks at Tablet (when meaningful)
	 * else Desktop.
	 *
	 * @param array  $attrs  Block attributes.
	 * @param string $base   Base attr name (e.g. `backgroundColor`).
	 * @param string $device 'tablet' | 'mobile'.
	 * @return bool True when an upstream device has a non-null/non-empty value.
	 */
	public static function has_upstream_value( $attrs, $base, $device ) {
		if ( 'tablet' === $device ) {
			return self::is_meaningful( isset( $attrs[ $base ] ) ? $attrs[ $base ] : null );
		}
		// Mobile: tablet first (when meaningful), else desktop.
		$tablet_val = isset( $attrs[ $base . 'Tablet' ] ) ? $attrs[ $base . 'Tablet' ] : null;
		if ( self::is_meaningful( $tablet_val ) ) {
			return true;
		}
		return self::is_meaningful( isset( $attrs[ $base ] ) ? $attrs[ $base ] : null );
	}

	/**
	 * PHP equivalent of JS `isMeaningful`: not undefined / not null / not
	 * empty string. Numeric 0 and `false` count as meaningful.
	 *
	 * @param mixed $value
	 * @return bool
	 */
	public static function is_meaningful( $value ) {
		if ( is_null( $value ) ) {
			return false;
		}
		if ( is_string( $value ) && '' === $value ) {
			return false;
		}
		return true;
	}

	/**
	 * Permissive CSS color sanitization — matches hex/hex8, rgb(a),
	 * hsl(a), named colors. Strips anything outside the safe character
	 * set. Production should validate strictly via wp_kses-style filter
	 * on hook.
	 *
	 * @param string $color Raw color value.
	 * @return string
	 */
	public static function sanitize_color( $color ) {
		return preg_replace( '/[^a-zA-Z0-9#%(),.\s\/-]/', '', $color );
	}
}
