import { DomHandler, ObjectUtils } from 'primevue/utils';
import BaseFocusTrap from './BaseFocusTrap';

const FocusTrap = BaseFocusTrap.extend('focustrap', {
    mounted(el, binding) {
        const { disabled } = binding.value || {};

        if (!disabled) {
            this.createHiddenFocusableElements(el, binding);
            this.bind(el, binding);
            this.autoElementFocus(el, binding);
        }

        el.setAttribute('data-pd-focustrap', true);

        this.$el = el;
    },
    updated(el, binding) {
        const { disabled } = binding.value || {};

        disabled && this.unbind(el);
    },
    unmounted(el) {
        this.unbind(el);
    },
    methods: {
        getComputedSelector(selector) {
            return `:not(.p-hidden-focusable):not([data-p-hidden-focusable="true"])${selector ?? ''}`;
        },
        bind(el, binding) {
            const { onFocusIn, onFocusOut } = binding.value || {};

            el.$_pfocustrap_mutationobserver = new MutationObserver((mutationList) => {
                mutationList.forEach((mutation) => {
                    if (mutation.type === 'childList' && !el.contains(document.activeElement)) {
                        const findNextFocusableElement = (_el) => {
                            const focusableElement = DomHandler.isFocusableElement(_el)
                                ? DomHandler.isFocusableElement(_el, this.getComputedSelector(el.$_pfocustrap_focusableselector))
                                    ? _el
                                    : DomHandler.getFirstFocusableElement(el, this.getComputedSelector(el.$_pfocustrap_focusableselector))
                                : DomHandler.getFirstFocusableElement(_el);

                            return ObjectUtils.isNotEmpty(focusableElement) ? focusableElement : _el.nextSibling && findNextFocusableElement(_el.nextSibling);
                        };

                        DomHandler.focus(findNextFocusableElement(mutation.nextSibling));
                    }
                });
            });

            el.$_pfocustrap_mutationobserver.disconnect();
            el.$_pfocustrap_mutationobserver.observe(el, {
                childList: true
            });

            el.$_pfocustrap_focusinlistener = (event) => onFocusIn && onFocusIn(event);
            el.$_pfocustrap_focusoutlistener = (event) => onFocusOut && onFocusOut(event);

            el.addEventListener('focusin', el.$_pfocustrap_focusinlistener);
            el.addEventListener('focusout', el.$_pfocustrap_focusoutlistener);
        },
        unbind(el) {
            el.$_pfocustrap_mutationobserver && el.$_pfocustrap_mutationobserver.disconnect();
            el.$_pfocustrap_focusinlistener && el.removeEventListener('focusin', el.$_pfocustrap_focusinlistener) && (el.$_pfocustrap_focusinlistener = null);
            el.$_pfocustrap_focusoutlistener && el.removeEventListener('focusout', el.$_pfocustrap_focusoutlistener) && (el.$_pfocustrap_focusoutlistener = null);
        },
        autoFocus(options) {
            this.autoElementFocus(this.$el, { value: { ...options, autoFocus: true } });
        },
        autoElementFocus(el, binding) {
            const { autoFocusSelector = '', firstFocusableSelector = '', autoFocus = false } = binding.value || {};
            let focusableElement = DomHandler.getFirstFocusableElement(el, `[autofocus]${this.getComputedSelector(autoFocusSelector)}`);

            autoFocus && !focusableElement && (focusableElement = DomHandler.getFirstFocusableElement(el, this.getComputedSelector(firstFocusableSelector)));
            DomHandler.focus(focusableElement);
        },
        onFirstHiddenElementFocus(event) {
            const { currentTarget, relatedTarget } = event;
            const focusableElement =
                relatedTarget === currentTarget.$_pfocustrap_lasthiddenfocusableelement || !this.$el?.contains(relatedTarget)
                    ? DomHandler.getFirstFocusableElement(currentTarget.parentElement, this.getComputedSelector(currentTarget.$_pfocustrap_focusableselector))
                    : currentTarget.$_pfocustrap_lasthiddenfocusableelement;

            DomHandler.focus(focusableElement);
        },
        onLastHiddenElementFocus(event) {
            const { currentTarget, relatedTarget } = event;
            const focusableElement =
                relatedTarget === currentTarget.$_pfocustrap_firsthiddenfocusableelement || !this.$el?.contains(relatedTarget)
                    ? DomHandler.getLastFocusableElement(currentTarget.parentElement, this.getComputedSelector(currentTarget.$_pfocustrap_focusableselector))
                    : currentTarget.$_pfocustrap_firsthiddenfocusableelement;

            DomHandler.focus(focusableElement);
        },
        createHiddenFocusableElements(el, binding) {
            const { tabIndex = 0, firstFocusableSelector = '', lastFocusableSelector = '' } = binding.value || {};

            const createFocusableElement = (onFocus) => {
                return DomHandler.createElement('span', {
                    class: 'p-hidden-accessible p-hidden-focusable',
                    tabIndex,
                    role: 'presentation',
                    'aria-hidden': true,
                    'data-p-hidden-accessible': true,
                    'data-p-hidden-focusable': true,
                    onFocus: onFocus?.bind(this)
                });
            };

            const firstFocusableElement = createFocusableElement(this.onFirstHiddenElementFocus);
            const lastFocusableElement = createFocusableElement(this.onLastHiddenElementFocus);

            firstFocusableElement.$_pfocustrap_lasthiddenfocusableelement = lastFocusableElement;
            firstFocusableElement.$_pfocustrap_focusableselector = firstFocusableSelector;
            firstFocusableElement.setAttribute('data-pc-section', 'firstfocusableelement');

            lastFocusableElement.$_pfocustrap_firsthiddenfocusableelement = firstFocusableElement;
            lastFocusableElement.$_pfocustrap_focusableselector = lastFocusableSelector;
            lastFocusableElement.setAttribute('data-pc-section', 'lastfocusableelement');

            el.prepend(firstFocusableElement);
            el.append(lastFocusableElement);
        }
    }
});

export default FocusTrap;
