<?php
/**
 * Background trait — PHP CSS emit. Owns image / gradient / video / shape /
 * overlay / scroll-effect emission for any block declaring
 * `"blocksifySupports": [..., "background"]`. Mirrors JS
 * `buildBackgroundDecls` + `buildOverlayDecls` in
 * `src/shared/controls/BackgroundControl/css.js` byte-for-byte.
 *
 * BG color is owned by the color trait (lives in ColorControl). This
 * trait's bag carries gradient + image + overlay + scrollEffect only.
 *
 * Output contract:
 *   - `::emit_decls( $attrs, $device )` — wrapper-rule decls
 *     (background-image stack + position + size + repeat + per-device
 *     `background-image:none;` explicit-clear sentinel).
 *   - `::emit_hover_decls( $attrs )` — `:hover`-rule decls for the
 *     gradient hover layer ONLY (the only hover-aware sub-field of this
 *     trait). Stacks hover gradient over base image so image survives
 *     the `background-image` override.
 *   - `::emit_overlay_rule( $selector, $attrs, $device )` — full child-div
 *     rule `<sel> > .bsy-bg-overlay { ... }`. Empty when no overlay.
 *   - `::emit_video_visibility_rules( $uid, $attrs, $device, $base )` —
 *     per-device `display:none/block` rules for `<video>` DOM child layers.
 *
 * @package Blocksify
 */

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

class Blocksify_Background_Trait {

	/**
	 * Attribute schema fragment. Mirror of `src/shared/traits/background.js`
	 * `attributes` const. See spacing trait header for rationale + sync rule.
	 */
	public static function attributes() {
		return array(
			'backgroundImage'           => array( 'type' => array( 'object', 'null' ) ),
			'backgroundImageTablet'     => array( 'type' => array( 'object', 'null' ) ),
			'backgroundImageMobile'     => array( 'type' => array( 'object', 'null' ) ),
			'backgroundGradient'        => array( 'type' => array( 'string', 'null' ) ),
			'backgroundGradientTablet'  => array( 'type' => array( 'string', 'null' ) ),
			'backgroundGradientMobile'  => array( 'type' => array( 'string', 'null' ) ),
			'backgroundGradientHover'   => array( 'type' => array( 'string', 'null' ) ),
			'backgroundVideo'           => array( 'type' => array( 'object', 'null' ) ),
			'backgroundVideoTablet'     => array( 'type' => array( 'object', 'null' ) ),
			'backgroundVideoMobile'     => array( 'type' => array( 'object', 'null' ) ),
			'backgroundShape'           => array( 'type' => array( 'object', 'null' ) ),
			'backgroundShapeTablet'     => array( 'type' => array( 'object', 'null' ) ),
			'backgroundShapeMobile'     => array( 'type' => array( 'object', 'null' ) ),
			'backgroundOverlay'         => array( 'type' => array( 'object', 'null' ) ),
			'backgroundOverlayTablet'   => array( 'type' => array( 'object', 'null' ) ),
			'backgroundOverlayMobile'   => array( 'type' => array( 'object', 'null' ) ),
			'backgroundScrollEffect'    => array( 'type' => array( 'object', 'null' ) ),
		);
	}

