/**
 * @license Copyright (c) 2017-2025, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md.
 */

import { BaseComponent } from '../components/base-component';
import { createOutsideClickWatcher } from '../helpers/create-outside-click-watcher';
import { isHiddenElement } from '../helpers/is-hidden-element';
import { onHeaderResize, createHeaderSpacerManager } from './header';
import { onEscapeKeyPress } from '../helpers/escape-handler';
import { isMobile } from '../helpers/is-mobile';
import { bodyScroll } from '../helpers/body-scroll';
import { toggleHeight } from '../helpers/toggle-height';
import { animateElement } from '../helpers/animate-element';

/**
 * API Table of Contents component.
 */
export class ApiTableOfContents extends BaseComponent {
	/**
	 * Creates an API table of contents component.
	 *
	 * @param element - The API ToC container element.
	 */
	constructor( element ) {
		super( {
			isVisible: element.classList.contains( 'is-visible' ),
			isLinkClicked: false,
			headerHeight: null
		} );

		this.elements = {
			toc: element,
			tocList: element.querySelector( '.api-toc__content' ),
			hideBtn: document.querySelector( '.js-api-toc-hide-btn' ),
			showBtn: document.querySelector( '.js-api-toc-show-btn' ),
			tocTogglingButtons: element.querySelectorAll( '.js-api-toc-button' )
		};
	}

	/**
	 * Initializes all table of contents components on the page.
	 */
	static attach() {
		const toc = document.querySelector( '.js-api-toc' );

		if ( toc ) {
			new ApiTableOfContents( toc ).init();
		}
	}

	/**
	 * Initializes the API table of contents component with event listeners.
	 */
	init() {
		const { hideBtn, showBtn, toc, tocList, tocTogglingButtons } = this.elements;

		hideBtn?.addEventListener( 'click', () => {
			this.animateToC();
			this.setState( { isVisible: false } );
		} );

		showBtn?.addEventListener( 'click', () => {
			this.animateToC();
			this.setState( { isVisible: true } );
		} );

		tocList?.addEventListener( 'click', event => {
			if ( event.target.tagName !== 'A' ) {
				return;
			}

			const headings = this.elements.tocList.querySelectorAll( 'a' );

			headings.forEach( heading => heading.classList.remove( 'is-active' ) );
			event.target.classList.add( 'is-active' );

			this.isLinkClicked = true;
			this.setState( { isVisible: false } );

			setTimeout( () => {
				this.isLinkClicked = false;
			}, 1000 );
		} );

		// Do not show the ToC button if there are no items in the ToC.
		if ( tocList?.children.length === 0 ) {
			showBtn.classList.add( 'is-hidden' );
		}

		tocTogglingButtons?.forEach( button => {
			button.addEventListener( 'click', async () => {
				const listElement = button.nextElementSibling;

				if ( !listElement ) {
					return;
				}

				button.classList.toggle( 'api-toc__list-button--open' );

				const toggleState = await toggleHeight( listElement, 'api-toc__list--open' );
				const isExpanded = toggleState === 'visible';

				// Update ARIA attributes for accessibility
				listElement.setAttribute( 'aria-hidden', !isExpanded );
				button.setAttribute( 'aria-expanded', isExpanded );
			} );
		} );

		this.headerResizeUnsubscribe = onHeaderResize( height => {
			this.setState( { headerHeight: height } );
		} );

		this.headerSpacerManager = createHeaderSpacerManager( {
			overlay: toc,
			setSpacing: ( overlay, height ) => {
				if ( height ) {
					overlay.style.setProperty( '--header-height', height );
				} else {
					overlay.style.removeProperty( '--header-height' );
				}
			}
		} );

		this.render();
	}

	/**
	 * Renders the table of contents based on current state.
	 *
	 * @param prevState - Previous component state.
	 * @param state - Current component state.
	 */
	render( prevState, state = this.state ) {
		const { toc, hideBtn, showBtn } = this.elements;
		const { isVisible, headerHeight } = state;

		if ( !!prevState?.isVisible !== !!isVisible ) {
			animateElement( toc, isVisible, {
				duration: 150,
				displayValue: 'block',
				...!this.isLinkClicked && isMobile() && {
					onEntering: () => {
						bodyScroll( { shouldBeStopped: true } );
						this.headerSpacerManager.show();
					},
					onExiting: () => {
						bodyScroll( { shouldBeStopped: false } );
					},
					onExited: () => {
						this.headerSpacerManager.hide();
					}
				}
			} );

			this.outsideClickCleanup?.();
			this.escapeHandlerCleanup?.();

			if ( isVisible ) {
				this.setupOutsideClickWatcher();
				this.setupEscapeHandler();
				hideBtn.focus();
				showBtn?.setAttribute( 'aria-expanded', 'true' );
			} else {
				showBtn?.setAttribute( 'aria-expanded', 'false' );
			}
		}

		if ( headerHeight !== null && ( !isMobile() || !isHiddenElement( toc ) ) ) {
			toc.style.setProperty( '--header-height', `${ headerHeight }px` );
		}
	}

	/**
	 * Toggles the scroll position of the body when the ToC is visible.
	 * If the ToC is visible, it prevents scrolling and saves the current scroll position.
	 * If the ToC is hidden, it restores the scroll position.
	 */
	bodyScrollToggle() {
		const { isVisible } = this.state;

		bodyScroll( { shouldBeStopped: isVisible } );
	}

	/**
	 * Sets up the outside click watcher to hide the ToC.
	 */
	setupOutsideClickWatcher() {
		this.outsideClickCleanup = createOutsideClickWatcher( {
			elements: [ this.elements.toc, this.elements.showBtn ],
			callback: () => this.setState( { isVisible: false } )
		} );
	}

	/**
	 * Sets the escape key handler to hide the ToC.
	 */
	setupEscapeHandler() {
		this.escapeHandlerCleanup = onEscapeKeyPress( () => this.setState( { isVisible: false } ) );
	}

	animateToC() {
		const { toc } = this.elements;
		const animationClassName = 'c-toc--transition';

		toc.classList.add( animationClassName );

		toc.addEventListener( 'transitionend', () => {
			toc.classList.remove( animationClassName );
		}, {
			once: true
		} );
	}
}
