import {
	IAccessibilityComponent,
	IAccessibilityKeyboardHandlers,
} from './interfaces';
import { isFormElement } from '../../utils';

class HSAccessibilityObserver {
	private components: IAccessibilityComponent[] = [];
	private currentlyOpenedComponents: IAccessibilityComponent[] = [];
	private activeComponent: IAccessibilityComponent | null = null;

	private readonly allowedKeybindings = new Set([
		'Escape',
		'Enter',
		' ',
		'Space',
		'ArrowDown',
		'ArrowUp',
		'ArrowLeft',
		'ArrowRight',
		'Tab',
		'Home',
		'End',
	]);

	constructor() {
		this.initGlobalListeners();
	}

	private initGlobalListeners(): void {
		document.addEventListener('keydown', (evt) =>
			this.handleGlobalKeydown(evt),
		);

		document.addEventListener('focusin', (evt) =>
			this.handleGlobalFocusin(evt),
		);
	}

	private isAllowedKeybinding(evt: KeyboardEvent): boolean {
		if (this.allowedKeybindings.has(evt.key)) {
			return true;
		}

		if (
			evt.key.length === 1 &&
			/^[a-zA-Z]$/.test(evt.key) &&
			!evt.metaKey &&
			!evt.ctrlKey &&
			!evt.altKey &&
			!evt.shiftKey
		) {
			return true;
		}

		return false;
	}

	private getActiveComponent(el: HTMLElement) {
		if (!el) return null;

		const containingComponents = this.components.filter(
			(comp) =>
				comp.wrapper.contains(el) ||
				(comp.context && comp.context.contains(el)),
		);

		if (containingComponents.length === 0) return null;
		if (containingComponents.length === 1) return containingComponents[0];

		let closestComponent = null;
		let minDistance = Number.MAX_SAFE_INTEGER;

		for (const comp of containingComponents) {
			let distance = 0;
			let current = el;

			while (current && current !== comp.wrapper && current !== comp.context) {
				distance++;
				current = current.parentElement;
			}

			if (distance < minDistance) {
				minDistance = distance;
				closestComponent = comp;
			}
		}

		return closestComponent;
	}

	private getActiveComponentForKey(
		el: HTMLElement,
		key: string,
	): IAccessibilityComponent | null {
		if (!el) return null;

		const containingComponents = this.components.filter(
			(component) =>
				component.wrapper.contains(el) ||
				(component.context && component.context.contains(el)),
		);

		if (containingComponents.length === 0) return null;

		const hasHandlerForKey = (component: IAccessibilityComponent): boolean => {
			const handlers = component.handlers;

			switch (key) {
				case 'Escape':
					return !!handlers.onEsc;
				case 'Enter':
					return !!handlers.onEnter;
				case ' ':
				case 'Space':
					return !!handlers.onSpace;
				case 'ArrowDown':
				case 'ArrowUp':
				case 'ArrowLeft':
				case 'ArrowRight':
					return !!handlers.onArrow;
				case 'Tab':
					return !!handlers.onTab || !!handlers.onShiftTab;
				case 'Home':
					return !!handlers.onHome;
				case 'End':
					return !!handlers.onEnd;
				default:
					return !!handlers.onFirstLetter;
			}
		};

		const candidates = containingComponents.filter(hasHandlerForKey);

		if (candidates.length === 0) return this.getActiveComponent(el);
		if (candidates.length === 1) return candidates[0];

		let closestComponent: IAccessibilityComponent | null = null;
		let minDistance = Number.MAX_SAFE_INTEGER;

		for (const candidate of candidates) {
			let distance = 0;
			let current: HTMLElement | null = el;

			while (
				current &&
				current !== candidate.wrapper &&
				current !== candidate.context
			) {
				distance++;

				current = current.parentElement;
			}

			if (distance < minDistance) {
				minDistance = distance;
				closestComponent = candidate;
			}
		}

		return closestComponent;
	}

