// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview A card that displays the offline status of an app. It contains
 * detailed information such as a progress bar the indicates the status of
 * syncing and allows you to perform actions (such as manually go offline).
 *
 * @see ../demos/offline.html
 */

goog.provide('goog.ui.OfflineStatusCard');
goog.provide('goog.ui.OfflineStatusCard.EventType');

goog.require('goog.dom');
goog.require('goog.events.EventType');
goog.require('goog.structs.Map');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.GearsStatusType');
goog.require('goog.ui.ProgressBar');



/**
 * A offline status card.
 * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
 * @constructor
 * @extends {goog.ui.Component}
 */
goog.ui.OfflineStatusCard = function(opt_domHelper) {
  goog.ui.Component.call(this, opt_domHelper);

  /**
   * The progress bar for showing the status of syncing.
   * @type {goog.ui.ProgressBar}
   * @private
   */
  this.progressBar_ = new goog.ui.ProgressBar(opt_domHelper);
  this.addChild(this.progressBar_);

  /**
   * A map of action element uid/action event type pairs.
   * @type {goog.structs.Map}
   * @private
   */
  this.actionMap_ = new goog.structs.Map();
};
goog.inherits(goog.ui.OfflineStatusCard, goog.ui.Component);


/**
 * Event types dispatched by the component.
 * @enum {string}
 */
goog.ui.OfflineStatusCard.EventType = {
  /** Dispatched when the user wants the card to be dismissed. */
  DISMISS: 'dismiss'
};


/**
 * Whether the component is dirty and requires an upate to its display.
 * @type {boolean}
 * @protected
 */
goog.ui.OfflineStatusCard.prototype.dirty = false;


/**
 * The status of the component.
 * @type {goog.ui.GearsStatusType}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.status_ =
    goog.ui.GearsStatusType.NOT_INSTALLED;


/**
 * The element that holds the status message.
 * @type {Element}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.statusEl_ = null;


/**
 * The element that, when clicked, performs the appropriate action (such as
 * pausing synchronization).
 * @type {Element}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.actionEl_ = null;


/**
 * The element that displays additional messaging.
 * @type {Element}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.messageEl_ = null;


/**
 * The element that holds the progress bar and progress status.
 * @type {Element}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.progressEl_ = null;


/**
 * The element that holds the progress status.
 * @type {Element}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.progressStatusEl_ = null;


/**
 * The element that holds the close button.
 * @type {Element}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.closeEl_ = null;


/**
 * CSS class name for the element.
 * @type {string}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.className_ =
    goog.getCssName('goog-offlinestatuscard');


/**
 * CSS class name for the shadow element.
 * @type {string}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.shadowClassName_ =
    goog.getCssName('goog-offlinestatuscard-shadow');


/**
 * CSS class name for the content element.
 * @type {string}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.contentClassName_ =
    goog.getCssName('goog-offlinestatuscard-content');


/**
 * CSS class name for the status element.
 * @type {string}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.statusClassName_ =
    goog.getCssName('goog-offlinestatuscard-status');


/**
 * CSS class name for the action element.
 * @type {string}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.actionClassName_ =
    goog.getCssName('goog-offlinestatuscard-action');


/**
 * CSS class name for each action item contained in the action element.
 * @type {string}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.actionItemClassName_ =
    goog.getCssName('goog-offlinestatuscard-action-item');


/**
 * CSS class name for the last action item contained in the action element.
 * @type {string}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.lastActionItemClassName_ =
    goog.getCssName('goog-offlinestatuscard-action-item-last');


/**
 * CSS class name for the message element.
 * @type {string}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.messageClassName_ =
    goog.getCssName('goog-offlinestatuscard-message');


/**
 * CSS class name for the progress bar status element.
 * @type {string}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.progressBarStatusClassName_ =
    goog.getCssName('goog-offlinestatuscard-progressbarstatus');


/**
 * CSS class name for the close card element.
 * @type {string}
 * @private
 */
