/**
 * \file zptabs.js
 * Tabs widget. Extends base Widget class (utils/zpwidget.js).
 *
 * Copyright (c) 2004-2005 by Zapatec, Inc.
 * http://www.zapatec.com
 * 1700 MLK Way, Berkeley, California,
 * 94709, U.S.A.
 * All rights reserved.
 */

/**
 * \internal Constructor.
 *
 * \param objArgs [object] object properies.
 */
Zapatec.Tab = function(objArgs) {
  // Reference to corresponding a element
  this.linkNode = null;
  // Reference to corresponding div element
  this.container = null;
  // Child element which gets focus first (needed for keyboard navigation)
  this.focusOn = null;
  // Keyboard navigation type
  this.tab2tab = false;
  // URL of the content
  this.url = null;
  // Tab number
  this.index = null;
  // Initialize
  if (arguments.length > 0) this.init(objArgs);
};

/**
 * \internal Initializes object.
 *
 * \param objArgs [string] object properies:
 * {
 *   tabs: [object] Zapatec.Tabs object,
 *   id: [string, optional] id of the tab,
 *   innerHTML: [string] label,
 *   accessKey: [string, optional] access key,
 *   title: [string, optional] title,
 *   content: [object, optional] HTMLElement object holding content of the tab,
 *   url: [string, optional] URL of the content
 * }
 */
Zapatec.Tab.prototype.init = function(objArgs) {
  // Check arguments
  if (!objArgs || !objArgs.tabs || typeof objArgs.innerHTML == 'undefined') {
    return;
  }
  var objTabs = objArgs.tabs;
  if (!objTabs.config.tabBar || !objTabs.config.tabs) {
    return;
  }
  // Get content URL
  if (typeof objArgs.url == 'string') {
    this.url = objArgs.url;
  }
  // Get keyboard navigation type
  this.tab2tab = objTabs.config.tab2tab ? true : false;
  // Get container
  if (objArgs.content) {
    this.container = objArgs.content;
  } else {
    // Create container
    this.container = Zapatec.Utils.createElement('div', objTabs.config.tabs);
  }
  // Hide content
  this.container.style.display = 'none';
  // Get id
  if (objArgs.id) {
    this.id = objArgs.id;
  } else {
    this.id = this.container.getAttribute('id');
    if (typeof this.id == 'string') {
      this.container.removeAttribute('id');
    }
  }
  if (typeof this.id != 'string' || !this.id.length) {
    // Generate unique id
    this.id = Zapatec.Utils.generateID('tab');
  }
  // Create link node
  this.linkNode = Zapatec.Utils.createElement('a');
  this.linkNode.setAttribute('href', '#' + this.id);
  // Need "<span><span>" for themes
  this.linkNode.innerHTML = '<span><span>' + objArgs.innerHTML +
   '</span></span>';
  if (objArgs.accessKey) {
    this.linkNode.accessKey = objArgs.accessKey;
  }
  if (objArgs.title) {
    this.linkNode.setAttribute('title', objArgs.title);
  }
  var self = this;
  // Mouse navigation support
  this.linkNode.onclick = function() {
    objTabs.changeTab(self.id);
    if (this.blur) {
      this.blur();
    }
    return true; // To change document.URL
  };
  // Keyboard navigation support
  this.linkNode.tabIndex = Zapatec.Tab.tabIndex;
  if (!this.tab2tab) {
    // Next tabIndex is reserved for tab content
    Zapatec.Tab.tabIndex += 2;
  // } else {
    // No need to increase tabIndex
  }
  // Activate tab on focus
  this.linkNode.onfocus = function() {
    objTabs.changeTab(self.id);
    // Continue event
    return true;
  };
  // Setup keys
  this.linkNode.onkeydown = function(ev) {
    ev || (ev = window.event);
    switch (ev.keyCode) {
      case 13: // Enter
      case 32: // Space
        if (self.focusOn && self.focusOn.focus) {
          self.focusOn.focus();
        }
        // Stop event
        return false;
    }
    // Continue event
    return true;
  }
  // Determine child element which gets focus first
  if (this.container.hasChildNodes()) {
    this.getFocusOn();
  }
  // Put this tab on tab bar
  objTabs.config.tabBar.appendChild(this.linkNode);
  this.index = objTabs.tabsArray.length;
  // Attach this Tab to Tabs object
  objTabs.tabsArray[this.index] = this;
  objTabs.tabs[this.id] = this;
};

