// Copyright 2009 Google Inc.  All Rights Reserved.

/**
 * @fileoverview  JavaScript library used for a scroller widget.
 * @supported  This code should run on IE6+7, FF2+3, Safari3+4.
 * @author  keekim@google.com (KeeKim Heng)
 * @author  jeremydw@google.com (Jeremy Weinstein)
 */


/**
 * See if the global namespace object 'goog' exists. If it doesn't, create it.
 */
var goog = goog || {};


/**
 * See if the object goog.ui exists. If it doesn't create it.
 */
goog.ui = goog.ui || {};


/**
 * Constructor for a scroller widget. Checks for wrong hash id in URL
 * @param {Object} config  An object contain configuration properties.
 * @return {null}  Null if fails to init.
 * @constructor
 */
goog.ui.Scroller = function(config) {
  this.scroller = document.getElementById(config.scrollerId);
  this.pagePrevious = document.getElementById(config.scrollerPagePreviousId);
  this.pageNext = document.getElementById(config.scrollerPageNextId);
  this.itemsContainer = document.getElementById(config.itemsContainerId);
  this.selectedItemClass = config.selectedItemClass;
  this.selectedPageLink = config.selectedPageLinkClass;
  this.disabledControlClass = config.disabledControlClass;
  this.displayItemContentClass = config.displayItemContentClass;

  this.defaultSelectedItem = config.defaultSelectedItem || null;
  this.centerFlag = config.centerFlag || null;
  this.callbackOnSelect = config.callbackOnSelect || null;
  this.centerOnInitFlag = config.centerOnInitFlag || null;
  this.trackerId = config.trackerId || null;

  this.currentPage = 1;
  this.items = []; // an ordered array of item objects.
  this.itemLookup = []; // a lookup array for item by ID.
  this.pageLinks = [];
  this.isAnimating = false;

  var item;

  this.scroller.style.overflow = 'hidden';
  this.scroller.className += ' ' + config.noOverflowClass;

  if (!this.checkAndInitItems(config)) {
    this.scroller.style.overflow = 'auto';
    this.removeClass(this.scroller, config.noOverflowClass);
    return null;
  }

  // Add handlers and hides all links and items.
  for (var i = 0; i < this.items.length; i++) {
    item = this.items[i];

    // Stops the page from scrolling automatically
    item.item.id = item.item.id + '_old';
    item.link.onclick = this.createlinkClickHandler(this, item.id);
    item.item.style.display = 'none';
  }

  /**
  /* Sets currentId to what's in the document hash.
  /*    If that is not a valid item, use the default.
  /*    If there is no default, select a random item.
  */

  this.currentId = this.getStringAfterHash(location.href);
  if (!this.isValidItem(this.currentId)) {
    this.currentId = this.defaultSelectedItem || this.selectRandomItem();
  }

  this.selectItem(this.currentId);

  this.pageCount = Math.ceil((this.items.length - 1) / (this.itemsPerPage - 1));

  if (this.centerFlag) {
    this.pagePrevious.onclick = this.
      createPageLinkClickHandlerWithCentering(this, -1);
    this.pageNext.onclick = this.
      createPageLinkClickHandlerWithCentering(this, 1);
  } else {
    this.pagePrevious.onclick = this.createChangePageHandler(this, -1);
    this.pageNext.onclick = this.createChangePageHandler(this, 1);
  }

  if (config.scrollerPageLinksId) {
    for (i = 0; i < this.pageCount; i++) {
      pageLink = document.createElement('a');
      pageLink.innerHTML = i + 1;
      pageLink.href = 'javascript: void 0';

      if (this.centerFlag) {
        pageLink.onclick = this.createPageLinkClickHandlerWithCentering(this);
      } else {
        pageLink.onclick = this.createPageLinkClickHandler(this, i + 1);
      }

      document.getElementById(config.scrollerPageLinksId).appendChild(pageLink);
      this.pageLinks[i] = pageLink;
    }
  }
  if (this.centerFlag || this.centerOnInitFlag) {
    this.centerOnItem(this.currentId);
  } else {
    this.scrollToPage(Math.floor(this.itemLookup[this.currentId].pos /
      (this.itemsPerPage - 1)) + 1);
  }
  this.callbackOnSelect ? eval(this.callbackOnSelect) : null;
  document.onkeyup = this.createKeyUpHandler(this);
  document.getElementById(config.scrollerControlsId).style.display = 'block';
};


