/*
 * File: default.js
 * Author: Jay Llacuna
 * 
 * Global javascript file for Vaska Web site.
 */

var YUI =
  {
    anim:         YAHOO.util.Anim,
    customEvent:  YAHOO.util.CustomEvent,
    dom:          YAHOO.util.Dom,
    getByClass:   YAHOO.util.Dom.getElementsByClassName,
    easing:       YAHOO.util.Easing,
    effect:       YAHOO.widget.ContainerEffect,
    event:        YAHOO.util.Event,
    region:       YAHOO.util.Region,
    ua:           YAHOO.env.ua,
    widget:       YAHOO.widget
  },
  
  overlay,
  videoOverlays = [],
  map,
  
  // Classes
  SyncNav,
  ProductLink,
  OverlayMgr,
  VideoOverlay,
  Carousel,
  StoreMap,
  GeoLocator;

/* Add onload event handlers. */
YUI.event.on(window, 'load', function()
{
  // Array of videos.
  var videos =
    [
	 { id: 'vidCleanQueen', url: 'http://www.youtube.com/v/3mxmvgBj-SY&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewCleanQueen' },
     { id: 'vidLaundryLove', url: 'http://www.youtube.com/v/EZEns2k2QEo&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewLaundryLove' }
	  /*,{ id: 'vidVaskaVsTide', url: 'http://www.youtube.com/v/5fkPKQiFF68&hl=en&fs=1&autoplay=1&showsearch=0&enablejsapi=1&playerapiid=viewVaskaVsTide' } */
	],
    syncNav,

    // API token for Alice.com
    aliceApiToken = '9c01d9e4-e46a-102c-93a3-0022197ae1f4',
    aliceLinks;

  // Attach behaviors to links that launch new windows
  YUI.dom.batch(document.getElementsByTagName('a'), function(o)
  {
    if (YUI.dom.getAttribute(o, 'rel') == 'new')
    {
      YUI.event.on(o, 'click', function(e)
      {
        window.open(this.href, 'newwin');
        YUI.event.stopEvent(e);
      });
    }
  });

  // Change where to buy links to point to ajax version.
  YUI.dom.batch(document.getElementsByTagName('a'), function(o)
  {
	var href = YUI.dom.getAttribute(o, 'href');
	if (href.match(/\/where_to_buy/))
	{
      YUI.dom.setAttribute(o, 'href', href.replace(/\/where_to_buy/, '/where_to_buy/ajax'));
	}
  });
  
  // Initialize Alice.com API
  aliceLinks = YUI.getByClass('btnAliceProd', 'a');
  if (aliceLinks.length > 0 && window.alice)
  {
	alice.initialize({ api_token: aliceApiToken });
	
	// Attach an event handler to Alice links. Assume the UPC code is in the rel attribute.
	YUI.event.on(aliceLinks, 'click', function(e)
	{
      alice.viewProduct(
      {
    	upc: YUI.dom.getAttribute(this, 'rel'),
    	initialView: '#buy_now_panel'
      });
      YUI.event.stopEvent(e); 
    });
	
	YUI.event.on(YUI.getByClass('btnAliceView', 'a'), 'click', function(e)
    {
	  alice.viewProduct(
	  {
	    upc: YUI.dom.getAttribute(this, 'rel'),
	    initialView: '#image_panel'
	  });
	  YUI.event.stopEvent(e); 
	});
  }

  // See if there's a nav to sync.
  syncNav = YUI.dom.get('syncNav');
  if (syncNav)
  {
	// Convert to a SyncNav object.
    syncNav = new SyncNav(syncNav);
    
    // Find product links and attach each to the SyncNav.
    YUI.dom.batch(YUI.getByClass('prodLink', 'a'), function(o)
	{
      new ProductLink(o, syncNav);
    });
  }

  if (YUI.dom.get('videoOverlay'))
  {
    overlay = new OverlayMgr('videoOverlay');
  }

  // Use swfobject to load the videos if the corresponding div exists in the page.
  YUI.dom.batch(videos, function(video)
  {
    if (YUI.dom.get(video.id))
    {
      // Determine the width and height of the video.
      // Default to 481 x 389 for YouTube standard def.
      var width = video.width || 481,
          height = video.height || 389,
          // Set up params and attributes for flash embed.
          params = { allowscriptaccess: 'always', allowfullscreen: 'true' },
          attrs = { name: video.id };
      
      // Set any custom base or flashvars, if provided.
      if (video.base) params.base = video.base;
      if (video.flashvars) params.flashvars = video.flashvars;
      swfobject.embedSWF(video.url, video.id, width, height, '9.0.0', 'flash/expressInstall.swf', false, params, attrs);
    }
  });

  // Set up links to video overlays.
  YUI.dom.batch(YUI.getByClass('videoOverlayTrigger', 'a'), function(o)
  {
    var videoContent = o.getAttribute('rel');
    
    if (typeof(videoOverlays[videoContent]) == 'undefined')
    {
      videoOverlays[videoContent] = new VideoOverlay(overlay, videoContent);
    }
    
    YUI.event.on(o, 'click', function(e)
    {
      videoOverlays[this.getAttribute('rel')].show();
      YUI.event.stopEvent(e); 
    });
  });
  
  // Set up the store locator map.
  if (YUI.dom.get('mapCanvas'))
  {
	map = new StoreMap('mapCanvas', 'storeList');
  }
});

/**
 * Resets the form associated with field.
 * @param field HTML input element.
 * @return false to disable default button event.
 */
function resetForm(field)
{
  field.form.reset();
  return false;
}

/**
 * Returns the width of an element.
 * @requires YAHOO.util.Dom
 * @param {String | HTMLElement} el Reference to the element.
 */
function getElementWidth(el)
{
  var region = YUI.dom.getRegion(el);
  return (region.right - region.left);
}

/**
 * Returns the height of an element.
 * @requires YAHOO.util.Dom
 * @param {String | HTMLElement} el Reference to the element.
 */
