// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// require: event_target.js

cr.define('cr.ui', function() {
  /** @const */ var EventTarget = cr.EventTarget;
  /** @const */ var Menu = cr.ui.Menu;

  /**
   * Handles context menus.
   * @constructor
   * @extends {cr.EventTarget}
   * @implements {EventListener}
   */
  function ContextMenuHandler() {
    /** @private {!EventTracker} */
    this.showingEvents_ = new EventTracker();
  }

  ContextMenuHandler.prototype = {
    __proto__: EventTarget.prototype,

    /**
     * The menu that we are currently showing.
     * @type {cr.ui.Menu}
     */
    menu_: null,
    get menu() {
      return this.menu_;
    },

    /**
     * Shows a menu as a context menu.
     * @param {!Event} e The event triggering the show (usually a contextmenu
     *     event).
     * @param {!cr.ui.Menu} menu The menu to show.
     */
    showMenu: function(e, menu) {
      menu.updateCommands(assertInstanceof(e.currentTarget, Node));
      if (!menu.hasVisibleItems())
        return;

      this.menu_ = menu;
      menu.classList.remove('hide-delayed');
      menu.show({x: e.screenX, y: e.screenY});
      menu.contextElement = e.currentTarget;

      // When the menu is shown we steal a lot of events.
      var doc = menu.ownerDocument;
      var win = /** @type {!Window} */ (doc.defaultView);
      this.showingEvents_.add(doc, 'keydown', this, true);
      this.showingEvents_.add(doc, 'mousedown', this, true);
      this.showingEvents_.add(doc, 'touchstart', this, true);
      this.showingEvents_.add(doc, 'focus', this);
      this.showingEvents_.add(win, 'popstate', this);
      this.showingEvents_.add(win, 'resize', this);
      this.showingEvents_.add(win, 'blur', this);
      this.showingEvents_.add(menu, 'contextmenu', this);
      this.showingEvents_.add(menu, 'activate', this);
      this.positionMenu_(e, menu);

      var ev = new Event('show');
      ev.element = menu.contextElement;
      ev.menu = menu;
      this.dispatchEvent(ev);
    },

    /**
     * Hide the currently shown menu.
     * @param {cr.ui.HideType=} opt_hideType Type of hide.
     *     default: cr.ui.HideType.INSTANT.
     */
    hideMenu: function(opt_hideType) {
      var menu = this.menu;
      if (!menu)
        return;

      if (opt_hideType == cr.ui.HideType.DELAYED)
        menu.classList.add('hide-delayed');
      else
        menu.classList.remove('hide-delayed');
      menu.hide();
      var originalContextElement = menu.contextElement;
      menu.contextElement = null;
      this.showingEvents_.removeAll();
      menu.selectedIndex = -1;
      this.menu_ = null;

      // On windows we might hide the menu in a right mouse button up and if
      // that is the case we wait some short period before we allow the menu
      // to be shown again.
      this.hideTimestamp_ = cr.isWindows ? Date.now() : 0;

      var ev = new Event('hide');
      ev.element = originalContextElement;
      ev.menu = menu;
      this.dispatchEvent(ev);
    },

    /**
     * Positions the menu
     * @param {!Event} e The event object triggering the showing.
     * @param {!cr.ui.Menu} menu The menu to position.
     * @private
     */
    positionMenu_: function(e, menu) {
      // TODO(arv): Handle scrolled documents when needed.

      var element = e.currentTarget;
      var x, y;
      // When the user presses the context menu key (on the keyboard) we need
      // to detect this.
      if (this.keyIsDown_) {
        var rect = element.getRectForContextMenu ?
            element.getRectForContextMenu() :
            element.getBoundingClientRect();
        var offset = Math.min(rect.width, rect.height) / 2;
        x = rect.left + offset;
        y = rect.top + offset;
      } else {
        x = e.clientX;
        y = e.clientY;
      }

      cr.ui.positionPopupAtPoint(x, y, menu);
    },

    /**
     * Handles event callbacks.
     * @param {!Event} e The event object.
     */
    handleEvent: function(e) {
      // Keep track of keydown state so that we can use that to determine the
      // reason for the contextmenu event.
      switch (e.type) {
        case 'keydown':
          this.keyIsDown_ = !e.ctrlKey && !e.altKey &&
              // context menu key or Shift-F10
              (e.keyCode == 93 && !e.shiftKey || e.key == 'F10' && e.shiftKey);
          break;

        case 'keyup':
          this.keyIsDown_ = false;
          break;
      }

      // Context menu is handled even when we have no menu.
      if (e.type != 'contextmenu' && !this.menu)
        return;

      switch (e.type) {
        case 'mousedown':
          if (!this.menu.contains(e.target)) {
            this.hideMenu();
            if (e.button == 0 /* Left button */ && (cr.isLinux || cr.isMac)) {
              // Emulate Mac and Linux, which swallow native 'mousedown' events
              // that close menus.
              e.preventDefault();
              e.stopPropagation();
            }
          } else
            e.preventDefault();
          break;

        case 'touchstart':
          if (!this.menu.contains(e.target))
            this.hideMenu();
          break;

        case 'keydown':
          if (e.key == 'Escape') {
            this.hideMenu();
            e.stopPropagation();
            e.preventDefault();

            // If the menu is visible we let it handle all the keyboard events.
          } else if (this.menu) {
            this.menu.handleKeyDown(e);
            e.preventDefault();
            e.stopPropagation();
          }
          break;

        case 'activate':
          var hideDelayed =
              e.target instanceof cr.ui.MenuItem && e.target.checkable;
          this.hideMenu(
              hideDelayed ? cr.ui.HideType.DELAYED : cr.ui.HideType.INSTANT);
          break;

        case 'focus':
          if (!this.menu.contains(e.target))
            this.hideMenu();
          break;

        case 'blur':
          this.hideMenu();
          break;

        case 'popstate':
        case 'resize':
          this.hideMenu();
          break;

        case 'contextmenu':
          if ((!this.menu || !this.menu.contains(e.target)) &&
              (!this.hideTimestamp_ || Date.now() - this.hideTimestamp_ > 50))
            this.showMenu(e, e.currentTarget.contextMenu);
          e.preventDefault();
          // Don't allow elements further up in the DOM to show their menus.
          e.stopPropagation();
          break;
      }
    },

    /**
     * Adds a contextMenu property to an element or element class.
     * @param {!Element|!Function} elementOrClass The element or class to add
     *     the contextMenu property to.
     */
    addContextMenuProperty: function(elementOrClass) {
      var target = typeof elementOrClass == 'function' ?
          elementOrClass.prototype :
          elementOrClass;

      target.__defineGetter__('contextMenu', function() {
        return this.contextMenu_;
      });
      target.__defineSetter__('contextMenu', function(menu) {
        var oldContextMenu = this.contextMenu;

        if (typeof menu == 'string' && menu[0] == '#') {
          menu = this.ownerDocument.getElementById(menu.slice(1));
          cr.ui.decorate(menu, Menu);
        }

        if (menu === oldContextMenu)
          return;

        if (oldContextMenu && !menu) {
          this.removeEventListener('contextmenu', contextMenuHandler);
          this.removeEventListener('keydown', contextMenuHandler);
          this.removeEventListener('keyup', contextMenuHandler);
        }
        if (menu && !oldContextMenu) {
          this.addEventListener('contextmenu', contextMenuHandler);
          this.addEventListener('keydown', contextMenuHandler);
          this.addEventListener('keyup', contextMenuHandler);
        }

        this.contextMenu_ = menu;

        if (menu && menu.id)
          this.setAttribute('contextmenu', '#' + menu.id);

        cr.dispatchPropertyChange(this, 'contextMenu', menu, oldContextMenu);
      });

      if (!target.getRectForContextMenu) {
        /**
         * @return {!ClientRect} The rect to use for positioning the context
         *     menu when the context menu is not opened using a mouse position.
         */
        target.getRectForContextMenu = function() {
          return this.getBoundingClientRect();
        };
      }
    },

    /**
     * Sets the given contextMenu to the given element. A contextMenu property
     * would be added if necessary.
     * @param {!Element} element The element or class to set the contextMenu to.
     * @param {!cr.ui.Menu} contextMenu The contextMenu property to be set.
     */
    setContextMenu: function(element, contextMenu) {
      if (!element.contextMenu)
        this.addContextMenuProperty(element);
      element.contextMenu = contextMenu;
    }
  };

  /**
   * The singleton context menu handler.
   * @type {!ContextMenuHandler}
   */
  var contextMenuHandler = new ContextMenuHandler;

  // Export
  return {
    contextMenuHandler: contextMenuHandler,
  };
});
