/**
 * @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 { tween } from '../helpers/tween';
import { lerp } from '../helpers/lerp';
import { timeout } from '../helpers/timeout';
import { focusFirstInteractive } from '../helpers/focus-first-interactive';
import { easings } from '../helpers/cubic-bezier';
import { onMobileOverlayStateChange } from '../components/mobile-overlay';

/**
 * Mobile Navigation component.
 * Handles slide transitions between different navigation levels.
 */
export class MobileNav extends BaseComponent {
	/**
	 * Creates a mobile navigation component.
	 *
	 * @param container - The mobile navigation container element
	 */
	constructor( container ) {
		super( {
			currentSlideId: null,
			isAnimating: false,
			targetSlideId: null,
			direction: null
		} );

		this.container = container;
		this.elements = this.queryElements( container );
	}

	/**
	 * Returns the ID of the initial slide to show.
	 */
	get initialSlideId() {
		return this.container.dataset.initialSlideId;
	}

	/**
	 * Queries and organizes all required DOM elements.
	 *
	 * @param container - The component container element
	 * @returns Organized object with element references
	 */
	queryElements( container ) {
		const slides = Array.from( container.querySelectorAll( '.c-mobile-nav__slide' ) ).reduce(
			( slides, slideElement ) => {
				const slideId = slideElement.dataset.slideId;
				slides[ slideId ] = {
					element: slideElement,
					position: 0
				};
				return slides;
			},
			Object.create( null )
		);

		return {
			slides,
			navigation: Array.from( container.querySelectorAll( '.js-mobile-nav-navigate' ) )
		};
	}

	/**
	 * Initializes all mobile navigation components on the page.
	 */
	static attach() {
		const containers = document.querySelectorAll( '.c-mobile-nav' );

		for ( const container of containers ) {
			new MobileNav( container ).init();
		}
	}

	/**
	 * Initializes the mobile navigation component with event listeners.
	 */
	init() {
		// Check if there's an initial slide ID specified
		const { initialSlideId } = this;

		// Set up initial state - show first slide
		if ( Object.keys( this.elements.slides ).length > 0 ) {
			// Get the first slide ID as fallback
			const firstSlideId = Object.keys( this.elements.slides )[ 0 ];

			// Use the initial slide ID if it exists and is valid, otherwise use the first slide
			const slideToShow = initialSlideId && this.elements.slides[ initialSlideId ] ? initialSlideId : firstSlideId;

			this.setState( { currentSlideId: slideToShow } );
		}

		// Add click listeners to navigation buttons
		this.elements.navigation.forEach( button => {
			button.addEventListener( 'click', this.handleNavigationClick.bind( this ) );
		} );

		// Add listener for mobile overlay opening
		onMobileOverlayStateChange( {
			eventName: 'opening',
			callback: this.onOverlayOpen.bind( this ),
			target: this.container.closest( '.js-mobile-overlay' )
		} );
	}

	/**
	 * Handles mobile overlay opening by resetting to initial slide without animation
	 */
	onOverlayOpen() {
		// Get the initial or first slide ID
		const { initialSlideId } = this;
		const firstSlideId = Object.keys( this.elements.slides )[ 0 ];
		const slideToShow = initialSlideId && this.elements.slides[ initialSlideId ] ? initialSlideId : firstSlideId;

		// Only navigate if we're not already on that slide
		if ( slideToShow !== this.state.currentSlideId ) {
			this.navigateToSlide( {
				targetSlideId: slideToShow,
				animate: false
			} );
		}
	}

	/**
	 * Handles clicks on navigation buttons to transition between slides.
	 *
	 * @param event - The click event
	 */
	handleNavigationClick( event ) {
		const button = event.currentTarget;
		const targetSlideId = button.dataset.navigateToSlide;
		const direction = button.dataset.navigateDirection;

		// Don't do anything if already animating
		if ( this.state.isAnimating || !targetSlideId || !direction ) {
			return;
		}

		// Get target slide element
		const targetSlide = this.elements.slides[ targetSlideId ];

		// Verify target slide exists
		if ( !targetSlide ) {
			console.error( `Target slide with ID "${ targetSlideId }" not found` );
			return;
		}

		// Update state to trigger navigation through render
		this.setState( {
			targetSlideId,
			direction,
			isAnimating: true
		} );
	}