function getElementHeight(el)
{
  var region = YUI.dom.getRegion(el);
  return (region.bottom - region.top);
}

/**
 * Class for handling a synchronized navigation.
 * Allows other objects to trigger a focus change or to subscribe to a focus change in the nav.
 * <p>Usage: var newSyncNav = new SyncNav(el);</p>
 * @class SyncNav
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.util.CustomEvent
 * @constructor
 * @param {String | HTMLElement} el Reference to the nav element.
 */
SyncNav = function(el)
{
  this.FOCUS_CLASS = 'navFocus';

  this.el = YUI.dom.get(el);
  
  // Get all the nav items.
  this.navItems = this.el.getElementsByTagName('a');
  
  // Attach event handlers to nav item mouseovers.
  YUI.dom.batch(this.navItems, function(o)
  {
	YUI.event.on(o, 'mouseover', this.mouseoverHandler, o, this);
    YUI.event.on(o, 'mouseout', this.mouseoutHandler, o, this);
  }, this, true);
  
  // Create a custom event for a change in focus.
  this.onfocuschange = new YUI.customEvent('focusChange', this);
}

SyncNav.prototype =
{
  /**
   * Event handler for a nav item mouseover.
   * @param {Event} e The event associated with the mouseover.
   * @param {HTMLElement} navItem The nav item that was moused over.
   */
  mouseoverHandler: function(e, navItem)
  {
	// Fire onfocuschange event. Pass the key of the nav item.
	this.onfocuschange.fire(this.getKey(navItem));
  },
  
  /**
   * Event handler for a nav item mouseout.
   * @param {Event} e The event associated with the mouseout.
   * @param {HTMLElement} navItem The nav item that was moused out.
   */
  mouseoutHandler: function(e, navItem)
  {
	// Fire onfocuschange event. Use a blank key.
	this.onfocuschange.fire('');
  },
  
  /**
   * Changes the focus of the nav.
   * Highlights the nav item(s) with matching key.
   * @param {String} key The key to determine which nav item(s) gain focus.
   */
  changeFocus: function(key)
  {
	YUI.dom.batch(this.navItems, function(o)
	{
	  if (this.getKey(o) == key)
	  {
	    YUI.dom.addClass(o, this.FOCUS_CLASS);
	  }
	  else
	  {
	    YUI.dom.removeClass(o, this.FOCUS_CLASS);
	  }
	}, this, true);
  },
  
  /**
   * Retrieves the key for the nav item.
   * Uses the nav item's rel attribute.
   * @param {HTMLElement} navItem Nav item link element to retrieve the key for.
   */
  getKey: function(navItem)
  {
    return navItem.getAttribute('rel');
  }
}

/**
 * Class for handling product links on product overview page.
 * Subscribes to a SyncNav object to synchronize focus changes.
 * <p>Usage: var newProductLink = new ProductLink(el, nav);</p>
 * @class ProductLink
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.util.CustomEvent
 * @constructor
 * @param {String | HTMLElement} el Reference to the product link element.
 * @param {SyncNav} nav Reference to a SyncNav object.
 */
ProductLink = function(el, nav)
{
  this.FOCUS_CLASS = 'prodFocus';

  this.el = YUI.dom.get(el);
  
  this.nav = nav;
  
  // Determine the key for the product link based on the rel attribute.
  this.key = this.el.getAttribute('rel');
  
  // Subscribe to mouseover events.
  YUI.event.on(this.el, 'mouseover', this.mouseoverHandler, this, true);
  YUI.event.on(this.el, 'mouseout', this.mouseoutHandler, this, true);
  
  // Subscribe to the nav's onfocuschange event.
  this.nav.onfocuschange.subscribe(this.navFocusChangeHandler, this, true);
}

ProductLink.prototype =
{
  /**
   * Event handler for a mouseover.
   * Changes the focus of the nav.
   * @param {Event} e The event associated with the mouseover.
   */
  mouseoverHandler: function(e)
  {
	// Use this object's key.
	this.nav.changeFocus(this.key);
  },
  
  /**
   * Event handler for a mouseout.
   * Changes the focus of the nav.
   * @param {Event} e The event associated with the mouseout.
   */
  mouseoutHandler: function(e)
  {
	// Use a blank key.
	this.nav.changeFocus('');
  },
  
  /**
   * Event handler for a change in nav focus.
   * Turns on focus to the link if the nav key matches the link key.
   * @param {Event} e The event associated with the nav focus change.
   * @param {String} key The key identifier for the nav item that gained focus.
   */
  navFocusChangeHandler: function(e, key)
  {
	if (this.key == key)
	{
      YUI.dom.addClass(this.el, this.FOCUS_CLASS);
	}
	else
	{
	  YUI.dom.removeClass(this.el, this.FOCUS_CLASS);
	}
  }
}


/**
 * Class for overlay/popup functionality.
 * <p>Usage: var newOverlayMgr = new OverlayMgr(overlayEl);</p>
 * @class OverlayMgr
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.widget.Panel
 * @requires YAHOO.widget.ContainerEffect
 * @constructor
 * @param {String | HTMLElement} overlayEl Reference to the overlay container.
 */