goog.ui.OfflineStatusCard.prototype.closeCardClassName_ =
    goog.getCssName('goog-offlinestatuscard-closecard');


/**
 * Gets the progress bar.
 * @return {goog.ui.ProgressBar} The progress bar.
 */
goog.ui.OfflineStatusCard.prototype.getProgressBar = function() {
  return this.progressBar_;
};


/**
 * Gets the status of the offline component of the app.
 * @return {goog.ui.GearsStatusType} The offline status.
 */
goog.ui.OfflineStatusCard.prototype.getStatus = function() {
  return this.status_;
};


/**
 * Sets the status of the offline component of the app.
 * @param {goog.ui.GearsStatusType} status The offline status.
 */
goog.ui.OfflineStatusCard.prototype.setStatus = function(status) {
  if (this.status_ != status) {
    this.dirty = true;
  }
  this.status_ = status;
  if (this.isInDocument()) {
    this.update();
  }
};


/**
 * Creates the initial DOM representation for the component.
 * @override
 */
goog.ui.OfflineStatusCard.prototype.createDom = function() {
  var dom = this.getDomHelper();
  this.setElementInternal(dom.createDom('div', this.className_,
      dom.createDom('div', this.shadowClassName_,
          dom.createDom('div', this.contentClassName_,
              this.closeEl_ = dom.createDom('div', this.closeCardClassName_),
              this.statusEl_ = dom.createDom('div', this.statusClassName_),
              this.progressEl_ = dom.createDom('div', null,
                  this.progressBarStatusEl_ =
                      dom.createDom('div', this.progressBarStatusClassName_)),
              this.actionEl_ = dom.createDom('div', this.actionClassName_),
              this.messageEl_ = dom.createDom('div',
                  this.messageClassName_)))));

  // Create and append the DOM of the progress bar.
  this.progressBar_.createDom();
  dom.insertSiblingBefore(
      this.progressBar_.getElement(), this.progressBarStatusEl_);

  this.createAdditionalDom();

  this.update();
};


/** @override */
goog.ui.OfflineStatusCard.prototype.enterDocument = function() {
  goog.ui.OfflineStatusCard.superClass_.enterDocument.call(this);

  // Listen for changes to the progress bar.
  var handler = this.getHandler();
  handler.listen(this.progressBar_, goog.ui.Component.EventType.CHANGE,
      this.handleProgressChange_);

  // Listen for a click on the action element.
  handler.listen(
      this.actionEl_, goog.events.EventType.CLICK, this.handleActionClick_);

  // Listen for the click on the close element.
  handler.listen(this.closeEl_, goog.events.EventType.CLICK, this.closePopup_);

  // Update the component if it is dirty.
  if (this.dirty) {
    this.update();
  }
};


/**
 * Allows subclasses to initialize additional DOM structures during createDom.
 * @protected
 */
goog.ui.OfflineStatusCard.prototype.createAdditionalDom = function() {
};


/**
 * Sends an event to OfflineStatusComponent to dismiss the popup.
 * @private
 */
goog.ui.OfflineStatusCard.prototype.closePopup_ = function() {
  this.dispatchEvent(goog.ui.OfflineStatusCard.EventType.DISMISS);
};


/**
 * Updates the display of the component.
 */
goog.ui.OfflineStatusCard.prototype.update = function() {
  if (this.getElement()) {
    var status = this.getStatus();
    var dom = this.getDomHelper();

    this.configureStatusElement(status);
    this.configureActionLinks(status);
    this.configureProgressElement(status);

    // Configure the message element.
    var message = this.getAdditionalMessage(status);
    var messageEl = this.messageEl_;
    goog.style.setElementShown(messageEl, message);
    if (message) {
      dom.setTextContent(messageEl, message);
    }

    // Clear the dirty state.
    this.dirty = false;
  }
};


