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

import { BaseComponent } from './base-component';
import { createKeyboardFocusCycler, getHorizontalKeysMap } from '../helpers/create-keyboard-focus-cycler';

/**
 * Component for managing tabbed interfaces.
 */
export class Tabs extends BaseComponent {
	/**
	 * Creates a tabs component.
	 *
	 * @param element - The tabs container element.
	 */
	constructor( element ) {
		super( {
			activeTabId: null
		} );

		this.elements = {
			container: element,
			tabButtons: [ ...element.querySelectorAll( '.c-tabs__button' ) ],
			tabPanels: [ ...element.querySelectorAll( '.c-tabs__panel' ) ]
		};

		// Find initial active tab
		this.state.activeTabId = this.findInitialActiveTabId();
	}

	/**
	 * Initializes tabs components for all matching elements on the page.
	 */
	static attach() {
		const tabsElements = document.querySelectorAll( '.js-tabs' );

		for ( const element of tabsElements ) {
			element.tabs = new Tabs( element ).init();
		}
	}

	/**
	 * Returns the ID of the currently active tab.
	 */
	get activeTabId() {
		return this.state.activeTabId;
	}

	/**
	 * Returns the currently active tab button element.
	 */
	get activeTabPanel() {
		return this.findTabPanelByTabId( this.state.activeTabId );
	}

	/**
	 * Returns the tab buttons within the tabs component.
	 */
	get tabsPanels() {
		return this.elements.tabPanels;
	}

	/**
	 * Finds the ID of the initially active tab based on the DOM.
	 *
	 * @returns The ID of the active tab or null if none found.
	 */
	findInitialActiveTabId() {
		const { tabButtons } = this.elements;

		for ( const button of tabButtons ) {
			if ( button.getAttribute( 'aria-selected' ) === 'true' ) {
				return button.dataset.tabId;
			}
		}

		// If no active tab is found, return the first tab's ID
		if ( tabButtons.length > 0 ) {
			return tabButtons[ 0 ].dataset.tabId;
		}

		return null;
	}

	/**
	 * Finds the tab panel by its ID.
	 */
	findTabPanelByTabId( tabId ) {
		return findTabElementByTabId( this.elements.tabPanels, tabId );
	}

	/**
	 * Initializes the tabs component with event listeners.
	 */
	init() {
		const { container, tabButtons } = this.elements;

		if ( !container ) {
			return;
		}

		// Add click event listeners to tab buttons
		tabButtons.forEach( button => {
			button.addEventListener( 'click', () => {
				this.activateTab( button.dataset.tabId );
			} );
		} );

		// Setup keyboard navigation with arrow keys
		this.setupKeyboardNavigation();

		// Initial render to make sure state matches DOM
		this.render( null, this.state );

		return this;
	}

	/**
	 * Sets up keyboard navigation between tab buttons.
	 */
	setupKeyboardNavigation() {
		const { tabButtons } = this.elements;

		if ( !tabButtons.length ) {
			return;
		}

		// Create a focus cycler for the tab buttons
		this.focusCycler = createKeyboardFocusCycler( {
			items: tabButtons,
			keyMap: getHorizontalKeysMap(),
			focusElements: true,
			onFocusEnter: item => item.classList.add( 'is-highlighted' ),
			onFocusLeave: item => item.classList.remove( 'is-highlighted' )
		} );
	}

	/**
	 * Adds a listener for tab activation events.
	 */
	onTabActivation( callback ) {
		onTabActivation( this.elements.container, callback );
	}

	/**
	 * Activates a tab by its base ID.
	 *
	 * @param tabId - The base ID of the tab to activate.
	 */
	activateTab( tabId ) {
		if ( tabId !== this.state.activeTabId ) {
			this.setState( { activeTabId: tabId } );
		}
	}

	/**
	 * Renders the component based on current state.
	 * Updates the active tab and panel visibility.
	 *
	 * @param prevState - Previous component state
	 * @param state - Current component state
	 */
	render( prevState, state ) {
		if ( !state.activeTabId || ( prevState?.activeTabId === state.activeTabId ) ) {
			return;
		}

		const { tabButtons, tabPanels, container } = this.elements;

		// Update tab buttons
		tabButtons.forEach( button => {
			const isActive = button.dataset.tabId === state.activeTabId;

			button.setAttribute( 'aria-selected', isActive.toString() );
			button.tabIndex = isActive ? 0 : -1;
		} );

		// Update tab panels
		tabPanels.forEach( panel => {
			const isActive = panel.dataset.tabId === state.activeTabId;

			panel.setAttribute( 'aria-hidden', isActive ? 'false' : 'true' );
			panel.classList.toggle( 'is-active', isActive );
			panel.classList.toggle( 'is-hidden', !isActive );
		} );

		// Set current tab id in the container
		container.setAttribute( 'data-active-tab-id', state.activeTabId );

		// Find active tab button by data-tab-id to get its ID for aria-activedescendant
		const activePanel = findTabElementByTabId( tabPanels, state.activeTabId );

		if ( activePanel ) {
			container.setAttribute( 'aria-activedescendant', activePanel.id );
		}

		// Broadcast tab change event
		this.dispatchTabChangeEvent();
	}

	/**
	 * Dispatch a custom event to notify other components about the tab change.
	 */
	dispatchTabChangeEvent() {
		const tabChangeEvent = new CustomEvent( 'tabs:change', {
			bubbles: true,
			detail: {
				tabId: this.state.activeTabId
			}
		} );

		this.elements.container.dispatchEvent( tabChangeEvent );
	}
}

/**
 * Finds an element with the given tab ID from a collection of elements.
 *
 * @param elements - Collection of elements to search through
 * @param tabId - The tab ID to look for
 * @returns The matching element or null if not found
 */
function findTabElementByTabId( elements, tabId ) {
	return elements.find( element => element.dataset.tabId === tabId ) || null;
}

/**
 * Attaches a listener to the tab change event.
 *
 * @param element - The element within a tab component that should listen for changes
 * @param callback - Callback function to execute when tab changes, receives tabId
 * @returns A cleanup function to remove the event listener
 */
export function onTabActivation( element, callback ) {
	const handler = event => {
		callback( event.detail.tabId );
	};

	// Find the closest tabs container
	const tabsContainer = element.closest( '.js-tabs' );

	if ( !tabsContainer ) {
		console.warn( 'Element is not inside a tabs component' );
		return () => {};
	}

	tabsContainer.addEventListener( 'tabs:change', handler );

	return () => {
		tabsContainer.removeEventListener( 'tabs:change', handler );
	};
}

/**
 * Programmatically activates a specific tab in a tabs component.
 *
 * @param element - An element within the tab component or the tab container itself
 * @param tabId - The ID of the tab to activate
 * @returns Boolean indicating whether the activation was successful
 */
export function activateTab( element, tabId ) {
	// Find the closest tabs container
	const tabsContainer = element.closest( '.js-tabs' ) ||
		( element.classList.contains( 'js-tabs' ) ? element : null );

	if ( !tabsContainer ) {
		console.warn( 'Element is not inside a tabs component' );
		return false;
	}

	// Find the tab button with the matching tabId
	const tabButton = tabsContainer.querySelector( `.c-tabs__button[data-tab-id="${ tabId }"]` );

	if ( !tabButton ) {
		console.warn( `Tab with ID "${ tabId }" not found` );
		return false;
	}

	// Trigger a click on the tab button to activate it
	tabButton.click();
	return true;
}