/**
 * Counter that gets increased after Tab is added. Required for keyboard
 * navigation support.
 *
 * Note:
 * In Opera tabIndex property value must be > 0, otherwise node is ignored.
 * Mozilla starts travelling from nodes with tabIndex > 0.
 * IE starts travelling from nodes with tabIndex == 0.
 * All nodes without tabIndex set explicitly have tabIndex == 0.
 */
Zapatec.Tab.tabIndex = 1000;

/**
 * \internal Determines child node of the container which gets focus first.
 * Needed for keyboard navigation.
 */
Zapatec.Tab.prototype.getFocusOn = function() {
  // Remove old value
  this.focusOn = null;
  // Check keyboard navigation type
  if (this.tab2tab) {
    return;
  }
  // Put it in separate process to speed up initialization
  var self = this;
  setTimeout(function() {
    // Flag to determine lower tabIndex
    var iTabIndex = 0;
    // Gets element with lower tabIndex
    function parse(objNode) {
      var objChild = objNode.firstChild;
      while (objChild) {
        if (objChild.nodeType == 1) { // ELEMENT_NODE
          var strTag = objChild.tagName.toLowerCase();
          if (strTag == 'a' || strTag == 'input' || strTag == 'select' ||
           strTag == 'textarea' || strTag == 'button') {
            // Element may obtain focus
            if (!self.focusOn) {
              self.focusOn = objChild;
            } else if (objChild.tabIndex && objChild.tabIndex > 0 &&
             (!iTabIndex || iTabIndex > objChild.tabIndex)) {
              self.focusOn = objChild;
              iTabIndex = objChild.tabIndex;
            }
            if (!objChild.tabIndex) {
              objChild.tabIndex = self.linkNode.tabIndex + 1;
            }
          }
          parse(objChild);
        }
        objChild = objChild.nextSibling;
      }
    };
    // Parse tab contenet
    parse(self.container);
  }, 0);
};

/**
 * \internal Sets tab content from given HTML fragment.
 *
 * \param strHtml [string] HTML fragment.
 */
Zapatec.Tab.prototype.setInnerHtml = function(strHtml) {
  // Set tab content
  Zapatec.Transport.setInnerHtml({
    html: strHtml,
    container: this.container
  });
  // Determine child element which gets focus first
  this.getFocusOn();
}

/**
 * Constructor.
 *
 * \param objArgs [object] object properies.
 */
Zapatec.Tabs = function(objArgs) {
  // User configuration
  this.config = {};
  // Initialize object
  if (arguments.length > 0) this.init(objArgs);
};

// Inherit parent class
Zapatec.Tabs.prototype = new Zapatec.Widget();
Zapatec.Tabs.SUPERclass = Zapatec.Widget.prototype;

/**
 * Initializes object.
 *
 * Defines following config options:
 *
 * "tabBar" [object or string] Element or id of element that will hold the tab
 * bar.
 *
 * "tabs" [object or string] Element or id of element that will hold the tabs.
 * This option is also used as "source" if the last is not specified.
 *
 * "onInit" [function] Called when tabs are created. Users can perform
 * problem-specific initializations at this stage. No arguments.
 *
 * "onTabChange" [function] Called after the tab was changed. Receives
 * following object:
 * {
 *   oldTabId: [string] id of the old tab,
 *   newTabId: [string] id of the new tab
 * }
 *
 * "onBeforeTabChange" [function] Called when the tab is about to be changed,
 * just before. Receives following object:
 * {
 *   oldTabId: [string] id of the old tab,
 *   newTabId: [string] id of the new tab
 * }
 * Should return boolean. If returns other than "true", tab will not be changed.
 *
 * "ignoreUrl" [boolean] If true, "#tabId" part of URL is ignored and first tab
 * is opened after initialization.
 *
 * "tab2tab" [boolean] If true, pressing Tab key will open next tab. If false,
 * Tab key will also navigate through anchors and form fields inside tab.
 * Default: false.
 *
 * \param objArgs [object] object properies.
 */