OverlayMgr = function(overlayEl)
{
  // Class name for close buttons.
  this.CLOSE_CLASS = 'overlayClose';
  // http://www.cambiaresearch.com/c4/702b8cd1-e5b0-42e6-83ac-25f0306e3e25/Javascript-Char-Codes-Key-Codes.aspx
  this.ESC_CHAR_CODE = 27;

  // Define elements
  this.cont = YUI.dom.get(overlayEl);

  // Instantiate and render overlay.
  this.overlay = new YUI.widget.Panel(this.cont,
  {
    fixedcenter: false,
    modal:       true,
    visible:     false,
    close:       false,
    underlay:    'none',
    effect:      { effect: YUI.effect.FADE, duration: 0.5 }
  });
  
  // Add an event handler for when the overlay finishes hiding.
  this.overlay.hideEvent.subscribe(this.hideCompleteHandler, this, true);
  this.overlay.render(document.body);
  
  // Set up event handlers to close the overlay.
  this.btnsClose = YUI.getByClass(this.CLOSE_CLASS, 'a', this.cont);
  YUI.dom.batch(this.btnsClose, function(btnClose)
  {
	  YUI.event.on(btnClose, 'click', this.clickCloseHandler, this, true);
  }, this, true);

  // Close the overlay if the ESC key is pressed.
  YUI.event.on(document, 'keypress', this.keypressHandler, this, true);
  
  // Close the overlay if the mask is clicked on.
  this.overlay.buildMask(); // YUI does lazy creation of the mask. But we need it now.
  YUI.event.on(this.overlay.mask, 'click', this.clickMaskHandler, this, true)
  
  /* Create custom events for show and hide */
  this.onshow = new YUI.customEvent('show', this);
  this.onhide = new YUI.customEvent('hide', this);
  
  // Track states
  this.activeCont = null; // Active content selection
  this.shown = false;     // Shown status
}

OverlayMgr.prototype =
{
  /* Captures ESC keystroke. */
  keypressHandler: function(e)
  {
    if (YUI.event.getCharCode(e) == 27)
    {
      this.hide();
    }
  },
  
  /* Event handler for clicking a close button. */
  clickCloseHandler: function(e)
  {
    this.hide();
    YUI.event.stopEvent(e);
  },
  
  /* Event handler for clicking on the mask. */
  clickMaskHandler: function(e)
  {
    this.hide();
    YUI.event.stopEvent(e);
  },
  
  /* Loads the content for display in the overlay. */
  load: function(content)
  {
    this.clear();
    content = YUI.dom.get(content);
    YUI.dom.setStyle(content, 'display', 'block');
    this.activeCont = content;
  },
  
  /* Hides the active content */
  clear: function()
  {
    if (this.activeCont)
    {
      YUI.dom.setStyle(this.activeCont, 'display', 'none');
      this.activeCont = false;
    }
  },

  /* Shows the overlay */
  show: function(content)
  {
    // Load the content first so we can measure it.
	this.load(content);
    
    // Center the overlay in the window.
    var scrollTop =  YUI.dom.getDocumentScrollTop(),
        scrollLeft = YUI.dom.getDocumentScrollLeft(),
        yPos = scrollTop + (YUI.dom.getViewportHeight() - getElementHeight(this.cont))/2,
        xPos = scrollLeft + (YUI.dom.getViewportWidth() - getElementWidth(this.cont))/2;
    
    // Make sure the top and left of the overlay shows in the window.
    yPos = (yPos < scrollTop) ? scrollTop : yPos;
    xPos = (xPos < scrollLeft) ? scrollLeft : xPos;  
    
    this.overlay.cfg.setProperty('y', yPos);
    this.overlay.cfg.setProperty('x', xPos);
    this.overlay.show();
    this.shown = true;
    this.onshow.fire();
  },

  /* Hides the overlay */
  hide: function()
  {
    // FF2 on Mac has a problem clearing scrollbars on an overlay, so hide the active content now.
    if (YUI.ua.gecko && YUI.ua.gecko < 1.9 && (/Macintosh/).test(navigator.userAgent))
    {
      this.clear();
    }
    this.overlay.hide();
    this.shown = false;
    this.onhide.fire();
  },

  /* Reposition overlay so that it doesn't affect the size of the page */  
  hideCompleteHandler: function()
  {
    this.overlay.cfg.setProperty('y', 0);
    this.overlay.cfg.setProperty('x', 0);
  }
}

/**
 * Class for managing an overlay with video content.
 * Utilizes YouTube JavaScript Player API (http://code.google.com/apis/youtube/js_api_reference.html)
 * <p>Usage: var newVideoOverlay = new VideoOverlay(overlayMgr, videoContent);</p>
 * @class VideoOverlay
 * @requires OverlayMgr
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.widget.Panel
 * @requires YAHOO.widget.ContainerEffect
 * @constructor
 * @param {OverlayMgr} overlayMgr Reference to the OverlayMgr.
 * @param {String | HTMLElement} videoContent Reference to the video content.
 */
VideoOverlay = function(overlayMgr, videoContent)
{
  // Save the overlayMgr and videoContent.
  this.overlayMgr = overlayMgr;
  this.videoContent = YUI.dom.get(videoContent);

  // Find the video. Mozilla uses the embed tag.
  this.video = this.videoContent.getElementsByTagName('object')[0]
    || this.videoContent.getElementsByTagName('embed')[0];

  // Subscribe to hide event of overlayMgr.
  this.overlayMgr.onhide.subscribe(this.overlayHiddenHandler, this, true);
  
  // Track shown state.
  this.shown = false;
}

VideoOverlay.prototype =
{
  /**
   * Makes sure the video pauses if the user closed the overlay before the player was ready to receive API calls.
   */
  playerReadyHandler: function()
  {
    if (!this.shown)
    {
      this.pauseVideo();
    }
  },
  
  /* Stops the video if the overlay is hidden by itself. */
  overlayHiddenHandler: function(e)
  {
    if (this.shown)
    {
      this.pauseVideo();
      this.shown = false;
    }
  },

  // Show the video content in the overlay. Autoplay the video.
  show: function()
  {
    this.shown = true;
    this.overlayMgr.show(this.videoContent);
    this.playVideo();
  },
  
  // Hide the video content. Pause the video.
  hide: function()
  {
    this.pauseVideo();
    this.overlayMgr.hide();
    this.shown = false;
  },

  /* Pauses the video */  
  pauseVideo: function()
  {
    // If the video is ready to receive api calls and is not paused (state 2), pause it.
    if (this.video.getPlayerState && typeof(this.video.getPlayerState()) != 'undefined' && this.video.getPlayerState() != 2)
    {
      this.video.pauseVideo();
    }
    YUI.dom.setStyle(this.video, 'visibility', 'hidden');
  },
  
  /* Plays the video */  
  playVideo: function()
  {
    YUI.dom.setStyle(this.video, 'visibility', 'visible');
    // If the video is ready to receive api calls and is not playing (state 1), seek to the beginning and play it.
    if (this.video.getPlayerState && typeof(this.video.getPlayerState()) != 'undefined' && this.video.getPlayerState() != 1)
    {
      this.video.seekTo(0, true);
      this.video.playVideo();
    }
  }
}

