﻿import * as ko from "knockout";
import * as Html from "@paperbits/common/html";
import * as Utils from "@paperbits/common/utils";
import { Keys } from "@paperbits/common/keyboard";
import { View } from "@paperbits/common/ui";
import { BalloonOptions, BalloonActivationMethod, BalloonHandle, BalloonState } from "@paperbits/common/ui/balloons";
import { ViewStack } from "@paperbits/common/ui/viewStack";
import { Events } from "@paperbits/common/events";

const balloonHandlesProp = "balloonHandles";
const activeBalloonProp = "activeBalloon";
const balloonTopClass = "balloon-top";
const balloonBottomClass = "balloon-bottom";
const balloonLeftClass = "balloon-left";
const balloonRightClass = "balloon-right";

const balloonClass = "balloon";
const activeBalloonClass = "balloon-is-active";
const balloonTipClass = "balloon-tip";

enum Position {
    left = "left",
    right = "right",
    top = "top",
    bottom = "bottom"
}

enum Direction {
    vertical = "vertical",
    horizontal = "horizontal",
}

export class BalloonBindingHandler {
    constructor(viewStack: ViewStack) {
        ko.bindingHandlers["balloon"] = {
            init: (toggleElement: HTMLElement, valueAccessor: () => BalloonOptions) => {
                const options = ko.unwrap(valueAccessor());
                const activateOn = options.activateOn || BalloonActivationMethod.clickOrKeyDown;
                const balloonHandles: BalloonHandle[] = toggleElement[balloonHandlesProp] || [];

                if (!toggleElement[balloonHandlesProp]) {
                    toggleElement[balloonHandlesProp] = balloonHandles;
                }

                let inBalloon = false;
                let isHoverOver = false;
                let balloonView: View;
                let balloonElement: HTMLElement;
                let balloonTipElement: HTMLElement;
                let closeTimeout;
                let createBalloonElement: () => void;

                const balloonElementId = Utils.randomClassName();
                
                if (options.component) {
                    createBalloonElement = () => {
                        balloonElement = document.createElement("div");
                        balloonElement.id = balloonElementId;
                        balloonElement.classList.add(balloonClass);
                        ko.applyBindingsToNode(balloonElement, { component: options.component, dialog: {} }, null);
                        document.body.appendChild(balloonElement);
                    };
                }

                if (options.template) {
                    createBalloonElement = () => {
                        balloonElement = document.createElement("div");
                        balloonElement.classList.add(balloonClass);
                        ko.applyBindingsToNode(balloonElement, { template: options.template, dialog: {} }, null);
                        document.body.appendChild(balloonElement);
                    };
                }

                if (activateOn === BalloonActivationMethod.clickOrKeyDown) {
                    toggleElement.setAttribute(Html.AriaAttributes.expanded, "false");
                    toggleElement.setAttribute(Html.AriaAttributes.controls, balloonElementId);
                }

                const createBalloonTip = (): void => {
                    balloonTipElement = document.createElement("div");
                    balloonTipElement.classList.add(balloonTipClass);
                    document.body.appendChild(balloonTipElement);
                };

                const removeBalloon = (): void => {
                    balloonHandle.balloonState = BalloonState.closed;

                    if (!balloonElement) {
                        return;
                    }

                    delete toggleElement[activeBalloonProp];
                    ko.cleanNode(balloonElement);
                    balloonElement.remove();
                    balloonElement = null;
                    balloonTipElement.remove();
                    balloonTipElement = null;

                    viewStack.removeView(balloonView);
                };

                const resetCloseTimeout = () => {
                    if (options.closeTimeout) {
                        clearTimeout(closeTimeout);
                        balloonHandle.balloonState = BalloonState.closing;
                        closeTimeout = setTimeout(closeBalloon, options.closeTimeout);
                    }
                };

                const updatePosition = async (): Promise<void> => {
                    if (!balloonElement || !BalloonState.open) {
                        return;
                    }

                    const preferredPosition = options.position;
                    const preferredDirection = preferredPosition === Position.left || preferredPosition === Position.right
                        ? Direction.horizontal
                        : Direction.vertical;

                    const triggerRect = toggleElement.getBoundingClientRect();
                    const balloonRect = balloonElement.getBoundingClientRect();
                    const spaceTop = triggerRect.top;
                    const spaceBottom = window.innerHeight - triggerRect.bottom;
                    const spaceLeft = triggerRect.left;
                    const spaceRight = window.innerWidth - triggerRect.height;

                    const balloonTipSize = 10;
                    const egdeGap = 10;
                    const padding = 10;

                    let balloonLeft: number;
                    let balloonRight: number;
                    let balloonTop: number;
                    let balloonBottom: number;
                    let selectedPosition: string;

                    let positionX: string;
                    let positionY: string;
                    let availableSpaceX: number;
                    let availableSpaceY: number;
                    let balloonHeight: number = balloonRect.height;
                    let balloonWidth: number = balloonRect.width;

                    let balloonTipX: number;
                    let balloonTipY: number;

                    if (preferredDirection === Direction.vertical) {
                        if (spaceTop > spaceBottom) {
                            positionY = Position.top;
                            availableSpaceY = spaceTop - egdeGap - padding;
                        }
                        else {
                            positionY = Position.bottom;
                            availableSpaceY = spaceBottom - egdeGap - padding;
                        }
                    }
                    else {
                        if (spaceLeft > spaceRight) {
                            positionX = Position.left;
                            availableSpaceX = spaceLeft - egdeGap;
                        }
                        else {
                            positionX = Position.right;
                            availableSpaceX = spaceRight - egdeGap;
                        }
                    }

                    if (balloonRect.height > availableSpaceY) {
                        balloonHeight = availableSpaceY;
                    }

                    if (balloonRect.width > availableSpaceX) {
                        balloonWidth = availableSpaceX;
                    }

                    switch (positionY) {
                        case Position.top:
                            balloonTop = triggerRect.top;

                            if ((balloonTop - balloonHeight) < 0) {
                                positionY = Position.bottom;
                            }
                            break;

                        case Position.bottom:
                            balloonTop = triggerRect.top + triggerRect.height;

                            if (balloonTop + balloonHeight > window.innerHeight) {
                                positionY = Position.top;
                            }
                            break;
                    }

                    switch (positionX) {
                        case Position.left:
                            balloonLeft = triggerRect.left;

                            if ((balloonLeft - balloonWidth) < 0) {
                                positionX = Position.right;
                            }
                            break;

                        case Position.right:
                            balloonLeft = triggerRect.left + triggerRect.width;

                            if (balloonLeft + balloonWidth > window.innerWidth) {
                                positionX = Position.left;
                            }
                            break;
                    }

                    balloonTipElement.classList.remove(balloonTopClass);
                    balloonTipElement.classList.remove(balloonBottomClass);
                    balloonTipElement.classList.remove(balloonLeftClass);
                    balloonTipElement.classList.remove(balloonRightClass);

                    if (preferredDirection === Direction.vertical) {
                        switch (positionY) {
                            case Position.top:
                                balloonTop = triggerRect.top - balloonHeight - padding;
                                balloonLeft = triggerRect.left + Math.floor(triggerRect.width / 2) - Math.floor(balloonWidth / 2);
                                balloonTipX = triggerRect.left + Math.floor(triggerRect.width / 2) - Math.floor(balloonTipSize / 2);
                                balloonTipY = triggerRect.top - Math.floor(balloonTipSize / 2) - padding;
                                balloonTipElement.classList.add(balloonTopClass);
                                selectedPosition = Position.top;
                                break;

                            case Position.bottom:
                                balloonTop = triggerRect.top + triggerRect.height + padding;
                                balloonLeft = triggerRect.left + Math.floor(triggerRect.width / 2) - Math.floor(balloonWidth / 2);
                                balloonTipX = triggerRect.left + Math.floor(triggerRect.width / 2) - Math.floor(balloonTipSize / 2);
                                balloonTipY = triggerRect.bottom - Math.floor(balloonTipSize / 2) + padding;
                                balloonTipElement.classList.add(balloonBottomClass);
                                selectedPosition = Position.bottom;
                                break;
                        }
                    }
                    else {
                        switch (positionX) {
                            case Position.left:
                                balloonTop = triggerRect.top + Math.floor(triggerRect.height / 2) - Math.floor(balloonHeight / 2);
                                balloonLeft = triggerRect.left - balloonWidth - padding;
                                balloonTipX = triggerRect.left - Math.floor(balloonTipSize / 2) - padding;
                                balloonTipY = triggerRect.top + Math.floor(triggerRect.height / 2) - Math.floor(balloonTipSize / 2);
                                balloonTipElement.classList.add(balloonLeftClass);
                                selectedPosition = Position.left;
                                break;

                            case Position.right:
                                balloonTop = triggerRect.top + Math.floor(triggerRect.height / 2) - Math.floor(balloonHeight / 2);
                                balloonLeft = triggerRect.right + padding;
                                balloonTipX = triggerRect.right - Math.floor(balloonTipSize / 2) + padding;
                                balloonTipY = triggerRect.top + Math.floor(triggerRect.height / 2) - Math.floor(balloonTipSize / 2);
                                balloonTipElement.classList.add(balloonRightClass);
                                selectedPosition = Position.right;
                                break;
                        }
                    }

                    if (balloonTop < egdeGap) {
                        balloonTop = egdeGap;
                    }

                    if (balloonTop + balloonHeight > innerHeight - egdeGap) {
                        balloonBottom = egdeGap;
                    }
                    else {
                        balloonBottom = innerHeight - (balloonTop + balloonHeight);
                    }

                    if (balloonLeft < egdeGap) {
                        balloonLeft = egdeGap;
                    }

                    delete balloonElement.style.top;
                    delete balloonElement.style.bottom;
                    delete balloonElement.style.left;
                    delete balloonElement.style.right;

                    if (balloonTop + balloonHeight > window.innerHeight) {
                        const overflowCorrection = (balloonTop + balloonHeight) - window.innerHeight;
                        balloonTop = balloonTop - overflowCorrection;
                    }

                    switch (selectedPosition) {
                        case Position.top:
                            balloonElement.style.bottom = `${balloonBottom}px`;
                            balloonElement.style.left = `${balloonLeft}px`;
                            break;

                        case Position.bottom:
                            balloonElement.style.top = `${balloonTop}px`;
                            balloonElement.style.left = `${balloonLeft}px`;
                            break;

                        case Position.left:
                            balloonElement.style.top = `${balloonTop}px`;
                            balloonElement.style.height = `${balloonHeight}px`;
                            // balloonElement.style.right = `${balloonRight}px`; // TODO: Make it work
                            balloonElement.style.left = `${balloonLeft}px`;
                            break;

                        case Position.right:
                            balloonElement.style.top = `${balloonTop}px`;
                            balloonElement.style.height = `${balloonHeight}px`;
                            balloonElement.style.left = `${balloonLeft}px`;
                            break;
                    }

                    balloonElement.style.maxHeight = availableSpaceY + "px";
                    balloonElement.style.maxWidth = availableSpaceX + "px";

                    balloonTipElement.style.top = `${balloonTipY}px`;
                    balloonTipElement.style.left = `${balloonTipX}px`;
                };

                const openBalloon = (returnFocusTo?: HTMLElement): void => {
                    if (options.isDisabled && options.isDisabled()) {
                        return;
                    }

                    resetCloseTimeout();

                    const existingBalloonHandle: BalloonHandle = toggleElement[activeBalloonProp];

                    if (existingBalloonHandle) {
                        if (existingBalloonHandle.balloonState == BalloonState.opening || existingBalloonHandle.balloonState == BalloonState.open) {
                            return;
                        }

                        if (existingBalloonHandle.activateOn === BalloonActivationMethod.hoverOrFocus) {
                            existingBalloonHandle.close();
                        }
                        else {
                            return;
                        }
                    }

                    setImmediate(() => { // give chance to view stack to clear unrelated views
                        if (!document.contains(toggleElement)) {
                            return;
                        }

                        const activeBalloonHandle: BalloonHandle = toggleElement[activeBalloonProp];

                        if (activeBalloonHandle) {
                            if (activateOn === BalloonActivationMethod.hoverOrFocus) {
                                return;
                            }
                            else {
                                activeBalloonHandle.close();
                            }
                        }

                        createBalloonElement();
                        createBalloonTip();

                        balloonView = {
                            close: closeBalloon,
                            element: balloonElement,
                            returnFocusTo: returnFocusTo,
                            hitTest: (targetElement) => {
                                const element =
                                    Html.closest(targetElement, x => x === balloonElement) ||
                                    Html.closest(targetElement, x => x === toggleElement);

                                return !!element;
                            }
                        };

                        viewStack.runHitTest(toggleElement);
                        viewStack.pushView(balloonView);

                        if (activateOn === BalloonActivationMethod.clickOrKeyDown) {
                            toggleElement.setAttribute(Html.AriaAttributes.expanded, "true");
                        }

                        toggleElement[activeBalloonProp] = balloonHandle;

                        balloonElement.classList.add(activeBalloonClass);
                        requestAnimationFrame(updatePosition);
                        balloonHandle.balloonState = BalloonState.open;

                        if (options.onOpen) {
                            options.onOpen();
                        }

                        if (activateOn === BalloonActivationMethod.hoverOrFocus) {
                            balloonElement.addEventListener(Events.MouseEnter, () => {
                                inBalloon = true;
                            });

                            balloonElement.addEventListener(Events.MouseLeave, () => {
                                inBalloon = false;
                                checkCloseHoverBalloon();
                            });
                        }
                    });
                };

                const closeBalloon = (): void => {
                    if (!balloonElement) {
                        return;
                    }

                    if (options.onClose) {
                        options.onClose();
                    }

                    removeBalloon();

                    if (activateOn === BalloonActivationMethod.clickOrKeyDown) {
                        toggleElement.setAttribute(Html.AriaAttributes.expanded, "false");
                    }
                };

                const toggle = (): void => {
                    resetCloseTimeout();

                    const existingBalloonHandle: BalloonHandle = toggleElement[activeBalloonProp];

                    if (existingBalloonHandle && balloonHandle !== existingBalloonHandle) {
                        existingBalloonHandle.close();
                    }

                    if (balloonHandle.balloonState === BalloonState.open) {
                        if (!options.closeTimeout) {
                            balloonHandle.balloonState = BalloonState.closing;
                            closeBalloon();
                        }
                    }
                    else {
                        balloonHandle.balloonState = BalloonState.opening;
                        openBalloon(toggleElement);
                    }
                };

                const balloonHandle: BalloonHandle = {
                    open: openBalloon,
                    close: closeBalloon,
                    toggle: toggle,
                    updatePosition: () => requestAnimationFrame(updatePosition),
                    activateOn: activateOn,
                    balloonState: BalloonState.closed
                };

                if (balloonHandles.some(x => x.activateOn === activateOn)) {
                    throw new Error(`The element already has balloon with activation method "${activateOn}".`);
                }

                balloonHandles.push(balloonHandle);

                if (options.onCreated) {
                    options.onCreated(balloonHandle);
                }

                const getBaloonHandleFor = (...methods: BalloonActivationMethod[]): BalloonHandle => {
                    const balloonHandles: BalloonHandle[] = toggleElement[balloonHandlesProp];

                    const handle = methods.map(x => balloonHandles.find(y => y.activateOn === x)).find(x => !!x);
                    return handle;
                };

                const onPointerDown = async (event: MouseEvent): Promise<void> => {
                    if (!toggleElement) {
                        return;
                    }

                    const targetElement = <HTMLElement>event.target;
                    const element = Html.closest(targetElement, (node) => node === toggleElement);

                    if (!element) {
                        return;
                    }

                    const handle = getBaloonHandleFor(BalloonActivationMethod.clickOrKeyDown);

                    if (handle) {
                        handle.toggle();
                    }
                };

                const onFocus = (): void => {
                    const existingBalloonHandle: BalloonHandle = toggleElement[activeBalloonProp];

                    if (existingBalloonHandle) {
                        return;
                    }

                    openBalloon();
                };

                const onBlur = (): void => {
                    closeBalloon();
                };

                const onMouseEnter = (event: MouseEvent): void => {
                    const handle = getBaloonHandleFor(BalloonActivationMethod.hoverOrFocus, BalloonActivationMethod.all);

                    if (!handle) {
                        return;
                    }

                    isHoverOver = true;

                    balloonHandle.balloonState = BalloonState.opening;

                    setTimeout(() => {
                        if (!isHoverOver) {
                            return;
                        }

                        openBalloon();
                    }, options.delay || 0);
                };

                const onMouseLeave = (event: MouseEvent): void => {
                    isHoverOver = false;
                    checkCloseHoverBalloon();
                };

                const checkCloseHoverBalloon = (): void => {
                    setTimeout(() => {
                        if (!isHoverOver && !inBalloon) {
                            balloonHandle.balloonState = BalloonState.closing;
                            closeBalloon();
                        }
                    }, 50);
                };

                const onKeyDown = (event: KeyboardEvent): void => {
                    switch (event.key) {
                        case Keys.Enter:
                        case Keys.Space:
                            event.preventDefault();

                            const handle = getBaloonHandleFor(BalloonActivationMethod.clickOrKeyDown, BalloonActivationMethod.all);

                            if (handle) {
                                handle.toggle();
                            }

                            break;
                    }
                };

                const onClick = (event: MouseEvent): void => {
                    event.preventDefault();
                };

                const onScroll = async (event: MouseEvent): Promise<void> => {
                    if (!balloonElement) {
                        return;
                    }

                    requestAnimationFrame(updatePosition);
                };

                if (options.closeOn) {
                    options.closeOn.subscribe(() => closeBalloon());
                }

                toggleElement.addEventListener(Events.Click, onClick);
                window.addEventListener(Events.Scroll, onScroll, true);

                switch (activateOn) {
                    case BalloonActivationMethod.clickOrKeyDown:
                        toggleElement.addEventListener(Events.KeyDown, onKeyDown);
                        document.addEventListener(Events.MouseDown, onPointerDown, true);
                        break;

                    case BalloonActivationMethod.hoverOrFocus:
                        toggleElement.addEventListener(Events.MouseEnter, onMouseEnter);
                        toggleElement.addEventListener(Events.MouseLeave, onMouseLeave);
                        toggleElement.addEventListener(Events.Focus, onFocus);
                        toggleElement.addEventListener(Events.Blur, onBlur);

                        break;

                    case BalloonActivationMethod.all:
                        toggleElement.addEventListener(Events.KeyDown, onKeyDown);
                        toggleElement.addEventListener(Events.MouseEnter, onMouseEnter);
                        toggleElement.addEventListener(Events.MouseLeave, onMouseLeave);
                        // toggleElement.addEventListener(Events.Focus, onFocus);
                        // toggleElement.addEventListener(Events.Blur, onBlur);
                        break;

                    default:
                        throw new Error(`Unknown balloon trigger event: ${activateOn}`);
                }

                ko.utils.domNodeDisposal.addDisposeCallback(toggleElement, () => {
                    balloonHandle.balloonState = BalloonState.closing;
                    window.removeEventListener(Events.MouseDown, onPointerDown, true);
                    toggleElement.removeEventListener(Events.Click, onClick);

                    switch (activateOn) {
                        case BalloonActivationMethod.clickOrKeyDown:
                            toggleElement.removeEventListener(Events.KeyDown, onKeyDown);
                            break;

                        case BalloonActivationMethod.hoverOrFocus:
                            toggleElement.removeEventListener(Events.MouseEnter, onMouseEnter);
                            toggleElement.removeEventListener(Events.MouseLeave, onMouseLeave);
                            break;

                        case BalloonActivationMethod.all:
                            toggleElement.removeEventListener(Events.KeyDown, onKeyDown);
                            toggleElement.removeEventListener(Events.MouseEnter, onMouseEnter);
                            toggleElement.removeEventListener(Events.MouseLeave, onMouseLeave);
                            break;

                        default:
                            throw new Error(`Unknown balloon trigger event: ${activateOn}`);
                    }

                    removeBalloon();
                    window.removeEventListener(Events.Scroll, onScroll, true);
                });
            }
        };
    }
}