// Reference: https://github.com/WordPress/gutenberg/blob/release/16.4/packages/block-editor/src/components/block-preview/auto.js

/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
 * External dependencies
 */
import { useResizeObserver, pure, useRefEffect } from '@wordpress/compose';
import { useMemo, useContext } from '@wordpress/element';
import { Disabled } from '@wordpress/components';
import {
	__unstableEditorStyles as EditorStyles,
	__unstableIframe as Iframe,
	privateApis as blockEditorPrivateApis,
	BlockList,
	// @ts-ignore No types for this exist yet.
} from '@wordpress/block-editor';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
import { noop } from 'lodash';

/**
 * Internal dependencies
 */
import { LogoBlockContext } from './logo-block-context';
import {
	FontFamiliesLoader,
	FontFamily,
} from './sidebar/global-styles/font-pairing-variations/font-families-loader';
import { SYSTEM_FONT_SLUG } from './sidebar/global-styles/font-pairing-variations/constants';

const MAX_HEIGHT = 2000;
// @ts-ignore No types for this exist yet.
const { Provider: DisabledProvider } = Disabled.Context;

// This is used to avoid rendering the block list if the sizes change.
let MemoizedBlockList: typeof BlockList | undefined;

const { useGlobalSetting } = unlock( blockEditorPrivateApis );

export type ScaledBlockPreviewProps = {
	viewportWidth?: number;
	containerWidth: number;
	minHeight?: number;
	settings: {
		styles: string[];
		[ key: string ]: unknown;
	};
	additionalStyles: string;
	onClickNavigationItem: ( event: MouseEvent ) => void;
};

