import * as Arrays from "@paperbits/common/arrays";
import { Keys, MouseButtons } from "@paperbits/common";
import { Events } from "@paperbits/common/events";
import { AriaAttributes, DataAttributes, closest, getFocusableElements } from "@paperbits/common/html";
import { ToggleableType, ToggleableTriggerEvent, ToggleableHandle } from "./";


const showClassName = "show";
const popupContainerClass = "popup-container";

export class ToggleablesManager {
    private stack: ToggleableHandle[] = [];

    constructor() {
        this.onClick = this.onClick.bind(this);
        this.onKeyDown = this.onKeyDown.bind(this);
        this.onMouseEnter = this.onMouseEnter.bind(this);
        this.onPopupRequest = this.onPopupRequest.bind(this);
    }

    private openTogglable(toggleElement: HTMLElement, toggleType: ToggleableType, triggerEvent: ToggleableTriggerEvent): void {
        let toggleableHandle: ToggleableHandle;

        switch (toggleType) {
            case ToggleableType.popup:
                const targetSelector = toggleElement.getAttribute(DataAttributes.Target);

                if (!targetSelector) {
                    return;
                }

                const targetElement = <HTMLElement>document.querySelector(targetSelector);

                if (!targetElement) {
                    return;
                }

                toggleableHandle = this.configurePopup(toggleElement, targetElement, triggerEvent);
                break;

            case ToggleableType.dropdown: {
                const targetElement = <HTMLElement>toggleElement.parentElement.querySelector(".dropdown");
                toggleableHandle = this.configureToggleable(toggleElement, targetElement);
                break;
            }

            case ToggleableType.collapsible: {
                const targetElement = <HTMLElement>toggleElement.closest(".collapsible-panel");
                toggleableHandle = this.configureToggleable(toggleElement, targetElement);
                break;
            }

            default:
                console.warn(`Unknown data-toggle value ${toggleType}`);
        }

        this.stack.push(toggleableHandle);
    };

    private configureToggleable(toggleElement: HTMLElement, targetElement: HTMLElement): ToggleableHandle {
        if (!toggleElement || !targetElement) {
            return;
        }

        const dismissElements: HTMLElement[] = Arrays.coerce(targetElement.querySelectorAll(`[${DataAttributes.Dismiss}]`));

        const onDisimissElementMouseDown = (event: MouseEvent) => {
            closeTarget();
        }

        const openTarget = (): void => {
            this.openTogglableInternal(targetElement, toggleElement);

            setImmediate(() => addEventListener(Events.MouseDown, clickOutside));

            for (const dismissElement of dismissElements) {
                dismissElement.addEventListener(Events.Click, onDisimissElementMouseDown);
            }
        };

        const closeTarget = (): void => {
            this.closeTogglable(targetElement, toggleElement);
            removeEventListener(Events.MouseDown, clickOutside);

            for (const dismissElement of dismissElements) {
                dismissElement.removeEventListener(Events.Click, onDisimissElementMouseDown);
            }
        };

        const clickOutside = (event: MouseEvent) => {
            const clickTarget = <HTMLElement>event.target;

            if (clickTarget.nodeName === "BODY") {
                return;
            }

            const isClickInsideTarget = targetElement.contains(clickTarget);

            if (isClickInsideTarget) {
                return;
            }

            closeTarget();
        };

        if (!targetElement.classList.contains(showClassName)) {
            openTarget();
        }

        const togglableHandle: ToggleableHandle = {
            targetElement: targetElement,
            toggleElement: toggleElement,
            close: closeTarget
        };

        return togglableHandle;
    };

    /**
     * Closes togglable element.
     * @param targetElement - The element that needs to be closed.
     * @param toggleElement - The element that triggered opening the target.
     */
    private closeTogglable(targetElement: HTMLElement, toggleElement: HTMLElement, returnFocus: boolean = true): void {
        targetElement.classList.remove(showClassName);

        if (toggleElement) {
            this.stack.pop();
            toggleElement.setAttribute(AriaAttributes.expanded, "false");


            const triggerEvent = toggleElement.getAttribute(DataAttributes.TriggerEvent);

            if (triggerEvent === ToggleableTriggerEvent.Hover) {
                return; // on "hover" focus doesn't change
            }

            if (returnFocus) {
                setImmediate(() => toggleElement.focus());
            }
        }
    }

