<?php
/**
 * Responsive element visibility helpers.
 *
 * @package JOCBM
 */

namespace JOCBM\Services;

defined( 'ABSPATH' ) || exit;

/**
 * Desktop / tablet / mobile content visibility → CSS display.
 */
final class ElementsVisibility {

	public const BREAKPOINTS = array( 'desktop', 'tablet', 'mobile' );

	/**
	 * @param mixed                $value Value.
	 * @param array<int, string>   $keys  Known keys.
	 * @return array{desktop: array<string, bool>, tablet: ?array<string, bool>, mobile: ?array<string, bool>}
	 */
	public static function normalize( $value, array $keys = array() ): array {
		if ( self::is_flat( $value ) ) {
			return array(
				'desktop' => self::resolve_map( $value, $keys ),
				'tablet'  => null,
				'mobile'  => null,
			);
		}

		if ( self::is_breakpoint_map( $value ) ) {
			$desktop = self::resolve_map( $value['desktop'] ?? null, $keys );
			$key_list = array_keys( $desktop );
			return array(
				'desktop' => $desktop,
				'tablet'  => ( isset( $value['tablet'] ) && is_array( $value['tablet'] ) )
					? self::resolve_map( $value['tablet'], $key_list )
					: null,
				'mobile'  => ( isset( $value['mobile'] ) && is_array( $value['mobile'] ) )
					? self::resolve_map( $value['mobile'], $key_list )
					: null,
			);
		}

		return array(
			'desktop' => self::default_map( $keys ),
			'tablet'  => null,
			'mobile'  => null,
		);
	}

	/**
	 * @param mixed              $value      Value.
	 * @param string             $breakpoint Breakpoint.
	 * @param array<int, string> $keys       Keys.
	 * @return array<string, bool>
	 */
	public static function effective( $value, string $breakpoint = 'desktop', array $keys = array() ): array {
		$n   = self::normalize( $value, $keys );
		$out = $n['desktop'];
		if ( ( 'tablet' === $breakpoint || 'mobile' === $breakpoint ) && is_array( $n['tablet'] ) ) {
			$out = $n['tablet'];
		}
		if ( 'mobile' === $breakpoint && is_array( $n['mobile'] ) ) {
			$out = $n['mobile'];
		}
		return $out;
	}

	/**
	 * @param mixed              $value Value.
	 * @param array<int, string> $keys  Keys.
	 * @return array{desktop: array<string, bool>, tablet?: array<string, bool>, mobile?: array<string, bool>}
	 */
	public static function sanitize( $value, array $keys = array() ): array {
		$n   = self::normalize( $value, $keys );
		$out = array( 'desktop' => $n['desktop'] );
		if ( is_array( $n['tablet'] ) ) {
			$out['tablet'] = $n['tablet'];
		}
		if ( is_array( $n['mobile'] ) ) {
			$out['mobile'] = $n['mobile'];
		}
		return $out;
	}

	/**
	 * @param string             $selector Selector.
	 * @param mixed              $value    Value.
	 * @param array<int, string> $keys     Keys.
	 */
	public static function to_css( string $selector, $value, array $keys = array() ): string {
		if ( '' === $selector ) {
			return '';
		}
		$n        = self::normalize( $value, $keys );
		$key_list = $keys ?: array_keys( $n['desktop'] );
		$desktop  = self::effective( $n, 'desktop', $key_list );
		$tablet   = self::effective( $n, 'tablet', $key_list );
		$mobile   = self::effective( $n, 'mobile', $key_list );
		$parts    = array();
		$desktop_hide = self::visibility_hide_rules( $selector, $desktop );
		if ( '' !== $desktop_hide ) {
			$parts[] = '@media (min-width: 1025px){' . $desktop_hide . '}';
		}
		$tablet_hide = self::visibility_hide_rules( $selector, $tablet );
		if ( '' !== $tablet_hide ) {
			$parts[] = '@media (max-width: 1024px) and (min-width: 481px){' . $tablet_hide . '}';
		}
		$mobile_hide = self::visibility_hide_rules( $selector, $mobile );
		if ( '' !== $mobile_hide ) {
			$parts[] = '@media (max-width: 480px){' . $mobile_hide . '}';
		}
		return implode( "\n", $parts );
	}