/**
 * Selects a random item from all available items.
 * @return {string}  The id of the random item.
 */
goog.ui.Scroller.prototype.selectRandomItem = function() {
  return this.items[Math.floor(Math.random() *
    (this.items.length - 1)) + 1].id;
};


/**
 * Checks to see if an item is a valid item from the list
 *    of all available items.
 * @param {string} id  The id of the item to check.
 * @return {boolean}  Indicates if the item is valid or not.
 */
goog.ui.Scroller.prototype.isValidItem = function(id) {
  for (var i in this.items) {
    if (this.items[i].id == id) {
      return true;
    }
  }
  return false;
};


/**
 * Check all item links refer to valid IDs and produce an array of item objects.
 *    We need to derive the total inner width of the widget. To do this, we add
 *    a temporary div to with width, and get its width.
 * @param {Object} config   An object contain configuration properties.
 * @return {boolean}  Indicates if the check succeeded or failed.
 */
goog.ui.Scroller.prototype.checkAndInitItems = function(config) {
  var id, item, links = this.scroller.getElementsByTagName('a');
  var tempDiv = document.createElement('div');

  this.scroller.appendChild(tempDiv);
  var contentsWidth = config.contentsWidth || tempDiv.offsetWidth;

  this.scroller.removeChild(tempDiv);

  this.totalWidth = 0;

  for (var i = 0; i < links.length; i++) {
    id = this.getStringAfterHash(links[i].href);
    item = document.getElementById(id);

    this.linkWidth = config.itemWidth || links[i].offsetWidth;
    this.totalWidth += this.linkWidth;
    this.itemsPerPage = contentsWidth / this.linkWidth;

    this.itemLookup[id] = { link: links[i],
        item: item,
        pos: i,
        id: id,
        onpage: Math.floor(i / this.itemsPerPage) + 1
        };
    this.items[i] = this.itemLookup[id];

    // Scroller width must be evenly divisible by item width.
    if (Math.floor(this.itemsPerPage) != this.itemsPerPage) {
      return false;
    }
  }
  this.scroller.getElementsByTagName('ul')[0].style.width = this.totalWidth +
      'px';
  return true;
};


/**
 * Creates a page link click handler.
 * @param {goog.ui.Scroller} me  Handle to the Scroller object.
 * @param {number} page  The page to scroll to.
 * @return {Function}  The event handler.
 */
goog.ui.Scroller.prototype.createPageLinkClickHandler = function(me, page) {
  return function() {
    me.scrollToPage(page);
  };
};


/**
 * Creates a page link click handler.
 * @param {goog.ui.Scroller} me  The scroller object.
 * @param {number} dir  1 for right, -1 for left.
 * @return {Function}  Handler that scrolls the page on a key press.
 */
goog.ui.Scroller.prototype.createPageLinkClickHandlerWithCentering =
    function(me, dir) {
  return function() {
    var targetPos = me.currentPos + Math.floor(me.itemsPerPage / 2) * dir;

    if (targetPos > 0 && targetPos < me.items.length && !me.isAnimating) {
      me.centerOnItem(me.items[targetPos].id);
    }
  };
};


/**
 * Creates a keyup handler for the page so users can use the arrow keys.
 * @param {goog.ui.Scroller} me  Handle to the Scroller object.
 * @return {Function}  The event handler.
 */
goog.ui.Scroller.prototype.createKeyUpHandler = function(me) {
  return function(e) {
    if (!e) {
      e = window.event;
    }
    var pos = me.itemLookup[me.currentId].pos, targetPos = pos;

    switch (e.keyCode) {
      case 37:
        targetPos = pos - 1;
        break;
      case 39:
        targetPos = pos + 1;
        break;
    }
    if (targetPos < 0) {
      targetPos = me.items.length - 1;
    } else if (targetPos >= me.items.length) {
      targetPos = 0;
    }
    if (targetPos != pos) {
      me.selectItem(me.items[targetPos].id);
      me.scrollToItem(me.items[targetPos].id);
      window.location = '#' + me.items[targetPos].id;
      if (me.centerFlag) {
        me.centerOnItem(me.items[targetPos].id);
      }
    }
  };
};


