<?php
/**
 * Color trait — PHP CSS emit. Owns text / link / background color
 * emission for any block declaring `"blocksifySupports": [..., "color"]`.
 *
 * Output contract:
 *   - `::emit_decls( $attrs, $device )` returns wrapper-rule color
 *     decls (`color:` + `background-color:`) plus the per-device
 *     explicit-clear sentinels. Caller (`emit_wrapper_rules` in
 *     `Blocksify_CSS_Generator`) appends to its in-progress decl string
 *     for the wrapper rule.
 *   - `::emit_link_rule( $selector, $attrs, $device )` returns the FULL
 *     descendant rule `<sel> a { color: <link>; }` ready to append. Empty
 *     string when no link color set.
 *
 * Mirrors JS `buildColorPreviewCss` byte-for-byte. Hover layer emit lives
 * in `Blocksify_CSS_Generator::emit_hover_wrapper_rules` (still inline
 * there until we extract a hover dispatcher; the trait pattern handles
 * base + responsive only for now).
 *
 * Storage: three INDEPENDENT flat-suffix attrs (NOT a bag). Hover scope
 * single-layer per attr — no `*HoverTablet` / `*HoverMobile`.
 *
 * @package Blocksify
 */

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

class Blocksify_Color_Trait {

	/**
	 * Attribute schema fragment. Mirror of `src/shared/traits/color.js`
	 * `attributes` const. See spacing trait header for rationale + sync rule.
	 */
	public static function attributes() {
		return array(
			'textColor'             => array( 'type' => array( 'string', 'null' ) ),
			'textColorTablet'       => array( 'type' => array( 'string', 'null' ) ),
			'textColorMobile'       => array( 'type' => array( 'string', 'null' ) ),
			'textColorHover'        => array( 'type' => array( 'string', 'null' ) ),
			'linkColor'             => array( 'type' => array( 'string', 'null' ) ),
			'linkColorTablet'       => array( 'type' => array( 'string', 'null' ) ),
			'linkColorMobile'       => array( 'type' => array( 'string', 'null' ) ),
			'linkColorHover'        => array( 'type' => array( 'string', 'null' ) ),
			'backgroundColor'       => array( 'type' => array( 'string', 'null' ) ),
			'backgroundColorTablet' => array( 'type' => array( 'string', 'null' ) ),
			'backgroundColorMobile' => array( 'type' => array( 'string', 'null' ) ),
			'backgroundColorHover'  => array( 'type' => array( 'string', 'null' ) ),
		);
	}

	/**
	 * Wrapper-rule color decls for one device. Includes per-device
	 * explicit-clear sentinels for tablet / mobile.
	 *
	 * @param array  $attrs       Block attributes (flat-suffix shape).
	 * @param string $device      'desktop' | 'tablet' | 'mobile'.
	 * @param string $part_prefix Optional part name (SPEC-trait-parts §3.3).
	 * @return string Decl string, no selector / braces.
	 */
	public static function emit_decls( $attrs, $device, $part_prefix = '' ) {
		$decls = '';
		if ( 'desktop' !== $device ) {
			$decls .= self::emit_explicit_clears( $attrs, $device, $part_prefix );
		}
		$decls .= self::build_decls( self::collect_bag( $attrs, $device, $part_prefix ) );
		return $decls;
	}

	/**
	 * Build the descendant-anchor rule for link color when set. Returns
	 * full rule (selector + braces) so caller can append as-is.
	 *
	 * Link rule is ROOT-ONLY by design (parts e.g. tab buttons have no
	 * `<a>` descendants). When `$part_prefix` is non-empty, returns ''
	 * to mirror the JS-side `_ColorRow` cssPreview suppression. PHP
	 * structurally never invokes `emit_link_rule` from
	 * `emit_part_wrapper_rules` — the explicit early-return here is a
	 * symmetric guard so callers that DO pass a part prefix get
	 * consistent behavior with JS.
	 *
	 * @param string $selector    Wrapper selector (e.g. `.bsy-section-abc12345`).
	 * @param array  $attrs       Block attributes.
	 * @param string $device      'desktop' | 'tablet' | 'mobile'.
	 * @param string $part_prefix Optional part name. '' = root (default).
	 * @return string CSS rule or '' when no link color or non-empty part_prefix.
	 */
	public static function emit_link_rule( $selector, $attrs, $device, $part_prefix = '' ) {
		if ( '' !== $part_prefix ) {
			return '';
		}
		$link_value = isset( $attrs[ Blocksify_Attrs::key( 'linkColor', $device ) ] )
			? $attrs[ Blocksify_Attrs::key( 'linkColor', $device ) ]
			: null;
		$decls = self::build_link_decls( $link_value );
		if ( '' === $decls ) {
			return '';
		}
		return $selector . ' a{' . $decls . '}';
	}

