/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 Tabs library for gadgets.
 */

/**
 * @class Tab class for gadgets.
 * You create tabs using the TabSet addTab() method.
 * To get Tab objects,
 * use the TabSet getSelectedTab() or getTabs() methods.
 *
 * <p>
 * <b>See also:</b>
 * <a href="gadgets.TabSet.html">TabSet</a>
 * </p>
 *
 * @name gadgets.Tab
 * @description Creates a new Tab.
 */

/**
 * @param {gadgets.TabSet} handle The associated gadgets.TabSet instance.
 * @private
 * @constructor
 */
gadgets.Tab = function(handle) {
  this.handle_ = handle;
  this.td_ = null;
  this.contentContainer_ = null;
  this.callback_ = null;
};

/**
 * Returns the label of the tab as a string (may contain HTML).
 * @return {string} Label of the tab.
 */
gadgets.Tab.prototype.getName = function() {
  return this.td_.innerHTML;
};

/**
 * Returns the HTML element that contains the tab's label.
 * @return {Element} The HTML element of the tab's label.
 */
gadgets.Tab.prototype.getNameContainer = function() {
  return this.td_;
};

/**
 * Returns the HTML element where the tab content is rendered.
 * @return {Element} The HTML element of the content container.
 */
gadgets.Tab.prototype.getContentContainer = function() {
  return this.contentContainer_;
};

/**
 * Returns the callback function that is executed when the tab is selected.
 * @return {function(string)} The callback function of the tab.
 */
gadgets.Tab.prototype.getCallback = function() {
  return this.callback_;
};

/**
 * Returns the tab's index.
 * @return {number} The tab's index.
 */
gadgets.Tab.prototype.getIndex = function() {
  var tabs = this.handle_.getTabs();
  for (var i = 0; i < tabs.length; ++i) {
    if (this === tabs[i]) {
      return i;
    }
  }
  return -1;
};

/**
 * @class A class gadgets can use to make tabs.
 * @description Creates a new TabSet object
 *
 * @constructor
 * @param {string=} opt_moduleId Optional suffix for the ID of tab container.
 * @param {string=} opt_defaultTab Optional tab name that specifies the name of
 *                   of the tab that is selected after initialization.
 *                   If this parameter is omitted, the first tab is selected by
 *                   default.
 * @param {Element=} opt_container The HTML element to contain the tabs.  If
 *                    omitted, a new div element is created and inserted at the
 *                    very top.
 */
gadgets.TabSet = function(opt_moduleId, opt_defaultTab, opt_container) {
  this.moduleId_ = opt_moduleId || 0;
  this.domIdFilter_ = new RegExp('^[A-Za-z]([0-9a-zA-Z_:.-]+)?$');
  this.selectedTab_ = null;
  this.tabs_ = [];
  this.tabsAdded_ = 0;
  this.defaultTabName_ = opt_defaultTab || '';
  this.leftNavContainer_ = null;
  this.rightNavContainer_ = null;
  this.navTable_ = null;
  this.tabsContainer_ = null;
  this.rtl_ = document.body.dir === 'rtl';
  this.prefs_ = new gadgets.Prefs();
  this.selectedTabIndex_ = this.prefs_.getString('selectedTab');
  this.mainContainer_ = this.createMainContainer_(opt_container);
  this.tabTable_ = this.createTabTable_();
  this.displayTabs(false);
  //  gadgets.TabSet.addCSS_([  ].join(''));
};

gadgets.config.register('tabset', {}, function(configuration) {
  // Injects the default stylesheet for tabs
  gadgets.TabSet.addCSS_(configuration['tabs']['css'].join(''));
});


/**
 * Adds a new tab based on the name-value pairs specified in opt_params.
 * @param {string} tabName Label of the tab to create.
 * @param {string|Object=} opt_params Optional parameter object. The following
 *                   properties are supported:
 *                   .contentContainer An existing HTML element to be used as
 *                     the tab content container. If omitted, the tabs
 *                     library creates one.
 *                   .callback A callback function to be executed when the tab
 *                     is selected.
 *                   .tooltip A tooltip description that pops up when user moves
 *                     the mouse cursor over the tab.
 *                   .index The index at which to insert the tab. If omitted,
 *                     the new tab is appended to the end.
 * @return {string} DOM id of the tab container.
 */
