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

cr.define('cr.ui', function() {
  /**
   * Constructor for FocusManager singleton. Checks focus of elements to ensure
   * that elements in "background" pages (i.e., those in a dialog that is not
   * the topmost overlay) do not receive focus.
   * @constructor
   */
  function FocusManager() {
  }

  FocusManager.prototype = {
    /**
     * Whether focus is being transferred backward or forward through the DOM.
     * @type {boolean}
     * @private
     */
    focusDirBackwards_: false,

    /**
     * Determines whether the |child| is a descendant of |parent| in the page's
     * DOM.
     * @param {Element} parent The parent element to test.
     * @param {Element} child The child element to test.
     * @return {boolean} True if |child| is a descendant of |parent|.
     * @private
     */
    isDescendantOf_: function(parent, child) {
      var current = child;

      while (current) {
        current = current.parentNode;
        if (typeof(current) == 'undefined' ||
            typeof(current) == 'null' ||
            current === document.body) {
          return false;
        } else if (current === parent) {
          return true;
        }
      }

      return false;
    },

    /**
     * Returns the parent element containing all elements which should be
     * allowed to receive focus.
     * @return {Element} The element containing focusable elements.
     */
    getFocusParent: function() {
      return document.body;
    },

    /**
     * Returns the elements on the page capable of receiving focus.
     * @return {Array.Element} The focusable elements.
     */
    getFocusableElements_: function() {
      var focusableDiv = this.getFocusParent();

      // Create a TreeWalker object to traverse the DOM from |focusableDiv|.
      var treeWalker = document.createTreeWalker(
          focusableDiv,
          NodeFilter.SHOW_ELEMENT,
          { acceptNode: function(node) {
              var style = window.getComputedStyle(node);
              // Reject all hidden nodes. FILTER_REJECT also rejects these
              // nodes' children, so non-hidden elements that are descendants of
              // hidden <div>s will correctly be rejected.
              if (node.hidden || style.display == 'none' ||
                  style.visibility == 'hidden') {
                return NodeFilter.FILTER_REJECT;
              }

              // Skip nodes that cannot receive focus. FILTER_SKIP does not
              // cause this node's children also to be skipped.
              if (node.disabled || node.tabIndex < 0)
                return NodeFilter.FILTER_SKIP;

              // Accept nodes that are non-hidden and focusable.
              return NodeFilter.FILTER_ACCEPT;
            }
          },
          false);

      var focusable = [];
      while (treeWalker.nextNode())
        focusable.push(treeWalker.currentNode);

      return focusable;
    },

    /**
     * Dispatches an 'elementFocused' event to notify an element that it has
     * received focus. When focus wraps around within the a page, only the
     * element that has focus after the wrapping receives an 'elementFocused'
     * event. This differs from the native 'focus' event which is received by
     * an element outside the page first, followed by a 'focus' on an element
     * within the page after the FocusManager has intervened.
     * @param {Element} element The element that has received focus.
     * @private
     */
    dispatchFocusEvent_: function(element) {
      cr.dispatchSimpleEvent(element, 'elementFocused', true, false);
    },

    /**
     * Attempts to focus the appropriate element in the current dialog.
     * @private
     */
    setFocus_: function() {
      // If |this.focusDirBackwards_| is true, the user has pressed "Shift+Tab"
      // and has caused the focus to be transferred backward, outside of the
      // current dialog. In this case, loop around and try to focus the last
      // element of the dialog; otherwise, try to focus the first element of the
      // dialog.
      var focusableElements = this.getFocusableElements_();
      var element = this.focusDirBackwards_ ? focusableElements.pop() :
                                              focusableElements.shift();
      if (element) {
        element.focus();
        this.dispatchFocusEvent_(element);
      }
    },

    /**
     * Attempts to focus the first element in the current dialog.
     */
    focusFirstElement: function() {
      this.focusFirstElement_();
    },

    /**
     * Handler for focus events on the page.
     * @param {Event} event The focus event.
     * @private
     */
    onDocumentFocus_: function(event) {
      // If the element being focused is a descendant of the currently visible
      // page, focus is valid.
      if (this.isDescendantOf_(this.getFocusParent(), event.target)) {
        this.dispatchFocusEvent_(event.target);
        return;
      }

      // The target of the focus event is not in the topmost visible page and
      // should not be focused.
      event.target.blur();

      // Attempt to wrap around focus within the current page.
      this.setFocus_();
    },

    /**
     * Handler for keydown events on the page.
     * @param {Event} event The keydown event.
     * @private
     */
    onDocumentKeyDown_: function(event) {
      /** @const */ var tabKeyCode = 9;

      if (event.keyCode == tabKeyCode) {
        // If the "Shift" key is held, focus is being transferred backward in
        // the page.
        this.focusDirBackwards_ = event.shiftKey ? true : false;
      }
    },

    /**
     * Initializes the FocusManager by listening for events in the document.
     */
    initialize: function() {
      document.addEventListener('focus', this.onDocumentFocus_.bind(this),
          true);
      document.addEventListener('keydown', this.onDocumentKeyDown_.bind(this),
          true);
    },
  };

  return {
    FocusManager: FocusManager,
  };
});
