/**
 * Peer tab next to Gutenberg's Page / Block settings tabs.
 * Shows template variants for the selected library block.
 */
import {
	Fragment,
	createElement,
	createPortal,
	useCallback,
	useEffect,
	useRef,
	useState,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';
import { useDispatch, useSelect } from '@wordpress/data';
import { brandBlockIcon } from '../shared/brandLogo';
import LibraryVariantSwitcher, {
	hasLibraryVariants,
} from './LibraryVariantSwitcher';
import { catalogByName, LIB_PREFIX } from './libraryVariants';

const TAB_CLASS = 'jocbm-variant-settings-tab';
const HOST_CLASS = 'jocbm-variant-settings-host';
const BODY_ACTIVE = 'jocbm-variant-tab-active';

function findSidebarShell() {
	return (
		document.querySelector( '.interface-complementary-area' ) ||
		document.querySelector( '.editor-sidebar' ) ||
		document.querySelector( '.edit-post-sidebar' )
	);
}

function findTabHeader( shell ) {
	if ( ! shell ) {
		return null;
	}
	return (
		shell.querySelector( '.editor-sidebar__panel-tabs' ) ||
		shell.querySelector( '.edit-post-sidebar__panel-tabs' )
	);
}

/**
 * The real tab strip. Core renders the close button as a sibling of the tab
 * list inside a space-between panel header, so appending to the header pushes
 * our tab to the far right — we must land inside the tablist itself.
 *
 * @param {Element|null} shell Sidebar root.
 * @return {Element|null} Element with role=tablist.
 */
function findTabList( shell ) {
	const header = findTabHeader( shell );
	return (
		header?.querySelector( '[role="tablist"]' ) ||
		shell?.querySelector( '[role="tablist"]' ) ||
		( header?.getAttribute?.( 'role' ) === 'tablist' ? header : null ) ||
		null
	);
}

/** Copy core's emotion tab classes so the injected tab matches exactly. */
function adoptPeerTabClasses( tab, tabList ) {
	const peer = Array.from( tabList.children ).find(
		( el ) =>
			el !== tab &&
			( el.getAttribute?.( 'role' ) === 'tab' ||
				el.tagName === 'BUTTON' )
	);
	if ( ! peer ) {
		return;
	}
	const adopted = Array.from( peer.classList ).filter(
		( cls ) => ! cls.startsWith( 'jocbm-' )
	);
	if ( ! adopted.length ) {
		return;
	}
	const signature = adopted.join( ' ' );
	if ( tab.dataset.jocbmAdopted === signature ) {
		return;
	}
	tab.dataset.jocbmAdopted = signature;
	tab.className = `${ TAB_CLASS } ${ signature }`;
}

function openBlockSidebar( openGeneralSidebar ) {
	if ( typeof openGeneralSidebar !== 'function' ) {
		return;
	}
	try {
		openGeneralSidebar( 'edit-post/block' );
	} catch ( e ) {
		try {
			openGeneralSidebar( 'edit-site/block' );
		} catch ( e2 ) {
			// Ignore — sidebar may already be open.
		}
	}
}

function VariantSettingsPlugin() {
	const [ tabHost, setTabHost ] = useState( null );
	const [ panelHost, setPanelHost ] = useState( null );
	const [ active, setActive ] = useState( false );
	const activeRef = useRef( active );
	activeRef.current = active;

	const { clientId, meta } = useSelect( ( select ) => {
		const be = select( 'core/block-editor' );
		const block = be?.getSelectedBlock?.();
		if ( ! block?.name || ! String( block.name ).startsWith( LIB_PREFIX ) ) {
			return { clientId: null, meta: null };
		}
		const fromCatalog = catalogByName().get( block.name ) || null;
		return {
			clientId: block.clientId,
			meta: fromCatalog
				? { ...fromCatalog, name: block.name }
				: { name: block.name },
		};
	}, [] );

	const available = Boolean( meta && hasLibraryVariants( meta ) );
	const editPostDispatch = useDispatch( 'core/edit-post' );
	const openGeneralSidebar = editPostDispatch?.openGeneralSidebar;

	const deactivate = useCallback( () => {
		setActive( false );
	}, [] );

	const activate = useCallback( () => {
		openBlockSidebar( openGeneralSidebar );
		setActive( true );
	}, [ openGeneralSidebar ] );

	// Drop the Variant tab when the selection no longer supports variants.
	useEffect( () => {
		if ( ! available && active ) {
			setActive( false );
		}
	}, [ available, active ] );

	// Mount tab + panel hosts into Gutenberg's sidebar chrome.
	useEffect( () => {
		if ( ! available ) {
			setTabHost( null );
			setPanelHost( null );
			document
				.querySelectorAll( `.${ TAB_CLASS }, .${ HOST_CLASS }` )
				.forEach( ( el ) => el.remove() );
			return undefined;
		}

		let disposed = false;
		let frame = 0;

		const sync = () => {
			frame = 0;
			if ( disposed ) {
				return;
			}
			const shell = findSidebarShell();
			const tabList = findTabList( shell );
			if ( ! tabList || ! shell ) {
				return;
			}

			observer.disconnect();
			try {
				let tab = tabList.querySelector( `.${ TAB_CLASS }` );
				if ( ! tab ) {
					tab = document.createElement( 'button' );
					tab.type = 'button';
					tab.setAttribute( 'role', 'tab' );
					tab.className = TAB_CLASS;
				}
				if ( tab.parentNode !== tabList ) {
					tabList.appendChild( tab );
				}
				adoptPeerTabClasses( tab, tabList );
				setTabHost( ( prev ) => ( prev === tab ? prev : tab ) );

				let host = shell.querySelector( `.${ HOST_CLASS }` );
				if ( ! host ) {
					host = document.createElement( 'div' );
					host.className = HOST_CLASS;
				}
				if ( host.parentNode !== shell ) {
					shell.appendChild( host );
				}
				// Overlay sits below the tab strip — measured so we never depend
				// on core's inner panel class names.
				const header = findTabHeader( shell );
				const offset = header
					? Math.round(
							header.getBoundingClientRect().bottom -
								shell.getBoundingClientRect().top
					  )
					: 48;
				host.style.top = `${ Math.max( 0, offset ) }px`;
				setPanelHost( ( prev ) => ( prev === host ? prev : host ) );
			} finally {
				if ( ! disposed ) {
					observer.observe( document.body, {
						childList: true,
						subtree: true,
					} );
				}
			}
		};

		const observer = new MutationObserver( () => {
			if ( disposed || frame ) {
				return;
			}
			frame = window.requestAnimationFrame( sync );
		} );

		sync();

		return () => {
			disposed = true;
			if ( frame ) {
				window.cancelAnimationFrame( frame );
			}
			observer.disconnect();
			document
				.querySelectorAll( `.${ TAB_CLASS }, .${ HOST_CLASS }` )
				.forEach( ( el ) => el.remove() );
		};
	}, [ available ] );

	// Body class toggles CSS that hides the Block inspector while Variant is open.
	useEffect( () => {
		document.body.classList.toggle( BODY_ACTIVE, available && active );
		return () => {
			document.body.classList.remove( BODY_ACTIVE );
		};
	}, [ available, active ] );

	// Sync tab button attributes + leave Variant when Page/Block is clicked.
	useEffect( () => {
		if ( ! tabHost ) {
			return undefined;
		}

		tabHost.setAttribute( 'aria-selected', active ? 'true' : 'false' );
		tabHost.classList.toggle( 'is-active', active );
		tabHost.tabIndex = active ? 0 : -1;

		const onTabClick = ( event ) => {
			event.preventDefault();
			event.stopPropagation();
			activate();
		};
		tabHost.addEventListener( 'click', onTabClick );

		const tabList = tabHost.parentElement;
		const onPeerClick = ( event ) => {
			const btn = event.target?.closest?.(
				'button, [role="tab"], a'
			);
			if ( ! btn || btn === tabHost ) {
				return;
			}
			if ( activeRef.current ) {
				deactivate();
			}
		};
		tabList?.addEventListener( 'click', onPeerClick );

		return () => {
			tabHost.removeEventListener( 'click', onTabClick );
			tabList?.removeEventListener( 'click', onPeerClick );
		};
	}, [ tabHost, active, activate, deactivate ] );

	if ( ! available ) {
		return null;
	}

	return createElement(
		Fragment,
		null,
		tabHost
			? createPortal(
					createElement(
						'span',
						{ className: 'jocbm-variant-settings-tab__label' },
						__( 'Variant', 'joc-blockora-morph' )
					),
					tabHost
			  )
			: null,
		panelHost && active
			? createPortal(
					createElement( LibraryVariantSwitcher, {
						meta,
						clientId,
					} ),
					panelHost
			  )
			: null
	);
}

registerPlugin( 'joc-blockora-morph-variant-tab', {
	render: VariantSettingsPlugin,
	icon: brandBlockIcon( 20 ),
} );