gadgets.TabSet.prototype.addTab = function(tabName, opt_params) {
  if (typeof opt_params === 'string') {
    opt_params = {contentContainer: document.getElementById(arguments[1]),
      callback: arguments[2]};
  }

  var params = opt_params || {};

  var tabIndex = -1;
  if (params.index >= 0 && params.index < this.tabs_.length) {
    tabIndex = params.index;
  }
  var tab = this.createTab_(tabName, {
    contentContainer: params.contentContainer,
    callback: params.callback,
    tooltip: params.tooltip
  });

  var tr = this.tabTable_.rows[0];
  if (this.tabs_.length > 0) {
    var filler = document.createElement('td');
    filler.className = this.cascade_('tablib_spacerTab');
    filler.appendChild(document.createTextNode(' '));

    var ref = tabIndex < 0 ? tr.cells[tr.cells.length - 1] : this.tabs_[tabIndex].td_;
    tr.insertBefore(filler, ref);
    tr.insertBefore(tab.td_, tabIndex < 0 ? ref : filler);
  } else {
    tr.insertBefore(tab.td_, tr.cells[tr.cells.length - 1]);
  }

  if (tabIndex < 0) {
    tabIndex = this.tabs_.length;
    this.tabs_.push(tab);
  } else {
    this.tabs_.splice(tabIndex, 0, tab);

    // Inserting may change selected tab's index
    this.saveSelectedTabIndex_();
  }

  var selectedIndex = parseInt(this.selectedTabIndex_, 10);
  if (isNaN(selectedIndex)) {
    if (tabName == this.defaultTabName_ || (!this.defaultTabName_ && tabIndex === 0)) {
      this.selectTab_(tab);
    }
  } else if (selectedIndex == tabIndex) {
    this.selectTab_(tab, true);
  }

  this.tabsAdded_++;
  this.displayTabs(true);
  this.adjustNavigation_();

  return tab.contentContainer_.id;
};

/**
 * Removes a tab at tabIndex and all of its associated content.
 * @param {number} tabIndex Index of the tab to remove.
 */
gadgets.TabSet.prototype.removeTab = function(tabIndex) {
  var tab = this.tabs_[tabIndex];
  if (tab) {
    if (tab === this.selectedTab_) {
      var maxIndex = this.tabs_.length - 1;
      if (maxIndex > 0) {
        this.selectTab_(tabIndex < maxIndex ?
            this.tabs_[tabIndex + 1] :
            this.tabs_[tabIndex - 1]);
      }
    }
    var tr = this.tabTable_.rows[0];
    if (this.tabs_.length > 1) {
      tr.removeChild(tabIndex ? tab.td_.previousSibling : tab.td_.nextSibling);
    }
    tr.removeChild(tab.td_);
    this.mainContainer_.removeChild(tab.contentContainer_);
    this.tabs_.splice(tabIndex, 1);
    this.adjustNavigation_();
    if (this.tabs_.length === 0) {
      this.displayTabs(false);
      this.selectedTab_ = null;
    }
  }
};

/**
 * Returns the currently selected tab object.
 * @return {gadgets.Tab} The currently selected tab object.
 */
gadgets.TabSet.prototype.getSelectedTab = function() {
  return this.selectedTab_;
};

/**
 * Selects the tab at tabIndex and fires the tab's callback function if it
 * exists. If the tab is already selected, the callback is not fired.
 * @param {number} tabIndex Index of the tab to select.
 */
gadgets.TabSet.prototype.setSelectedTab = function(tabIndex) {
  if (this.tabs_[tabIndex]) {
    this.selectTab_(this.tabs_[tabIndex]);
  }
};

/**
 * Swaps the positions of tabs at tabIndex1 and tabIndex2. The selected tab
 * does not change, and no callback functions are called.
 * @param {number} tabIndex1 Index of the first tab to swap.
 * @param {number} tabIndex2 Index of the secnod tab to swap.
 */
gadgets.TabSet.prototype.swapTabs = function(tabIndex1, tabIndex2) {
  var tab1 = this.tabs_[tabIndex1];
  var tab2 = this.tabs_[tabIndex2];
  if (tab1 && tab2) {
    var tr = tab1.td_.parentNode;
    var slot = tab1.td_.nextSibling;
    tr.insertBefore(tab1.td_, tab2.td_);
    tr.insertBefore(tab2.td_, slot);
    this.tabs_[tabIndex1] = tab2;
    this.tabs_[tabIndex2] = tab1;
  }
};


/**
 * Returns an array of all existing tab objects.
 * @return {Array.<gadgets.Tab>} Array of all existing tab objects.
 */
