/**
 * Responsive element visibility (desktop / tablet / mobile).
 * Legacy flat { key: bool } is treated as desktop-only.
 * Front-end hide uses CSS so one DOM works across breakpoints.
 */

import { BREAKPOINTS } from './elementsOrder';

const MEDIA = {
	desktop: '(min-width: 1025px)',
	tablet: '(max-width: 1024px) and (min-width: 481px)',
	mobile: '(max-width: 480px)',
};

/**
 * @param {unknown} value
 * @return {boolean}
 */
function isVisibilityMap( value ) {
	return (
		value &&
		typeof value === 'object' &&
		! Array.isArray( value ) &&
		BREAKPOINTS.some( ( bp ) =>
			Object.prototype.hasOwnProperty.call( value, bp )
		)
	);
}

/**
 * @param {unknown} value
 * @return {boolean}
 */
function isFlatVisibility( value ) {
	if ( ! value || typeof value !== 'object' || Array.isArray( value ) ) {
		return false;
	}
	if ( isVisibilityMap( value ) ) {
		return false;
	}
	return Object.keys( value ).every(
		( key ) => typeof value[ key ] === 'boolean' || value[ key ] == null
	);
}

/**
 * @param {string[]} keys
 * @return {Record<string, boolean>}
 */
export function defaultVisibilityMap( keys = [] ) {
	const out = {};
	( Array.isArray( keys ) ? keys : [] ).forEach( ( key ) => {
		if ( typeof key === 'string' && key ) {
			out[ key ] = true;
		}
	} );
	return out;
}

/**
 * @param {unknown}  value
 * @param {string[]} keys
 * @return {Record<string, boolean>}
 */
export function resolveVisibilityMap( value, keys = [] ) {
	const base = defaultVisibilityMap( keys );
	if ( ! value || typeof value !== 'object' || Array.isArray( value ) ) {
		return base;
	}
	Object.keys( base ).forEach( ( key ) => {
		if ( Object.prototype.hasOwnProperty.call( value, key ) ) {
			base[ key ] = value[ key ] !== false;
		}
	} );
	// Keep unknown keys from value (forward-compat).
	Object.keys( value ).forEach( ( key ) => {
		if ( typeof key === 'string' && key && ! Object.prototype.hasOwnProperty.call( base, key ) ) {
			base[ key ] = value[ key ] !== false;
		}
	} );
	return base;
}

/**
 * @param {Record<string, boolean>} a
 * @param {Record<string, boolean>} b
 * @param {string[]} keys
 * @return {boolean}
 */
function sameVisibilityMap( a, b, keys ) {
	const list =
		Array.isArray( keys ) && keys.length
			? keys
			: Array.from(
					new Set( [
						...Object.keys( a || {} ),
						...Object.keys( b || {} ),
					] )
			  );
	return list.every(
		( key ) => ( a?.[ key ] !== false ) === ( b?.[ key ] !== false )
	);
}

/**
 * @param {unknown}  value
 * @param {string[]} [keys]
 * @return {{ desktop: Record<string, boolean>, tablet: Record<string, boolean>|null, mobile: Record<string, boolean>|null }}
 */
export function normalizeElementsVisibility( value, keys = [] ) {
	if ( isFlatVisibility( value ) ) {
		return {
			desktop: resolveVisibilityMap( value, keys ),
			tablet: null,
			mobile: null,
		};
	}
	if ( isVisibilityMap( value ) ) {
		const desktop = resolveVisibilityMap( value.desktop, keys );
		const keyList = Object.keys( desktop );
		return {
			desktop,
			tablet:
				value.tablet &&
				typeof value.tablet === 'object' &&
				! Array.isArray( value.tablet )
					? resolveVisibilityMap( value.tablet, keyList )
					: null,
			mobile:
				value.mobile &&
				typeof value.mobile === 'object' &&
				! Array.isArray( value.mobile )
					? resolveVisibilityMap( value.mobile, keyList )
					: null,
		};
	}
	return {
		desktop: defaultVisibilityMap( keys ),
		tablet: null,
		mobile: null,
	};
}

/**
 * @param {unknown}  value
 * @param {string}   [breakpoint]
 * @param {string[]} [keys]
 * @return {Record<string, boolean>}
 */
export function effectiveElementsVisibility(
	value,
	breakpoint = 'desktop',
	keys = []
) {
	const n = normalizeElementsVisibility( value, keys );
	let out = n.desktop;
	if ( breakpoint === 'tablet' || breakpoint === 'mobile' ) {
		if ( n.tablet ) {
			out = n.tablet;
		}
	}
	if ( breakpoint === 'mobile' && n.mobile ) {
		out = n.mobile;
	}
	return out;
}

/**
 * @param {unknown}  value
 * @param {string[]} [keys]
 * @return {{ desktop: Record<string, boolean>, tablet?: Record<string, boolean>, mobile?: Record<string, boolean> }}
 */