	/**
	 * Wrapper-rule background decls (image stack + position + size + repeat
	 * + per-device explicit-clear sentinel).
	 *
	 * @param string $part_prefix Optional part name (SPEC-trait-parts §3.3).
	 *                            Note: overlay/video/shape DOM children are
	 *                            root-only (separate emit methods); part
	 *                            scope only applies to image + gradient.
	 */
	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;
	}

	/**
	 * Hover-rule decls. Currently only the gradient layer is hover-aware
	 * (image / video / shape / overlay don't get hover variants — preload
	 * cost + DOM-state complexity). Stacks hover gradient over base
	 * image so the image layer survives the `background-image`
	 * override (mirrors JS preview-css.js hover branch byte-for-byte).
	 *
	 * Image is read from the desktop base key — hover is single-value
	 * (no per-device variants per FOUNDATION §6.4 hover scope), so on
	 * tablet/mobile viewports the hover rule shows base-desktop image
	 * underneath the hover gradient. Accepted edge case (alt would be 3
	 * separate hover rules per breakpoint, not worth the complexity for
	 * the rare image+gradient+hover combo).
	 *
	 * @param array $attrs Block attributes.
	 * @return string Decls or empty when no hover gradient set.
	 */
	public static function emit_hover_decls( $attrs, $part_prefix = '' ) {
		$gradient_hover_key = Blocksify_Attrs::part_hover_key( $part_prefix, 'backgroundGradient' );
		$image_base_key     = Blocksify_Attrs::part_key( $part_prefix, 'backgroundImage', 'desktop' );
		// scrollEffect is single-value (no per-device variants) — root-only,
		// shared across parts.
		if ( ! isset( $attrs[ $gradient_hover_key ] )
			|| ! is_string( $attrs[ $gradient_hover_key ] )
			|| '' === $attrs[ $gradient_hover_key ] ) {
			return '';
		}
		$bag = array(
			'gradient'     => $attrs[ $gradient_hover_key ],
			'image'        => isset( $attrs[ $image_base_key ] ) ? $attrs[ $image_base_key ] : null,
			'scrollEffect' => isset( $attrs['backgroundScrollEffect'] ) ? $attrs['backgroundScrollEffect'] : null,
		);
		return self::build_decls( $bag );
	}

	/**
	 * Overlay child-div rule for one device (full rule with selector +
	 * braces). Empty when no overlay decls.
	 */
	public static function emit_overlay_rule( $selector, $attrs, $device ) {
		$bag   = self::collect_bag( $attrs, $device );
		$decls = '';
		if ( 'desktop' !== $device ) {
			$decls .= self::emit_overlay_explicit_clear( $attrs, $device );
		}
		$decls .= self::build_overlay_decls(
			isset( $bag['overlay'] ) ? $bag['overlay'] : null
		);
		if ( '' === $decls ) {
			return '';
		}
		return $selector . ' > .bsy-bg-overlay{' . $decls . '}';
	}

	/**
	 * Per-device DOM hide rules for `<video>` child layers — Mirrors JS
	 * `buildVideoVisibilityPreviewCss` byte-for-byte.
	 */
	public static function emit_video_visibility_rules( $uid, $attrs, $device, $base_class ) {
		$suffix    = 'tablet' === $device ? 'Tablet' : 'Mobile';
		$video_key = 'backgroundVideo' . $suffix;
		$prefix    = '.' . $base_class . $uid . ' > ';
		$rules     = '';

		$has_override     = array_key_exists( $video_key, $attrs )
			&& Blocksify_Attrs::is_meaningful( $attrs[ $video_key ] );
		$is_null_sentinel = array_key_exists( $video_key, $attrs )
			&& is_null( $attrs[ $video_key ] );

		if ( ! $has_override && ! $is_null_sentinel ) {
			return '';
		}
		if ( $is_null_sentinel
			&& ! Blocksify_Attrs::has_upstream_value( $attrs, 'backgroundVideo', $device ) ) {
			return '';
		}

		if ( 'tablet' === $device ) {
			$rules .= $prefix . '.bsy-bg-video--desktop{display:none;}';
		} else {
			$rules .= $prefix . '.bsy-bg-video--desktop,'
				. $prefix . '.bsy-bg-video--tablet{display:none;}';
		}

		if ( $has_override ) {
			$rules .= $prefix . '.bsy-bg-video--' . $device
				. '{display:block !important;}';
		}

		return $rules;
	}

	// ====================================================================
	// Internal helpers
	// ====================================================================

	/**
	 * Resolve { gradient, image, overlay, scrollEffect } for one device.
	 *
	 * Note: overlay + scrollEffect remain root-only (separate emit
	 * methods + single-value runtime gating, not part-scoped). Only
	 * gradient + image accept the part prefix.
	 */
	protected static function collect_bag( $attrs, $device, $part_prefix = '' ) {
		$gradient_key = Blocksify_Attrs::part_key( $part_prefix, 'backgroundGradient', $device );
		$image_key    = Blocksify_Attrs::part_key( $part_prefix, 'backgroundImage', $device );
		// Overlay key is root-only (only the per-instance class hosts the
		// `> .bsy-bg-overlay` child div — `emit_overlay_rule` is not
		// part-aware in V1). Read root key regardless of $part_prefix.
		$overlay_key = Blocksify_Attrs::key( 'backgroundOverlay', $device );
		return array(
			'gradient'     => isset( $attrs[ $gradient_key ] ) ? $attrs[ $gradient_key ] : null,
			'image'        => isset( $attrs[ $image_key ] ) ? $attrs[ $image_key ] : null,
			'overlay'      => isset( $attrs[ $overlay_key ] ) ? $attrs[ $overlay_key ] : null,
			// Scroll effect does NOT cascade per device — single
			// non-suffixed attr; runtime gates per device via `triggerOn`
			// + matchMedia. Pass through every state so build_decls can
			// skip the bg-image layer (DOM <img> takes over).
			'scrollEffect' => isset( $attrs['backgroundScrollEffect'] )
				? $attrs['backgroundScrollEffect']
				: null,
		);
	}

	/**
	 * Per-device explicit-clear sentinel for `backgroundImage` /
	 * `backgroundGradient`. Single `background-image:none;` reset clears
	 * both layers when ANY is explicit-null on the device.
	 */
	protected static function emit_explicit_clears( $attrs, $device, $part_prefix = '' ) {
		$clears      = '';
		$image_bases = array( 'backgroundImage', 'backgroundGradient' );
		foreach ( $image_bases as $bare ) {
			$base = '' === $part_prefix ? $bare : $part_prefix . ucfirst( $bare );
			$k    = Blocksify_Attrs::part_key( $part_prefix, $bare, $device );
			if ( array_key_exists( $k, $attrs ) && is_null( $attrs[ $k ] )
				&& Blocksify_Attrs::has_upstream_value( $attrs, $base, $device ) ) {
				$clears .= 'background-image:none;';
				break;
			}
		}
		return $clears;
	}

	/**
	 * Explicit-clear for the overlay layer (`display:none;` on the child
	 * div) when tablet/mobile has explicit-null `backgroundOverlay` AND an
	 * upstream device holds a meaningful overlay.
	 */
	protected static function emit_overlay_explicit_clear( $attrs, $device ) {
		$suffix = 'tablet' === $device ? 'Tablet' : 'Mobile';
		$key    = 'backgroundOverlay' . $suffix;
		if ( array_key_exists( $key, $attrs ) && is_null( $attrs[ $key ] )
			&& Blocksify_Attrs::has_upstream_value( $attrs, 'backgroundOverlay', $device ) ) {
			return 'display:none;';
		}
		return '';
	}

	/**
	 * Build the background decl string for one state. Layers stacked via
	 * comma-separated `background-image`:
	 *   1. Gradient (top)
	 *   2. Image (bottom — bg-image url())
	 *
	 * Image renders as DOM `<img>` (Motion-driven) instead of CSS bg-image
	 * when scroll effect is active. Other layers still emit normally.
	 *
	 * Video + shape NOT emitted via CSS — DOM elements (`<video>`, `<svg>`).
	 */
	protected static function build_decls( $bag ) {
		if ( ! is_array( $bag ) ) {
			return '';
		}
		$decls    = '';
		$gradient = isset( $bag['gradient'] ) ? $bag['gradient'] : null;
		$image    = isset( $bag['image'] ) ? $bag['image'] : null;
		$scroll   = isset( $bag['scrollEffect'] ) ? $bag['scrollEffect'] : null;

		$scroll_active = is_array( $scroll )
			&& isset( $scroll['enabled'] ) && true === $scroll['enabled'];
		$image_active  = is_array( $image )
			&& ! empty( $image['url'] )
			&& is_string( $image['url'] )
			&& ! $scroll_active;

		$layers = array();

		if ( is_string( $gradient ) && '' !== $gradient ) {
			$layers[] = array(
				'image'    => $gradient,
				'position' => '0 0',
				'size'     => 'auto',
				'repeat'   => 'no-repeat',
			);
		}
		if ( $image_active ) {
			$image_url = esc_url_raw( $image['url'] );
			if ( '' !== $image_url ) {
				$layers[] = array(
					'image'    => "url('" . $image_url . "')",
					'position' => self::image_position_value( $image ),
					'size'     => self::image_size_value( $image ),
					'repeat'   => 'no-repeat',
				);
			} else {
				$image_active = false;
			}
		}

		if ( empty( $layers ) ) {
			return $decls;
		}

		$decls .= 'background-image:' . implode( ', ', array_map( function ( $l ) {
			return $l['image'];
		}, $layers ) ) . ';';
		$decls .= 'background-position:' . implode( ', ', array_map( function ( $l ) {
			return $l['position'];
		}, $layers ) ) . ';';
		$decls .= 'background-size:' . implode( ', ', array_map( function ( $l ) {
			return $l['size'];
		}, $layers ) ) . ';';
		$decls .= 'background-repeat:' . implode( ', ', array_map( function ( $l ) {
			return $l['repeat'];
		}, $layers ) ) . ';';

		if ( $image_active && isset( $image['attachment'] )
			&& in_array( $image['attachment'], array( 'scroll', 'fixed' ), true ) ) {
			$count = count( $layers );
			$att   = array();
			for ( $i = 0; $i < $count; $i++ ) {
				$att[] = ( $i === $count - 1 ) ? $image['attachment'] : 'scroll';
			}
			$decls .= 'background-attachment:' . implode( ', ', $att ) . ';';
		}

		return $decls;
	}

	/**
	 * Build the overlay decl string for the `.bsy-bg-overlay` child div.
	 * Color-only — a flat tint via a single-layer `linear-gradient` of a
	 * `color-mix` so opacity applies through ONE path to theme tokens
	 * (`var(--wp--preset--color--…)`) + custom colors. No `enabled` flag,
	 * no image layer. Mirrors JS `buildOverlayDecls` byte-for-byte.
	 */
	protected static function build_overlay_decls( $overlay ) {
		if ( ! is_array( $overlay ) ) {
			return '';
		}
		$color = isset( $overlay['color'] ) && is_string( $overlay['color'] )
			? $overlay['color']
			: '';
		if ( '' === $color ) {
			return '';
		}
		$sanitized = Blocksify_Attrs::sanitize_color( $color );
		if ( '' === $sanitized ) {
			return '';
		}
		$pct  = self::overlay_opacity_pct( isset( $overlay['opacity'] ) ? $overlay['opacity'] : null );
		$tint = 'color-mix(in srgb, ' . $sanitized . ' ' . $pct . '%, transparent)';
		return 'background-image: linear-gradient(' . $tint . ', ' . $tint . ');';
	}

	/**
	 * Clamp opacity (0..1) → integer percent for color-mix. Default 40%
	 * (matches OVERLAY_DEFAULTS). Mirrors JS `overlayOpacityPct`.
	 */
	protected static function overlay_opacity_pct( $opacity ) {
		$o = is_numeric( $opacity ) ? (float) $opacity : 0.4;
		$o = max( 0, min( 1, $o ) );
		return (int) round( $o * 100 );
	}

	// ====================================================================
	// Image position + size sanitization helpers (main bg image)
	// ====================================================================

	protected static function image_position_value( $image ) {
		if ( isset( $image['position'] ) && is_string( $image['position'] ) ) {
			$pos = preg_replace( '/[^a-zA-Z0-9%\s.-]/', '', $image['position'] );
			if ( '' !== $pos ) {
				return $pos;
			}
		}
		return '50% 50%';
	}

	protected static function image_size_value( $image ) {
		if ( isset( $image['size'] ) && is_string( $image['size'] ) ) {
			if ( 'custom' === $image['size'] ) {
				$w = isset( $image['customWidth'] ) ? self::format_dimension( $image['customWidth'] ) : 'auto';
				$h = isset( $image['customHeight'] ) ? self::format_dimension( $image['customHeight'] ) : 'auto';
				return $w . ' ' . $h;
			}
			if ( in_array( $image['size'], array( 'cover', 'contain', 'auto' ), true ) ) {
				return $image['size'];
			}
		}
		return 'cover';
	}

	protected static function format_dimension( $dim ) {
		if ( ! is_array( $dim ) ) {
			return 'auto';
		}
		if ( ! isset( $dim['value'] ) || ! is_numeric( $dim['value'] ) ) {
			return 'auto';
		}
		$value = (float) $dim['value'];
		$unit  = isset( $dim['unit'] ) && is_string( $dim['unit'] ) && '' !== $dim['unit']
			? $dim['unit']
			: 'px';
		$formatted = rtrim( rtrim( sprintf( '%F', $value ), '0' ), '.' );
		return $formatted . $unit;
	}
}