gadgets.TabSet.prototype.getTabs = function() {
  return this.tabs_;
};

/**
 * Sets the alignment of tabs. Tabs are center-aligned by default.
 * @param {string} align 'left', 'center', or 'right'.
 * @param {number=} opt_offset Optional parameter to set the number of pixels
 *                   to offset tabs from the left or right edge. The default
 *                   value is 3px.
 */
gadgets.TabSet.prototype.alignTabs = function(align, opt_offset) {
  var tr = this.tabTable_.rows[0];
  var left = tr.cells[0];
  var right = tr.cells[tr.cells.length - 1];
  var offset = isNaN(opt_offset) ? '3px' : opt_offset + 'px';
  left.style.width = align === 'left' ? offset : '';
  right.style.width = align === 'right' ? offset : '';
  // In Opera and potentially some other browsers, changes to the width of
  // table cells aren't rendered.  To fix this, we force to re-render the
  // table by hiding and showing it again.
  this.tabTable_.style.display = 'none';
  this.tabTable_.style.display = '';
};

/**
 * Shows or hides tabs and all associated content.
 * @param {boolean} display true to show tabs; false to hide tabs.
 */
gadgets.TabSet.prototype.displayTabs = function(display) {
  this.mainContainer_.style.display = display ? 'block' : 'none';
};

/**
 * Returns the tab headers container element.
 * @return {Element} The tab headers container element.
 */
gadgets.TabSet.prototype.getHeaderContainer = function() {
  return this.tabTable_;
};

/**
 * Helper method that returns an HTML container element to which all tab-related
 * content will be appended.
 * This container element is created and inserted as the first child of the
 * gadget if opt_element is not specified.
 * @param {Element=} opt_element Optional HTML container element.
 * @return {Element} HTML container element.
 * @private
 */
gadgets.TabSet.prototype.createMainContainer_ = function(opt_element) {
  var newId = 'tl_' + this.moduleId_;
  var container = opt_element || document.getElementById(newId);

  if (!container) {
    container = document.createElement('div');
    container.id = newId;
    document.body.insertBefore(container, document.body.firstChild);
  }

  container.className = this.cascade_('tablib_main_container') + ' ' +
      container.className;

  return container;
};

/**
 * Helper method that expands a class name into two class names.
 * @param {string} label CSS class.
 * @return {string} Expanded class names.
 * @private
 */
gadgets.TabSet.prototype.cascade_ = function(label) {
  return label + ' ' + label + this.moduleId_;
};

/**
 * Helper method that creates the tabs table and inserts it into the main
 * container as the first child.
 * @return {Element} HTML element of the tab container table.
 * @private
 */
