/*
 * Copyright (C) 2011 Google Inc. All rights reserved.
 * Copyright (C) 2017 Sony Interactive Entertainment Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

WI.SoftContextMenu = class SoftContextMenu
{
    constructor(items, parentMenu)
    {
        this._items = items;
        this._parentMenu = parentMenu;
    }

    // Public

    show(event)
    {
        const isSubMenu = !!this._parentMenu;

        this._contextMenuElement = document.createElement("div");
        this._contextMenuElement.className = "soft-context-menu";
        this._contextMenuElement.style.left = event.pageX + "px";
        this._contextMenuElement.style.top = event.pageY + "px";
        this._contextMenuElement.tabIndex = 0;
        this._contextMenuElement.addEventListener("keydown", this._menuKeyDown.bind(this), false);
        for (let item of this._items)
            this._contextMenuElement.appendChild(this._createMenuItem(item));

        // Install glass pane capturing events.
        if (!isSubMenu) {
            this._glassPaneElement = document.createElement("div");
            this._glassPaneElement.className = "soft-context-menu-glass-pane";
            this._glassPaneElement.addEventListener("mousedown", this._glassPaneMouseDown.bind(this), false);
            this._glassPaneElement.appendChild(this._contextMenuElement);
            document.body.appendChild(this._glassPaneElement);
            this._focus();
            this._consumeEvent(event, true);
        } else
            this._parentGlassPaneElement().appendChild(this._contextMenuElement);

        this._repositionMenuOnScreen(isSubMenu);
    }

    // Private

    _consumeEvent(event, preventDefault)
    {
        event.stopImmediatePropagation();

        if (preventDefault)
            event.preventDefault();

        event.handled = true;
    }

    _parentGlassPaneElement()
    {
        if (!this._parentMenu)
            return null;

        if (this._parentMenu._glassPaneElement)
            return this._parentMenu._glassPaneElement;

        return this._parentMenu._parentGlassPaneElement();
    }

    _createMenuItem(item)
    {
        if (item.type === "separator")
            return this._createSeparator();

        const checkmark = "\u2713";
        const blackRightPointingTriangle = "\u25b6";

        const menuItemElement = document.createElement("div");
        menuItemElement.className = "item";
        if (!item.enabled)
            menuItemElement.classList.add("disabled");

        const checkMarkElement = document.createElement("span");
        checkMarkElement.textContent = item.checked ? checkmark : "";
        checkMarkElement.className = "checkmark";
        menuItemElement.appendChild(checkMarkElement);

        const labelElement = document.createElement("span");
        labelElement.textContent = item.label;
        labelElement.className = "label";
        menuItemElement.appendChild(labelElement);

        if (item.type === "subMenu") {
            const subMenuArrowElement = document.createElement("span");
            subMenuArrowElement.textContent = blackRightPointingTriangle;
            subMenuArrowElement.className = "submenu-arrow";
            menuItemElement.appendChild(subMenuArrowElement);

            menuItemElement._subItems = item.subItems;
        } else
            menuItemElement._actionId = item.id;

        menuItemElement.addEventListener("contextmenu", this._menuItemContextMenu.bind(this), false);
        menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
        menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
        menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false);
        menuItemElement.addEventListener("mouseout", this._menuItemMouseOut.bind(this), false);

        return menuItemElement;
    }

    _createSeparator()
    {
        const separatorElement = document.createElement("div");
        separatorElement.className = "separator";
        separatorElement._isSeparator = true;
        separatorElement.createChild("div", "line");

        separatorElement.addEventListener("contextmenu", this._menuItemContextMenu.bind(this), false);
        separatorElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
        separatorElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
        separatorElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false);

        return separatorElement;
    }

    _repositionMenuOnScreen(isSubMenu)
    {
        if (this._contextMenuElement.offsetLeft + this._contextMenuElement.offsetWidth > document.body.offsetWidth) {
            if (isSubMenu) {
                const parentContextMenuElement = this._parentMenu._contextMenuElement;
                const leftOfParent = parentContextMenuElement.offsetLeft - this._contextMenuElement.offsetWidth + 2;
                const fromParentRight = this._contextMenuElement.offsetLeft - this._contextMenuElement.offsetWidth + 2;
                this._contextMenuElement.style.left = (leftOfParent >= 0 ? leftOfParent : fromParentRight) + "px";
            } else {
                const leftOfCursor = this._contextMenuElement.offsetLeft - this._contextMenuElement.offsetWidth;
                const fromRightEdge = document.body.offsetWidth - this._contextMenuElement.offsetWidth;
                this._contextMenuElement.style.left = (leftOfCursor >= 0 ? leftOfCursor : fromRightEdge) + "px";
            }
        }

        if (this._contextMenuElement.offsetTop + this._contextMenuElement.offsetHeight > document.body.offsetHeight) {
            const aboveCursor = this._contextMenuElement.offsetTop - this._contextMenuElement.offsetHeight;
            const fromBottomEdge = document.body.offsetHeight - this._contextMenuElement.offsetHeight;
            this._contextMenuElement.style.top = (!isSubMenu && aboveCursor >= 0 ? aboveCursor : fromBottomEdge) + "px";
        }
    }

    _showSubMenu(menuItemElement)
    {
        if (menuItemElement._subMenuTimer) {
            clearTimeout(menuItemElement._subMenuTimer);
            menuItemElement._subMenuTimer = 0;
        }

        if (this._subMenu)
            return;

        this._subMenu = new WI.SoftContextMenu(menuItemElement._subItems, this);
        this._subMenu.show({
            pageX: this._contextMenuElement.offsetLeft + menuItemElement.offsetWidth,
            pageY: this._contextMenuElement.offsetTop + menuItemElement.offsetTop - 4
        });
    }

    _hideSubMenu()
    {
        if (!this._subMenu)
            return;

        this._subMenu._discardSubMenus();
        this._focus();
    }

    _menuItemContextMenu(event)
    {
        // Prevent our non-native context menu from getting a native context menu on right-click.
        this._consumeEvent(event, true);
    }

    _menuItemMouseDown(event)
    {
        // Prevent menu from disappearing before mouseup.
        this._consumeEvent(event, true);
    }

    _menuItemMouseUp(event)
    {
        this._triggerAction(event.target, event);
        this._consumeEvent(event);
    }

    _menuItemMouseOver(event)
    {
        this._highlightMenuItem(event.target._isSeparator ? null : event.target);
    }

    _menuItemMouseOut(event)
    {
        const shouldUnhighlight = !this._subMenu || !event.relatedTarget ||
            this._contextMenuElement.contains(event.relatedTarget) ||
            event.relatedTarget.classList.contains("soft-context-menu-glass-pane");

        if (shouldUnhighlight)
            this._highlightMenuItem(null);
    }

    _menuKeyDown(event)
    {
        switch (event.keyIdentifier) {
        case "Up":
            this._highlightPrevious();
            break;
        case "Down":
            this._highlightNext();
            break;
        case "Left":
            if (this._parentMenu) {
                this._highlightMenuItem(null);
                this._parentMenu._hideSubMenu();
            }
            break;
        case "Enter":
            if (!isEnterKey(event))
                break;
            // falls through
        case "U+0020": // Space
            if (this._highlightedMenuItemElement && !this._highlightedMenuItemElement._subItems)
                this._triggerAction(this._highlightedMenuItemElement, event);
            // falls through
        case "Right":
            if (this._highlightedMenuItemElement && this._highlightedMenuItemElement._subItems) {
                this._showSubMenu(this._highlightedMenuItemElement);
                this._subMenu._focus();
                this._subMenu._highlightNext();
            }
            break;
        case "U+001B": // Escape
            this._discardMenu(true, event);
            break;
        }
        this._consumeEvent(event, true);
    }

    _glassPaneMouseDown(event)
    {
        this._discardMenu(true, event);
        this._consumeEvent(event);
    }

    _focus()
    {
        this._contextMenuElement.focus();
    }

    _triggerAction(menuItemElement, event)
    {
        if (!menuItemElement._subItems) {
            this._discardMenu(true, event);
            if (typeof menuItemElement._actionId === "number") {
                WI.ContextMenu.contextMenuItemSelected(menuItemElement._actionId);
                menuItemElement._actionId = null;
            }
            return;
        }

        this._showSubMenu(menuItemElement);
        this._consumeEvent(event);
    }

    _highlightMenuItem(menuItemElement, skipSubMenuExpansion)
    {
        if (this._highlightedMenuItemElement === menuItemElement)
            return;

        this._hideSubMenu();
        if (this._highlightedMenuItemElement) {
            this._highlightedMenuItemElement.classList.remove("highlighted");

            if (this._highlightedMenuItemElement._subItems && this._highlightedMenuItemElement._subMenuTimer) {
                clearTimeout(this._highlightedMenuItemElement._subMenuTimer);
                this._highlightedMenuItemElement._subMenuTimer = 0;
            }
        }

        this._highlightedMenuItemElement = menuItemElement;
        if (this._highlightedMenuItemElement) {
            this._highlightedMenuItemElement.classList.add("highlighted");
            this._contextMenuElement.focus();

            if (!skipSubMenuExpansion && this._highlightedMenuItemElement._subItems && !this._highlightedMenuItemElement._subMenuTimer)
                this._highlightedMenuItemElement._subMenuTimer = setTimeout(this._showSubMenu.bind(this, this._highlightedMenuItemElement), 150);
        }
    }

    _highlightPrevious()
    {
        let menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.previousSibling : this._contextMenuElement.lastChild;

        while (menuItemElement && menuItemElement._isSeparator)
            menuItemElement = menuItemElement.previousSibling;

        if (menuItemElement)
            this._highlightMenuItem(menuItemElement, true);
    }

    _highlightNext()
    {
        let menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.nextSibling : this._contextMenuElement.firstChild;

        while (menuItemElement && menuItemElement._isSeparator)
            menuItemElement = menuItemElement.nextSibling;

        if (menuItemElement)
            this._highlightMenuItem(menuItemElement, true);
    }

    _discardMenu(closeParentMenus, event)
    {
        if (this._subMenu && !closeParentMenus)
            return;

        if (this._glassPaneElement) {
            const glassPane = this._glassPaneElement;
            this._glassPaneElement = null;
            // This can re-enter discardMenu due to blur.
            document.body.removeChild(glassPane);

            if (this._parentMenu) {
                this._parentMenu._subMenu = null;
                if (closeParentMenus)
                    this._parentMenu._discardMenu(closeParentMenus, event);
            }

            if (event)
                this._consumeEvent(event, true);
        } else if (this._parentMenu && this._contextMenuElement.parentElement) {
            this._discardSubMenus();

            if (closeParentMenus)
                this._parentMenu._discardMenu(closeParentMenus, event);

            if (event)
                this._consumeEvent(event, true);
        }
    }

    _discardSubMenus()
    {
        if (this._subMenu)
            this._subMenu._discardSubMenus();

        if (this._contextMenuElement.parentElement)
            this._contextMenuElement.parentElement.removeChild(this._contextMenuElement);

        if (this._parentMenu)
            this._parentMenu._subMenu = null;
    }
};