export function serializeElementsVisibility( value, keys = [] ) {
	const n = normalizeElementsVisibility( value, keys );
	const out = { desktop: n.desktop };
	if ( n.tablet ) {
		out.tablet = n.tablet;
	}
	if ( n.mobile ) {
		out.mobile = n.mobile;
	}
	return out;
}

/**
 * Set one key’s visibility at a breakpoint (sparse tablet/mobile when equal to parent).
 *
 * @param {unknown}  value
 * @param {string}   breakpoint
 * @param {string}   key
 * @param {boolean}  visible
 * @param {string[]} [keys]
 */
export function setVisibilityKeyAtBreakpoint(
	value,
	breakpoint,
	key,
	visible,
	keys = []
) {
	const bp = BREAKPOINTS.includes( breakpoint ) ? breakpoint : 'desktop';
	const n = normalizeElementsVisibility( value, keys );
	const keyList =
		Array.isArray( keys ) && keys.length
			? keys
			: Object.keys( n.desktop );
	const nextMap = {
		...effectiveElementsVisibility( n, bp, keyList ),
		[ key ]: Boolean( visible ),
	};

	if ( bp === 'desktop' ) {
		const out = { ...n, desktop: resolveVisibilityMap( nextMap, keyList ) };
		if ( out.tablet && sameVisibilityMap( out.tablet, out.desktop, keyList ) ) {
			out.tablet = null;
		}
		const tabletEff = out.tablet || out.desktop;
		if ( out.mobile && sameVisibilityMap( out.mobile, tabletEff, keyList ) ) {
			out.mobile = null;
		}
		return out;
	}

	if ( bp === 'tablet' ) {
		const out = { ...n };
		out.tablet = sameVisibilityMap( nextMap, out.desktop, keyList )
			? null
			: resolveVisibilityMap( nextMap, keyList );
		const tabletEff = out.tablet || out.desktop;
		if ( out.mobile && sameVisibilityMap( out.mobile, tabletEff, keyList ) ) {
			out.mobile = null;
		}
		return out;
	}

	const tabletEff = n.tablet || n.desktop;
	return {
		...n,
		mobile: sameVisibilityMap( nextMap, tabletEff, keyList )
			? null
			: resolveVisibilityMap( nextMap, keyList ),
	};
}

/**
 * Replace the full visibility map at a breakpoint (from UI toggle spreading).
 *
 * @param {unknown}                 value
 * @param {string}                  breakpoint
 * @param {Record<string, boolean>} nextMap
 * @param {string[]}                [keys]
 */
export function setVisibilityMapAtBreakpoint(
	value,
	breakpoint,
	nextMap,
	keys = []
) {
	const bp = BREAKPOINTS.includes( breakpoint ) ? breakpoint : 'desktop';
	const n = normalizeElementsVisibility( value, keys );
	const keyList =
		Array.isArray( keys ) && keys.length
			? keys
			: Object.keys( n.desktop );
	const resolved = resolveVisibilityMap( nextMap, keyList );

	if ( bp === 'desktop' ) {
		const out = { ...n, desktop: resolved };
		if ( out.tablet && sameVisibilityMap( out.tablet, out.desktop, keyList ) ) {
			out.tablet = null;
		}
		const tabletEff = out.tablet || out.desktop;
		if ( out.mobile && sameVisibilityMap( out.mobile, tabletEff, keyList ) ) {
			out.mobile = null;
		}
		return out;
	}

	if ( bp === 'tablet' ) {
		const out = { ...n };
		out.tablet = sameVisibilityMap( resolved, out.desktop, keyList )
			? null
			: resolved;
		const tabletEff = out.tablet || out.desktop;
		if ( out.mobile && sameVisibilityMap( out.mobile, tabletEff, keyList ) ) {
			out.mobile = null;
		}
		return out;
	}

	const tabletEff = n.tablet || n.desktop;
	return {
		...n,
		mobile: sameVisibilityMap( resolved, tabletEff, keyList )
			? null
			: resolved,
	};
}

/**
 * Only emit hide rules — never force display when shown (preserves flex/inline-block).
 * For column heroes, also hide `.bm-hero__media` so the grid column collapses
 * (not an empty cell).
 *
 * @param {string}                  selector
 * @param {Record<string, boolean>} map
 * @return {string}
 */
function visibilityHideRules( selector, map ) {
	const parts = [];
	Object.keys( map || {} ).forEach( ( key ) => {
		const safe = String( key || '' ).replace( /[^a-zA-Z0-9_-]/g, '' );
		if ( ! safe || map[ key ] !== false ) {
			return;
		}
		parts.push(
			`${ selector } [data-bm-el="${ safe }"]{display:none!important}`
		);
		if ( safe === 'image' ) {
			parts.push(
				`${ selector } .bm-hero__media{display:none!important}`,
				`${ selector } .bm-hero__grid{grid-template-columns:minmax(0,1fr)!important}`
			);
		}
		// Accordion content bottom padding is collapsed while buttons show;
		// restore it when the buttons row is visibility-hidden.
		if ( safe === 'buttons' ) {
			parts.push(
				`${ selector } .bm-accordion__content{padding-bottom:var(--bm-accordionContent-padding-bottom, 1rem)!important}`
			);
		}
	} );
	return parts.join( '' );
}