/**
 * Captures YouTubePlayerReady event. See YouTube JavaScript Player API (http://code.google.com/apis/youtube/js_api_reference.html)
 * Calls playerReadyHandler for appropriate VideoOverlay.
 * @param {String} playerApiId The id of the YouTube video that has become ready to receive API calls.
 */
function onYouTubePlayerReady(playerApiId)
{
  videoOverlays[playerApiId].playerReadyHandler();
}

/**
 * Vertical scrolling carousel class.
 * Provides ability to select a specific item in the carousel by index.
 * Also allows a "no items" message to be displayed in the carousel.
 * <p>Usage: var newCarousel = new Carousel(el);</p>
 * @class Carousel
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @constructor
 * @param {String | HTMLElement} el Reference to the carousel container element.
 */
Carousel = function(el)
{
  // Item constants
  this.ITEM_CONTAINER_CLASS = 'items';
  this.ITEM_CONTAINER_TYPE = 'ul';
  this.ITEM_TYPE = 'li';
  this.ITEM_HEIGHT = 160;
  
  // Message constants
  this.MESSAGE_CLASS = 'message';
  this.NO_MESSAGE = null;
 
  // Button constants
  this.PREV_CLASS = 'prev';
  this.NEXT_CLASS = 'next';
  this.DISABLED_CLASS = 'disabled';
  this.BUTTON_TYPE = 'a';
  
  // Display constants
  this.TOTAL_CLASS = 'total';
  this.PAGE_CLASS = 'page';
  this.PAGE_TOTAL_CLASS = 'pageTotal';
  this.DISPLAY_TYPE = 'span';
  
  // Selection constants
  this.SELECTED_CLASS = 'selected';
  this.NOT_SELECTED_INDEX = -1;
  
  // Other
  this.PAGE_SIZE = 3;
  this.DURATION = 0.3;
  this.EASE = YUI.easing.easeOut;
  
  // Get the carousel element.
  this.carousel = YUI.dom.get(el);
  
  // Get the item container and the items.
  this.itemContainer = YUI.getByClass(this.ITEM_CONTAINER_CLASS, this.ITEM_CONTAINER_TYPE, this.carousel)[0];
  this.items = this.itemContainer.getElementsByTagName(this.ITEM_TYPE);

  // Get the buttons and attach event handlers.
  this.prevBtns = YUI.getByClass(this.PREV_CLASS, this.BUTTON_TYPE, this.carousel);
  this.nextBtns = YUI.getByClass(this.NEXT_CLASS, this.BUTTON_TYPE, this.carousel);
  
  YUI.event.on(this.prevBtns, 'click', this.prevBtnHandler, this, true);
  YUI.event.on(this.nextBtns, 'click', this.nextBtnHandler, this, true);
  
  // Get status display elements.
  this.displays =
  {
    total:		YUI.getByClass(this.TOTAL_CLASS, this.DISPLAY_TYPE, this.carousel),
  	page:		YUI.getByClass(this.PAGE_CLASS, this.DISPLAY_TYPE, this.carousel),
  	pageTotal:	YUI.getByClass(this.PAGE_TOTAL_CLASS, this.DISPLAY_TYPE, this.carousel)
  };
  
  // Start at first item.
  this.index = 0;
  
  // Start with no selection.
  this.selectedIndex = this.NOT_SELECTED_INDEX;
  
  // Start with no message.
  this.message = this.NO_MESSAGE;
  
  // Update the display.
  this.updateDisplay();
}

