// Copyright 2010 Google Inc.  All Rights Reserved.

/**
 * @fileoverview A scroller widget, used in conjunction with
 * http://www.google.com/css/modules/scroller/g-scroller.css.
 *
 * This file requires http://www.google.com/js/gweb/core.js
 * to work correctly.
 *
 * @author jeremydw (Jeremy Weinstein)
 */

/**
 * Initialize gweb namespace.
 */
var gweb = gweb || {};


/**
 * Initialize gweb.ui namespace.
 */
gweb.ui = gweb.ui || {};


/**
 * Constructor for a scroller widget.
 * @param {object} conf An object with configuration parameters.
 *     scrollerId: The id of the scroller.
 *     oneAtATimeFlag (optional): Whether to scroll by one tab at a time or an
 *         entire page.
 *     slidesContainerId (optional): The id of the slide container. If left
 *         blank, assume no slides.
 *     centerFlag (optional): Whether to center the scroller on selected tab.
 *     randomFlag (optional): Whether to pick a random element on load.
 *     selectItemCallback (optional): Callback function for when an item is
 *          selected.
 *     defaultItem (optional): The id of the default-selected item. If left
 *         blank, select first tab.
 * @constructor
 */
gweb.ui.Scroller = function(conf) {
  this.scrollerEl = gweb.dom.getElement(conf.scrollerId);
  this.slidesContainerEl = gweb.dom.getElement(conf.slidesContainerId);
  this.viewportEl = gweb.dom.query('ul', this.scrollerEl)[0];
  this.oneAtATimeFlag = conf.oneAtATimeFlag;
  this.centerFlag = conf.centerFlag;
  this.randomFlag = conf.randomFlag;
  this.selectItemCallback = conf.selectItemCallback;

  this.totalTabWidth;
  this.tabWidth = gweb.dom.query('ul li', this.scrollerEl)[0].scrollWidth;
  this.tabsPerPage = Math.ceil(this.viewportEl.parentNode.offsetWidth /
      this.tabWidth);
  // Force overflow to hidden. By default this is 'scroll' to enable
  // functionality for browsers without JS enabled.
  this.viewportEl.parentNode.style.overflow = 'hidden';
  this.pageWidth = this.tabWidth * this.tabsPerPage;

  this.prevEl = gweb.dom.query('.g-scroller-controls .g-scroller-previous',
      this.scrollerEl)[0];
  this.nextEl = gweb.dom.query('.g-scroller-controls .g-scroller-next',
      this.scrollerEl)[0];

  this.isAnimating = false;
  this.hasSelectedItem = false;
  this.itemIds = [];
  this.selectedItem;
  this.currentPage;
  this.items = {};

  this.initItemsObject_();  // Sets this.totalTabWidth.
  this.scrollLimit = (this.tabWidth * this.tabsPerPage) - this.totalTabWidth;

  // Set the arrows to disabled by default.
  gweb.dom.classes.add(this.nextEl, 'disabled-prev');
  gweb.dom.classes.add(this.prevEl, 'disabled-prev');

  gweb.events.listen(this.nextEl, 'click', function(e) {
    this.scroll('right');
    e.preventDefault();
  }, null, this);
  gweb.events.listen(this.prevEl, 'click', function(e) {
    this.scroll('left');
    e.preventDefault();
  }, null, this);

  // Handle keypress events.
  gweb.events.listen(this.scrollerEl, 'keyup', function(e) {
    switch (e.keyCode) {
      //37 is the key code for the left arrow key.
      case 37:
        var itemIndex = this.selectedItem.index - 1;
      break;
      //39 is the key code for the right arrow key.
      case 39:
        var itemIndex = this.selectedItem.index + 1;
      break;
    }
    var item = this.items[this.itemIds[itemIndex]];

    if (item) {
      this.selectItem(item);
    }
  }, null, this);

  // Set the default item to the item specified in the hash if it exists,
  // otherwise, set it to the default item specified in the conf.
  var specifiedItem = this.getStringAfterHash(location.href);

  // If randomFlag is set and there is no specified item, pick a random one.
  if (this.randomFlag && !specifiedItem) {
    specifiedItem = this.itemIds[Math.floor(Math.random() *
        this.itemIds.length)];
  }
  // If no defaultItem is set, use the first one in the list.
  if (!conf.defaultItem && !specifiedItem && !this.randomFlag) {
    specifiedItem = this.itemIds[0];
  }

  if (this.isValidItem(specifiedItem)) {
    var itemToSelect = specifiedItem;
  } else if (this.isValidItem(conf.defaultItem)) {
    var itemToSelect = conf.defaultItem;
  } else {
    var itemToSelect = this.items[this.itemIds[0]];
  }

  this.selectItem(itemToSelect);
};