Zapatec.Tabs.prototype.init = function(objArgs) {
  // Define config options
  this.config.tabBar = null;
  this.config.tabs = null;
  this.config.onInit = null;
  this.config.onTabChange = null;
  this.config.onBeforeTabChange = null;
  this.config.ignoreUrl = false;
  this.config.tab2tab = false;
  // Call parent init
  Zapatec.Tabs.SUPERclass.init.call(this, objArgs);
  // Continue initialization
  this.config.tabBar = Zapatec.Widget.getElementById(this.config.tabBar);
  if (!this.config.tabBar) {
    return;
  }
  this.config.tabs = Zapatec.Widget.getElementById(this.config.tabs);
  if (!this.config.tabs) {
    return;
  }
  // Apply theme
  Zapatec.Utils.addClass(this.config.tabBar, this.getClassName({
    prefix: 'zpTabs'
  }));
  Zapatec.Utils.addClass(this.config.tabs, this.getClassName({
    prefix: 'zpTabs',
    suffix: 'Content'
  }));
  // To maintain by id
  this.tabs = {};
  // To maintain by index
  this.tabsArray = [];
  // Call parent method to load data from the specified source
  this.loadData();
  // Index of the current tab
  this.currentIndex = -1;
  // onInit
  if (typeof this.config.onInit == 'function') {
    this.config.onInit();
  }
  // Go to first tab
  if (this.tabsArray.length) {
    var strId = this.tabsArray[0].id;
    if (!this.config.ignoreUrl) {
      if (/#([^\/]+)$/.test(document.URL) && this.tabs[RegExp.$1]) {
        strId = RegExp.$1;
      }
    }
    this.changeTab(strId);
  }
};

/**
 * \internal Loads data from the JSON source.
 *
 * Following format is recognized:
 * \code
 * {
 *   tabs: [
 *     {
 *       id: [string, optional] id of the tab,
 *       innerHTML: [string] label,
 *       accessKey: [string, optional] access key,
 *       title: [string] title,
 *       url: [string] URL of the content
 *     },
 *     ...
 *   ]
 * }
 * \endcode
 *
 * \param objSource [object] JSON object.
 */
Zapatec.Tabs.prototype.loadDataJson = function(objSource) {
  // Check arguments
  if (!this.config.tabBar || !this.config.tabs) {
    return;
  }
  if (!objSource || !objSource.tabs || !objSource.tabs.length) {
    return;
  }
  // Parse source
  var iLen = objSource.tabs.length;
  for (var iTab = 0; iTab < iLen; iTab++) {
    var objTabDef = objSource.tabs[iTab];
    objTabDef.tabs = this;
    // Create tab
    var objTab = new Zapatec.Tab(objTabDef);
    // Return id of the tab trough objSource
    if (objTab.id) {
      objTabDef.id = objTab.id;
    }
  }
};

/**
 * \internal Loads data from the HTML source.
 *
 * Following format is recognized:
 * \code
 * <div id="tabs">
 *   <div id="tab-begin">
 *     <label>The first tab</label>
 *     ... the tab contents here ...
 *   </div>
 *   <div id="tab-second">
 *     <label>The second tab</label>
 *     ... the tab contents here ...
 *   </div>
 *   ...
 * </div>
 * \endcode
 *
 * \param objSource [object] HTMLElement object.
 */
Zapatec.Tabs.prototype.loadDataHtml = function(objSource) {
  // Check arguments
  if (!this.config.tabBar || !this.config.tabs) {
    return;
  }
  if (!objSource) {
    objSource = this.config.tabs;
  }
  // Parse source
  var iLen = objSource.childNodes.length;
  for (var iChild = 0; iChild < iLen; iChild++) {
    var objChild = objSource.childNodes[iChild];
    if (objChild.nodeType == 1) { // ELEMENT_NODE
      // Get label
      var objLabel = Zapatec.Utils.getFirstChild(objChild, 'label');
      if (!objLabel) {
        continue;
      }
      // Create tab
      var objTab = new Zapatec.Tab({
        tabs: this,
        innerHTML: objLabel.innerHTML,
        accessKey: objLabel.getAttribute('accesskey'),
        title: objLabel.getAttribute('title'),
        content: objChild
      });
      // Return id of the tab trough container element
      if (objTab.id) {
        objTab.container.setAttribute('id', objTab.id);
      }
      // Remove label
      objLabel.parentNode.removeChild(objLabel);
    }
  }
};