	/**
	 * Resolve textColor + backgroundColor for one state. Mirrors JS
	 * `buildColorPreviewCss` resolver. Link color is NOT included here —
	 * it lives on a descendant `a` selector and is read directly via
	 * `bg_attr_key( 'linkColor', $device )` at emit time.
	 */
	protected static function collect_bag( $attrs, $device, $part_prefix = '' ) {
		$text_key = Blocksify_Attrs::part_key( $part_prefix, 'textColor', $device );
		$bg_key   = Blocksify_Attrs::part_key( $part_prefix, 'backgroundColor', $device );
		return array(
			'textColor'       => isset( $attrs[ $text_key ] ) ? $attrs[ $text_key ] : null,
			'backgroundColor' => isset( $attrs[ $bg_key ] )   ? $attrs[ $bg_key ]   : null,
		);
	}

	/**
	 * Build the wrapper-rule color decl string. Mirrors JS `buildColorDecls`
	 * in `ColorControl/css.js` byte-for-byte.
	 */
	protected static function build_decls( $bag ) {
		if ( ! is_array( $bag ) ) {
			return '';
		}
		$decls = '';
		$text  = isset( $bag['textColor'] ) ? $bag['textColor'] : null;
		if ( is_string( $text ) && '' !== $text ) {
			$sanitized = Blocksify_Attrs::sanitize_color( $text );
			if ( '' !== $sanitized ) {
				$decls .= 'color:' . $sanitized . ';';
			}
		}
		$bg = isset( $bag['backgroundColor'] ) ? $bag['backgroundColor'] : null;
		if ( is_string( $bg ) && '' !== $bg ) {
			$sanitized = Blocksify_Attrs::sanitize_color( $bg );
			if ( '' !== $sanitized ) {
				$decls .= 'background-color:' . $sanitized . ';';
			}
		}
		return $decls;
	}

	/**
	 * Build the descendant-anchor decl string for link color. Mirrors JS
	 * `buildLinkColorDecls` byte-for-byte.
	 */
	protected static function build_link_decls( $link ) {
		if ( ! is_string( $link ) || '' === $link ) {
			return '';
		}
		$sanitized = Blocksify_Attrs::sanitize_color( $link );
		if ( '' === $sanitized ) {
			return '';
		}
		return 'color:' . $sanitized . ';';
	}

	/**
	 * Hover-layer wrapper-rule decls (text + bg color from `*Hover` keys).
	 * Returned for `Blocksify_CSS_Generator::emit_hover_wrapper_rules` to
	 * append onto its `<sel>:hover{...}` rule. No explicit-clears here —
	 * hover layer is single-state per attr (FOUNDATION §6.4).
	 */
	public static function emit_hover_decls( $attrs, $part_prefix = '' ) {
		$text_key = Blocksify_Attrs::part_hover_key( $part_prefix, 'textColor' );
		$bg_key   = Blocksify_Attrs::part_hover_key( $part_prefix, 'backgroundColor' );
		return self::build_decls( array(
			'textColor'       => isset( $attrs[ $text_key ] ) ? $attrs[ $text_key ] : null,
			'backgroundColor' => isset( $attrs[ $bg_key ] )   ? $attrs[ $bg_key ]   : null,
		) );
	}

	/**
	 * Hover-layer link rule. Lives on `<wrapper> a:hover` — fires when
	 * the LINK itself is hovered (standard CSS convention). Returns full
	 * rule or '' when no `linkColorHover` or non-empty part_prefix
	 * (symmetric with `emit_link_rule` — link layer is root-only).
	 */
	public static function emit_link_hover_rule( $base_selector, $attrs, $part_prefix = '' ) {
		if ( '' !== $part_prefix ) {
			return '';
		}
		$decls = self::build_link_decls(
			isset( $attrs['linkColorHover'] ) ? $attrs['linkColorHover'] : null
		);
		if ( '' === $decls ) {
			return '';
		}
		return $base_selector . ' a:hover{' . $decls . '}';
	}

	/**
	 * Emit per-device explicit-clear sentinels for textColor /
	 * backgroundColor. Three independent attrs → independent resets.
	 * Link color reset is NOT emitted here (would land on the wrapper
	 * rule but link color lives on the descendant `a` selector).
	 */
	protected static function emit_explicit_clears( $attrs, $device, $part_prefix = '' ) {
		$clears        = '';
		$text_base     = '' === $part_prefix ? 'textColor' : $part_prefix . 'TextColor';
		$bg_base       = '' === $part_prefix ? 'backgroundColor' : $part_prefix . 'BackgroundColor';
		$text_key      = Blocksify_Attrs::part_key( $part_prefix, 'textColor', $device );
		$bg_key        = Blocksify_Attrs::part_key( $part_prefix, 'backgroundColor', $device );
		if ( array_key_exists( $text_key, $attrs ) && is_null( $attrs[ $text_key ] )
			&& Blocksify_Attrs::has_upstream_value( $attrs, $text_base, $device ) ) {
			$clears .= 'color:revert;';
		}
		if ( array_key_exists( $bg_key, $attrs ) && is_null( $attrs[ $bg_key ] )
			&& Blocksify_Attrs::has_upstream_value( $attrs, $bg_base, $device ) ) {
			$clears .= 'background-color:transparent;';
		}
		return $clears;
	}
}
