// 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.

/**
 * @fileoverview The menu that shows tabs from sessions on other devices.
 */

/**
 * @typedef {{collapsed: boolean,
 *            deviceType: string,
 *            modifiedTime: string,
 *            name: string,
 *            tag: string,
 *            windows: Array<WindowData>}}
 * @see chrome/browser/ui/webui/ntp/foreign_session_handler.cc
 */
var SessionData;

/**
 * @typedef {{sessionId: number,
 *            tabs: Array,
 *            timestamp: number,
 *            type: string,
 *            userVisibleTimestamp: string}}
 * @see chrome/browser/ui/webui/ntp/foreign_session_handler.cc
 */
var WindowData;

cr.define('ntp', function() {
  'use strict';

  /** @const */ var ContextMenuButton = cr.ui.ContextMenuButton;
  /** @const */ var Menu = cr.ui.Menu;
  /** @const */ var MenuItem = cr.ui.MenuItem;
  /** @const */ var MenuButton = cr.ui.MenuButton;

  /**
   * @constructor
   * @extends {cr.ui.MenuButton}
   */
  var OtherSessionsMenuButton = cr.ui.define('button');

  // Histogram buckets for UMA tracking of menu usage.
  /** @const */ var HISTOGRAM_EVENT = {
      INITIALIZED: 0,
      SHOW_MENU: 1,
      LINK_CLICKED: 2,
      LINK_RIGHT_CLICKED: 3,
      SESSION_NAME_RIGHT_CLICKED: 4,
      SHOW_SESSION_MENU: 5,
      COLLAPSE_SESSION: 6,
      EXPAND_SESSION: 7,
      OPEN_ALL: 8
  };
  /** @const */ var HISTOGRAM_EVENT_LIMIT =
      HISTOGRAM_EVENT.OPEN_ALL + 1;

  /**
   * Record an event in the UMA histogram.
   * @param {number} eventId The id of the event to be recorded.
   * @private
   */
  function recordUmaEvent_(eventId) {
    chrome.send('metricsHandler:recordInHistogram',
        ['NewTabPage.OtherSessionsMenu', eventId, HISTOGRAM_EVENT_LIMIT]);
  }

  OtherSessionsMenuButton.prototype = {
    __proto__: MenuButton.prototype,

    decorate: function() {
      MenuButton.prototype.decorate.call(this);
      this.menu = new Menu;
      cr.ui.decorate(this.menu, Menu);
      this.menu.menuItemSelector = '[role=menuitem]';
      this.menu.classList.add('footer-menu');
      this.menu.addEventListener('contextmenu',
                                 this.onContextMenu_.bind(this), true);
      document.body.appendChild(this.menu);

      // Create the context menu that appears when the user right clicks
      // on a device name.
      this.deviceContextMenu_ = DeviceContextMenuController.getInstance().menu;
      document.body.appendChild(this.deviceContextMenu_);

      this.promoMessage_ = $('other-sessions-promo-template').cloneNode(true);
      this.promoMessage_.removeAttribute('id');  // Prevent a duplicate id.

      this.sessions_ = [];
      this.anchorType = cr.ui.AnchorType.ABOVE;
      this.invertLeftRight = true;

      // Initialize the images for the drop-down buttons that appear beside the
      // session names.
      MenuButton.createDropDownArrows();

      recordUmaEvent_(HISTOGRAM_EVENT.INITIALIZED);
    },

    /**
     * Initialize this element.
     * @param {boolean} signedIn Is the current user signed in?
     */
    initialize: function(signedIn) {
      this.updateSignInState(signedIn);
    },

    /**
     * Handle a context menu event for an object in the menu's DOM subtree.
     */
    onContextMenu_: function(e) {
      // Only record the action if it occurred in one of the menu items or
      // on one of the session headings.
      if (findAncestorByClass(e.target, 'footer-menu-item')) {
        recordUmaEvent_(HISTOGRAM_EVENT.LINK_RIGHT_CLICKED);
      } else {
        var heading = findAncestorByClass(e.target, 'session-heading');
        if (heading) {
          recordUmaEvent_(HISTOGRAM_EVENT.SESSION_NAME_RIGHT_CLICKED);

          // Let the context menu know which session it was invoked on,
          // since they all share the same instance of the menu.
          DeviceContextMenuController.getInstance().setSession(
              heading.sessionData_);
        }
      }
    },

    /**
     * Hides the menu.
     * @override
     */
    hideMenu: function() {
      // Don't hide if the device context menu is currently showing.
      if (this.deviceContextMenu_.hidden)
        MenuButton.prototype.hideMenu.call(this);
    },

    /**
     * Shows the menu, first rebuilding it if necessary.
     * TODO(estade): the right of the menu should align with the right of the
     * button.
     * @override
     */
    showMenu: function(shouldSetFocus) {
      if (this.sessions_.length == 0)
        chrome.send('getForeignSessions');
      recordUmaEvent_(HISTOGRAM_EVENT.SHOW_MENU);
      MenuButton.prototype.showMenu.apply(this, arguments);

      // Work around https://bugs.webkit.org/show_bug.cgi?id=85884.
      this.menu.scrollTop = 0;
    },

    /**
     * Reset the menu contents to the default state.
     * @private
     */
    resetMenuContents_: function() {
      this.menu.innerHTML = '';
      this.menu.appendChild(this.promoMessage_);
    },

    /**
     * Create a custom click handler for a link, so that clicking on a link
     * restores the session (including back stack) rather than just opening
     * the URL.
     */
    makeClickHandler_: function(sessionTag, windowId, tabId) {
      var self = this;
      return function(e) {
        recordUmaEvent_(HISTOGRAM_EVENT.LINK_CLICKED);
        chrome.send('openForeignSession', [sessionTag, windowId, tabId,
            e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
        e.preventDefault();
      };
    },

    /**
     * Add the UI for a foreign session to the menu.
     * @param {SessionData} session Object describing the foreign session.
     */
    addSession_: function(session) {
      var doc = this.ownerDocument;

      var section = doc.createElement('section');
      this.menu.appendChild(section);

      var heading = doc.createElement('h3');
      heading.className = 'session-heading';
      heading.textContent = session.name;
      heading.sessionData_ = session;
      section.appendChild(heading);

      var dropDownButton = new ContextMenuButton;
      dropDownButton.classList.add('drop-down');
      // Keep track of the drop down that triggered the menu, so we know
      // which element to apply the command to.
      function handleDropDownFocus(e) {
        DeviceContextMenuController.getInstance().setSession(session);
      }
      dropDownButton.addEventListener('mousedown', handleDropDownFocus);
      dropDownButton.addEventListener('focus', handleDropDownFocus);
      heading.appendChild(dropDownButton);

      var timeSpan = doc.createElement('span');
      timeSpan.className = 'details';
      timeSpan.textContent = session.modifiedTime;
      heading.appendChild(timeSpan);

      cr.ui.contextMenuHandler.setContextMenu(heading,
                                              this.deviceContextMenu_);

      if (!session.collapsed)
        section.appendChild(this.createSessionContents_(session));
    },

    /**
     * Create the DOM tree representing the tabs and windows in a session.
     * @param {SessionData} session The session model object.
     * @return {Element} A single div containing the list of tabs & windows.
     * @private
     */
    createSessionContents_: function(session) {
      var doc = this.ownerDocument;
      var contents = doc.createElement('div');

      for (var i = 0; i < session.windows.length; i++) {
        var window = session.windows[i];

        // Show a separator between multiple windows in the same session.
        if (i > 0)
          contents.appendChild(doc.createElement('hr'));

        for (var j = 0; j < window.tabs.length; j++) {
          var tab = window.tabs[j];
          var a = doc.createElement('a');
          a.className = 'footer-menu-item';
          a.textContent = tab.title;
          a.href = tab.url;
          a.style.backgroundImage = getFaviconImageSet(tab.url);

          var clickHandler = this.makeClickHandler_(
              session.tag, String(window.sessionId), String(tab.sessionId));
          a.addEventListener('click', clickHandler);
          contents.appendChild(a);
          cr.ui.decorate(a, MenuItem);
        }
      }

      return contents;
    },

    /**
     * Sets the menu model data. An empty list means that either there are no
     * foreign sessions, or tab sync is disabled for this profile.
     * |isTabSyncEnabled| makes it possible to distinguish between the cases.
     *
     * @param {Array<SessionData>} sessionList Array of objects describing the
     *     sessions from other devices.
     * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile?
     */
    setForeignSessions: function(sessionList, isTabSyncEnabled) {
      this.sessions_ = sessionList;
      this.resetMenuContents_();
      if (sessionList.length > 0) {
        // Rebuild the menu with the new data.
        for (var i = 0; i < sessionList.length; i++) {
          this.addSession_(sessionList[i]);
        }
      }

      // The menu button is shown iff tab sync is enabled.
      this.hidden = !isTabSyncEnabled;
    },

    /**
     * Called when this element is initialized, and from the new tab page when
     * the user's signed in state changes,
     * @param {boolean} signedIn Is the user currently signed in?
     */
    updateSignInState: function(signedIn) {
      if (signedIn)
        chrome.send('getForeignSessions');
      else
        this.hidden = true;
    },
  };

  /**
   * Controller for the context menu for device names in the list of sessions.
   * This class is designed to be used as a singleton.
   *
   * @constructor
   */
  function DeviceContextMenuController() {
    this.__proto__ = DeviceContextMenuController.prototype;
    this.initialize();
  }
  cr.addSingletonGetter(DeviceContextMenuController);

  DeviceContextMenuController.prototype = {

    initialize: function() {
      var menu = new cr.ui.Menu;
      cr.ui.decorate(menu, cr.ui.Menu);
      menu.classList.add('device-context-menu');
      menu.classList.add('footer-menu-context-menu');
      this.menu = menu;
      this.collapseItem_ = this.appendMenuItem_('collapseSessionMenuItemText');
      this.collapseItem_.addEventListener('activate',
                                          this.onCollapseOrExpand_.bind(this));
      this.expandItem_ = this.appendMenuItem_('expandSessionMenuItemText');
      this.expandItem_.addEventListener('activate',
                                        this.onCollapseOrExpand_.bind(this));
      this.openAllItem_ = this.appendMenuItem_('restoreSessionMenuItemText');
      this.openAllItem_.addEventListener('activate',
                                         this.onOpenAll_.bind(this));
    },

    /**
     * Appends a menu item to |this.menu|.
     * @param {string} textId The ID for the localized string that acts as
     *     the item's label.
     */
    appendMenuItem_: function(textId) {
      var button = cr.doc.createElement('button');
      this.menu.appendChild(button);
      cr.ui.decorate(button, cr.ui.MenuItem);
      button.textContent = loadTimeData.getString(textId);
      return button;
    },

    /**
     * Handler for the 'Collapse' and 'Expand' menu items.
     * @param {Event} e The activation event.
     * @private
     */
    onCollapseOrExpand_: function(e) {
      this.session_.collapsed = !this.session_.collapsed;
      this.updateMenuItems_();
      chrome.send('setForeignSessionCollapsed',
                  [this.session_.tag, this.session_.collapsed]);
      chrome.send('getForeignSessions');  // Refresh the list.

      var eventId = this.session_.collapsed ?
          HISTOGRAM_EVENT.COLLAPSE_SESSION : HISTOGRAM_EVENT.EXPAND_SESSION;
      recordUmaEvent_(eventId);
    },

    /**
     * Handler for the 'Open all' menu item.
     * @param {Event} e The activation event.
     * @private
     */
    onOpenAll_: function(e) {
      chrome.send('openForeignSession', [this.session_.tag]);
      recordUmaEvent_(HISTOGRAM_EVENT.OPEN_ALL);
    },

    /**
     * Set the session data for the session the context menu was invoked on.
     * This should never be called when the menu is visible.
     * @param {Object} session The model object for the session.
     */
    setSession: function(session) {
      this.session_ = session;
      this.updateMenuItems_();
    },

    /**
     * Set the visibility of the Expand/Collapse menu items based on the state
     * of the session that this menu is currently associated with.
     * @private
     */
    updateMenuItems_: function() {
      this.collapseItem_.hidden = this.session_.collapsed;
      this.expandItem_.hidden = !this.session_.collapsed;
    }
  };

  return {
    OtherSessionsMenuButton: OtherSessionsMenuButton,
  };
});