/**
 * 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.
 */
goog.ui.Scroller.prototype.getStringAfterHash = function(str) {
  var regex = new RegExp('#([^&]*)');
  var results = regex.exec(str);
  return (results ? results[1] : null);
};


/**
 * Scroll to a specific page.
 *    Disables appropriate scroll controls if at the first or last page.
 * @param {number} page  The page to scroll to.
 * @param {boolean} fastFlag  Flag to say scroll without animation.
 */
goog.ui.Scroller.prototype.scrollToPage = function(page, fastFlag) {
  this.currentPage = page;
  this.setControls(page);
  var x = (page - 1) * (this.linkWidth * (this.itemsPerPage - 1));

  if (fastFlag) {
    this.scroller.scrollLeft = x;
  } else {
    this.isAnimating = true;
    this.animateScroll(this.scroller, x, 10);
  }
};


/**
 * Enables or disables page controls and sets appropriate styling.
 * @param {number} page  The page to set controls for.
 */
goog.ui.Scroller.prototype.setControls = function(page) {
  for (var i = 0; i < this.pageLinks.length; i++) {
    if (i == page - 1) {
      this.pageLinks[i].className += ' ' + this.selectedPageLink;
    } else {
      this.removeClass(this.pageLinks[i], this.selectedPageLink);
    }
  }
  var itemsHalfPage = Math.floor(this.itemsPerPage / 2);

  if ((page == 1 && !this.centerFlag) || (this.currentPos <= itemsHalfPage)) {
    this.pagePrevious.className += ' ' + this.disabledControlClass;
  } else {
    this.removeClass(this.pagePrevious, this.disabledControlClass);
  }
  if ((page == this.pageCount && !this.centerFlag) ||
      (this.currentPos >= this.items.length - itemsHalfPage)) {
    this.pageNext.className += ' ' + this.disabledControlClass;
  } else {
    this.removeClass(this.pageNext, this.disabledControlClass);
  }
};


/**
 * Produce a scroll animation.
 *    On completion, the item is styled for selection.
 * @param {Element} el  The element to animate.
 * @param {number} x  The x position to scroll to.
 * @param {number} steps  The number of remaining animatino steps left.
 */
goog.ui.Scroller.prototype.animateScroll = function(el, x, steps) {
  var me = this;

  if (steps > 0) {
    el.scrollLeft = Math.floor((el.scrollLeft + x) / 2);
    window.setTimeout(function() {
        me.animateScroll(el, x, steps - 1);
        }, 90);
  } else {
    el.scrollLeft = x;
    me.isAnimating = false;
    this.selectItem(this.currentId);
    // this.itemLookup[this.currentId].item.link.focus();
  }
};


/**
 * Creates a click handler for a scoller item link.
 * @param {goog.ui.Scroller} me  Handle to the Scroller object.
 * @param {string} id  The id of the item link being clicked.
 * @return {Function}  The event handler.
 */
goog.ui.Scroller.prototype.createlinkClickHandler = function(me, id) {
  return function() {
    me.selectItem(id);
    me.centerFlag ? me.centerOnItem(id) : null;
  };
};


/**
 * Unselects the old item link and selects the new one.
 * @param {string} id  The id of the item link being clicked.
 */
goog.ui.Scroller.prototype.selectItem = function(id) {
  this.setControls(this.currentPage);
  this.itemLookup[this.currentId].item.style.display = 'none';
  this.removeClass(this.itemLookup[this.currentId].link,
      this.selectedItemClass);
  this.formerId = id,
  this.currentId = id;
  var selectedItem = this.itemLookup[id];

  selectedItem.item.style.display = 'block';
  if (this.displayItemContentClass &&
      selectedItem.item.className.indexOf(this.displayItemContentClass < 0)) {
    selectedItem.item.className += ' ' + this.displayItemContentClass;
  }
  selectedItem.link.className = this.selectedItemClass;
  this.callbackOnSelect ? eval(this.callbackOnSelect) : null;
  this.trackerId ? this.trackClick() : null;
};