/**
 * Initializes the items object and sets click actions. The items object is a
 * dictionary containing the slide and tab elements for each item.
 * @private
 */
gweb.ui.Scroller.prototype.initItemsObject_ = function() {
  var tabs = gweb.dom.query('div.slide');
  var len = tabs.length;

  this.totalTabWidth = this.tabWidth * len;
  this.viewportEl.style.width = this.totalTabWidth + 'px';

  for (var p = 0, i = 0; p < len; p++, i++) {
    var tab = tabs[p];
    var id = tab.getAttribute('id');
    var slide = gweb.dom.getElement(id);

    // Create an item for the items object.
    this.items[id] = {};
    var item = this.items[id];

    item.index = i;
    item.slide = slide;
    item.tab = tab;
    item.id = id;
    item.onPage = Math.ceil((i + 1) / this.tabsPerPage);
    // Set the max page to the last page.
    this.maxPage = item.onPage;
    this.itemIds.push(item.id);

    // Only add event listeners if a slide exists for this item.
    if (item.slide) {
      // Replace the ID of the slide so the page doesn't jump when changing the
      // location hash.
      item.slide.id = id + '_scroller';
      gweb.dom.classes.add(slide, 'hidden');
    }
    gweb.events.listen(tab, 'click', gweb.bind(this.selectItem, this, item));
  }
};


/**
 * Handler for clicking on a tab.
 * @param {object} item The item to select.
 * @param {object} opt_e The event object.
 */
gweb.ui.Scroller.prototype.selectItem = function(item, opt_e) {
  if (typeof(item) == 'string') {
    item = this.getItemFromId(item);
  }
  var selectedTabs =
      gweb.dom.query('.g-scroller-viewport div.slide.selected',
      this.scrollerEl);
  var all_slides = gweb.dom.query('.g-scroller-slide',
      this.slidesContainerEl);

  for (var i = 0, len = selectedTabs.length; i < len; i++) {
    gweb.dom.classes.remove(selectedTabs[i], 'selected');
  }
  for (var i = 0, len = all_slides.length; i < len; i++) {
    gweb.dom.classes.add(all_slides[i], 'hidden');
  }

  gweb.dom.classes.add(item.tab, 'selected');

  // Only hide slide if it exists.
  if (item.slide) {
    gweb.dom.classes.remove(item.slide, 'hidden');
  }
  if (this.centerFlag) {
    this.centerOnItem(item);
  }
  this.selectedItem = item;
  this.currentPage = item.onPage;

  if (this.selectItemCallback) {
    this.selectItemCallback(item);
  }

  this.hasSelectedItem = true;
};


/**
 * Scrolls the viewport. This method is called when a user clicks a
 *     directional scroll arrow. Handles scrolls for one tab at a time
 *     as well as entire pages of tabs.
 * @param {<'right'|'left'>string} direction The direction to scroll.
 */