	private getDistanceToComponent(
		el: HTMLElement,
		component: IAccessibilityComponent,
	): number {
		let distance = 0;
		let current: HTMLElement | null = el;

		while (
			current &&
			current !== component.wrapper &&
			current !== component.context
		) {
			distance++;
			current = current.parentElement;
		}

		return distance;
	}

	private getComponentsByNesting(el: HTMLElement): IAccessibilityComponent[] {
		if (!el) return [];

		const containingComponents = this.components.filter(
			(component) =>
				component.wrapper.contains(el) ||
				(component.context && component.context.contains(el)),
		);

		if (containingComponents.length <= 1) return containingComponents;

		return [...containingComponents].sort(
			(a, b) =>
				this.getDistanceToComponent(el, b) - this.getDistanceToComponent(el, a),
		);
	}

	private getSequentialHandlersForKey(
		el: HTMLElement,
		key: 'Enter' | 'Space',
	): Array<(evt?: KeyboardEvent) => boolean | void> {
		const components = this.getComponentsByNesting(el);

		if (components.length === 0) return [];

		return components
			.map((component) => {
				if (key === 'Enter') return component.handlers.onEnter;

				return component.handlers.onSpace;
			})
			.filter(
				(handler): handler is (evt?: KeyboardEvent) => boolean | void =>
					typeof handler === 'function',
			);
	}

	private executeSequentialHandlers(
		handlers: Array<(evt?: KeyboardEvent) => boolean | void>,
		evt?: KeyboardEvent,
	): {
		called: boolean;
		stopped: boolean;
	} {
		let called = false;
		let stopped = false;

		for (const handler of handlers) {
			called = true;

			const result = handler(evt);

			if (result === false) {
				stopped = true;
				break;
			}
		}

		return { called, stopped };
	}

	private handleGlobalFocusin(evt: FocusEvent): void {
		const target = evt.target as HTMLElement;
		this.activeComponent = this.getActiveComponent(target);
	}