	/**
	 * @param string               $block_selector Selector.
	 * @param array<string, mixed> $attrs          Attrs.
	 * @param array<int, string>   $keys           Keys.
	 */
	public static function to_css_for_attrs( string $block_selector, array $attrs, array $keys = array() ): string {
		if ( '' === $block_selector ) {
			return '';
		}
		$parts = array();
		if ( isset( $attrs['elementsVisibility'] ) ) {
			$css = self::to_css( $block_selector, $attrs['elementsVisibility'], $keys );
			if ( '' !== $css ) {
				$parts[] = $css;
			}
		}
		$slides = isset( $attrs['slides'] ) && is_array( $attrs['slides'] ) ? $attrs['slides'] : array();
		foreach ( $slides as $index => $slide ) {
			if ( ! is_array( $slide ) || ! isset( $slide['elementsVisibility'] ) ) {
				continue;
			}
			$sel = $block_selector . ' [data-bm-slide-index="' . (int) $index . '"]';
			$css = self::to_css( $sel, $slide['elementsVisibility'], $keys );
			if ( '' !== $css ) {
				$parts[] = $css;
			}
		}
		return implode( "\n\n", $parts );
	}

	/**
	 * @param string               $key   Key.
	 * @param array<string, mixed> $attrs Attrs.
	 * @param string               $breakpoint Breakpoint.
	 */
	public static function is_visible( string $key, array $attrs, string $breakpoint = 'desktop' ): bool {
		$map = self::effective( $attrs['elementsVisibility'] ?? null, $breakpoint );
		if ( ! array_key_exists( $key, $map ) ) {
			return true;
		}
		return false !== $map[ $key ];
	}

	/**
	 * @param string               $key   Key.
	 * @param array<string, mixed> $attrs Attrs.
	 */
	public static function is_visible_on_any( string $key, array $attrs ): bool {
		return self::is_visible( $key, $attrs, 'desktop' )
			|| self::is_visible( $key, $attrs, 'tablet' )
			|| self::is_visible( $key, $attrs, 'mobile' );
	}

	/**
	 * @param mixed $value Value.
	 */
	private static function is_breakpoint_map( $value ): bool {
		if ( ! is_array( $value ) || array() === $value ) {
			return false;
		}
		foreach ( self::BREAKPOINTS as $bp ) {
			if ( array_key_exists( $bp, $value ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @param mixed $value Value.
	 */
	private static function is_flat( $value ): bool {
		if ( ! is_array( $value ) || self::is_breakpoint_map( $value ) ) {
			return false;
		}
		foreach ( $value as $v ) {
			if ( ! is_bool( $v ) && null !== $v ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * @param array<int, string> $keys Keys.
	 * @return array<string, bool>
	 */
	private static function default_map( array $keys ): array {
		$out = array();
		foreach ( $keys as $key ) {
			if ( is_string( $key ) && '' !== $key ) {
				$out[ $key ] = true;
			}
		}
		return $out;
	}

	/**
	 * @param mixed              $value Value.
	 * @param array<int, string> $keys  Keys.
	 * @return array<string, bool>
	 */
	private static function resolve_map( $value, array $keys ): array {
		$base = self::default_map( $keys );
		if ( ! is_array( $value ) ) {
			return $base;
		}
		foreach ( $base as $key => $_ ) {
			if ( array_key_exists( $key, $value ) ) {
				$base[ $key ] = false !== $value[ $key ];
			}
		}
		foreach ( $value as $key => $v ) {
			if ( is_string( $key ) && '' !== $key && ! array_key_exists( $key, $base ) ) {
				$base[ $key ] = false !== $v;
			}
		}
		return $base;
	}

	/**
	 * Hide-only rules — never force display when shown (keeps flex / inline-block).
	 * Column heroes: also hide `.bm-hero__media` so the grid column collapses.
	 *
	 * @param string              $selector Selector.
	 * @param array<string, bool> $map      Map.
	 */
	private static function visibility_hide_rules( string $selector, array $map ): string {
		$parts = array();
		foreach ( $map as $key => $visible ) {
			if ( false !== $visible ) {
				continue;
			}
			$safe = preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $key ) ?? '';
			if ( '' === $safe ) {
				continue;
			}
			$parts[] = $selector . ' [data-bm-el="' . $safe . '"]{display:none!important}';
			if ( 'image' === $safe ) {
				$parts[] = $selector . ' .bm-hero__media{display:none!important}';
				$parts[] = $selector . ' .bm-hero__grid{grid-template-columns:minmax(0,1fr)!important}';
			}
		}
		return implode( '', $parts );
	}
}