/**
 * Creates a click handler for a scoller item link.
 * @param {string} id  Scrolls to a specific page, centers on item, and selects.
 */
goog.ui.Scroller.prototype.selectSpecificItem = function(id) {
  if (this.centerFlag) {
    this.setControls();
    this.centerOnItem(id);
  } else {
    this.scrollToItem(id);
  }
  this.selectItem(id);
};


/**
 * Creates a click handler for a scoller item link.
 * @param {goog.ui.Scroller} me  Handle to the Scroller object.
 * @param {string} id  The id of the item link being focused.
 * @return {Function}  The event handler.
 */
goog.ui.Scroller.prototype.createlinkFocusHandler = function(me, id) {
  return function() {
    me.scrollToItem(id);
  };
};


/**
 * Sends a GA Event Tracking hit for the currently selected item.
 *     Doesn't send a hit on the first select for this scroller object.
 * Requires GA.js to be loaded externally.
 */
goog.ui.Scroller.prototype.trackClick = function() {
  if (this.alreadyTracked) {
    if (typeof(_gat) != 'undefined' && this.trackerId) {
      var scrollTracker = _gat._getTracker(this.trackerId);

      scrollTracker._trackEvent('Scroller', 'Item Clicked', this.currentId);
      console.log(this.currentId);
    }
  }
  this.alreadyTracked = true;
};


/**
 * Centers the scroller on the specified item.
 * @param {string} id  The id of the item link to center on.
 */
goog.ui.Scroller.prototype.centerOnItem = function(id) {
  this.currentPage = this.itemLookup[id].onpage;
  this.currentPos = this.itemLookup[id].pos;

  var centerLoc = this.linkWidth *
      (this.itemLookup[id].pos) -
      (this.linkWidth * Math.floor(this.itemsPerPage / 2));
  var steps = Math.ceil(
      Math.abs(this.scroller.scrollLeft - centerLoc) / 90
      ) + 1;

  (steps > 20) ? steps = 10 : null;
  if (!this.isAnimating) {
    this.isAnimating = true;
    this.animateScroll(this.scroller, centerLoc, steps);
  }
};


/**
 * Scrolls to an item. Page up or down the items until the target item would be
 *    visible to determine the page to scroll to.
 *    IE automatically scrolls on focus, so jump the scroll back.
 * @param {string} id  The id of the item link being clicked.
 */
goog.ui.Scroller.prototype.scrollToItem = function(id) {
  var pos, pageSteps = 0, inPageFlag = false;

  this.setControls(this.currentPage);
  this.centerFlag ? null : this.scrollToPage(this.currentPage, true);
  while (!inPageFlag) {
    pos = this.itemLookup[id].pos - ((this.scroller.scrollLeft /
        this.linkWidth)) - (pageSteps * (this.itemsPerPage - 1));
    if (pos < 0) {
      pageSteps -= 1;
    } else if (pos > this.itemsPerPage - 1) {
      pageSteps += 1;
    } else {
      inPageFlag = true;
    }
  }
  if (pageSteps) {
    this.setControls(this.currentPage + pageSteps);
    this.centerFlag ? null : this.scrollToPage(this.currentPage + pageSteps);
  }
};


/**
 * Creates a click handler for a scoller item link.
 *    This updates the currently visible item, and updates the selected
 *    item link.
 * @param {goog.ui.Scroller} me  Handle to the Scroller object.
 * @param {number} dir  The direction (-1 or 1) to scroll in.
 * @return {Function}  The event handler.
 */
goog.ui.Scroller.prototype.createChangePageHandler = function(me, dir) {
  return function() {
    var newPage = me.currentPage + dir;

    if (newPage > 0 && newPage <= me.pageCount) {
      me.scrollToPage(newPage);
    }
  };
};


/**
 * Removes all instances of class name from an element.
 * @param {Element} el  The object whose class is changed.
 * @param {string} name  The class name to be removed.
 */
goog.ui.Scroller.prototype.removeClass = function(el, name) {
  el.className = el.className.replace(new RegExp(name, 'g'), '');
};