/**
 * Display a new tab. If onBeforeTabChange() returns false, the operation is
 * cancelled.
 *
 * \param strNewTabId [string] id of the new tab.
 */
Zapatec.Tabs.prototype.changeTab = function(strNewTabId) {
  var strCurrTabId = null;
  var objTab = null;
  if (this.tabsArray[this.currentIndex]) {
    strCurrTabId = this.tabsArray[this.currentIndex].id;
    objTab = this.tabsArray[this.currentIndex];
  }
  if (strCurrTabId != strNewTabId) {
    // Check if callback function allows to change tab
    var boolChangeTab = true;
    if (typeof this.config.onBeforeTabChange == 'function') {
      boolChangeTab = this.config.onBeforeTabChange({
        oldTabId: strCurrTabId,
        newTabId: strNewTabId
      });
    }
    if (!boolChangeTab) {
      // Return focus back
      if (objTab && objTab.linkNode.focus) {
        // Need to focus on tab first because FF 1.5 seems to have separate
        // focus for links
        objTab.linkNode.focus();
        // Focus on content (in separate thread to let it focus on tab first)
        setTimeout(function() {
          if (objTab.focusOn && objTab.focusOn.focus) {
            objTab.focusOn.focus();
          }
        }, 0);
      }
      return;
    }
    // Change tab
    if (objTab) {
      objTab.container.style.display = 'none';
      Zapatec.Utils.removeClass(objTab.linkNode, 'zpTabsActive');
    }
    objTab = this.tabs[strNewTabId];
    objTab.container.style.display = 'block';
    Zapatec.Utils.addClass(objTab.linkNode, 'zpTabsActive');
    this.currentIndex = objTab.index;
    // Load content from external source if needed
    if (!objTab.container.childNodes.length && objTab.url) {
      var self = this;
      Zapatec.Transport.fetch({
        url: objTab.url,
        onLoad: function(objRequest) {
          // Populate tab
          objTab.setInnerHtml(objRequest.responseText);
          // onTabChange
          if (typeof self.config.onTabChange == 'function') {
            self.config.onTabChange({
              oldTabId: strCurrTabId,
              newTabId: strNewTabId
            });
          }
        }
      });
    } else {
      // onTabChange
      if (typeof this.config.onTabChange == 'function') {
        this.config.onTabChange({
          oldTabId: strCurrTabId,
          newTabId: strNewTabId
        });
      }
    }
  }
};

/**
 * Moves to the next tab.
 */
Zapatec.Tabs.prototype.nextTab = function() {
  if (this.currentIndex < this.tabsArray.length - 1) {
    this.changeTab(this.tabsArray[this.currentIndex + 1].id);
  } else {
    this.firstTab();
  }
};

/**
 * Moves to the previous tab.
 */
Zapatec.Tabs.prototype.prevTab = function() {
  if (this.currentIndex > 0) {
    this.changeTab(this.tabsArray[this.currentIndex - 1].id);
  } else {
    this.lastTab();
  }
};

/**
 * Moves to the first tab.
 */
Zapatec.Tabs.prototype.firstTab = function() {
  this.changeTab(this.tabsArray[0].id);
};

/**
 * Moves to the last tab.
 */
Zapatec.Tabs.prototype.lastTab = function() {
  this.changeTab(this.tabsArray[this.tabsArray.length - 1].id);
};

/**
 * Indicates if current tab is first.
 *
 * \return [boolean] true if we are at the first tab, false otherwise.
 */
Zapatec.Tabs.prototype.isFirstTab = function() {
  return this.currentIndex == 0;
};

/**
 * Indicates if current tab is last.
 *
 * \return [boolean] true if we are at the last tab, false otherwise.
 */
Zapatec.Tabs.prototype.isLastTab = function() {
  return this.currentIndex == this.tabsArray.length - 1;
};
;
Zapatec.Utils.addEvent(window, 'load', Zapatec.Utils.checkActivation);
