// 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.dialogs', function() {
  /**
   * @constructor
   */
  function BaseDialog(parentNode) {
    this.parentNode_ = parentNode;
    this.document_ = parentNode.ownerDocument;

    // The DOM element from the dialog which should receive focus when the
    // dialog is first displayed.
    this.initialFocusElement_ = null;

    // The DOM element from the parent which had focus before we were displayed,
    // so we can restore it when we're hidden.
    this.previousActiveElement_ = null;

    this.initDom_();
  }

  /**
   * Default text for Ok and Cancel buttons.
   *
   * Clients should override these with localized labels.
   */
  BaseDialog.OK_LABEL = '[LOCALIZE ME] Ok';
  BaseDialog.CANCEL_LABEL = '[LOCALIZE ME] Cancel';

  /**
   * Number of miliseconds animation is expected to take, plus some margin for
   * error.
   */
  BaseDialog.ANIMATE_STABLE_DURATION = 500;

  /** @private */
  BaseDialog.prototype.initDom_ = function() {
    var doc = this.document_;
    this.container_ = doc.createElement('div');
    this.container_.className = 'cr-dialog-container';
    this.container_.addEventListener(
        'keydown', this.onContainerKeyDown_.bind(this));
    this.shield_ = doc.createElement('div');
    this.shield_.className = 'cr-dialog-shield';
    this.container_.appendChild(this.shield_);
    this.container_.addEventListener(
        'mousedown', this.onContainerMouseDown_.bind(this));

    this.frame_ = doc.createElement('div');
    this.frame_.className = 'cr-dialog-frame';
    // Elements that have negative tabIndex can be focused but are not traversed
    // by Tab key.
    this.frame_.tabIndex = -1;
    this.container_.appendChild(this.frame_);

    this.title_ = doc.createElement('div');
    this.title_.className = 'cr-dialog-title';
    this.frame_.appendChild(this.title_);

    this.closeButton_ = doc.createElement('div');
    this.closeButton_.className = 'cr-dialog-close';
    this.closeButton_.addEventListener('click', this.onCancelClick_.bind(this));
    this.frame_.appendChild(this.closeButton_);

    this.text_ = doc.createElement('div');
    this.text_.className = 'cr-dialog-text';
    this.frame_.appendChild(this.text_);

    this.buttons = doc.createElement('div');
    this.buttons.className = 'cr-dialog-buttons';
    this.frame_.appendChild(this.buttons);

    this.okButton_ = doc.createElement('button');
    this.okButton_.className = 'cr-dialog-ok';
    this.okButton_.textContent = BaseDialog.OK_LABEL;
    this.okButton_.addEventListener('click', this.onOkClick_.bind(this));
    this.buttons.appendChild(this.okButton_);

    this.cancelButton_ = doc.createElement('button');
    this.cancelButton_.className = 'cr-dialog-cancel';
    this.cancelButton_.textContent = BaseDialog.CANCEL_LABEL;
    this.cancelButton_.addEventListener(
        'click', this.onCancelClick_.bind(this));
    this.buttons.appendChild(this.cancelButton_);

    this.initialFocusElement_ = this.okButton_;
  };

  /** @private {Function|undefined} */
  BaseDialog.prototype.onOk_ = null;

  /** @private {Function|undefined} */
  BaseDialog.prototype.onCancel_ = null;

  /** @private */
  BaseDialog.prototype.onContainerKeyDown_ = function(event) {
    // Handle Escape.
    if (event.keyCode == 27 && !this.cancelButton_.disabled) {
      this.onCancelClick_(event);
      event.stopPropagation();
      // Prevent the event from being handled by the container of the dialog.
      // e.g. Prevent the parent container from closing at the same time.
      event.preventDefault();
    }
  };

  /** @private */
  BaseDialog.prototype.onContainerMouseDown_ = function(event) {
    if (event.target == this.container_) {
      var classList = this.frame_.classList;
      // Start 'pulse' animation.
      classList.remove('pulse');
      setTimeout(classList.add.bind(classList, 'pulse'), 0);
      event.preventDefault();
    }
  };

  /** @private */
  BaseDialog.prototype.onOkClick_ = function(event) {
    this.hide();
    if (this.onOk_)
      this.onOk_();
  };

  /** @private */
  BaseDialog.prototype.onCancelClick_ = function(event) {
    this.hide();
    if (this.onCancel_)
      this.onCancel_();
  };

  /** @param {string} label */
  BaseDialog.prototype.setOkLabel = function(label) {
    this.okButton_.textContent = label;
  };

  /** @param {string} label */
  BaseDialog.prototype.setCancelLabel = function(label) {
    this.cancelButton_.textContent = label;
  };

  BaseDialog.prototype.setInitialFocusOnCancel = function() {
    this.initialFocusElement_ = this.cancelButton_;
  };

  /**
   * @param {string} message
   * @param {Function=} opt_onOk
   * @param {Function=} opt_onCancel
   * @param {Function=} opt_onShow
   */
  BaseDialog.prototype.show = function(
      message, opt_onOk, opt_onCancel, opt_onShow) {
    this.showWithTitle('', message, opt_onOk, opt_onCancel, opt_onShow);
  };

  /**
   * @param {string} title
   * @param {string} message
   * @param {Function=} opt_onOk
   * @param {Function=} opt_onCancel
   * @param {Function=} opt_onShow
   */
  BaseDialog.prototype.showHtml = function(
      title, message, opt_onOk, opt_onCancel, opt_onShow) {
    this.text_.innerHTML = message;
    this.show_(title, opt_onOk, opt_onCancel, opt_onShow);
  };

  /** @private */
  BaseDialog.prototype.findFocusableElements_ = function(doc) {
    var elements =
        Array.prototype.filter.call(doc.querySelectorAll('*'), function(n) {
          return n.tabIndex >= 0;
        });

    var iframes = doc.querySelectorAll('iframe');
    for (var i = 0; i < iframes.length; i++) {
      // Some iframes have an undefined contentDocument for security reasons,
      // such as chrome://terms (which is used in the chromeos OOBE screens).
      var iframe = iframes[i];
      var contentDoc;
      try {
        contentDoc = iframe.contentDocument;
      } catch (e) {
      }  // ignore SecurityError
      if (contentDoc)
        elements = elements.concat(this.findFocusableElements_(contentDoc));
    }
    return elements;
  };

  /**
   * @param {string} title
   * @param {string} message
   * @param {Function=} opt_onOk
   * @param {Function=} opt_onCancel
   * @param {Function=} opt_onShow
   */
  BaseDialog.prototype.showWithTitle = function(
      title, message, opt_onOk, opt_onCancel, opt_onShow) {
    this.text_.textContent = message;
    this.show_(title, opt_onOk, opt_onCancel, opt_onShow);
  };

  /**
   * @param {string} title
   * @param {Function=} opt_onOk
   * @param {Function=} opt_onCancel
   * @param {Function=} opt_onShow
   * @private
   */
  BaseDialog.prototype.show_ = function(
      title, opt_onOk, opt_onCancel, opt_onShow) {
    // Make all outside nodes unfocusable while the dialog is active.
    this.deactivatedNodes_ = this.findFocusableElements_(this.document_);
    this.tabIndexes_ = this.deactivatedNodes_.map(function(n) {
      return n.getAttribute('tabindex');
    });
    this.deactivatedNodes_.forEach(function(n) {
      n.tabIndex = -1;
    });

    this.previousActiveElement_ = this.document_.activeElement;
    this.parentNode_.appendChild(this.container_);

    this.onOk_ = opt_onOk;
    this.onCancel_ = opt_onCancel;

    if (title) {
      this.title_.textContent = title;
      this.title_.hidden = false;
    } else {
      this.title_.textContent = '';
      this.title_.hidden = true;
    }

    var self = this;
    setTimeout(function() {
      // Note that we control the opacity of the *container*, but the top/left
      // of the *frame*.
      self.container_.classList.add('shown');
      self.initialFocusElement_.focus();
      setTimeout(function() {
        if (opt_onShow)
          opt_onShow();
      }, BaseDialog.ANIMATE_STABLE_DURATION);
    }, 0);
  };

  /** @param {Function=} opt_onHide */
  BaseDialog.prototype.hide = function(opt_onHide) {
    // Restore focusability.
    for (var i = 0; i < this.deactivatedNodes_.length; i++) {
      var node = this.deactivatedNodes_[i];
      if (this.tabIndexes_[i] === null)
        node.removeAttribute('tabindex');
      else
        node.setAttribute('tabindex', this.tabIndexes_[i]);
    }
    this.deactivatedNodes_ = null;
    this.tabIndexes_ = null;

    // Note that we control the opacity of the *container*, but the top/left
    // of the *frame*.
    this.container_.classList.remove('shown');

    if (this.previousActiveElement_) {
      this.previousActiveElement_.focus();
    } else {
      this.document_.body.focus();
    }
    this.frame_.classList.remove('pulse');

    var self = this;
    setTimeout(function() {
      // Wait until the transition is done before removing the dialog.
      self.parentNode_.removeChild(self.container_);
      if (opt_onHide)
        opt_onHide();
    }, BaseDialog.ANIMATE_STABLE_DURATION);
  };

  /**
   * AlertDialog contains just a message and an ok button.
   * @constructor
   * @extends {cr.ui.dialogs.BaseDialog}
   */
  function AlertDialog(parentNode) {
    BaseDialog.call(this, parentNode);
    this.cancelButton_.style.display = 'none';
  }

  AlertDialog.prototype = {__proto__: BaseDialog.prototype};

  /**
   * @param {Function=} opt_onOk
   * @param {Function=} opt_onShow
   * @override
   */
  AlertDialog.prototype.show = function(message, opt_onOk, opt_onShow) {
    return BaseDialog.prototype.show.call(
        this, message, opt_onOk, opt_onOk, opt_onShow);
  };

  /**
   * ConfirmDialog contains a message, an ok button, and a cancel button.
   * @constructor
   * @extends {cr.ui.dialogs.BaseDialog}
   */
  function ConfirmDialog(parentNode) {
    BaseDialog.call(this, parentNode);
  }

  ConfirmDialog.prototype = {__proto__: BaseDialog.prototype};

  /**
   * PromptDialog contains a message, a text input, an ok button, and a
   * cancel button.
   * @constructor
   * @extends {cr.ui.dialogs.BaseDialog}
   */
  function PromptDialog(parentNode) {
    BaseDialog.call(this, parentNode);
    this.input_ = this.document_.createElement('input');
    this.input_.setAttribute('type', 'text');
    this.input_.addEventListener('focus', this.onInputFocus.bind(this));
    this.input_.addEventListener('keydown', this.onKeyDown_.bind(this));
    this.initialFocusElement_ = this.input_;
    this.frame_.insertBefore(this.input_, this.text_.nextSibling);
  }

  PromptDialog.prototype = {__proto__: BaseDialog.prototype};

  PromptDialog.prototype.onInputFocus = function(event) {
    this.input_.select();
  };

  /** @private */
  PromptDialog.prototype.onKeyDown_ = function(event) {
    if (event.keyCode == 13) {  // Enter
      this.onOkClick_(event);
      event.preventDefault();
    }
  };

  /**
   * @param {string} message
   * @param {?} defaultValue
   * @param {Function=} opt_onOk
   * @param {Function=} opt_onCancel
   * @param {Function=} opt_onShow
   * @suppress {checkTypes}
   * TODO(fukino): remove suppression if there is a better way to avoid warning
   * about overriding method with different signature.
   */
  PromptDialog.prototype.show = function(
      message, defaultValue, opt_onOk, opt_onCancel, opt_onShow) {
    this.input_.value = defaultValue || '';
    return BaseDialog.prototype.show.call(
        this, message, opt_onOk, opt_onCancel, opt_onShow);
  };

  PromptDialog.prototype.getValue = function() {
    return this.input_.value;
  };

  /** @private */
  PromptDialog.prototype.onOkClick_ = function(event) {
    this.hide();
    if (this.onOk_)
      this.onOk_(this.getValue());
  };

  return {
    BaseDialog: BaseDialog,
    AlertDialog: AlertDialog,
    ConfirmDialog: ConfirmDialog,
    PromptDialog: PromptDialog
  };
});