Carousel.prototype =
{
  /**
   * Event handler for previous button click.
   * @param {Event} e The event associated with the button click.
   */
  prevBtnHandler: function(e)
  {
    this.movePrev();

	// Disable default click event.
    YUI.event.stopEvent(e);
  },
  
  /**
   * Event handler for next button click.
   * @param {Event} e The event associated with the button click.
   */
  nextBtnHandler: function(e)
  {
    this.moveNext();
		
	// Disable default click event.
    YUI.event.stopEvent(e);
  },

  /**
   * Moves to the item at index
   * @param {Integer} index The index of the item to move to.
   */
  move: function(index)
  {
	// Illegal move.
	if
	(
	  index == this.index
      || index < 0
      || (this.items.length  && index > this.items.length - 1)
      || (this.message && this.message != this.NO_MESSAGE)
    )
	{
	  return;
	}
	
	// Animate the movement.
	var destination = -1 * index * this.ITEM_HEIGHT,
        moveAnim = new YUI.anim(this.itemContainer,
	      {
            top: { to: destination }
          },
	      this.DURATION,
          this.EASE
        );
    moveAnim.animate();
    
    // Update our tracking index.
    this.index = index;

	this.updateDisplay();
  },
  
  /**
   * Moves to the previous page.
   */
  movePrev: function()
  {
	var newIndex = this.index - this.PAGE_SIZE;

    // Make sure we can move previous.
	if (newIndex >= 0)
	{
	  this.move(newIndex);
	}
  },
  
  /**
   * Moves to the next page.
   */
  moveNext: function()
  {
	var newIndex = this.index + this.PAGE_SIZE;
	
	// Make sure we can move next.
	if (newIndex <= this.items.length -1)
	{
	  this.move(newIndex);
	}
  },
  
  /**
   * Adds a new item to the carousel.
   * @param {HTMLElement} item Dom element to place inside the item.
   */
  addItem: function(item)
  {
	// Create an item, append the item element inside it, and add the item to the container.
	var itemEl = document.createElement(this.ITEM_TYPE);
	itemEl.appendChild(item);
	this.itemContainer.appendChild(itemEl);
	
	// Track the items.
	this.items[this.items.length] = itemEl;
	
    this.updateDisplay();
  },
  
  /**
   * Displays a message in the carousel area. Replaces any existing items in the carousel.
   * Useful for a "no items" display message.
   * @param {HTMLElement} messageEl A DOM element containing the message to display.
   */
  displayMessage: function(messageEl)
  {
	this.clearAll();
    var itemEl = document.createElement(this.ITEM_TYPE);
    YUI.dom.addClass(itemEl, this.MESSAGE_CLASS);
    itemEl.appendChild(messageEl);
    this.itemContainer.appendChild(itemEl);

    // Track the message.
    this.message = itemEl;
  },
  
  /**
   * Removes any existing message display.
   */
  clearMessage: function()
  {
    if (this.message && this.message != this.NO_MESSAGE)
    {
      this.itemContainer.removeChild(this.message);
      this.message = this.NO_MESSAGE;
    }
  },
  
  /**
   * Selects the item at index.
   * @param {Integer} index The index of the item to select.
   */
  selectItem: function(index)
  {
    this.deselectItems();
    
    // Don't select an invalid index.
    if (typeof(index) == 'undefined' || index < 0 || index > this.items.length - 1 || index == this.NOT_SELECTED_INDEX)
    {
      return;
    }
    
    YUI.dom.addClass(this.items[index], this.SELECTED_CLASS);
    this.selectedIndex = index;
    
    // Paginate to show the item.
    this.move(index - index % this.PAGE_SIZE);
  },
  
  /**
   * Deselects the selected item, if any.
   */
  deselectItems: function()
  {
    if (this.selectedIndex != this.NOT_SELECTED_INDEX)
    {
      YUI.dom.removeClass(this.items[this.selectedIndex], this.SELECTED_CLASS);
    }
    this.selectedIndex = this.NOT_SELECTED_INDEX;
  },
  
  /**
   * Updates the carousel status display.
   */
  updateDisplay: function()
  {
	// Calculate total items, current page, and total pages.
	var totalItems = this.items.length,
		currPage = this.index / this.PAGE_SIZE + 1,
	    totalPages = Math.ceil(totalItems / this.PAGE_SIZE) || 1;
	
	// Update all total displays.
    YUI.dom.batch(this.displays.total, function(totalEl)
	{
      totalEl.innerHTML = totalItems;
    });
    
    // Update all page displays.
    YUI.dom.batch(this.displays.page, function(pageEl)
    {
      pageEl.innerHTML = currPage;
    });
	
    // Update all total page displays.
	YUI.dom.batch(this.displays.pageTotal, function(pageTotalEl)
	{
	  pageTotalEl.innerHTML = totalPages;
	});
	
	// Enable/disable the previous buttons.
	if (currPage <= 1)
	{
	  YUI.dom.addClass(this.prevBtns, this.DISABLED_CLASS);
	}
	else
	{
	  YUI.dom.removeClass(this.prevBtns, this.DISABLED_CLASS);
	}
	
	// Enable/disable the next buttons.
	if (currPage >= totalPages)
	{
	  YUI.dom.addClass(this.nextBtns, this.DISABLED_CLASS);
	}
	else
	{
	  YUI.dom.removeClass(this.nextBtns, this.DISABLED_CLASS);
	}
  },

  /**
   * Clears the items from the carousel.
   */
  clearAll: function()
  {
	this.clearMessage();
	this.deselectItems();

	// Remove the items from the DOM.
    YUI.dom.batch(this.items, function(item)
    {
      this.itemContainer.removeChild(item);
	}, this, true);

    // Reset our items tracker.
    this.items = [];

    // Reset the carousel movement.
    this.move(0);

    this.updateDisplay();
  }
}

/**
 * Class for managing the store locator map.
 * <p>Usage: var newStoreMap = new StoreMap(el, listEl);</p>
 * @class StoreMap
 * @requires Ajax.Request (prototype)
 * @requires Carousel
 * @requires Function (prototype)
 * @requires Google Maps API
 * @constructor
 * @param {String | HTMLElement} el Reference to the map placeholder element.
 * @param {String | HTMLElement} listEl Reference to the list carousel element.
 */