gadgets.TabSet.prototype.createTabTable_ = function() {
  var table = document.createElement('table');
  table.id = this.mainContainer_.id + '_header';
  table.className = this.cascade_('tablib_table');
  table.cellSpacing = '0';
  table.cellPadding = '0';

  var tbody = document.createElement('tbody');
  var tr = document.createElement('tr');
  tbody.appendChild(tr);
  table.appendChild(tbody);

  var emptyTd = document.createElement('td');
  emptyTd.className = this.cascade_('tablib_emptyTab');
  emptyTd.appendChild(document.createTextNode(' '));
  tr.appendChild(emptyTd);
  tr.appendChild(emptyTd.cloneNode(true));

  // Construct a wrapper table around our tab table to house the navigation
  // elements. These elements will appear if the tab table overflows.
  var navTable = document.createElement('table');
  navTable.id = this.mainContainer_.id + '_navTable';
  navTable.style.width = '100%';
  navTable.cellSpacing = '0';
  navTable.cellPadding = '0';
  navTable.style.tableLayout = 'fixed';
  var navTbody = document.createElement('tbody');
  var navTr = document.createElement('tr');
  navTbody.appendChild(navTr);
  navTable.appendChild(navTbody);

  // Create the left navigation element.
  var leftNavTd = document.createElement('td');
  leftNavTd.className = this.cascade_('tablib_emptyTab') + ' ' +
                        this.cascade_('tablib_navContainer');
  leftNavTd.style.textAlign = 'left';
  leftNavTd.style.display = '';
  var leftNav = document.createElement('a');
  leftNav.href = 'javascript:void(0)';
  leftNav.innerHTML = '&laquo;';
  leftNavTd.appendChild(leftNav);
  navTr.appendChild(leftNavTd);

  // House the actual tab table in the middle, hiding any overflow.
  var tabNavTd = document.createElement('td');
  navTr.appendChild(tabNavTd);
  var wrapper = document.createElement('div');
  wrapper.style.width = '100%';
  wrapper.style.overflow = 'hidden';
  wrapper.appendChild(table);
  tabNavTd.appendChild(wrapper);

  // Create the right navigation element.
  var rightNavTd = document.createElement('td');
  rightNavTd.className = this.cascade_('tablib_emptyTab') + ' ' +
                         this.cascade_('tablib_navContainer');
  rightNavTd.style.textAlign = 'right';
  rightNavTd.style.display = '';
  var rightNav = document.createElement('a');
  rightNav.href = 'javascript:void(0)';
  rightNav.innerHTML = '&raquo;';
  rightNavTd.appendChild(rightNav);
  navTr.appendChild(rightNavTd);

  // Register onclick event handlers for smooth scrolling.
  var me = this;
  leftNav.onclick = function(event) {
    me.smoothScroll_(wrapper, -120);

    event = event || window.event;
    if (event.stopPropagation) {
      event.stopPropagation();
    } else {
      event.cancelBubble = true;
    }
    if (event.preventDefault) {
      event.preventDefault();
    } else {
      event.returnValue = false;
    }
  };
  rightNav.onclick = function(event) {
    me.smoothScroll_(wrapper, 120);

    event = event || window.event;
    if (event.stopPropagation) {
      event.stopPropagation();
    } else {
      event.cancelBubble = true;
    }
    if (event.preventDefault) {
      event.preventDefault();
    } else {
      event.returnValue = false;
    }
  };

  // Swap left and right scrolling if direction is RTL.
  if (this.rtl_) {
    var temp = leftNav.onclick;
    leftNav.onclick = rightNav.onclick;
    rightNav.onclick = temp;
  }

  // If we're already displaying tabs, then remove them.
  if (this.navTable_) {
    this.mainContainer_.replaceChild(navTable, this.navTable_);
  } else {
    this.mainContainer_.insertBefore(navTable, this.mainContainer_.firstChild);
    var adjustNavigationFn = function() {
      me.adjustNavigation_();
    };
    gadgets.util.attachBrowserEvent(window, 'resize', adjustNavigationFn, false);
  }

  this.navTable_ = navTable;
  this.leftNavContainer_ = leftNavTd;
  this.rightNavContainer_ = rightNavTd;
  this.tabsContainer_ = wrapper;

  return table;
};

/**
 * Helper method that shows or hides the navigation elements.
 * @private
 */
gadgets.TabSet.prototype.adjustNavigation_ = function() {
  this.leftNavContainer_.style.display = 'none';
  this.rightNavContainer_.style.display = 'none';
  if (this.tabsContainer_.scrollWidth <= this.tabsContainer_.offsetWidth) {
    if (this.tabsContainer_.scrollLeft) {
      // to avoid JS error in IE
      this.tabsContainer_.scrollLeft = 0;
    }
    return;
  }

  this.leftNavContainer_.style.display = '';
  this.rightNavContainer_.style.display = '';
  if (this.tabsContainer_.scrollLeft + this.tabsContainer_.offsetWidth >
      this.tabsContainer_.scrollWidth) {
    this.tabsContainer_.scrollLeft = this.tabsContainer_.scrollWidth -
                                     this.tabsContainer_.offsetWidth;
  } else if (this.rtl_) {
    this.tabsContainer_.scrollLeft = this.tabsContainer_.scrollWidth;
  }
};

/**
 * Helper method that smoothly scrolls the tabs container.
 * @param {Element} container The tabs container element.
 * @param {number} distance The amount of pixels to scroll right.
 * @private
 */
gadgets.TabSet.prototype.smoothScroll_ = function(container, distance) {
  var scrollAmount = 10;
  if (!distance) {
    return;
  } else {
    container.scrollLeft += (distance < 0) ? -scrollAmount : scrollAmount;
  }

  var nextScroll = Math.min(scrollAmount, Math.abs(distance));
  var me = this;
  var timeoutFn = function() {
    me.smoothScroll_(container, (distance < 0) ? distance + nextScroll :
                                                 distance - nextScroll);
  };
  setTimeout(timeoutFn, 10);
};

/**
 * Helper function that dynamically inserts CSS rules to the page.
 * @param {string} cssText CSS rules to inject.
 * @private
 */