    private openTogglableInternal(targetElement: HTMLElement, toggleElement: HTMLElement): void {
        targetElement.classList.add(showClassName);

        if (toggleElement) {
            toggleElement.setAttribute(AriaAttributes.expanded, "true");

            const triggerEvent = toggleElement.getAttribute(DataAttributes.TriggerEvent);

            if (triggerEvent === ToggleableTriggerEvent.Hover) {
                return; // on "hover" focus doesn't change
            }
        }

        const focusableElements = getFocusableElements(targetElement);

        if (focusableElements.length > 0) {
            const firstFocusableElement = <HTMLElement>focusableElements[0];
            firstFocusableElement.focus();
        }
    }

    private configurePopup(triggerElement: HTMLElement, targetElement: HTMLElement, triggerEvent: ToggleableTriggerEvent): ToggleableHandle {
        if (!targetElement) {
            return;
        }

        const isTargetOpen = (): boolean => {
            return targetElement.classList.contains(showClassName);
        };

        if (isTargetOpen()) {
            return;
        }

        const popupContainerElement: HTMLElement = targetElement.querySelector(`.${popupContainerClass}`);

        const repositionPopup = (event?: CustomEvent): void => {
            const computedStyles = getComputedStyle(popupContainerElement);

            if (computedStyles.position === "absolute") {
                const actualToggleElement: HTMLElement = event?.detail?.element || triggerElement;
                const toggleElementRect = actualToggleElement.getBoundingClientRect();
                const popupContainerElement: HTMLElement = targetElement.querySelector(`.${popupContainerClass}`);
                const popupContainerElementRect = popupContainerElement.getBoundingClientRect();
                const requestedPosition = actualToggleElement.getAttribute("data-position") || "bottom";

                const triggerHalfWidth = Math.floor(toggleElementRect.width / 2);
                const triggerHalfHeight = Math.floor(toggleElementRect.height / 2);
                const popupHalfWidth = Math.floor(popupContainerElementRect.width / 2);
                const popupHalfHeight = Math.floor(popupContainerElementRect.height / 2);

                const position = requestedPosition.split(" ");

                // Default assignments
                popupContainerElement.style.left = toggleElementRect.left + triggerHalfWidth - popupHalfWidth + "px";
                popupContainerElement.style.top = window.scrollY + toggleElementRect.top + triggerHalfHeight - popupHalfHeight + "px";

                if (position.includes("top")) {
                    popupContainerElement.style.top = window.scrollY + toggleElementRect.top - popupContainerElementRect.height - triggerHalfHeight + "px";
                }

                if (position.includes("bottom")) {
                    popupContainerElement.style.top = window.scrollY + toggleElementRect.bottom + "px";
                }

                if (position.includes("left")) {
                    popupContainerElement.style.left = toggleElementRect.left + "px";
                }

                if (position.includes("right")) {
                    popupContainerElement.style.left = toggleElementRect.right - popupContainerElementRect.width + "px";
                }

                return;
            }

            popupContainerElement.removeAttribute("style");
        };

        const dismissElements: HTMLElement[] = Arrays.coerce(targetElement.querySelectorAll(`[${DataAttributes.Dismiss}]`));

        const checkOutsideClick = (event: MouseEvent): void => {
            const eventTarget = <HTMLElement>event.target;

            if (eventTarget.nodeName === "BODY") {
                return;
            }

            const isTargetClicked = popupContainerElement.contains(eventTarget);

            if (isTargetClicked) {
                return;
            }

            const isToggleClicked = triggerElement?.contains(eventTarget);

            if (isToggleClicked) {
                return;
            }

            event.preventDefault();
            event.stopImmediatePropagation();

            closeTarget(false); // outside click doesn't return the focus
        };

        const checkOutsideMove = (event: MouseEvent) => {
            const eventTarget = <HTMLElement>event.target;

            if (eventTarget.nodeName === "BODY") {
                return;
            }

            const isTargetClicked = popupContainerElement.contains(eventTarget);

            if (isTargetClicked) {
                return;
            }

            const isToggleClicked = triggerElement.contains(eventTarget);

            if (isToggleClicked) {
                return;
            }

            event.preventDefault();
            event.stopImmediatePropagation();

            closeTarget();
        };

        const onDisimissElementMouseDown = (event: MouseEvent) => {
            closeTarget();
        }

        const closeTarget = (returnFocus: boolean = true): void => {
            for (const dismissElement of dismissElements) {
                dismissElement.removeEventListener(Events.MouseDown, onDisimissElementMouseDown);
            }

            switch (triggerEvent) {
                case ToggleableTriggerEvent.Click:
                    targetElement.ownerDocument.removeEventListener(Events.MouseDown, checkOutsideClick);
                    break;

                case ToggleableTriggerEvent.Hover:
                    targetElement.ownerDocument.removeEventListener(Events.MouseMove, checkOutsideMove);
                    break;
            }

            this.closeTogglable(targetElement, triggerElement, returnFocus)

            // Temporary hack to reposition popup:
            document.removeEventListener(Events.PopupRepositionRequest, repositionPopup);
        };

        const openTarget = (): void => {
            for (const dismissElement of dismissElements) {
                dismissElement.addEventListener(Events.MouseDown, onDisimissElementMouseDown);
            }

            this.openTogglableInternal(targetElement, triggerElement);

            setImmediate(() => {
                switch (triggerEvent) {
                    case ToggleableTriggerEvent.Click:
                        targetElement.ownerDocument.addEventListener(Events.MouseDown, checkOutsideClick);
                        break;

                    case ToggleableTriggerEvent.Hover:
                        targetElement.ownerDocument.addEventListener(Events.MouseMove, checkOutsideMove);
                        break;
                }

                repositionPopup();
            });

            // Temporary hack to reposition popup:
            document.addEventListener(Events.PopupRepositionRequest, repositionPopup);
        };

        if (!targetElement.classList.contains(showClassName)) {
            openTarget();
        }

        const togglableHandle: ToggleableHandle = {
            targetElement: targetElement,
            toggleElement: triggerElement,
            close: closeTarget
        };

        return togglableHandle;
    };