StoreMap = function(el, listEl)
{
  this.BACKGROUND_COLOR = '#bcb4db';

  // Map defaults
  this.DEFAULT_SEARCH = { address: 'Central Park, New York, NY', distance: 5 };
  this.DEFAULT_ZOOM = 15;

  // URLs
  this.ADDRESS_SEARCH_URL = '/stores/getstoresbyaddress.json';
  this.BOUNDS_SEARCH_URL = '/stores/getstoresbybounds.json';
  // URL for directions.
  this.GOOGLE_MAPS_URL = 'http://maps.google.com?daddr=';

  // Copy constants
  this.DISTANCE_UNITS = 'mile';
  this.SHOW_TEXT = 'Show store on map';
  this.DIRECTIONS_TEXT = 'Get driving directions';
  
  // No stores display constants
  this.NO_STORES_TEXT = 'There are no stores within a {distance} mile radius from your location. However, you can purchase Vaska products online.';
  this.NO_STORES_LINK_OPTIONS =
  {
	text: '<!-- for IE -->',
	href: 'http://vaska.alice.com',
	title: 'Buy Online at Alice, Always Free Shipping',
	className: 'btnAlice',
    openNewWindow: true
  };
  
  // Class names for contructed elements.
  this.ITEM_CLASS = 'item';
  this.INFO_CLASS = 'info';
  
  // Configuration for custom store icon.
  this.STORE_ICON_IMAGE = '/img/bg_store_icon.png';
  this.STORE_ICON_SIZE = { width: 26, height: 28 };
  this.STORE_ICON_ANCHOR = { x: 13, y: 9 };
	
  if (GBrowserIsCompatible())
  {
	var locator = new GeoLocator(),
	    ui;

    this.map = new GMap2($(el),
    {
      backgroundColor: this.BACKGROUND_COLOR
    });
    
    ui = this.map.getDefaultUI();
    // Remove terrain button.
    ui.maptypes.physical = false;
    this.map.setUI(ui);
    
    // See if Google figured out our location by IP.
    // If so, initiate an initial search based on our location.
    // Otherwise, initiate an initial search with the default.
    if (google.loader.ClientLocation)
    {
      this.searchByLatLng(google.loader.ClientLocation.latitude, google.loader.ClientLocation.longitude, this.DEFAULT_SEARCH.distance);
    }
    else
    {
      this.searchByAddress(this.DEFAULT_SEARCH.address, this.DEFAULT_SEARCH.distance);
    }
	
    // See if the user's browser will provide a more accurate location.
    // Usually this requires the user to provide permission, so we run this search after establishing a default location.
	locator.getCurrentPosition(function(position)
    {
	  this.searchByLatLng(position.coords.latitude, position.coords.longitude, this.DEFAULT_SEARCH.distance);
	}.bind(this));
	
    // Bind to map events (map move and info window close).
    GEvent.bind(this.map, 'moveend', this, this.mapMoveHandler);
    GEvent.bind(this.map, 'infowindowclose', this, this.infoWindowCloseHandler);
    
    // Create custom icon for stores.
    this.storeIcon = new GIcon(G_DEFAULT_ICON);
    this.storeIcon.image = this.STORE_ICON_IMAGE;
    this.storeIcon.shadow = false;
    this.storeIcon.iconSize = new GSize(this.STORE_ICON_SIZE.width, this.STORE_ICON_SIZE.height);
    this.storeIcon.iconAnchor = new GPoint(this.STORE_ICON_ANCHOR.x, this.STORE_ICON_ANCHOR.y);
  }
  
  // Memory cleanup
  Event.observe(window, 'unload', function()
  {
    GUnload();
  });

  // Get the store listing carousel.
  this.list = new Carousel(listEl);

  // Track our stores on the map.
  this.mapStores = {};

  // Track our stores in the listing.
  this.listStores = {};
}