	private handleGlobalKeydown(evt: KeyboardEvent): void {
		const target = evt.target as HTMLElement;
		this.activeComponent = this.getActiveComponentForKey(target, evt.key);
		const activeComponent = this.activeComponent;
		const isActivationKey =
			evt.key === 'Enter' || evt.key === ' ' || evt.key === 'Space';

		if (!activeComponent && !isActivationKey) return;

		if (!this.isAllowedKeybinding(evt)) {
			return;
		}

		switch (evt.key) {
			case 'Escape':
				if (!activeComponent) break;

				if (!activeComponent.isOpened) {
					const closestOpenParent = this.findClosestOpenParent(target);

					if (closestOpenParent?.handlers.onEsc) {
						closestOpenParent.handlers.onEsc();
						evt.preventDefault();
						evt.stopPropagation();
					}
				} else if (activeComponent.handlers.onEsc) {
					const escResult = activeComponent.handlers.onEsc();
					evt.preventDefault();
					evt.stopPropagation();

					if (escResult === false) {
						const closestOpenParent = this.findClosestOpenParent(target);

						if (closestOpenParent?.handlers.onEsc) {
							closestOpenParent.handlers.onEsc();
						}
					}
				}
				break;
			case 'Enter': {
				const enterHandlers = this.getSequentialHandlersForKey(target, 'Enter');

				if (enterHandlers.length === 0) break;

				const { called, stopped } = this.executeSequentialHandlers(
					enterHandlers,
					evt,
				);

				if (called && !isFormElement(target)) {
					evt.stopPropagation();
					evt.preventDefault();
				}

				if (stopped) {
					break;
				}
				break;
			}
			case ' ':
			case 'Space': {
				if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
					return;
				}

				const closestComponent = this.getActiveComponent(target);
				const spaceHandlers = this.getSequentialHandlersForKey(target, 'Space');

				if (spaceHandlers.length === 0) break;

				const { stopped } = this.executeSequentialHandlers(spaceHandlers);

				if (stopped || closestComponent?.handlers.onSpace) {
					evt.preventDefault();
					evt.stopPropagation();
				}
				break;
			}
			case 'ArrowDown':
			case 'ArrowUp':
			case 'ArrowLeft':
			case 'ArrowRight':
				if (!activeComponent) break;

				if (activeComponent.handlers.onArrow) {
					if (evt.metaKey || evt.ctrlKey || evt.altKey || evt.shiftKey) {
						return;
					}

					activeComponent.handlers.onArrow(evt);
					evt.preventDefault();
					evt.stopPropagation();
				}
				break;
			case 'Tab':
				if (!activeComponent) break;
				if (!activeComponent.handlers.onTab) break;

				const handler = evt.shiftKey
					? activeComponent.handlers.onShiftTab
					: activeComponent.handlers.onTab;

				if (handler) handler(evt);

				break;
			case 'Home':
				if (!activeComponent) break;
				if (activeComponent.handlers.onHome) {
					activeComponent.handlers.onHome();
					evt.preventDefault();
					evt.stopPropagation();
				}
				break;
			case 'End':
				if (!activeComponent) break;
				if (activeComponent.handlers.onEnd) {
					activeComponent.handlers.onEnd();
					evt.preventDefault();
					evt.stopPropagation();
				}
				break;
			default:
				if (!activeComponent) break;
				if (
					activeComponent.handlers.onFirstLetter &&
					evt.key.length === 1 &&
					/^[a-zA-Z]$/.test(evt.key)
				) {
					activeComponent.handlers.onFirstLetter(evt.key);

					if (!activeComponent.stopPropagation?.onFirstLetter) {
						return;
					} else {
						evt.preventDefault();
						evt.stopPropagation();
					}
				}
				break;
		}
	}

	private findClosestOpenParent(
		target: HTMLElement,
	): IAccessibilityComponent | null {
		let current = target.parentElement;

		while (current) {
			const parentComponent = this.currentlyOpenedComponents.find(
				(comp) => comp.wrapper === current && comp !== this.activeComponent,
			);

			if (parentComponent) {
				return parentComponent;
			}

			current = current.parentElement;
		}

		return null;
	}

	public registerComponent(
		wrapper: HTMLElement,
		handlers: IAccessibilityKeyboardHandlers,
		isOpened: boolean = true,
		name: string = '',
		selector: string = '',
		context?: HTMLElement,
		stopPropagation?: {
			[key: string]: boolean;
		},
	): IAccessibilityComponent {
		const component: IAccessibilityComponent = {
			wrapper,
			handlers,
			isOpened,
			name,
			selector,
			context,
			isRegistered: true,
			stopPropagation,
		};

		this.components.push(component);
		return component;
	}

	public updateComponentState(
		component: IAccessibilityComponent,
		isOpened: boolean,
	): void {
		component.isOpened = isOpened;

		if (isOpened) {
			if (!this.currentlyOpenedComponents.includes(component)) {
				this.currentlyOpenedComponents.push(component);
			}
		} else {
			this.currentlyOpenedComponents = this.currentlyOpenedComponents.filter(
				(comp) => comp !== component,
			);
		}
	}

	public unregisterComponent(component: IAccessibilityComponent): void {
		this.components = this.components.filter((comp) => comp !== component);
		this.currentlyOpenedComponents = this.currentlyOpenedComponents.filter(
			(comp) => comp !== component,
		);
	}

	public addAllowedKeybinding(key: string): void {
		this.allowedKeybindings.add(key);
	}

	public removeAllowedKeybinding(key: string): void {
		this.allowedKeybindings.delete(key);
	}

	public getAllowedKeybindings(): string[] {
		return Array.from(this.allowedKeybindings);
	}
}

export default HSAccessibilityObserver;