    public onPopupRequest(event: CustomEvent): void {
        const popupKey = event.detail;
        const targetSelector = `#${popupKey.replace("popups/", "popups")}`;
        const targetElement = <HTMLElement>document.querySelector(targetSelector);
        const triggerSelector = `[data-target="${targetSelector}"]`;
        const triggerElement = <HTMLElement>document.querySelector(triggerSelector);

        if (targetElement?.classList.contains(showClassName)) {
            return;
        }

        const openTargetElement = document.querySelector(`.${showClassName}`);

        if (openTargetElement) {
            openTargetElement.classList.remove(showClassName);
        }

        this.configurePopup(triggerElement, targetElement, ToggleableTriggerEvent.Click);
    };

    public onMouseEnter(event: MouseEvent): void {
        const eventTarget = <HTMLElement>event.target;
        const toggleElement = <HTMLElement>eventTarget.closest(`[${DataAttributes.Toggle}]`);

        if (!toggleElement) {
            return;
        }

        const triggerEvent = toggleElement.getAttribute(DataAttributes.TriggerEvent);

        if (triggerEvent !== ToggleableTriggerEvent.Hover) {
            return;
        }

        event.preventDefault();

        const toggleType = <ToggleableType>toggleElement.getAttribute(DataAttributes.Toggle);
        this.openTogglable(toggleElement, toggleType, ToggleableTriggerEvent.Hover);
    };

    public onKeyDown(event: KeyboardEvent): void {
        if (event.key === Keys.Escape) {
            const closestOpenTogglable = <HTMLElement>document.activeElement.closest(`.${showClassName}`);

            if (closestOpenTogglable) {
                /**
                 * TODO: 
                 * - Take into account native collapsibles like <select> element, before closing this one.
                 * - Keep track of which element was a toggle element to update "expand" state and set focus.
                 */
                const stackItem = this.stack.find(x => x.targetElement === closestOpenTogglable)

                this.closeTogglable(closestOpenTogglable, stackItem?.toggleElement);
            }

            return;
        }

        if (event.key !== Keys.Enter && event.key !== Keys.Space) {
            return;
        }
    };

    public onClick(event: MouseEvent): void {
        if (event.button !== MouseButtons.Main) {
            return;
        }

        const eventTarget = <HTMLElement>event.target;

        const dismissElement = <HTMLElement>eventTarget.closest(`[${DataAttributes.Dismiss}]`);

        if (dismissElement) {
            const closestOpenTogglable = <HTMLElement>document.activeElement.closest(`.${showClassName}`);

            if (closestOpenTogglable) {
                this.closeTogglable(closestOpenTogglable, null, false);
            }
            
            return;
        }

        const toggleElement = <HTMLElement>eventTarget.closest(`[${DataAttributes.Toggle}]`);

        if (!toggleElement) {
            return;
        }

        event.preventDefault();

        const toggleType = <ToggleableType>toggleElement.getAttribute(DataAttributes.Toggle);
        this.openTogglable(toggleElement, toggleType, ToggleableTriggerEvent.Click);
    };
}


const mananger = new ToggleablesManager();
addEventListener(Events.Click, mananger.onClick, true);
addEventListener(Events.KeyDown, mananger.onKeyDown, true);
document.documentElement.addEventListener(Events.MouseEnter, mananger.onMouseEnter, true);
document.addEventListener(Events.PopupRequest, mananger.onPopupRequest);