	/**
	 * Renders the component based on current state.
	 *
	 * @param prevState - Previous component state
	 * @param state - Current component state
	 */
	render( prevState, state ) {
		const { currentSlideId, targetSlideId, direction, isAnimating } = state;

		// Initial render - show first slide
		if ( !prevState.currentSlideId && currentSlideId ) {
			const slideElement = this.elements.slides[ currentSlideId ].element;
			slideElement.classList.remove( 'is-hidden' );
			slideElement.setAttribute( 'aria-hidden', 'false' );

			// Set appropriate aria-expanded states
			const firstSlideId = Object.keys( this.elements.slides )[ 0 ];
			if ( currentSlideId !== firstSlideId ) {
				this.elements.navigation.forEach( navButton => {
					if ( navButton.dataset.navigateToSlide === currentSlideId ) {
						navButton.setAttribute( 'aria-expanded', 'true' );
					}
				} );
			}
		}

		// Handle slide transitions
		if ( isAnimating && targetSlideId && !prevState.isAnimating ) {
			this.navigateToSlide( { targetSlideId, direction } );
		}

		// Update aria-expanded on navigation buttons when slide changes
		if ( currentSlideId !== prevState.currentSlideId ) {
			// If navigating backward, update buttons that point to previous slide
			this.elements.navigation.forEach( navButton => {
				if ( navButton.dataset.navigateToSlide === currentSlideId ) {
					navButton.setAttribute( 'aria-expanded', 'true' );
				} else if ( navButton.dataset.navigateToSlide === prevState.currentSlideId ) {
					navButton.setAttribute( 'aria-expanded', 'false' );
				}
			} );
		}
	}

	/**
	 * Navigates to a specific slide with optional animation.
	 *
	 * @param options - Navigation options
	 * @param options.targetSlideId - The ID of the slide to navigate to
	 * @param options.direction - The direction of navigation ('forward' or 'backward')
	 * @param options.animate - Whether to animate the transition (default: true)
	 */
	async navigateToSlide( { targetSlideId, direction, animate = true } ) {
		const { currentSlideId } = this.state;
		const { slides } = this.elements;

		// Don't navigate if already on target slide
		if ( currentSlideId === targetSlideId ) {
			this.setState( { isAnimating: false } );
			return;
		}

		// Get the current and target slide elements
		const currentSlide = slides[ currentSlideId ].element;
		const targetSlide = slides[ targetSlideId ].element;

		// Ensure proper DOM order - place target slide after or before current slide
		// based on navigation direction for better accessibility
		if ( direction === 'forward' ) {
			// If navigating forward, place target slide after current slide
			currentSlide.parentNode.insertBefore( targetSlide, currentSlide.nextSibling );
		} else {
			// If navigating backward, place target slide before current slide
			currentSlide.parentNode.insertBefore( targetSlide, currentSlide );
		}

		// For non-animated transitions, skip animation and directly update the view
		if ( !animate ) {
			// Make target slide visible
			targetSlide.classList.remove( 'is-hidden' );
			targetSlide.setAttribute( 'aria-hidden', 'false' );

			// Focus the first interactive element in the target slide
			await focusFirstInteractive( targetSlide );

			// Hide current slide
			currentSlide.classList.add( 'is-hidden' );
			currentSlide.setAttribute( 'aria-hidden', 'true' );

			// Update state
			this.setState( {
				currentSlideId: targetSlideId,
				targetSlideId: null,
				direction: null,
				isAnimating: false
			} );

			return;
		}

		// Determine animation direction (positive for forward, negative for backward)
		const moveDirection = direction === 'forward' ? 1 : -1;

		// Set up initial positions
		// Current slide at 0%, target slide positioned at +/-100%
		targetSlide.style.transform = `translateX(${ moveDirection * 100 }%)`;
		currentSlide.style.transform = 'translateX(0%)';

		// Make target slide visible but keep aria-hidden for now
		targetSlide.classList.remove( 'is-hidden' );

		// Force reflow to ensure the initial position is applied
		// eslint-disable-next-line no-void
		void targetSlide.offsetHeight;

		try {
			// Create and start tween animation
			await tween( {
				duration: 80,
				onUpdate: progress => {
					// Apply cubic-bezier easing to the progress
					const easedProgress = easings.cubicOut( progress );

					// Move current slide out (-100% or 100%)
					currentSlide.style.transform = `translateX(${ lerp( 0, -moveDirection * 100, easedProgress ) }%)`;

					// Move target slide in (from 100% or -100% to 0%)
					targetSlide.style.transform = `translateX(${ lerp( moveDirection * 100, 0, easedProgress ) }%)`;
				}
			} ).start();

			// Animation completed successfully, let's wait to ensure rendering is complete
			await timeout( 100 );

			// Update state
			this.setState( {
				currentSlideId: targetSlideId,
				targetSlideId: null,
				direction: null,
				isAnimating: false
			} );

			// Focus the first interactive element in the target slide
			targetSlide.setAttribute( 'aria-hidden', 'false' );

			await focusFirstInteractive( targetSlide );

			currentSlide.setAttribute( 'aria-hidden', 'true' );

			// Hide previous slide after animation
			currentSlide.classList.add( 'is-hidden' );

			// Reset transforms after animation
			currentSlide.style.transform = '';
			targetSlide.style.transform = '';
		} catch ( error ) {
			// Animation was interrupted or failed
			console.warn( 'Slide animation interrupted:', error );

			// Reset animation state
			this.setState( {
				targetSlideId: null,
				direction: null,
				isAnimating: false
			} );
		}
	}
}