function ScaledBlockPreview( {
	viewportWidth,
	containerWidth,
	minHeight,
	settings,
	additionalStyles,
	onClickNavigationItem,
}: ScaledBlockPreviewProps ) {
	const { setLogoBlock } = useContext( LogoBlockContext );
	const [ fontFamilies ] = useGlobalSetting(
		'typography.fontFamilies.theme'
	) as [ FontFamily[] ];

	const externalFontFamilies = fontFamilies.filter(
		( { slug } ) => slug !== SYSTEM_FONT_SLUG
	);

	if ( ! viewportWidth ) {
		viewportWidth = containerWidth;
	}

	// @ts-ignore No types for this exist yet.
	const [ contentResizeListener, { height: contentHeight } ] =
		useResizeObserver();

	// Avoid scrollbars for pattern previews.
	const editorStyles = useMemo( () => {
		return [
			{
				css: 'body{height:auto;overflow:hidden;border:none;padding:0;}',
				__unstableType: 'presets',
			},
			...settings.styles,
		];
	}, [ settings.styles ] );

	// Initialize on render instead of module top level, to avoid circular dependency issues.
	MemoizedBlockList = MemoizedBlockList || pure( BlockList );
	const scale = containerWidth / viewportWidth;

	return (
		<DisabledProvider value={ true }>
			<Iframe
				contentRef={ useRefEffect( ( bodyElement: HTMLBodyElement ) => {
					const {
						ownerDocument: { documentElement },
					} = bodyElement;

					documentElement.classList.add(
						'block-editor-block-preview__content-iframe'
					);
					documentElement.style.position = 'absolute';
					documentElement.style.width = '100%';

					// Necessary for contentResizeListener to work.
					bodyElement.style.boxSizing = 'border-box';
					bodyElement.style.position = 'absolute';
					bodyElement.style.width = '100%';

					let navigationContainers: NodeListOf< HTMLDivElement >;
					let siteTitles: NodeListOf< HTMLAnchorElement >;
					const onClickNavigation = ( event: MouseEvent ) => {
						event.preventDefault();
						onClickNavigationItem( event );
					};

					const onMouseMove = ( event: MouseEvent ) => {
						event.stopImmediatePropagation();
					};

					const possiblyRemoveAllListeners = () => {
						bodyElement.removeEventListener(
							'mousemove',
							onMouseMove,
							false
						);
						if ( navigationContainers ) {
							navigationContainers.forEach( ( element ) => {
								element.removeEventListener(
									'click',
									onClickNavigation
								);
							} );
						}

						if ( siteTitles ) {
							siteTitles.forEach( ( element ) => {
								element.removeEventListener(
									'click',
									onClickNavigation
								);
							} );
						}
					};

					const onChange = () => {
						// Remove contenteditable and inert attributes from editable elements so that users can click on navigation links.
						bodyElement
							.querySelectorAll(
								'.block-editor-rich-text__editable[contenteditable="true"]'
							)
							.forEach( ( element ) => {
								element.removeAttribute( 'contenteditable' );
							} );

						bodyElement
							.querySelectorAll( '*[inert="true"]' )
							.forEach( ( element ) => {
								element.removeAttribute( 'inert' );
							} );

						possiblyRemoveAllListeners();
						navigationContainers = bodyElement.querySelectorAll(
							'.wp-block-navigation__container'
						);
						navigationContainers.forEach( ( element ) => {
							element.addEventListener(
								'click',
								onClickNavigation,
								true
							);
						} );

						siteTitles = bodyElement.querySelectorAll(
							'.wp-block-site-title a'
						);
						siteTitles.forEach( ( element ) => {
							element.addEventListener(
								'click',
								onClickNavigation,
								true
							);
						} );

						// Get the current logo block client ID from DOM and set it in the logo block context. This is used for the logo settings. See: ./sidebar/sidebar-navigation-screen-logo.tsx
						// Ideally, we should be able to get the logo block client ID from the block editor store but it is not available.
						// We should update this code once the there is a selector in the block editor store that can be used to get the logo block client ID.
						const siteLogo = bodyElement.querySelector(
							'.wp-block-site-logo'
						);

						const blockClientId = siteLogo
							? siteLogo.getAttribute( 'data-block' )
							: null;

						setLogoBlock( {
							clientId: blockClientId,
							isLoading: false,
						} );
					};

					// Stop mousemove event listener to disable block tool insertion feature.
					bodyElement.addEventListener(
						'mousemove',
						onMouseMove,
						true
					);

					const observer = new window.MutationObserver( onChange );

					observer.observe( bodyElement, {
						attributes: true,
						characterData: false,
						subtree: true,
						childList: true,
					} );

					return () => {
						observer.disconnect();
						possiblyRemoveAllListeners();
						setLogoBlock( {
							clientId: null,
							isLoading: true,
						} );
					};
				}, [] ) }
				aria-hidden
				tabIndex={ -1 }
				style={ {
					width: viewportWidth,
					height: contentHeight,
					// This is a catch-all max-height for patterns.
					// Reference: https://github.com/WordPress/gutenberg/pull/38175.
					maxHeight: MAX_HEIGHT,
					minHeight:
						scale !== 0 && scale < 1 && minHeight
							? minHeight / scale
							: minHeight,
				} }
			>
				<EditorStyles styles={ editorStyles } />
				<style>
					{ `
						.block-editor-block-list__block::before,
						.is-selected::after,
						.is-hovered::after,
						.block-list-appender {
							display: none !important;
						}

						.block-editor-block-list__block.is-selected {
							box-shadow: none !important;
						}

						.block-editor-rich-text__editable {
							pointer-events: none !important;
						}

						.wp-block-site-title .block-editor-rich-text__editable {
							pointer-events: all !important;
						}

						.wp-block-navigation .wp-block-pages-list__item__link {
							pointer-events: all !important;
							cursor: pointer !important;
						}

						${ additionalStyles }
					` }
				</style>
				{ contentResizeListener }
				<MemoizedBlockList renderAppender={ false } />
				{ /* Only load font families when there are two font families (font-paring selection). Otherwise, it is not needed. */ }
				{ externalFontFamilies.length === 2 && (
					<FontFamiliesLoader
						fontFamilies={ externalFontFamilies }
						onLoad={ noop }
					/>
				) }
			</Iframe>
		</DisabledProvider>
	);
}

export const AutoHeightBlockPreview = (
	props: Omit< ScaledBlockPreviewProps, 'containerWidth' >
) => {
	const [ containerResizeListener, { width: containerWidth } ] =
		useResizeObserver();

	return (
		<>
			<div style={ { position: 'relative', width: '100%', height: 0 } }>
				{ containerResizeListener }
			</div>
			<div className="auto-block-preview__container">
				{ !! containerWidth && (
					<ScaledBlockPreview
						{ ...props }
						containerWidth={ containerWidth }
					/>
				) }
			</div>
		</>
	);
};
