/**
 * Recursively traverse an active tree and run callback on each non-inert node element.
 *
 * @param {Node} node The starting node to recursively traverse.
 * @param {Function} callback Function to call on each node element.
 */
function traverseActiveTreeRecursively(node, callback) {
    if (!node) {
        return;
    }
    if (node.nodeType === Node.ELEMENT_NODE) {
        // inert is only supported by Chrome for now (behind a flag)
        if (node.hasAttribute('inert')) {
            return;
        }
        if (callback) {
            callback(node);
        }
        // If the element has a shadow root, traverse that
        if (node.shadowRoot) {
            traverseActiveTreeRecursively(node.shadowRoot, callback);
            return;
        }
        // if it's a slot element, get all assigned nodes and traverse them
        if (node.localName === 'slot') {
            const slottedNodes = node.assignedNodes({ flatten: true });
            for (let i = 0; i < slottedNodes.length; i++) {
                traverseActiveTreeRecursively(slottedNodes[i], callback);
            }
            return;
        }
    }
    let child = node.firstChild;
    while (child != null) {
        traverseActiveTreeRecursively(child, callback);
        child = child.nextSibling;
    }
}

/**
 *
 * Returns all tabbable elements within a containing element. Tabbable elements are:
 * a visible/non-disabled element that has a tabIndex of 0 and is not within a custom
 * element with tabindex attribute of “-1" on it.
 *
 * @param {Element} container The element to search for tabbable element.
 * @returns {Array} Tabbable elements.
 */
export function findAllTabbableElements(container) {
    const result = [];
    const isVisible = element => {
        const { width, height } = element.getBoundingClientRect();
        const nonZeroSize = width > 0 || height > 0;
        return nonZeroSize && getComputedStyle(element).visibility !== 'hidden';
    };
    const isParentCustomElementTabbable = element => {
        const parentRoot = container.getRootNode();
        const ownerDocument = element.ownerDocument;
        let root = element.getRootNode();
        while (root !== parentRoot && root !== ownerDocument) {
            const host = root.host;
            if (host.getAttribute('tabindex') === '-1') {
                return false;
            }
            root = host && host.getRootNode();
        }
        return true;
    };
    const isTabbable = element =>
        element.tabIndex === 0 &&
        !element.disabled &&
        isVisible(element) &&
        isParentCustomElementTabbable(element);

    traverseActiveTreeRecursively(container, element => {
        // Remove the try/catch once https://github.com/salesforce/lwc/issues/1421 is fixed
        try {
            if (isTabbable(element)) {
                result.push(element);
            }
        } catch (e) {
            // ignore, remove once the above LWC issue is fixed.
        }
    });
    return result;
}

/**
 * Finds the element that currently has focus, even when the element is part of a shadow root.
 *
 * @returns {Element} Element that has focus.
 */
export function getElementWithFocus() {
    let focusedElement = document.activeElement;
    let currentFocusedElement = focusedElement;

    while (focusedElement && focusedElement.shadowRoot) {
        focusedElement = currentFocusedElement.shadowRoot.activeElement;
        if (focusedElement) {
            currentFocusedElement = focusedElement;
        }
    }

    return currentFocusedElement;
}

/**
 *
 * Traps the focus by moving the focus to firstTabbableElement when Tab is pressed on
 * the lastTabbableElement, and moves the focus to the lastTabbableElement when Shift-Tab is pressed
 * on the firstTabbableElement.
 *
 * @param {Element} firstTabbableElement Element to focus to after Tab is hit on the last tabbable element.
 * @param {Element} lastTabbableElement Element to focus after Shift-Tab is hit on the first tabbable element.
 *
 * @returns {Function} A function that upon execution performs cleanup, ie. removes the installed keydown
 * event listeners.
 */
export function trapFocus({ firstTabbableElement, lastTabbableElement }) {
    if (!firstTabbableElement) {
        throw new Error(
            'Must have at least one tabbable element to trap focus'
        );
    }

    const callbackFirstTabbableElement = event => {
        if (event.key === 'Tab') {
            if (event.shiftKey) {
                event.preventDefault();

                if (lastTabbableElement) {
                    lastTabbableElement.focus();
                }
            } else if (!lastTabbableElement) {
                // If there's no last tabbable element it means there's no content besides the first tabbable element,
                // so keep the focus on it.
                event.preventDefault();
            }
        }
    };

    const callbackLastTabbableElement = event => {
        if (event.key === 'Tab' && !event.shiftKey) {
            event.preventDefault();

            firstTabbableElement.focus();
        }
    };

    const removeFirstTabbableElementListener = addKeyDownListener(
        firstTabbableElement,
        callbackFirstTabbableElement
    );
    const removeLastTabbableElementListener = addKeyDownListener(
        lastTabbableElement,
        callbackLastTabbableElement
    );

    return () => {
        removeFirstTabbableElementListener();
        removeLastTabbableElementListener();
    };
}

function addKeyDownListener(element, callback) {
    if (element) {
        element.addEventListener('keydown', callback);
    }
    return () => {
        if (element && document.body.contains(element)) {
            element.removeEventListener('keydown', callback);
        }
    };
}