/**
 * Full responsive visibility CSS.
 * Exclusive media queries per breakpoint so hides do not need a “revert” unhide.
 *
 * @param {string}   selector
 * @param {unknown}  value
 * @param {string[]} [keys]
 * @return {string}
 */
export function toElementsVisibilityCss( selector, value, keys = [] ) {
	if ( ! selector ) {
		return '';
	}
	const n = normalizeElementsVisibility( value, keys );
	const keyList =
		Array.isArray( keys ) && keys.length
			? keys
			: Object.keys( n.desktop );
	const desktop = effectiveElementsVisibility( n, 'desktop', keyList );
	const tablet = effectiveElementsVisibility( n, 'tablet', keyList );
	const mobile = effectiveElementsVisibility( n, 'mobile', keyList );
	const parts = [];
	const desktopHide = visibilityHideRules( selector, desktop );
	if ( desktopHide ) {
		parts.push( `@media ${ MEDIA.desktop }{${ desktopHide }}` );
	}
	const tabletHide = visibilityHideRules( selector, tablet );
	if ( tabletHide ) {
		parts.push( `@media ${ MEDIA.tablet }{${ tabletHide }}` );
	}
	const mobileHide = visibilityHideRules( selector, mobile );
	if ( mobileHide ) {
		parts.push( `@media ${ MEDIA.mobile }{${ mobileHide }}` );
	}
	return parts.join( '\n' );
}

/**
 * Bake visibility for one breakpoint (editor device preview).
 *
 * @param {string}   selector
 * @param {unknown}  value
 * @param {string}   breakpoint
 * @param {string[]} [keys]
 * @return {string}
 */
export function toElementsVisibilityCssForBreakpoint(
	selector,
	value,
	breakpoint = 'desktop',
	keys = []
) {
	if ( ! selector ) {
		return '';
	}
	const map = effectiveElementsVisibility( value, breakpoint, keys );
	return visibilityHideRules( selector, map );
}

/**
 * @param {string}   blockSelector
 * @param {object}   attrs
 * @param {string[]} [keys]
 * @param {string|null} [breakpoint]
 * @return {string}
 */
export function toElementsVisibilityCssForAttrs(
	blockSelector,
	attrs = {},
	keys = [],
	breakpoint = null
) {
	if ( ! blockSelector ) {
		return '';
	}
	const value = attrs?.elementsVisibility;
	const bake =
		breakpoint != null && breakpoint !== ''
			? ( sel, val, keyList ) =>
					toElementsVisibilityCssForBreakpoint(
						sel,
						val,
						breakpoint,
						keyList
					)
			: ( sel, val, keyList ) =>
					toElementsVisibilityCss( sel, val, keyList );

	const parts = [];
	if ( value != null ) {
		const css = bake( blockSelector, value, keys );
		if ( css ) {
			parts.push( css );
		}
	}
	const slides = Array.isArray( attrs.slides ) ? attrs.slides : [];
	slides.forEach( ( slide, index ) => {
		if ( ! slide || typeof slide !== 'object' || ! slide.elementsVisibility ) {
			return;
		}
		const sel = `${ blockSelector } [data-bm-slide-index="${ index }"]`;
		const css = bake( sel, slide.elementsVisibility, keys );
		if ( css ) {
			parts.push( css );
		}
	} );
	return parts.join( '\n\n' );
}

/**
 * True when the key is visible for the given breakpoint (default desktop).
 *
 * @param {string}   key
 * @param {object}   attrs
 * @param {string}   [breakpoint]
 * @param {string[]} [keys]
 * @return {boolean}
 */
export function isElementVisibleAt(
	key,
	attrs = {},
	breakpoint = 'desktop',
	keys = []
) {
	const map = effectiveElementsVisibility(
		attrs?.elementsVisibility,
		breakpoint,
		keys
	);
	if ( ! Object.prototype.hasOwnProperty.call( map, key ) ) {
		return true;
	}
	return map[ key ] !== false;
}

/**
 * True when visible on at least one breakpoint (keep in DOM for CSS hide).
 *
 * @param {string}   key
 * @param {object}   attrs
 * @param {string[]} [keys]
 * @return {boolean}
 */
export function isElementVisibleOnAnyBreakpoint( key, attrs = {}, keys = [] ) {
	return (
		isElementVisibleAt( key, attrs, 'desktop', keys ) ||
		isElementVisibleAt( key, attrs, 'tablet', keys ) ||
		isElementVisibleAt( key, attrs, 'mobile', keys )
	);
}