gadgets.TabSet.addCSS_ = function(cssText) {
  var head = document.getElementsByTagName('head')[0];
  if (head) {
    var styleElement = document.createElement('style');
    styleElement.type = 'text/css';
    if (styleElement.styleSheet) {
      styleElement.styleSheet.cssText = cssText;
    } else {
      styleElement.appendChild(document.createTextNode(cssText));
    }
    head.insertBefore(styleElement, head.firstChild);
  }
};

/**
 * Helper method that creates a new gadgets.Tab object.
 * @param {string} tabName Label of the tab to create.
 * @param {Object} params Parameter object. The following properties
 *                   are supported:
 *                   .contentContainer An existing HTML element to be used as
 *                     the tab content container. If omitted, the tabs
 *                     library creates one.
 *                   .callback A callback function to be executed when the tab
 *                     is selected.
 *                   .tooltip A tooltip description that pops up when user moves
 *                     the mouse cursor over the tab.
 * @return {gadgets.Tab} A new gadgets.Tab object.
 * @private
 */
gadgets.TabSet.prototype.createTab_ = function(tabName, params) {
  var tab = new gadgets.Tab(this);
  tab.contentContainer_ = params.contentContainer;
  tab.callback_ = params.callback;
  tab.td_ = document.createElement('td');
  tab.td_.title = params.tooltip || '';
  tab.td_.innerHTML = html_sanitize(tabName);
  tab.td_.className = this.cascade_('tablib_unselected');
  tab.td_.onclick = this.setSelectedTabGenerator_(tab);

  if (!tab.contentContainer_) {
    tab.contentContainer_ = document.createElement('div');
    tab.contentContainer_.id = this.mainContainer_.id + '_' + this.tabsAdded_;
    this.mainContainer_.appendChild(tab.contentContainer_);
  } else if (tab.contentContainer_.parentNode !== this.mainContainer_) {
    this.mainContainer_.appendChild(tab.contentContainer_);
  }
  tab.contentContainer_.style.display = 'none';
  tab.contentContainer_.className = this.cascade_('tablib_content_container') +
      ' ' + tab.contentContainer_.className;
  return tab;
};

/**
 * Helper method that creates a function to select the specified tab.
 * @param {gadgets.Tab} tab The tab to select.
 * @return {function()} Callback function to select the tab.
 * @private
 */
gadgets.TabSet.prototype.setSelectedTabGenerator_ = function(tab) {
  return function() { tab.handle_.selectTab_(tab); };
};

/**
 * Helper method that selects a tab and unselects the previously selected.
 * If the tab is already selected, then callback is not executed.
 * @param {gadgets.Tab} tab The tab to select.
 * @private
 */
gadgets.TabSet.prototype.selectTab_ = function(tab, opt_inhibit_save) {
  if (this.selectedTab_ === tab) {
    return;
  }

  if (this.selectedTab_) {
    this.selectedTab_.td_.className = this.cascade_('tablib_unselected');
    this.selectedTab_.td_.onclick =
        this.setSelectedTabGenerator_(this.selectedTab_);
    this.selectedTab_.contentContainer_.style.display = 'none';
  }

  tab.td_.className = this.cascade_('tablib_selected');
  tab.td_.onclick = null;
  tab.contentContainer_.style.display = 'block';
  this.selectedTab_ = tab;

  // Remember which tab is selected only if nosave is not true.
  var nosave = (opt_inhibit_save === true) ? true : false;
  if (!nosave) {
    this.saveSelectedTabIndex_();
  }

  if (typeof tab.callback_ === 'function') {
    tab.callback_(tab.contentContainer_.id);
  }
};

gadgets.TabSet.prototype.saveSelectedTabIndex_ = function() {
  try {
    var currentTabIndex = this.selectedTab_.getIndex();
    if (currentTabIndex >= 0) {
      this.selectedTabIndex_ = currentTabIndex;
      this.prefs_.set('selectedTab', currentTabIndex);
    }
  } catch (e) {
    // ignore.  setprefs is optional for tablib.
  }
};

// Aliases for legacy code

/**
 * @type {gadgets.TabSet}
 * @deprecated
 */
var _IG_Tabs = gadgets.TabSet;
_IG_Tabs.prototype.moveTab = _IG_Tabs.prototype.swapTabs;

/**
 * @param {string} tabName
 * @param {function()} callback
 * @deprecated
 */
_IG_Tabs.prototype.addDynamicTab = function(tabName, callback) {
  return this.addTab(tabName, {callback: callback});
};