StoreMap.prototype =
{
  /**
   * Sets the location for the map.
   * @param {float} lat The latitude of the location.
   * @param {float} lon The longitude of the location.
   * @param {int} zoom The zoom level for the map.
   */
  setLocation: function(lat, lon, zoom)
  {
	var latLng = new GLatLng(lat, lon);
	
	if (!this.locationMarker)
	{
      // Create and add a new location marker.
      this.locationMarker = new GMarker(latLng);
      this.map.addOverlay(this.locationMarker);
	}
	else
	{
	  this.locationMarker.setLatLng(latLng);
	}
	
    // Center the map.
	this.map.setCenter(latLng, zoom);
  },
  
  /**
   * Convenience function for running a search by address.
   * @param {String} address The address to search.
   * @param {Integer} distance The distance from address to search within.
   */
  searchByAddress: function(address, distance)
  {
	var searchParams =
	{
	  'data[Store][address]': address,
      'data[Store][distance]': distance
	};
	new Ajax.Request(this.ADDRESS_SEARCH_URL,
	{
	  method: 'post',
	  parameters: searchParams,
	  onSuccess: this.loadStoresByAddress.bind(this)
    });
  },
  
  /**
   * Convenience function for running a search by latitude/longitude.
   * @param {Float} lat The latitude to search.
   * @param {Float} lng The longitude to search.
   * @param {Integer} distance The distance from the coordinates to search within.
   */
  searchByLatLng: function(lat, lng, distance)
  {
    this.searchByAddress(lat + ' ' + lng, distance);
  },
  
  /**
   * Event handler for an address search. Sets the location and loads the stores.
   * @param {Object} transport The AJAX transport object associated with the address search.
   * @param {Object} json The JSON object with the address search results.
   */
  loadStoresByAddress: function(transport, json)
  {
	// Check to make sure our json object was returned successfully.
	if (!json)
	{
	  return;
	}

	var location = json.location,
		distance = json.distance,
	    stores = json.stores,
	    maxLat = 0,
	    maxLon = 0,
	    currStore,
	    i,
	    zoom,
	    storeItem;
	
	// Make sure coords are treated as floats.
	location.lat = parseFloat(location.lat);
	location.lon = parseFloat(location.lon);
	
	// Clear the store listing.
    this.list.clearAll();
    this.listStores = {};
    
    // Close any open info windows on the map.
    this.map.closeInfoWindow();
	
	if (stores.length == 0)
	{
	  zoom = this.DEFAULT_ZOOM;
	  this.list.displayMessage(this.createNoStoresMessage(distance));
	}
	else
	{
	  // Load the stores.
	  this.loadStoresInMap(stores);
	  
	  for (i = 0; i < stores.length; i++)
	  {
		currStore = stores[i].Store;
		  
        // Determine the furthest store lat and lon from the location to figure out our bounds.
		maxLat = Math.max(maxLat, Math.abs(location.lat - currStore.lat));
		maxLon = Math.max(maxLon, Math.abs(location.lon - currStore.lon));
		
		// Add the item to the store listing.
		storeItem = this.createStoreItem(currStore);
		this.list.addItem(storeItem);
		// Track the index of the store in the list.
		this.listStores[currStore.id] = i;
	  }

	  // Calculate map zoom level to fit the bounds of our furthest lat and lon.
	  zoom = this.map.getBoundsZoomLevel(
		new GLatLngBounds(
		  new GLatLng(location.lat - maxLat, location.lon - maxLon),
		  new GLatLng(location.lat + maxLat, location.lon + maxLon)
		)
	  );
	}
	
	// Zoom the map to the loccation.
    this.setLocation(location.lat, location.lon, zoom);
  },
  
  /**
   * Utility method to create a link DOM element.
   * @param {Object} options Link options. Properties include text (required), href (required), title, className, and openNewWindow (boolean).
   * @return {HTMLElement} A link with the options specified.
   */
  createLink: function(options)
  {
    var link = document.createElement('a');
    
    link.innerHTML = options.text;
    link.href = options.href;
    
    link.setAttribute('title', (options.title) ? options.title : options.text);

    if (options.className) Element.addClassName(link, options.className);

    if (options.openNewWindow)
    {
      link.setAttribute('rel', 'new');
      Element.observe(link, 'click', function(e)
      {
        window.open(this.href, 'newwin');
        e.stop();
      });
    }
    
    return link;
  },
  
  /**
   * Creates a "no stores" display message.
   * @param {Integer} distance The distance from address for which there are no stores.
   * @return {HTMLElement} DOM element containing the "no stores" display message.
   */
  createNoStoresMessage: function(distance)
  {
	var messageEl = document.createElement('div'),
		paragraph = document.createElement('p'),
	    link = this.createLink(this.NO_STORES_LINK_OPTIONS);
	
	paragraph.innerHTML = this.NO_STORES_TEXT.replace('{distance}', distance);
    
	messageEl.appendChild(paragraph);
    messageEl.appendChild(link);
    
    return messageEl;
  },
  
  /**
   * Creates an HTML block element (div) for displaying the store address.
   * @param {Object} store The store JSON object as returned by AJAX.
   * @return {HTMLElement} A div containing the formatted store address.
   */
  createAddressBlock: function(store)
  {
	var block = document.createElement('div'),
    
		// Assemble the address text.
	    address = store.address_1 + '<br />';
    
    // address_2 is optional.
    if (store.address_2)
    {
      address += store.address_2 + '<br />';
    }
    address += store.city + ' ' + store.state.toUpperCase() + ', ' + store.zip + '<br />';
    address += store.phone;
    
    block.innerHTML = address;
    
    return block;
  },
  
  /**
   * Creates a link to Google Maps for directions to the store.
   * @param {Object} store The store JSON object as returned by AJAX.
   * @return {HTMLElement} A link element with href to Google Maps for directions to the store.
   */
  createDirectionsLink: function(store)
  {
	// Assemble address for directions href.
	var directions =  store.address_1;

	// address_2 is optional.
	if (store.address_2)
    {
      directions += ' ' + store.address_2;
    }
    directions += ', ' + store.city + ' ' + store.state.toUpperCase() + ', ' + store.zip;

    return this.createLink(
    {
      text: this.DIRECTIONS_TEXT,
      href: this.GOOGLE_MAPS_URL + escape(directions),
      title: this.DIRECTIONS_TEXT + ' to ' + store.name,
      openNewWindow: true
    });
  },
  
  /**
   * Creates a store dom element for display in the store listing.
   * @param {Object] store The store JSON object as returned by AJAX.
   * @return {HTMLElement} A dom element for displaying the store in the store listing.
   */
  createStoreItem: function(store)
  {
	// Create our dom elements.
    var item = document.createElement('div'),
        name = document.createElement('h3'),
        address = this.createAddressBlock(store),
        showLink = this.createLink({ text: this.SHOW_TEXT, href: '#' }),
        dirLink = this.createDirectionsLink(store),
        br = document.createElement('br'),
        
        // Round the distance to the nearest 0.1
        distance = Math.round(store.distance * 10) / 10;

    // Store name and distance. Make units plural if distance is > 1.
    name.innerHTML = store.name + ' (' + distance + ' ' + this.DISTANCE_UNITS + (distance > 1 ? 's' : '') + ')';
    
    // Set show link.
    Element.observe(showLink, 'click', this.showClickHandler.bindAsEventListener(this, store.id));
    
    // Put the dom elements together.
    item.appendChild(name);
    item.appendChild(address);
    item.appendChild(showLink);
    item.appendChild(br.cloneNode(true));
    item.appendChild(dirLink);
    item.appendChild(br.cloneNode(true));
    Element.addClassName(item, this.ITEM_CLASS);

    return item;
  },
  
  /**
   * Creates a store dom element for display in the info window.
   * @param {Object] store The store JSON object as returned by AJAX.
   * @return {HTMLElement} A dom element for displaying the store in the info window.
   */
  createStoreInfo: function(store)
  {
	// Create our dom elements.
    var info = document.createElement('div'),
        name = document.createElement('h3'),
        address = this.createAddressBlock(store),
        dirLink = this.createDirectionsLink(store),
        br = document.createElement('br');

    // Store name.
    name.innerHTML = store.name;

    // Put the dom elements together.
    info.appendChild(name);
    info.appendChild(address);
    if (store.url)
    {
      info.appendChild(this.createLink(
      {
    	text: store.url,
    	href: 'http://' + store.url,
    	openNewWindow: true
      }));
      info.appendChild(br.cloneNode(true));
    }
    info.appendChild(dirLink);
    info.appendChild(br.cloneNode(true));
    Element.addClassName(info, this.INFO_CLASS);

    return info;
  },
  
  /**
   * Loads new stores onto the map.
   * @param {Array} stores Array of store JSON objects as returned by AJAX.
   */
  loadStoresInMap: function(stores)
  {
	var i,
	    currStore,
	    newMarker;

	for (i = 0; i < stores.length; i++)
	{
	  currStore = stores[i].Store;
	  
	  // Make sure store coords are treated as floats.
	  currStore.lat = parseFloat(currStore.lat);
	  currStore.lon = parseFloat(currStore.lon);
	  
	  // Only add the store if it hasn't already been added.
	  if (!this.mapStores[currStore.id])
	  {
		// Create and add a new store marker
		newMarker = new GMarker(new GLatLng(currStore.lat, currStore.lon), { icon: this.storeIcon });
	    this.map.addOverlay(newMarker);

	    // Attach an event handler for a marker click.
	    GEvent.addListener(newMarker, 'click', this.markerClickHandler.bindAsEventListener(this, currStore.id));
	    
	    // Save the new store.
	    this.mapStores[currStore.id] = { marker: newMarker, storeInfo: currStore };
	  }
    }
  },
  
  /**
   * Form validation for address search form.
   * @param {Form element} form The address search form.
   * @return False if the address field is blank or contains spaces. 
   */
  validateForm: function(form)
  {
    return !form.StoreAddress.value.match(/^\s*$/);
  },
  
  /**
   * Event handler for a map marker click.
   * @param {Event} e The event associated with the marker click.
   * @param {Integer} id The id of the store that the marker belongs to.
   */
  markerClickHandler: function(e, id)
  {
	var store = this.mapStores[id],
        listIndex = this.listStores[id];
    this.map.openInfoWindow(store.marker.getLatLng(), this.createStoreInfo(store.storeInfo));
	this.list.selectItem(listIndex);
  },
  
  /**
   * Event handler for map info window closing.
   */
  infoWindowCloseHandler: function()
  {
	// Deselect the selected item in the store listing, if any.
    this.list.deselectItems();
  },
  
  /**
   * Event handler for show store on map link.
   * @param {Event} e The event associated with the link click.
   * @param {Integer} id The id of the store whose link was clicked on.
   */
  showClickHandler: function(e, id)
  {
	var store = this.mapStores[id],
	    listIndex = this.listStores[id];
	this.map.setCenter(store.marker.getLatLng(), this.DEFAULT_ZOOM);
	this.map.openInfoWindow(store.marker.getLatLng(), this.createStoreInfo(store.storeInfo));
	this.list.selectItem(listIndex);
	e.stop();
  },
  
  /**
   * Handles a map move event. Also fires when the map is zoomed.
   * Makes an Ajax call for the stores within the new map bounds.
   */
  mapMoveHandler: function()
  {
	// Get the map bounds and format an object for posting to AJAX in CakePHP-friendly format.
	var mapBounds = this.map.getBounds(),
	    swLatLng = mapBounds.getSouthWest(),
	    neLatLng = mapBounds.getNorthEast(),
	    // Round the coords to the nearest 0.0001
	    coords =
	      {
	        'data[Store][swLat]': Math.round(swLatLng.lat()*10000)/10000,
	        'data[Store][swLng]': Math.round(swLatLng.lng()*10000)/10000,
	        'data[Store][neLat]': Math.round(neLatLng.lat()*10000)/10000,
	        'data[Store][neLng]': Math.round(neLatLng.lng()*10000)/10000
	      };

	// Make Ajax call to retrieve the stores within the bounds.
	new Ajax.Request(this.BOUNDS_SEARCH_URL,
	{
	  method: 'post',
	  parameters: coords,
	  onSuccess: function(transport, storesJson)
	  {
		// Load the new stores.
		if (storesJson)
		{
		  this.loadStoresInMap(storesJson);
		}
	  }.bind(this)
	});
  }
}