gweb.ui.Scroller.prototype.scroll = function(direction) {
  var position;

  if (this.oneAtATimeFlag) {
    if (direction == 'right') {
      // Scroll a single tab right.
      position = this.viewportEl.offsetLeft - this.tabWidth;
    } else {
      // Scroll a single tab left.
      position = this.viewportEl.offsetLeft + this.tabWidth;
    }
  }
  else {
    if (direction == 'right') {
      // Scroll an entire page right.
      position = this.viewportEl.offsetLeft - this.pageWidth;

      if (!this.isAnimating) {
        this.currentPage++;
        if (this.currentPage > this.maxPage) {
          this.currentPage = this.maxPage;
        }
      }
    } else {
      // Scroll an entire page left.
      position = this.viewportEl.offsetLeft + this.pageWidth;

      if (!this.isAnimating) {
        this.currentPage--;
        if (this.currentPage < 1) {
          this.currentPage = 1;
        }
      }
    }
  }
  this.animateToPosition(position);
};


/**
 * Looks up an item from the items object based on its id.
 * @param {string} itemId The id of the item to lookup.
 * @return {object} The item from the items object.
 */
gweb.ui.Scroller.prototype.getItemFromId = function(itemId) {
  return this.items[itemId];
};


/**
 * Animates the viewport to a specified position.
 * @param {integer} position The position to scroll the viewport to.
 */
gweb.ui.Scroller.prototype.animateToPosition = function(position) {
  // Verify we aren't scrolling beyond the limits and set the controls.
  if (position >= 0) {
    position = 0;
    gweb.dom.classes.add(this.prevEl, 'disabled-prev');
    gweb.dom.classes.remove(this.nextEl, 'disabled-next');
  } else if (!(position <= this.scrollLimit)){
    gweb.dom.classes.remove(this.prevEl, 'disabled-prev');
  }
  if (position <= this.scrollLimit) {
    position = this.scrollLimit;
    gweb.dom.classes.add(this.nextEl, 'disabled-next');
    gweb.dom.classes.remove(this.prevEl, 'disabled-prev');
  } else {
    gweb.dom.classes.remove(this.nextEl, 'disabled-next');
  }

  // If we can't move anywhere, disable everything.
  if (position >= 0 && position <= this.scrollLimit) {
    gweb.dom.classes.add(this.nextEl, 'disabled-next');
    gweb.dom.classes.add(this.prevEl, 'disabled-prev');
  }

  // Do the animation only if we aren't already animating.
  if (!this.isAnimating) {
    this.isAnimating = true;
    this.currentPosition = position;
    gweb.fx.slideX(this.viewportEl, position, gweb.bind(function() {
      this.isAnimating = false;
    }, this));
  }
};


/**
 * Centers the viewport on an item.
 * @param {object} item The item from the items object.
 */
gweb.ui.Scroller.prototype.centerOnItem = function(item) {
  if (typeof(item) == 'string') {
    item = this.getItemFromId(item);
  }
  // Determine the position when this item is centered.
  var position = (Math.floor(this.tabsPerPage / 2) * this.tabWidth) -
      (item.index * this.tabWidth);

  this.animateToPosition(position);
};


/**
* Returns the content after the hash symbol or null if there is none.
* @param {string} str The string to check containing a hash symbol.
* @return {string?} The content after the hash or null.
*/
gweb.ui.Scroller.prototype.getStringAfterHash = function(str) {
  var regex = new RegExp('#([^&]*)');
  var results = regex.exec(str);
  return (results ? results[1] : null);
};


/**
* Verifies an item ID is present in the item list.
* @param {string} itemId The id of the item to check.
* @return {boolean} Whether the given item exists in the items object.
*/
gweb.ui.Scroller.prototype.isValidItem = function(itemId) {
  return itemId in this.items;
};


/**
 * Returns the content after the hash symbol or null if there is none.
 * @param {string} str  The string to check containing a hash symbol.
 * @return {string?}  The content after the hash or null.
 */
gweb.ui.Scroller.prototype.getStringAfterHash = function(str) {
  var regex = new RegExp('#([^&]*)');
  var results = regex.exec(str);
  return (results ? results[1] : null);
};