/**
 * Set the message to display in the status portion of the card.
 * @param {goog.ui.GearsStatusType} status The offline status.
 */
goog.ui.OfflineStatusCard.prototype.configureStatusElement = function(status) {
  /**
   * @desc Tell the user whether they are online, offline, or syncing to
   *     Gears.
   */
  var MSG_OFFLINE_STATUS = goog.getMsg(
      'Status: {$msg}', {'msg': this.getStatusMessage(status)});
  this.getDomHelper().setTextContent(this.statusEl_, MSG_OFFLINE_STATUS);
};


/**
 * Set the action element to show correct action(s) for a particular status.
 * @param {goog.ui.GearsStatusType} status The offline status.
 */
goog.ui.OfflineStatusCard.prototype.configureActionLinks = function(status) {
  // Configure the action element.
  var actions = this.getActions(status);
  goog.dom.removeChildren(this.actionEl_);
  this.actionMap_.clear();

  if (actions) {
    var lastIdx = actions.length - 1;
    for (var i = 0; i <= lastIdx; i++) {
      // Ensure there is no padding to the right of the last action link.
      this.createLinkNode_(actions[i], i == lastIdx ?
          this.lastActionItemClassName_ : this.actionItemClassName_);
    }
  }
};


/**
 * Creates an action link element and styles it.
 * @param {Object} action An action object with message and event type.
 * @param {string} className The css class name to style the link with.
 * @private
 */
goog.ui.OfflineStatusCard.prototype.createLinkNode_ = function(
    action, className) {
  var actionEl = this.actionEl_;
  var dom = this.getDomHelper();
  var a = dom.createDom('span', className);
  dom.appendChild(actionEl, a);
  // A text node is needed here in order for links to wrap.
  dom.appendChild(actionEl, dom.createTextNode(' '));
  this.actionMap_.set(goog.getUid(a), action.eventType);
  goog.style.setElementShown(a, true);
  dom.setTextContent(a, action.message);
};


/**
 * Configure the progress bar element.
 * @param {goog.ui.GearsStatusType} status The offline status.
 */
goog.ui.OfflineStatusCard.prototype.configureProgressElement =
    function(status) {
  var showProgress = this.shouldShowProgressBar(status);
  goog.style.setElementShown(this.progressEl_, showProgress);
  if (showProgress) {
    this.updateProgressStatus();
  }
};


/**
 * Returns true if we want to display a progress bar.
 * @param {goog.ui.GearsStatusType} status The offline status.
 * @return {boolean} Whether we want to display a progress bar.
 */
goog.ui.OfflineStatusCard.prototype.shouldShowProgressBar = function(status) {
  return status == goog.ui.GearsStatusType.SYNCING ||
      status == goog.ui.GearsStatusType.CAPTURING;
};


/**
 * Handles a CHANGE event of the progress bar. Updates the status.
 * @param {goog.events.Event} e The event.
 * @private
 */
goog.ui.OfflineStatusCard.prototype.handleProgressChange_ = function(e) {
  this.updateProgressStatus();
};


/**
 * Handles a CLICK event on the action element. Dispatches the appropriate
 * action event type.
 * @param {goog.events.BrowserEvent} e The event.
 * @private
 */
goog.ui.OfflineStatusCard.prototype.handleActionClick_ = function(e) {
  var actionEventType = /** @type {string} */ (this.actionMap_.get(
      goog.getUid(e.target)));
  if (actionEventType) {
    this.dispatchEvent(actionEventType);
  }
};


/**
 * Updates the status of the progress bar.
 * @protected
 */
goog.ui.OfflineStatusCard.prototype.updateProgressStatus = function() {
  this.getDomHelper().setTextContent(
      this.progressBarStatusEl_, this.getProgressStatusMessage());
};


/**
 * Gets the status message for the progress bar.
 * @return {string} The status message for the progress bar.
 */