/**
 * Geolocation class to determine the user's location using various geolocation apis.
 * Supports W3C Geolocation API included with HTML 5 (http://dev.w3.org/geo/api/spec-source.html)
 * Should work with Firefox 3.5, iPhone OS 3.0, Opera, IE8 (experimental)
 * Also works with Firefox 3.0 and the Geode plugin.
 * Also works with Bondi Geolocation widget.
 * Could be expanded to work with Google Gears.
 * Could be expanded to work with Blackberry's geolocation API (buggy, hackish).
 * <p>Usage: var newGeoLocator = new GeoLocator();</p>
 * @class GeoLocator
 * @requires Function (prototype)
 * @constructor
 */
GeoLocator = function()
{
  this.geo = null;

  // Find something to geolocate with.
  if (typeof(navigator) != 'undefined' && typeof(navigator.geolocation) != 'undefined')
  {
    this.geo = navigator.geolocation;
  }
  else if (typeof(bondi) != 'undefined' && typeof(bondi.geolocation) != 'undefined')
  {
	this.geo = bondi.geolocation;
  }
}

GeoLocator.prototype =
{
  /**
   * Gets the current position of the user. Follows W3C Geolocation API spec.
   * @param {Function} successCallback Callback function for successfully retrieving the location. Passes the user's location as a parameter. (http://dev.w3.org/geo/api/spec-source.html#position_interface)
   * @param {Function} errorCallback Callback function for failing to retrieve the location. Passes an error object as a parameter. (http://dev.w3.org/geo/api/spec-source.html#position_error_interface)
   * @param {Object} options Position options as per W3C Geolocation API (enableHighAccuracy, timeout, maximumAge). (http://dev.w3.org/geo/api/spec-source.html#position_options_interface)
   */
  getCurrentPosition: function(successCallback, errorCallback, options)
  {
	if (this.geo)
	{
	  // We need to clean up the return values so call our own success callback first.
      this.geo.getCurrentPosition(this.successCallback.bind(this, successCallback), errorCallback, options);
	}
	else if (typeof(errorCallback) == 'function')
	{
	  // http://dev.w3.org/geo/api/spec-source.html#position_unavailable_error
	  errorCallback({ code: 2, message: 'Position unavailable' });
	}
  },
  
  /**
   * Callback function for successfully retrieving the user's location.
   * Cleans up the return value to match W3C Geolocation API spec.
   * @param {Function} callback Callback function for successfully retrieving the location. Passes the user's location as a parameter. (http://dev.w3.org/geo/api/spec-source.html#position_interface)
   * @param {Object} position The user's location. (http://dev.w3.org/geo/api/spec-source.html#position_interface)
   */
  successCallback: function(callback, position)
  {
	// Mozilla Geode returns the longitude and latitude at the base of the object.
	if (typeof(position.latitude) != 'undefined')
    {
	  position.coords = { latitude: position.latitude, longitude: position.longitude };
    }
	if (typeof(callback) == 'function')
	{
	  callback(position);
	}
  }
}