import { api, LightningElement, track } from 'lwc';
import labelClose from '@salesforce/label/LightningDialog.close';
import { classSet } from 'lightning/utils';
import {
    findAllTabbableElements,
    getElementWithFocus,
    trapFocus,
} from './focus';
import { makeEverythingExceptElementInert, restoreInertness } from './inert';

/**
 * A panel used to display content in a layer over the app.
 * */
export default class LightningDialog extends LightningElement {
    /**
     * Text to display in a heading at the top of the dialog.
     */
    @api header;

    @track _open = false;

    _queueShow = false;
    _savedInertElements = [];
    _removeListenerCallback;
    _connected = false;
    _savedActiveElement;

    connectedCallback() {
        this._connected = true;
    }

    disconnectedCallback() {
        this._connected = false;
        // The assumption is that the element is not removed from the DOM when opened,
        // if that assumption doesn't stand we'd need to restore inertness.
    }

    renderedCallback() {
        // check if it's the next render after the dialog was queued to show
        if (this._queueShow) {
            this._savedInertElements = makeEverythingExceptElementInert(
                this.template.host
            );
            this._focusAnElementAndTrapFocus();
            this._queueShow = false;
        }
    }
    /**
     * Opens the modal dialog.
     */
    @api
    showModal() {
        this._throwErrorIfNotConnected();

        this._saveActiveElement();

        if (!this._open) {
            this._open = true;
        }

        // Set the flag so that DOM operations can be executed in the renderedCallback method
        this._queueShow = true;
    }

    /**
     * Closes the modal dialog.
     */
    @api
    close() {
        if (!this._open) {
            return;
        }

        this._open = false;

        restoreInertness(this._savedInertElements);

        if (this._mutationObserver) {
            this._mutationObserver.disconnect();
        }

        this._returnFocus();

        const closeEvent = new CustomEvent('close');
        this.dispatchEvent(closeEvent);
    }

    get _labels() {
        return { close: labelClose };
    }

    /**
     * Gets the CSS classes applicable to the modal header element.
     *
     * @type {string}
     * @private
     */
    get _headerCssClasses() {
        const classes = classSet('slds-modal__header');
        classes.add({
            'slds-modal__header_empty': (this.header || '').length === 0,
        });

        return classes.toString();
    }

    _focusAnElementAndTrapFocus() {
        const dialogContentElement = this.template.querySelector(
            '[data-content-container]'
        );

        const allTabbableElements = findAllTabbableElements(
            dialogContentElement
        );

        const closeButton = this.template.querySelector('[data-close-button]');

        // Canceling the autofocus event prevents the dialog from focusing the first tabbable element
        const autoFocus = this.dispatchEvent(
            new CustomEvent('autofocus', { cancelable: true })
        );

        if (allTabbableElements.length > 0) {
            this._setupOrUpdateFocusTrapping(allTabbableElements);

            if (autoFocus) {
                const firstElementToFocus = allTabbableElements[0];
                firstElementToFocus.focus();
            }
        } else {
            closeButton.focus();
        }
        // Observe mutations of the dialog to ensure correct focus trapping when
        // the last element is either removed from the DOM and there's a new last element or
        // when the previously last element has changed its position (and is no longer the last element.
        this._mutationObserver = new MutationObserver(() => {
            const updatedTabbableElements = findAllTabbableElements(
                dialogContentElement
            );
            this._setupOrUpdateFocusTrapping(updatedTabbableElements);
        });
        this._mutationObserver.observe(this.template.host, {
            attributes: true,
            childList: true,
            subtree: true,
        });
    }

    _setupOrUpdateFocusTrapping(allTabbableElements) {
        const closeButton = this.template.querySelector('[data-close-button]');

        const newLastTabbableElement =
            allTabbableElements[allTabbableElements.length - 1];
        // If there's a new last tabbable element, remove the added keydown listener
        // from the previous element and add it to the new one.
        if (newLastTabbableElement !== this._lastTabbableElement) {
            if (this._removeListenerCallback) {
                this._removeListenerCallback();
            }
            this._removeListenerCallback = trapFocus({
                firstTabbableElement: closeButton,
                lastTabbableElement: newLastTabbableElement,
            });
            this._lastTabbableElement = newLastTabbableElement;
        }
    }

    _saveActiveElement() {
        this._savedActiveElement = getElementWithFocus();
    }

    _returnFocus() {
        const returnFocus = this.dispatchEvent(
            new CustomEvent('returnfocus', { cancelable: true })
        );

        // Canceling the focus return event prevents the dialog from returning the focus
        if (!returnFocus) {
            return;
        }

        if (
            this._savedActiveElement &&
            document.body.contains(this._savedActiveElement)
        ) {
            this._savedActiveElement.focus();
        } else {
            // eslint-disable-next-line no-console
            console.warn('Nothing to return focus to');
        }
    }

    _handleCloseClick() {
        this._dispatchCancelEventAndClose();
    }

    _dispatchCancelEventAndClose() {
        const close = this.dispatchEvent(
            new CustomEvent('cancel', { cancelable: true })
        );
        if (close) {
            this.close();
        }
    }

    _handleModalKeyDown(event) {
        const hasModifier = event.ctrlKey || event.metaKey || event.shiftKey;
        // 'Esc' is IE11 specific, remove when support is dropped
        if (!hasModifier && (event.key === 'Esc' || event.key === 'Escape')) {
            event.stopPropagation();
            event.preventDefault();

            this._dispatchCancelEventAndClose();
        }
    }

    _throwErrorIfNotConnected() {
        if (!this._connected) {
            throw new Error('dialog is not connected');
        }
    }
}