goog.ui.OfflineStatusCard.prototype.getProgressStatusMessage = function() {
  var pb = this.progressBar_;
  var percentValue = Math.round((pb.getValue() - pb.getMinimum()) /
                                (pb.getMaximum() - pb.getMinimum()) * 100);
  /** @desc The percent complete status of the syncing. */
  var MSG_OFFLINE_PERCENT_COMPLETE = goog.getMsg(
      '{$num}% complete.', {'num': percentValue});
  return MSG_OFFLINE_PERCENT_COMPLETE;
};


/**
 * Gets the status message for the given status.
 * @param {goog.ui.GearsStatusType} status The offline status.
 * @return {string} The status message.
 */
goog.ui.OfflineStatusCard.prototype.getStatusMessage = function(status) {
  var message = '';

  switch (status) {
    case goog.ui.GearsStatusType.OFFLINE:
      /** @desc Status shown when the app is offline. */
      var MSG_OFFLINE_STATUS_OFFLINE_MESSAGE = goog.getMsg(
          'Offline. No connection available.');
      message = MSG_OFFLINE_STATUS_OFFLINE_MESSAGE;
      break;
    case goog.ui.GearsStatusType.ONLINE:
      /** @desc Status shown when the app is online. */
      var MSG_OFFLINE_STATUS_ONLINE_MESSAGE = goog.getMsg('Online');
      message = MSG_OFFLINE_STATUS_ONLINE_MESSAGE;
      break;
    case goog.ui.GearsStatusType.SYNCING:
      /** @desc Status shown when the app is synchronizing. */
      var MSG_OFFLINE_STATUS_SYNCING_MESSAGE = goog.getMsg('Synchronizing...');
      message = MSG_OFFLINE_STATUS_SYNCING_MESSAGE;
      break;
    case goog.ui.GearsStatusType.CAPTURING:
      /** @desc Status shown when the app is capturing resources. */
      var MSG_OFFLINE_STATUS_CAPTURING_MESSAGE = goog.getMsg(
          'Updating software...');
      message = MSG_OFFLINE_STATUS_CAPTURING_MESSAGE;
      break;
    case goog.ui.GearsStatusType.ERROR:
      /** @desc Status shown when an error has occured. */
      var MSG_OFFLINE_STATUS_ERROR_MESSAGE = goog.getMsg(
          'Errors have been found.');
      message = MSG_OFFLINE_STATUS_ERROR_MESSAGE;
      break;
    default:
      break;
  }
  return message;
};


/**
 * Gets the action to display for the given status.
 * @param {goog.ui.GearsStatusType} status The offline status.
 * @return {Array.<Object>?} An array of action objects to display.
 */
goog.ui.OfflineStatusCard.prototype.getActions = function(status) {
  return null;
};


/**
 * Creates an action object containing a message for the action and event
 * type to dispatch if the action occurs.
 * @param {string} actionMessage The action message.
 * @param {string} actionEventType The action event type.
 * @return {Object} An object containing message and eventType properties.
 */
goog.ui.OfflineStatusCard.prototype.createActionObject = function(
    actionMessage, actionEventType) {
  return {message: actionMessage, eventType: actionEventType};
};


/**
 * Gets the additional message to display for the given status.
 * @param {goog.ui.GearsStatusType} status The offline status.
 * @return {string} The additional message.
 */
goog.ui.OfflineStatusCard.prototype.getAdditionalMessage = function(status) {
  return '';
};


/** @override */
goog.ui.OfflineStatusCard.prototype.disposeInternal = function() {
  goog.ui.OfflineStatusCard.superClass_.disposeInternal.call(this);

  this.progressBar_.dispose();
  this.progressBar_ = null;

  this.actionMap_.clear();
  this.actionMap_ = null;

  this.statusEl_ = null;
  this.actionEl_ = null;
  this.messageEl_ = null;
  this.progressEl_ = null;
  this.progressStatusEl_ = null;
};
