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

import { clamp } from './clamp';
import { isFocusable } from './is-focusable';
import { wrapAround } from './wrap-around';

/**
 * Creates a keyboard-based focus cycler for a list of elements.
 *
 * @param config - Configuration object
 * @param config.items - Elements to cycle through
 * @param config.keyMap - Key mappings for navigation
 * @param [config.onFocusEnter] - Callback when focus enters an element (item, index) => {}
 * @param [config.onFocusLeave] - Callback when focus leaves an element (item, index) => {}
 * @param [config.focusElements=true] - Whether to automatically focus DOM elements
 * @param [config.blurCheckDelay=15] - Delay in ms to check focus after blur events
 * @returns Control object with activate, deactivate methods and unmount function
 */
export function createKeyboardFocusCycler( config ) {
	return new KeyboardFocusCycler( config );
}

/**
 * Class that handles keyboard-based focus cycling for a list of elements.
 */
class KeyboardFocusCycler {
	/**
	 * Creates a new instance of KeyboardFocusCycler.
	 */
	constructor( {
		items,
		keyMap = getVerticalKeysMap(),
		onFocusEnter = () => {},
		onFocusLeave = () => {},
		focusElements = true,
		blurCheckDelay = 15
	} ) {
		this.items = items;
		this.keyMap = keyMap;
		this.onFocusEnter = onFocusEnter;
		this.onFocusLeave = onFocusLeave;
		this.focusElements = focusElements;
		this.blurCheckDelay = blurCheckDelay;

		this.currentIndex = -1;
		this.isActive = false;
		this.blurTimeout = null;

		// Bind methods to preserve 'this' context
		this._handleKeyDown = this._handleKeyDown.bind( this );
		this._handleItemFocus = this._handleItemFocus.bind( this );
		this._handleItemBlur = this._handleItemBlur.bind( this );

		// Attach event listeners
		window.addEventListener( 'keydown', this._handleKeyDown );
		this.items.forEach( item => {
			if ( isFocusable( item ) ) {
				item.addEventListener( 'focus', this._handleItemFocus );
				item.addEventListener( 'blur', this._handleItemBlur );
			}
		} );
	}

	/**
	 * Activate focus cycling starting at specified index
	 *
	 * @param [startIndex=0] - Index to start focus at
	 */
	activate( startIndex = 0 ) {
		const clampedIndex = clamp( 0, this.items.length - 1, startIndex );

		this.isActive = true;
		this._changeFocus( clampedIndex );
	}

	/**
	 * Activate focus cycling starting at the first item
	 */
	activateFirst() {
		this.activate( 0 );
	}

	/**
	 * Activate focus cycling starting at the last item
	 */
	activateLast() {
		this.activate( this.items.length - 1 );
	}

	/**
	 * Deactivate focus cycling
	 */
	deactivate() {
		if ( this.currentIndex !== -1 ) {
			this.onFocusLeave( this.items[ this.currentIndex ], this.currentIndex );
		}

		this.isActive = false;
		this.currentIndex = -1;
	}

	/**
	 * Remove all event listeners and clean up
	 */
	unmount() {
		window.removeEventListener( 'keydown', this._handleKeyDown );

		// Remove focus and blur listeners
		this.items.forEach( item => {
			if ( isFocusable( item ) ) {
				item.removeEventListener( 'focus', this._handleItemFocus );
				item.removeEventListener( 'blur', this._handleItemBlur );
			}
		} );

		clearTimeout( this.blurTimeout );
		this.deactivate();
	}

	/**
	 * Change focus from one element to another
	 * @param newIndex - Index of the element to focus
	 * @private
	 */
	_changeFocus( newIndex ) {
		const previousIndex = this.currentIndex;

		// Call callbacks when focus changes
		if ( previousIndex !== -1 ) {
			this.onFocusLeave( this.items[ previousIndex ], previousIndex );
		}

		this.currentIndex = newIndex;
		this.onFocusEnter( this.items[ this.currentIndex ], this.currentIndex );

		// Focus the element if it's focusable and option is enabled
		if ( this.focusElements && isFocusable( this.items[ this.currentIndex ] ) ) {
			this.items[ this.currentIndex ].focus();
		}
	}

	/**
	 * Check if any item has focus
	 * @returns True if any item has focus
	 * @private
	 */
	_hasAnyItemFocus() {
		return this.items.some( item => document.activeElement === item );
	}

	/**
	 * Handle keyboard navigation
	 * @param event - Keyboard event
	 * @private
	 */
	_handleKeyDown( event ) {
		if ( !this.isActive ) {
			return;
		}

		// Destructure key mappings.
		const { next, previous, first, last } = this.keyMap;
		const pressedKey = event.key;

		// Check if the pressed key is in our mapping.
		if (
			!next?.includes( pressedKey ) &&
			!previous?.includes( pressedKey ) &&
			!first?.includes( pressedKey ) &&
			!last?.includes( pressedKey )
		) {
			return;
		}

		event.preventDefault();

		let newIndex = this.currentIndex;

		// Determine the new index based on key pressed.
		if ( next?.includes( pressedKey ) ) {
			newIndex = wrapAround( 0, this.items.length - 1, this.currentIndex + 1 );
		} else if ( previous?.includes( pressedKey ) ) {
			newIndex = wrapAround( 0, this.items.length - 1, this.currentIndex - 1 );
		} else if ( first?.includes( pressedKey ) ) {
			newIndex = 0;
		} else if ( last?.includes( pressedKey ) ) {
			newIndex = this.items.length - 1;
		}

		this._changeFocus( newIndex );
	}

	/**
	 * Handle item focus
	 * @param event - Focus event
	 * @private
	 */
	_handleItemFocus( event ) {
		clearTimeout( this.blurTimeout );

		this.isActive = true;

		// Find the index of the focused item
		const index = this.items.indexOf( event.target );
		if ( index !== -1 && index !== this.currentIndex ) {
			this._changeFocus( index );
		}
	}

	/**
	 * Handle item blur
	 * @private
	 */
	_handleItemBlur() {
		// Clear any existing timeout
		clearTimeout( this.blurTimeout );

		// Set a new timeout to check focus after a small delay
		this.blurTimeout = setTimeout( () => {
			if ( !this._hasAnyItemFocus() ) {
				this.isActive = false;

				if ( this.currentIndex !== -1 ) {
					this.onFocusLeave( this.items[ this.currentIndex ], this.currentIndex );
					this.currentIndex = -1;
				}
			}
		}, this.blurCheckDelay );
	}
}

export function getVerticalKeysMap() {
	return {
		next: [ 'ArrowDown', 'Down' ],
		previous: [ 'ArrowUp', 'Up' ],
		first: [ 'Home' ],
		last: [ 'End' ]
	};
}

export function getHorizontalKeysMap() {
	return {
		next: [ 'ArrowRight', 'Right' ],
		previous: [ 'ArrowLeft', 'Left' ],
		first: [ 'Home' ],
		last: [ 'End' ]
	};
}
