/**! hopscotch - v0.2.2
 *
 * Copyright 2014 LinkedIn Corp. All rights reserved.
 *
 * Licensed 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.
 */
(function(context, namespace) {
	var Hopscotch,
		HopscotchBubble,
		HopscotchCalloutManager,
		HopscotchI18N,
		customI18N,
		customRenderer,
		customEscape,
		templateToUse = 'bubble_default',
		Sizzle = window.Sizzle || null,
		utils,
		callbacks,
		helpers,
		winLoadHandler,
		defaultOpts,
		winHopscotch      = context[namespace],
		undefinedStr      = 'undefined',
		waitingToStart    = false, // is a tour waiting for the document to finish
	// loading so that it can start?
		hasJquery         = (typeof window.jQuery !== undefinedStr),
		hasSessionStorage = false,
		isStorageWritable = false,
		document          = window.document;

	// If cookies are disabled, accessing sessionStorage can throw an error.
	// sessionStorage could also throw an error in Safari on write (even though it exists).
	// So, we'll try writing to sessionStorage to verify it's available.
	try {
		if(typeof window.sessionStorage !== undefinedStr){
			hasSessionStorage = true;
			sessionStorage.setItem('hopscotch.test.storage', 'ok');
			sessionStorage.removeItem('hopscotch.test.storage');
			isStorageWritable = true;
		}
	} catch (err) {}

	defaultOpts       = {
		smoothScroll:    true,
		scrollDuration:  1000,
		scrollTopMargin: 200,
		showCloseButton: true,
		showPrevButton:  false,
		showNextButton:  true,
		bubbleWidth:     280,
		bubblePadding:   15,
		arrowWidth:      20,
		skipIfNoElement: true,
		cookieName:      'hopscotch.tour.state'
	};

	if (winHopscotch) {
		// Hopscotch already exists.
		return;
	}

	if (!Array.isArray) {
		Array.isArray = function(obj) {
			return Object.prototype.toString.call(obj) === '[object Array]';
		};
	}

	/**
	 * Called when the page is done loading.
	 *
	 * @private
	 */
	winLoadHandler = function() {
		if (waitingToStart) {
			winHopscotch.startTour();
		}
	};

	/**
	 * utils
	 * =====
	 * A set of utility functions, mostly for standardizing to manipulate
	 * and extract information from the DOM. Basically these are things I
	 * would normally use jQuery for, but I don't want to require it for
	 * this framework.
	 *
	 * @private
	 */
	utils = {
		/**
		 * addClass
		 * ========
		 * Adds one or more classes to a DOM element.
		 *
		 * @private
		 */
		addClass: function(domEl, classToAdd) {
			var domClasses,
				classToAddArr,
				setClass,
				i,
				len;

			if (!domEl.className) {
				domEl.className = classToAdd;
			}
			else {
				classToAddArr = classToAdd.split(/\s+/);
				domClasses = ' ' + domEl.className + ' ';
				for (i = 0, len = classToAddArr.length; i < len; ++i) {
					if (domClasses.indexOf(' ' + classToAddArr[i] + ' ') < 0) {
						domClasses += classToAddArr[i] + ' ';
					}
				}
				domEl.className = domClasses.replace(/^\s+|\s+$/g,'');
			}
		},

		/**
		 * removeClass
		 * ===========
		 * Remove one or more classes from a DOM element.
		 *
		 * @private
		 */
		removeClass: function(domEl, classToRemove) {
			var domClasses,
				classToRemoveArr,
				currClass,
				i,
				len;

			classToRemoveArr = classToRemove.split(/\s+/);
			domClasses = ' ' + domEl.className + ' ';
			for (i = 0, len = classToRemoveArr.length; i < len; ++i) {
				domClasses = domClasses.replace(' ' + classToRemoveArr[i] + ' ', ' ');
			}
			domEl.className = domClasses.replace(/^\s+|\s+$/g,'');
		},

		/**
		 * hasClass
		 * ========
		 * Determine if a given DOM element has a class.
		 */
		hasClass: function(domEl, classToCheck){
			var classes;

			if(!domEl.className){ return false; }
			classes = ' ' + domEl.className + ' ';
			return (classes.indexOf(' ' + classToCheck + ' ') !== -1);
		},

		/**
		 * @private
		 */
		getPixelValue: function(val) {
			var valType = typeof val;
			if (valType === 'number') { return val; }
			if (valType === 'string') { return parseInt(val, 10); }
			return 0;
		},

		/**
		 * Inspired by Python... returns val if it's defined, otherwise returns the default.
		 *
		 * @private
		 */
		valOrDefault: function(val, valDefault) {
			return typeof val !== undefinedStr ? val : valDefault;
		},

		/**
		 * Invokes a single callback represented by an array.
		 * Example input: ["my_fn", "arg1", 2, "arg3"]
		 * @private
		 */
		invokeCallbackArrayHelper: function(arr) {
			// Logic for a single callback
			var fn;
			if (Array.isArray(arr)) {
				fn = helpers[arr[0]];
				if (typeof fn === 'function') {
					return fn.apply(this, arr.slice(1));
				}
			}
		},

		/**
		 * Invokes one or more callbacks. Array should have at most one level of nesting.
		 * Example input:
		 * ["my_fn", "arg1", 2, "arg3"]
		 * [["my_fn_1", "arg1", "arg2"], ["my_fn_2", "arg2-1", "arg2-2"]]
		 * [["my_fn_1", "arg1", "arg2"], function() { ... }]
		 * @private
		 */
		invokeCallbackArray: function(arr) {
			var i, len;

			if (Array.isArray(arr)) {
				if (typeof arr[0] === 'string') {
					// Assume there are no nested arrays. This is the one and only callback.
					return utils.invokeCallbackArrayHelper(arr);
				}
				else { // assume an array
					for (i = 0, len = arr.length; i < len; ++i) {
						utils.invokeCallback(arr[i]);
					}
				}
			}
		},

		/**
		 * Helper function for invoking a callback, whether defined as a function literal
		 * or an array that references a registered helper function.
		 * @private
		 */
		invokeCallback: function(cb) {
			if (typeof cb === 'function') {
				return cb();
			}
			if (typeof cb === 'string' && helpers[cb]) { // name of a helper
				return helpers[cb]();
			}
			else { // assuming array
				return utils.invokeCallbackArray(cb);
			}
		},

		/**
		 * If stepCb (the step-specific helper callback) is passed in, then invoke
		 * it first. Then invoke tour-wide helper.
		 *
		 * @private
		 */
		invokeEventCallbacks: function(evtType, stepCb) {
			var cbArr = callbacks[evtType],
				callback,
				fn,
				i,
				len;

			if (stepCb) {
				return this.invokeCallback(stepCb);
			}

			for (i=0, len=cbArr.length; i<len; ++i) {
				this.invokeCallback(cbArr[i].cb);
			}
		},

		/**
		 * @private
		 */
		getScrollTop: function() {
			var scrollTop;
			if (typeof window.pageYOffset !== undefinedStr) {
				scrollTop = window.pageYOffset;
			}
			else {
				// Most likely IE <=8, which doesn't support pageYOffset
				scrollTop = document.documentElement.scrollTop;
			}
			return scrollTop;
		},

		/**
		 * @private
		 */
		getScrollLeft: function() {
			var scrollLeft;
			if (typeof window.pageXOffset !== undefinedStr) {
				scrollLeft = window.pageXOffset;
			}
			else {
				// Most likely IE <=8, which doesn't support pageXOffset
				scrollLeft = document.documentElement.scrollLeft;
			}
			return scrollLeft;
		},

		/**
		 * @private
		 */
		getWindowHeight: function() {
			return window.innerHeight || document.documentElement.clientHeight;
		},

		/**
		 * @private
		 */
		getWindowWidth: function() {
			return window.innerWidth || document.documentElement.clientWidth;
		},

		/**
		 * @private
		 */
		addEvtListener: function(el, evtName, fn) {
			return el.addEventListener ? el.addEventListener(evtName, fn, false) : el.attachEvent('on' + evtName, fn);
		},

		/**
		 * @private
		 */
		removeEvtListener: function(el, evtName, fn) {
			return el.removeEventListener ? el.removeEventListener(evtName, fn, false) : el.detachEvent('on' + evtName, fn);
		},

		documentIsReady: function() {
			return document.readyState === 'complete' || document.readyState === 'interactive';
		},

		/**
		 * @private
		 */
		evtPreventDefault: function(evt) {
			if (evt.preventDefault) {
				evt.preventDefault();
			}
			else if (event) {
				event.returnValue = false;
			}
		},

		/**
		 * @private
		 */
		extend: function(obj1, obj2) {
			var prop;
			for (prop in obj2) {
				if (obj2.hasOwnProperty(prop)) {
					obj1[prop] = obj2[prop];
				}
			}
		},

		/**
		 * Helper function to get a single target DOM element. We will try to
		 * locate the DOM element through several ways, in the following order:
		 *
		 * 1) Passing the string into document.querySelector
		 * 2) Passing the string to jQuery, if it exists
		 * 3) Passing the string to Sizzle, if it exists
		 * 4) Calling document.getElementById if it is a plain id
		 *
		 * Default case is to assume the string is a plain id and call
		 * document.getElementById on it.
		 *
		 * @private
		 */
		getStepTargetHelper: function(target){
			var result = document.getElementById(target);

			//Backwards compatibility: assume the string is an id
			if (result) {
				return result;
			}
			if (hasJquery) {
				result = jQuery(target);
				return result.length ? result[0] : null;
			}
			if (Sizzle) {
				result = new Sizzle(target);
				return result.length ? result[0] : null;
			}
			if (document.querySelector) {
				try {
					return document.querySelector(target);
				} catch (err) {}
			}
			// Regex test for id. Following the HTML 4 spec for valid id formats.
			// (http://www.w3.org/TR/html4/types.html#type-id)
			if (/^#[a-zA-Z][\w-_:.]*$/.test(target)) {
				return document.getElementById(target.substring(1));
			}

			return null;
		},

		/**
		 * Given a step, returns the target DOM element associated with it. It is
		 * recommended to only assign one target per step. However, there are
		 * some use cases which require multiple step targets to be supplied. In
		 * this event, we will use the first target in the array that we can
		 * locate on the page. See the comments for getStepTargetHelper for more
		 * information.
		 *
		 * @private
		 */
		getStepTarget: function(step) {
			var queriedTarget;

			if (!step || !step.target) {
				return null;
			}

			if (typeof step.target === 'string') {
				//Just one target to test. Check and return its results.
				return utils.getStepTargetHelper(step.target);
			}
			else if (Array.isArray(step.target)) {
				// Multiple items to check. Check each and return the first success.
				// Assuming they are all strings.
				var i,
					len;

				for (i = 0, len = step.target.length; i < len; i++){
					if (typeof step.target[i] === 'string') {
						queriedTarget = utils.getStepTargetHelper(step.target[i]);

						if (queriedTarget) {
							return queriedTarget;
						}
					}
				}
				return null;
			}

			// Assume that the step.target is a DOM element
			return step.target;
		},

		/**
		 * Convenience method for getting an i18n string. Returns custom i18n value
		 * or the default i18n value if no custom value exists.
		 *
		 * @private
		 */
		getI18NString: function(key) {
			return customI18N[key] || HopscotchI18N[key];
		},

		// Tour session persistence for multi-page tours. Uses HTML5 sessionStorage if available, then
		// falls back to using cookies.
		//
		// The following cookie-related logic is borrowed from:
		// http://www.quirksmode.org/js/cookies.html

		/**
		 * @private
		 */
		setState: function(name,value,days) {
			var expires = '',
				date;

			if (hasSessionStorage && isStorageWritable) {
				try{
					sessionStorage.setItem(name, value);
				}
				catch(err){
					isStorageWritable = false;
					this.setState(name, value, days);
				}
			}
			else {
				if(hasSessionStorage){
					//Clear out existing sessionStorage key so the new value we set to cookie gets read.
					//(If we're here, we've run into an error while trying to write to sessionStorage).
					sessionStorage.removeItem(name);
				}
				if (days) {
					date = new Date();
					date.setTime(date.getTime()+(days*24*60*60*1000));
					expires = '; expires='+date.toGMTString();
				}
				document.cookie = name+'='+value+expires+'; path=/';
			}
		},

		/**
		 * @private
		 */
		getState: function(name) {
			var nameEQ = name + '=',
				ca = document.cookie.split(';'),
				i,
				c,
				state;

			//return value from session storage if we have it
			if (hasSessionStorage) {
				state = sessionStorage.getItem(name);
				if(state){
					return state;
				}
			}

			//else, try cookies
			for(i=0;i < ca.length;i++) {
				c = ca[i];
				while (c.charAt(0)===' ') {c = c.substring(1,c.length);}
				if (c.indexOf(nameEQ) === 0) {
					state = c.substring(nameEQ.length,c.length);
					break;
				}
			}

			return state;
		},

		/**
		 * @private
		 */
		clearState: function(name) {
			if (hasSessionStorage) {
				sessionStorage.removeItem(name);
			}
			else {
				this.setState(name,'',-1);
			}
		}
	};

	utils.addEvtListener(window, 'load', winLoadHandler);

	callbacks = {
		next:  [],
		prev:  [],
		start: [],
		end:   [],
		show:  [],
		error: [],
		close: []
	};

	/**
	 * helpers
	 * =======
	 * A map of functions to be used as callback listeners. Functions are
	 * added to and removed from the map using the functions
	 * Hopscotch.registerHelper() and Hopscotch.unregisterHelper().
	 */
	helpers = {};

	HopscotchI18N = {
		stepNums: null,
		nextBtn: 'Next',
		prevBtn: 'Back',
		doneBtn: 'Done',
		skipBtn: 'Skip',
		closeTooltip: 'Close'
	};

	customI18N = {}; // Developer's custom i18n strings goes here.

	/**
	 * HopscotchBubble
	 *
	 * @class The HopscotchBubble class represents the view of a bubble. This class is also used for Hopscotch callouts.
	 */
	HopscotchBubble = function(opt) {
		this.init(opt);
	};

	HopscotchBubble.prototype = {
		isShowing: false,

		currStep: undefined,

		/**
		 * setPosition
		 *
		 * Sets the position of the bubble using the bounding rectangle of the
		 * target element and the orientation and offset information specified by
		 * the JSON.
		 */
		setPosition: function(step) {
			var bubbleBoundingHeight,
				bubbleBoundingWidth,
				boundingRect,
				top,
				left,
				arrowOffset,
				targetEl     = utils.getStepTarget(step),
				el           = this.element,
				arrowEl      = this.arrowEl;

			bubbleBoundingWidth = el.offsetWidth;
			bubbleBoundingHeight = el.offsetHeight;
			utils.removeClass(el, 'fade-in-down fade-in-up fade-in-left fade-in-right');

			// Originally called it orientation, but placement is more intuitive.
			// Allowing both for now for backwards compatibility.
			if (!step.placement && step.orientation) {
				step.placement = step.orientation;
			}

			// SET POSITION
			boundingRect = targetEl.getBoundingClientRect();
			if (step.placement === 'top') {
				top = (boundingRect.top - bubbleBoundingHeight) - this.opt.arrowWidth;
				left = boundingRect.left;
			}
			else if (step.placement === 'bottom') {
				top = boundingRect.bottom + this.opt.arrowWidth;
				left = boundingRect.left;
			}
			else if (step.placement === 'left') {
				top = boundingRect.top;
				left = boundingRect.left - bubbleBoundingWidth - this.opt.arrowWidth;
			}
			else if (step.placement === 'right') {
				top = boundingRect.top;
				left = boundingRect.right + this.opt.arrowWidth;
			}
			else {
				throw 'Bubble placement failed because step.placement is invalid or undefined!';
			}

			// SET (OR RESET) ARROW OFFSETS
			if (step.arrowOffset !== 'center') {
				arrowOffset = utils.getPixelValue(step.arrowOffset);
			}
			else {
				arrowOffset = step.arrowOffset;
			}
			if (!arrowOffset) {
				arrowEl.style.top = '';
				arrowEl.style.left = '';
			}
			else if (step.placement === 'top' || step.placement === 'bottom') {
				arrowEl.style.top = '';
				if (arrowOffset === 'center') {
					arrowEl.style.left = Math.floor((bubbleBoundingWidth / 2) - arrowEl.offsetWidth/2) + 'px';
				}
				else {
					// Numeric pixel value
					arrowEl.style.left = arrowOffset + 'px';
				}
			}
			else if (step.placement === 'left' || step.placement === 'right') {
				arrowEl.style.left = '';
				if (arrowOffset === 'center') {
					arrowEl.style.top = Math.floor((bubbleBoundingHeight / 2) - arrowEl.offsetHeight/2) + 'px';
				}
				else {
					// Numeric pixel value
					arrowEl.style.top = arrowOffset + 'px';
				}
			}

			// HORIZONTAL OFFSET
			if (step.xOffset === 'center') {
				left = (boundingRect.left + targetEl.offsetWidth/2) - (bubbleBoundingWidth / 2);
			}
			else {
				left += utils.getPixelValue(step.xOffset);
			}
			// VERTICAL OFFSET
			if (step.yOffset === 'center') {
				top = (boundingRect.top + targetEl.offsetHeight/2) - (bubbleBoundingHeight / 2);
			}
			else {
				top += utils.getPixelValue(step.yOffset);
			}

			// ADJUST TOP FOR SCROLL POSITION
			if (!step.fixedElement) {
				top += utils.getScrollTop();
				left += utils.getScrollLeft();
			}

			// ACCOUNT FOR FIXED POSITION ELEMENTS
			el.style.position = (step.fixedElement ? 'fixed' : 'absolute');

			el.style.top = top + 'px';
			el.style.left = left + 'px';
		},

		/**
		 * Renders the bubble according to the step JSON.
		 *
		 * @param {Object} step Information defining how the bubble should look.
		 * @param {Number} idx The index of the step in the tour. Not used for callouts.
		 * @param {Function} callback Function to be invoked after rendering is finished.
		 */
		render: function(step, idx, callback) {
			var el = this.element,
				tourSpecificRenderer,
				customTourData,
				unsafe,
				currTour,
				totalSteps,
				nextBtnText,
				isLast,
				opts;

			// Cache current step information.
			if (step) {
				this.currStep = step;
			}
			else if (this.currStep) {
				step = this.currStep;
			}

			// Check current tour for total number of steps and custom render data
			if(this.opt.isTourBubble){
				currTour = winHopscotch.getCurrTour();
				if(currTour){
					customTourData = currTour.customData;
					tourSpecificRenderer = currTour.customRenderer;
					unsafe = currTour.unsafe;
					if(Array.isArray(currTour.steps)){
						totalSteps = currTour.steps.length;
						isLast = (idx === totalSteps - 1);
					}
				}
			}else{
				customTourData = step.customData;
				tourSpecificRenderer = step.customRenderer;
				unsafe = step.unsafe;
			}

			// Determine label for next button
			if(isLast){
				nextBtnText = utils.getI18NString('doneBtn');
			} else if(step.showSkip) {
				nextBtnText = utils.getI18NString('skipBtn');
			} else {
				nextBtnText = utils.getI18NString('nextBtn');
			}

			// Originally called it orientation, but placement is more intuitive.
			// Allowing both for now for backwards compatibility.
			if (!step.placement && step.orientation) {
				step.placement = step.orientation;
			}
			this.placement = step.placement;

			// Setup the configuration options we want to pass along to the template
			opts = {
				i18n: {
					prevBtn: utils.getI18NString('prevBtn'),
					nextBtn: nextBtnText,
					closeTooltip: utils.getI18NString('closeTooltip'),
					stepNum: this._getStepI18nNum(idx),
				},
				buttons:{
					showPrev: (utils.valOrDefault(step.showPrevButton, this.opt.showPrevButton) && (idx > 0)),
					showNext: utils.valOrDefault(step.showNextButton, this.opt.showNextButton),
					showCTA: utils.valOrDefault((step.showCTAButton && step.ctaLabel), false),
					ctaLabel: step.ctaLabel,
					showClose: utils.valOrDefault(this.opt.showCloseButton, true)
				},
				step:{
					num: idx,
					isLast: utils.valOrDefault(isLast, false),
					title: (step.title || ''),
					content: (step.content || ''),
					placement: step.placement,
					padding: utils.valOrDefault(step.padding, this.opt.bubblePadding),
					width: utils.getPixelValue(step.width) || this.opt.bubbleWidth,
					customData: (step.customData || {})
				},
				tour:{
					isTour: this.opt.isTourBubble,
					numSteps: totalSteps,
					unsafe: utils.valOrDefault(unsafe, false),
					customData: (customTourData || {})
				}
			};

			// Render the bubble's content.
			// Use tour renderer if available, then the global customRenderer if defined.
			if(typeof tourSpecificRenderer === 'function'){
				el.innerHTML = tourSpecificRenderer(opts);
			}
			else if(typeof tourSpecificRenderer === 'string'){
				if(!hopscotch.templates || (typeof hopscotch.templates[tourSpecificRenderer] !== 'function')){
					throw 'Bubble rendering failed - template "' + tourSpecificRenderer + '" is not a function.';
				}
				el.innerHTML = hopscotch.templates[tourSpecificRenderer](opts);
			}
			else if(customRenderer){
				el.innerHTML = customRenderer(opts);
			}
			else{
				if(!hopscotch.templates || (typeof hopscotch.templates[templateToUse] !== 'function')){
					throw 'Bubble rendering failed - template "' + templateToUse + '" is not a function.';
				}
				el.innerHTML = hopscotch.templates[templateToUse](opts);
			}

			// Find arrow among new child elements.
			children = el.children;
			numChildren = children.length;
			for (i = 0; i < numChildren; i++){
				node = children[i];

				if(utils.hasClass(node, 'hopscotch-arrow')){
					this.arrowEl = node;
				}
			}

			// Set z-index and arrow placement
			el.style.zIndex = (typeof step.zindex === 'number') ? step.zindex : 'auto';
			this._setArrow(step.placement);

			// Set bubble positioning
			// Make sure we're using visibility:hidden instead of display:none for height/width calculations.
			this.hide(false);
			this.setPosition(step);

			// only want to adjust window scroll for non-fixed elements
			if (callback) {
				callback(!step.fixedElement);
			}

			return this;
		},

		/**
		 * Get the I18N step number for the current step.
		 *
		 * @private
		 */
		_getStepI18nNum: function(idx) {
			var stepNumI18N = utils.getI18NString('stepNums');
			if (stepNumI18N && idx < stepNumI18N.length) {
				idx = stepNumI18N[idx];
			}
			else {
				idx = idx + 1;
			}
			return idx;
		},

		/**
		 * Sets which side the arrow is on.
		 *
		 * @private
		 */
		_setArrow: function(orientation) {
			utils.removeClass(this.arrowEl, 'down up right left');

			// Whatever the orientation is, we want to arrow to appear
			// "opposite" of the orientation. E.g., a top orientation
			// requires a bottom arrow.
			if (orientation === 'top') {
				utils.addClass(this.arrowEl, 'down');
			}
			else if (orientation === 'bottom') {
				utils.addClass(this.arrowEl, 'up');
			}
			else if (orientation === 'left') {
				utils.addClass(this.arrowEl, 'right');
			}
			else if (orientation === 'right') {
				utils.addClass(this.arrowEl, 'left');
			}
		},

		/**
		 * @private
		 */
		_getArrowDirection: function() {
			if (this.placement === 'top') {
				return 'down';
			}
			if (this.placement === 'bottom') {
				return 'up';
			}
			if (this.placement === 'left') {
				return 'right';
			}
			if (this.placement === 'right') {
				return 'left';
			}
		},

		show: function() {
			var self      = this,
				fadeClass = 'fade-in-' + this._getArrowDirection(),
				fadeDur   = 1000;

			utils.removeClass(this.element, 'hide');
			utils.addClass(this.element, fadeClass);
			setTimeout(function() {
				utils.removeClass(self.element, 'invisible');
			}, 50);
			setTimeout(function() {
				utils.removeClass(self.element, fadeClass);
			}, fadeDur);
			this.isShowing = true;
			return this;
		},

		hide: function(remove) {
			var el = this.element;

			remove = utils.valOrDefault(remove, true);
			el.style.top = '';
			el.style.left = '';

			// display: none
			if (remove) {
				utils.addClass(el, 'hide');
				utils.removeClass(el, 'invisible');
			}
			// opacity: 0
			else {
				utils.removeClass(el, 'hide');
				utils.addClass(el, 'invisible');
			}
			utils.removeClass(el, 'animate fade-in-up fade-in-down fade-in-right fade-in-left');
			this.isShowing = false;
			return this;
		},

		destroy: function() {
			var el = this.element;

			if (el) {
				el.parentNode.removeChild(el);
			}
			utils.removeEvtListener(el, 'click', this.clickCb);
		},

		_handleBubbleClick: function(evt){
			var action;

			// Override evt for IE8 as IE8 doesn't pass event but binds it to window
			evt = evt || window.event; // get window.event if argument is falsy (in IE)

			// get srcElement if target is falsy (IE)
			var targetElement = evt.target || evt.srcElement;

			//Recursively look up the parent tree until we find a match
			//with one of the classes we're looking for, or the triggering element.
			function findMatchRecur(el){
				/* We're going to make the assumption that we're not binding
				 * multiple event classes to the same element.
				 * (next + previous = wait... err... what?)
				 *
				 * In the odd event we end up with an element with multiple
				 * possible matches, the following priority order is applied:
				 * hopscotch-cta, hopscotch-next, hopscotch-prev, hopscotch-close
				 */
				if(el === evt.currentTarget){ return null; }
				if(utils.hasClass(el, 'hopscotch-cta')){ return 'cta'; }
				if(utils.hasClass(el, 'hopscotch-next')){ return 'next'; }
				if(utils.hasClass(el, 'hopscotch-prev')){ return 'prev'; }
				if(utils.hasClass(el, 'hopscotch-close')){ return 'close'; }
				/*else*/ return findMatchRecur(el.parentElement);
			}

			action = findMatchRecur(targetElement);

			//Now that we know what action we should take, let's take it.
			if (action === 'cta'){
				if (!this.opt.isTourBubble) {
					// This is a callout. Close the callout when CTA is clicked.
					winHopscotch.getCalloutManager().removeCallout(this.currStep.id);
				}
				// Call onCTA callback if one is provided
				if (this.currStep.onCTA) {
					utils.invokeCallback(this.currStep.onCTA);
				}
			}
			else if (action === 'next'){
				winHopscotch.nextStep(true);
			}
			else if (action === 'prev'){
				winHopscotch.prevStep(true);
			}
			else if (action === 'close'){
				if (this.opt.isTourBubble){
					var currStepNum   = winHopscotch.getCurrStepNum(),
						currTour      = winHopscotch.getCurrTour(),
						doEndCallback = (currStepNum === currTour.steps.length-1);

					utils.invokeEventCallbacks('close');

					winHopscotch.endTour(true, doEndCallback);
				} else {
					if (this.opt.onClose) {
						utils.invokeCallback(this.opt.onClose);
					}
					if (this.opt.id && !this.opt.isTourBubble) {
						// Remove via the HopscotchCalloutManager.
						// removeCallout() calls HopscotchBubble.destroy internally.
						winHopscotch.getCalloutManager().removeCallout(this.opt.id);
					}
					else {
						this.destroy();
					}
				}

				utils.evtPreventDefault(evt);
			}
			//Otherwise, do nothing. We didn't click on anything relevant.
		},

		init: function(initOpt) {
			var el              = document.createElement('div'),
				self            = this,
				resizeCooldown  = false, // for updating after window resize
				onWinResize,
				appendToBody,
				children,
				numChildren,
				node,
				i,
				opt;

			//Register DOM element for this bubble.
			this.element = el;

			//Merge bubble options with defaults.
			opt = {
				showPrevButton: defaultOpts.showPrevButton,
				showNextButton: defaultOpts.showNextButton,
				bubbleWidth:    defaultOpts.bubbleWidth,
				bubblePadding:  defaultOpts.bubblePadding,
				arrowWidth:     defaultOpts.arrowWidth,
				showNumber:     true,
				isTourBubble:   true
			};
			initOpt = (typeof initOpt === undefinedStr ? {} : initOpt);
			utils.extend(opt, initOpt);
			this.opt = opt;

			//Apply classes to bubble. Add "animated" for fade css animation
			el.className = 'hopscotch-bubble animated';
			if (!opt.isTourBubble) {
				utils.addClass(el, 'hopscotch-callout no-number');
			}

			/**
			 * Not pretty, but IE8 doesn't support Function.bind(), so I'm
			 * relying on closures to keep a handle of "this".
			 * Reset position of bubble when window is resized
			 *
			 * @private
			 */
			onWinResize = function() {
				if (resizeCooldown || !self.isShowing) {
					return;
				}

				resizeCooldown = true;
				setTimeout(function() {
					self.setPosition(self.currStep);
					resizeCooldown = false;
				}, 100);
			};

			//Add listener to reset bubble position on window resize
			utils.addEvtListener(window, 'resize', onWinResize);

			//Create our click callback handler and keep a
			//reference to it for later.
			this.clickCb = function(evt){
				self._handleBubbleClick(evt);
			};
			utils.addEvtListener(el, 'click', this.clickCb);

			//Hide the bubble by default
			this.hide();

			//Finally, append our new bubble to body once the DOM is ready.
			if (utils.documentIsReady()) {
				document.body.appendChild(el);
			}
			else {
				// Moz, webkit, Opera
				if (document.addEventListener) {
					appendToBody = function() {
						document.removeEventListener('DOMContentLoaded', appendToBody);
						window.removeEventListener('load', appendToBody);

						document.body.appendChild(el);
					};

					document.addEventListener('DOMContentLoaded', appendToBody, false);
				}
				// IE
				else {
					appendToBody = function() {
						if (document.readyState === 'complete') {
							document.detachEvent('onreadystatechange', appendToBody);
							window.detachEvent('onload', appendToBody);
							document.body.appendChild(el);
						}
					};

					document.attachEvent('onreadystatechange', appendToBody);
				}
				utils.addEvtListener(window, 'load', appendToBody);
			}
		}
	};

	/**
	 * HopscotchCalloutManager
	 *
	 * @class Manages the creation and destruction of single callouts.
	 * @constructor
	 */
	HopscotchCalloutManager = function() {
		var callouts = {};

		/**
		 * createCallout
		 *
		 * Creates a standalone callout. This callout has the same API
		 * as a Hopscotch tour bubble.
		 *
		 * @param {Object} opt The options for the callout. For the most
		 * part, these are the same options as you would find in a tour
		 * step.
		 */
		this.createCallout = function(opt) {
			var callout;

			if (opt.id) {
				if (callouts[opt.id]) {
					throw 'Callout by that id already exists. Please choose a unique id.';
				}
				opt.showNextButton = opt.showPrevButton = false;
				opt.isTourBubble = false;
				callout = new HopscotchBubble(opt);
				callouts[opt.id] = callout;
				if (opt.target) {
					callout.render(opt, null, function() {
						callout.show();
					});
				}
			}
			else {
				throw 'Must specify a callout id.';
			}
			return callout;
		};

		/**
		 * getCallout
		 *
		 * Returns a callout by its id.
		 *
		 * @param {String} id The id of the callout to fetch.
		 * @returns {Object} HopscotchBubble
		 */
		this.getCallout = function(id) {
			return callouts[id];
		};

		/**
		 * removeAllCallouts
		 *
		 * Removes all existing callouts.
		 */
		this.removeAllCallouts = function() {
			var calloutId,
				callout;

			for (calloutId in callouts) {
				if (callouts.hasOwnProperty(calloutId)) {
					this.removeCallout(calloutId);
				}
			}
		};

		/**
		 * removeAllCallout
		 *
		 * Removes an existing callout by id.
		 *
		 * @param {String} id The id of the callout to remove.
		 */
		this.removeCallout = function(id) {
			var callout = callouts[id];

			callouts[id] = null;
			if (!callout) { return; }

			callout.destroy();
		};
	};

	/**
	 * Hopscotch
	 *
	 * @class Creates the Hopscotch object. Used to manage tour progress and configurations.
	 * @constructor
	 * @param {Object} initOptions Options to be passed to `configure()`.
	 */
	Hopscotch = function(initOptions) {
		var self       = this, // for targetClickNextFn
			bubble,
			calloutMgr,
			opt,
			currTour,
			currStepNum,
			cookieTourId,
			cookieTourStep,
			_configure,

			/**
			 * getBubble
			 *
			 * Singleton accessor function for retrieving or creating bubble object.
			 *
			 * @private
			 * @param setOptions {Boolean} when true, transfers configuration options to the bubble
			 * @returns {Object} HopscotchBubble
			 */
				getBubble = function(setOptions) {
				if (!bubble) {
					bubble = new HopscotchBubble(opt);
				}
				if (setOptions) {
					utils.extend(bubble.opt, {
						bubblePadding:   getOption('bubblePadding'),
						bubbleWidth:     getOption('bubbleWidth'),
						showNextButton:  getOption('showNextButton'),
						showPrevButton:  getOption('showPrevButton'),
						showCloseButton: getOption('showCloseButton'),
						arrowWidth:      getOption('arrowWidth')
					});
				}
				return bubble;
			},

			/**
			 * Convenience method for getting an option. Returns custom config option
			 * or the default config option if no custom value exists.
			 *
			 * @private
			 * @param name {String} config option name
			 * @returns {Object} config option value
			 */
				getOption = function(name) {
				if (typeof opt === 'undefined') {
					return defaultOpts[name];
				}
				return utils.valOrDefault(opt[name], defaultOpts[name]);
			},

			/**
			 * getCurrStep
			 *
			 * @private
			 * @returns {Object} the step object corresponding to the current value of currStepNum
			 */
				getCurrStep = function() {
				var step;

				if (currStepNum < 0 || currStepNum >= currTour.steps.length) {
					step = null;
				}
				else {
					step = currTour.steps[currStepNum];
				}

				return step;
			},

			/**
			 * Used for nextOnTargetClick
			 *
			 * @private
			 */
				targetClickNextFn = function() {
				self.nextStep();
			},

			/**
			 * adjustWindowScroll
			 *
			 * Checks if the bubble or target element is partially or completely
			 * outside of the viewport. If it is, adjust the window scroll position
			 * to bring it back into the viewport.
			 *
			 * @private
			 * @param {Function} cb Callback to invoke after done scrolling.
			 */
				adjustWindowScroll = function(cb) {
				var bubble         = getBubble(),

				// Calculate the bubble element top and bottom position
					bubbleEl       = bubble.element,
					bubbleTop      = utils.getPixelValue(bubbleEl.style.top),
					bubbleBottom   = bubbleTop + utils.getPixelValue(bubbleEl.offsetHeight),

				// Calculate the target element top and bottom position
					targetEl       = utils.getStepTarget(getCurrStep()),
					targetBounds   = targetEl.getBoundingClientRect(),
					targetElTop    = targetBounds.top + utils.getScrollTop(),
					targetElBottom = targetBounds.bottom + utils.getScrollTop(),

				// The higher of the two: bubble or target
					targetTop      = (bubbleTop < targetElTop) ? bubbleTop : targetElTop,
				// The lower of the two: bubble or target
					targetBottom   = (bubbleBottom > targetElBottom) ? bubbleBottom : targetElBottom,

				// Calculate the current viewport top and bottom
					windowTop      = utils.getScrollTop(),
					windowBottom   = windowTop + utils.getWindowHeight(),

				// This is our final target scroll value.
					scrollToVal    = targetTop - getOption('scrollTopMargin'),

					scrollEl,
					yuiAnim,
					yuiEase,
					direction,
					scrollIncr,
					scrollTimeout,
					scrollTimeoutFn;

				// Target and bubble are both visible in viewport
				if (targetTop >= windowTop && (targetTop <= windowTop + getOption('scrollTopMargin') || targetBottom <= windowBottom)) {
					if (cb) { cb(); } // HopscotchBubble.show
				}

				// Abrupt scroll to scroll target
				else if (!getOption('smoothScroll')) {
					window.scrollTo(0, scrollToVal);

					if (cb) { cb(); } // HopscotchBubble.show
				}

				// Smooth scroll to scroll target
				else {
					// Use YUI if it exists
					if (typeof YAHOO             !== undefinedStr &&
						typeof YAHOO.env         !== undefinedStr &&
						typeof YAHOO.env.ua      !== undefinedStr &&
						typeof YAHOO.util        !== undefinedStr &&
						typeof YAHOO.util.Scroll !== undefinedStr) {
						scrollEl = YAHOO.env.ua.webkit ? document.body : document.documentElement;
						yuiEase = YAHOO.util.Easing ? YAHOO.util.Easing.easeOut : undefined;
						yuiAnim = new YAHOO.util.Scroll(scrollEl, {
							scroll: { to: [0, scrollToVal] }
						}, getOption('scrollDuration')/1000, yuiEase);
						yuiAnim.onComplete.subscribe(cb);
						yuiAnim.animate();
					}

					// Use jQuery if it exists
					else if (hasJquery) {
						jQuery('body, html').animate({ scrollTop: scrollToVal }, getOption('scrollDuration'), cb);
					}

					// Use my crummy setInterval scroll solution if we're using plain, vanilla Javascript.
					else {
						if (scrollToVal < 0) {
							scrollToVal = 0;
						}

						// 48 * 10 == 480ms scroll duration
						// make it slightly less than CSS transition duration because of
						// setInterval overhead.
						// To increase or decrease duration, change the divisor of scrollIncr.
						direction = (windowTop > targetTop) ? -1 : 1; // -1 means scrolling up, 1 means down
						scrollIncr = Math.abs(windowTop - scrollToVal) / (getOption('scrollDuration')/10);
						scrollTimeoutFn = function() {
							var scrollTop = utils.getScrollTop(),
								scrollTarget = scrollTop + (direction * scrollIncr);

							if ((direction > 0 && scrollTarget >= scrollToVal) ||
								(direction < 0 && scrollTarget <= scrollToVal)) {
								// Overshot our target. Just manually set to equal the target
								// and clear the interval
								scrollTarget = scrollToVal;
								if (cb) { cb(); } // HopscotchBubble.show
								window.scrollTo(0, scrollTarget);
								return;
							}

							window.scrollTo(0, scrollTarget);

							if (utils.getScrollTop() === scrollTop) {
								// Couldn't scroll any further.
								if (cb) { cb(); } // HopscotchBubble.show
								return;
							}

							// If we reached this point, that means there's still more to scroll.
							setTimeout(scrollTimeoutFn, 10);
						};

						scrollTimeoutFn();
					}
				}
			},

			/**
			 * goToStepWithTarget
			 *
			 * Helper function to increment the step number until a step is found where
			 * the step target exists or until we reach the end/beginning of the tour.
			 *
			 * @private
			 * @param {Number} direction Either 1 for incrementing or -1 for decrementing
			 * @param {Function} cb The callback function to be invoked when the step has been found
			 */
				goToStepWithTarget = function(direction, cb) {
				var target,
					step,
					goToStepFn;

				if (currStepNum + direction >= 0 &&
					currStepNum + direction < currTour.steps.length) {

					currStepNum += direction;
					step = getCurrStep();

					goToStepFn = function() {
						target = utils.getStepTarget(step);

						if (target) {
							// We're done! Return the step number via the callback.
							cb(currStepNum);
						}
						else {
							// Haven't found a valid target yet. Recursively call
							// goToStepWithTarget.
							utils.invokeEventCallbacks('error');
							goToStepWithTarget(direction, cb);
						}
					};

					if (step.delay) {
						setTimeout(goToStepFn, step.delay);
					}
					else {
						goToStepFn();
					}
				}
				else {
					cb(-1); // signal that we didn't find any step with a valid target
				}
			},

			/**
			 * changeStep
			 *
			 * Helper function to change step by going forwards or backwards 1.
			 * nextStep and prevStep are publicly accessible wrappers for this function.
			 *
			 * @private
			 * @param {Boolean} doCallbacks Flag for invoking onNext or onPrev callbacks
			 * @param {Number} direction Either 1 for "next" or -1 for "prev"
			 */
				changeStep = function(doCallbacks, direction) {
				var bubble = getBubble(),
					self = this,
					step,
					origStep,
					wasMultiPage,
					changeStepCb;

				bubble.hide();

				doCallbacks = utils.valOrDefault(doCallbacks, true);
				step = getCurrStep();
				origStep = step;
				if (direction > 0) {
					wasMultiPage = origStep.multipage;
				}
				else {
					wasMultiPage = (currStepNum > 0 && currTour.steps[currStepNum-1].multipage);
				}

				/**
				 * Callback for goToStepWithTarget
				 *
				 * @private
				 */
				changeStepCb = function(stepNum) {
					var doShowFollowingStep;

					if (stepNum === -1) {
						// Wasn't able to find a step with an existing element. End tour.
						return this.endTour(true);
					}

					if (doCallbacks) {
						if (direction > 0) {
							doShowFollowingStep = utils.invokeEventCallbacks('next', origStep.onNext);
						}
						else {
							doShowFollowingStep = utils.invokeEventCallbacks('prev', origStep.onPrev);
						}
					}

					// If the state of the tour is updated in a callback, assume the client
					// doesn't want to go to next step since they specifically updated.
					if (stepNum !== currStepNum) {
						return;
					}

					if (wasMultiPage) {
						// Update state for the next page
						utils.setState(getOption('cookieName'), currTour.id + ':' + currStepNum, 1);

						// Next step is on a different page, so no need to attempt to render it.
						return;
					}

					doShowFollowingStep = utils.valOrDefault(doShowFollowingStep, true);

					// If the onNext/onPrev callback returned false, halt the tour and
					// don't show the next step.
					if (doShowFollowingStep) {
						this.showStep(stepNum);
					}
					else {
						// Halt tour (but don't clear state)
						this.endTour(false);
					}
				};

				if (!wasMultiPage && getOption('skipIfNoElement')) {
					goToStepWithTarget(direction, function(stepNum) {
						changeStepCb.call(self, stepNum);
					});
				}
				else if (currStepNum + direction >= 0 && currStepNum + direction < currTour.steps.length) {
					// only try incrementing once, and invoke error callback if no target is found
					currStepNum += direction;
					step = getCurrStep();
					if (!utils.getStepTarget(step) && !wasMultiPage) {
						utils.invokeEventCallbacks('error');
						return this.endTour(true, false);
					}
					changeStepCb.call(this, currStepNum);
				}

				return this;
			},

			/**
			 * loadTour
			 *
			 * Loads, but does not display, tour.
			 *
			 * @private
			 * @param tour The tour JSON object
			 */
				loadTour = function(tour) {
				var tmpOpt = {},
					prop,
					tourState,
					tourPair;

				// Set tour-specific configurations
				for (prop in tour) {
					if (tour.hasOwnProperty(prop) &&
						prop !== 'id' &&
						prop !== 'steps') {
						tmpOpt[prop] = tour[prop];
					}
				}

				//this.resetDefaultOptions(); // reset all options so there are no surprises
				// TODO check number of config properties of tour
				_configure.call(this, tmpOpt, true);

				// Get existing tour state, if it exists.
				tourState = utils.getState(getOption('cookieName'));
				if (tourState) {
					tourPair            = tourState.split(':');
					cookieTourId        = tourPair[0]; // selecting tour is not supported by this framework.
					cookieTourStep      = tourPair[1];

					cookieTourStep    = parseInt(cookieTourStep, 10);
				}

				return this;
			},

			/**
			 * Find the first step to show for a tour. (What is the first step with a
			 * target on the page?)
			 */
				findStartingStep = function(startStepNum, cb) {
				var step,
					target,
					stepNum;

				currStepNum = startStepNum || 0;
				step        = getCurrStep();
				target      = utils.getStepTarget(step);

				if (target) {
					// First step had an existing target.
					cb(currStepNum);
					return;
				}

				if (!target) {
					// Previous target doesn't exist either. The user may have just
					// clicked on a link that wasn't part of the tour. Another possibility is that
					// the user clicked on the correct link, but the target is just missing for
					// whatever reason. In either case, we should just advance until we find a step
					// that has a target on the page or end the tour if we can't find such a step.
					utils.invokeEventCallbacks('error');

					if (getOption('skipIfNoElement')) {
						goToStepWithTarget(1, cb);
						return;
					}
					else {
						currStepNum = -1;
						cb(currStepNum);
					}
				}
			},

			showStepHelper = function(stepNum) {
				var step         = currTour.steps[stepNum],
					tourSteps    = currTour.steps,
					numTourSteps = tourSteps.length,
					cookieVal    = currTour.id + ':' + stepNum,
					bubble       = getBubble(),
					targetEl     = utils.getStepTarget(step),
					isLast,
					showBubble;

				showBubble = function() {
					bubble.show();
					utils.invokeEventCallbacks('show', step.onShow);
				};

				// Update bubble for current step
				currStepNum    = stepNum;

				bubble.hide(false);

				isLast = (stepNum === numTourSteps - 1);
				bubble.render(step, stepNum, function(adjustScroll) {
					// when done adjusting window scroll, call showBubble helper fn
					if (adjustScroll) {
						adjustWindowScroll(showBubble);
					}
					else {
						showBubble();
					}

					// If we want to advance to next step when user clicks on target.
					if (step.nextOnTargetClick) {
						utils.addEvtListener(targetEl, 'click', targetClickNextFn);
					}
				});

				utils.setState(getOption('cookieName'), cookieVal, 1);
			},

			/**
			 * init
			 *
			 * Initializes the Hopscotch object.
			 *
			 * @private
			 */
				init = function(initOptions) {
				if (initOptions) {
					//initOptions.cookieName = initOptions.cookieName || 'hopscotch.tour.state';
					this.configure(initOptions);
				}
			};

		/**
		 * getCalloutManager
		 *
		 * Gets the callout manager.
		 *
		 * @returns {Object} HopscotchCalloutManager
		 *
		 */
		this.getCalloutManager = function() {
			if (typeof calloutMgr === undefinedStr) {
				calloutMgr = new HopscotchCalloutManager();
			}

			return calloutMgr;
		};

		/**
		 * startTour
		 *
		 * Begins the tour.
		 *
		 * @param {Object} tour The tour JSON object
		 * @stepNum {Number} stepNum __Optional__ The step number to start from
		 * @returns {Object} Hopscotch
		 *
		 */
		this.startTour = function(tour, stepNum) {
			var bubble,
				currStepNum,
				self = this;

			// loadTour if we are calling startTour directly. (When we call startTour
			// from window onLoad handler, we'll use currTour)
			if (!currTour) {
				currTour = tour;
				loadTour.call(this, tour);
			}

			if (typeof stepNum !== undefinedStr) {
				if (stepNum >= currTour.steps.length) {
					throw 'Specified step number out of bounds.';
				}
				currStepNum = stepNum;
			}

			// If document isn't ready, wait for it to finish loading.
			// (so that we can calculate positioning accurately)
			if (!utils.documentIsReady()) {
				waitingToStart = true;
				return this;
			}

			if (typeof currStepNum === "undefined" && currTour.id === cookieTourId && typeof cookieTourStep !== undefinedStr) {
				currStepNum = cookieTourStep;
			}
			else if (!currStepNum) {
				currStepNum = 0;
			}

			// Find the current step we should begin the tour on, and then actually start the tour.
			findStartingStep(currStepNum, function(stepNum) {
				var target = (stepNum !== -1) && utils.getStepTarget(currTour.steps[stepNum]);

				if (!target) {
					// Should we trigger onEnd callback? Let's err on the side of caution
					// and not trigger it. Don't want weird stuff happening on a page that
					// wasn't meant for the tour. Up to the developer to fix their tour.
					self.endTour(false, false);
					return;
				}

				utils.invokeEventCallbacks('start');

				bubble = getBubble();
				// TODO: do we still need this call to .hide()? No longer using opt.animate...
				// Leaving it in for now to play it safe
				bubble.hide(false); // make invisible for boundingRect calculations when opt.animate == true

				self.isActive = true;

				if (!utils.getStepTarget(getCurrStep())) {
					// First step element doesn't exist
					utils.invokeEventCallbacks('error');
					if (getOption('skipIfNoElement')) {
						self.nextStep(false);
					}
				}
				else {
					self.showStep(stepNum);
				}
			});

			return this;
		};

		/**
		 * showStep
		 *
		 * Skips to a specific step and renders the corresponding bubble.
		 *
		 * @stepNum {Number} stepNum The step number to show
		 * @returns {Object} Hopscotch
		 */
		this.showStep = function(stepNum) {
			var step = currTour.steps[stepNum];
			if (step.delay) {
				setTimeout(function() {
					showStepHelper(stepNum);
				}, step.delay);
			}
			else {
				showStepHelper(stepNum);
			}
			return this;
		};

		/**
		 * prevStep
		 *
		 * Jump to the previous step.
		 *
		 * @param {Boolean} doCallbacks Flag for invoking onPrev callback. Defaults to true.
		 * @returns {Object} Hopscotch
		 */
		this.prevStep = function(doCallbacks) {
			changeStep.call(this, doCallbacks, -1);
			return this;
		};

		/**
		 * nextStep
		 *
		 * Jump to the next step.
		 *
		 * @param {Boolean} doCallbacks Flag for invoking onNext callback. Defaults to true.
		 * @returns {Object} Hopscotch
		 */
		this.nextStep = function(doCallbacks) {
			var step = getCurrStep(),
				targetEl = utils.getStepTarget(step);

			if (step.nextOnTargetClick) {
				// Detach the listener after we've clicked on the target OR the next button.
				utils.removeEvtListener(targetEl, 'click', targetClickNextFn);
			}
			changeStep.call(this, doCallbacks, 1);
			return this;
		};

		/**
		 * endTour
		 *
		 * Cancels out of an active tour.
		 *
		 * @param {Boolean} clearState Flag for clearing state. Defaults to true.
		 * @param {Boolean} doCallbacks Flag for invoking 'onEnd' callbacks. Defaults to true.
		 * @returns {Object} Hopscotch
		 */
		this.endTour = function(clearState, doCallbacks) {
			var bubble     = getBubble();
			clearState     = utils.valOrDefault(clearState, true);
			doCallbacks    = utils.valOrDefault(doCallbacks, true);
			currStepNum    = 0;
			cookieTourStep = undefined;

			bubble.hide();
			if (clearState) {
				utils.clearState(getOption('cookieName'));
			}
			if (this.isActive) {
				this.isActive = false;

				if (currTour && doCallbacks) {
					utils.invokeEventCallbacks('end');
				}
			}

			this.removeCallbacks(null, true);
			this.resetDefaultOptions();

			currTour = null;

			return this;
		};

		/**
		 * getCurrTour
		 *
		 * @return {Object} The currently loaded tour.
		 */
		this.getCurrTour = function() {
			return currTour;
		};

		/**
		 * getCurrTarget
		 *
		 * @return {Object} The currently visible target.
		 */
		this.getCurrTarget = function() {
			return utils.getStepTarget(getCurrStep());
		};

		/**
		 * getCurrStepNum
		 *
		 * @return {number} The current zero-based step number.
		 */
		this.getCurrStepNum = function() {
			return currStepNum;
		};

		/**
		 * refreshBubblePosition
		 *
		 * Tell hopscotch that the position of the current tour element changed
		 * and the bubble therefore needs to be redrawn
		 *
		 * @returns {Object} Hopscotch
		 */
		this.refreshBubblePosition = function() {
			bubble.setPosition(getCurrStep());
			return this;
		};

		/**
		 * listen
		 *
		 * Adds a callback for one of the event types. Valid event types are:
		 *
		 * @param {string} evtType "start", "end", "next", "prev", "show", "close", or "error"
		 * @param {Function} cb The callback to add.
		 * @param {Boolean} isTourCb Flag indicating callback is from a tour definition.
		 *    For internal use only!
		 * @returns {Object} Hopscotch
		 */
		this.listen = function(evtType, cb, isTourCb) {
			if (evtType) {
				callbacks[evtType].push({ cb: cb, fromTour: isTourCb });
			}
			return this;
		};

		/**
		 * unlisten
		 *
		 * Removes a callback for one of the event types, e.g. 'start', 'next', etc.
		 *
		 * @param {string} evtType "start", "end", "next", "prev", "show", "close", or "error"
		 * @param {Function} cb The callback to remove.
		 * @returns {Object} Hopscotch
		 */
		this.unlisten = function(evtType, cb) {
			var evtCallbacks = callbacks[evtType],
				i,
				len;

			for (i = 0, len = evtCallbacks.length; i < len; ++i) {
				if (evtCallbacks[i] === cb) {
					evtCallbacks.splice(i, 1);
				}
			}
			return this;
		};

		/**
		 * removeCallbacks
		 *
		 * Remove callbacks for hopscotch events. If tourOnly is set to true, only
		 * removes callbacks specified by a tour (callbacks set by external calls
		 * to hopscotch.configure or hopscotch.listen will not be removed). If
		 * evtName is null or undefined, callbacks for all events will be removed.
		 *
		 * @param {string} evtName Optional Event name for which we should remove callbacks
		 * @param {boolean} tourOnly Optional flag to indicate we should only remove callbacks added
		 *    by a tour. Defaults to false.
		 * @returns {Object} Hopscotch
		 */
		this.removeCallbacks = function(evtName, tourOnly) {
			var cbArr,
				i,
				len,
				evt;

			// If evtName is null or undefined, remove callbacks for all events.
			for (evt in callbacks) {
				if (!evtName || evtName === evt) {
					if (tourOnly) {
						cbArr = callbacks[evt];
						for (i=0, len=cbArr.length; i < len; ++i) {
							if (cbArr[i].fromTour) {
								cbArr.splice(i--, 1);
								--len;
							}
						}
					}
					else {
						callbacks[evt] = [];
					}
				}
			}
			return this;
		};

		/**
		 * registerHelper
		 * ==============
		 * Registers a helper function to be used as a callback function.
		 *
		 * @param {String} id The id of the function.
		 * @param {Function} id The callback function.
		 */
		this.registerHelper = function(id, fn) {
			if (typeof id === 'string' && typeof fn === 'function') {
				helpers[id] = fn;
			}
		};

		this.unregisterHelper = function(id) {
			helpers[id] = null;
		};

		this.invokeHelper = function(id) {
			var args = [],
				i,
				len;

			for (i = 1, len = arguments.length; i < len; ++i) {
				args.push(arguments[i]);
			}
			if (helpers[id]) {
				helpers[id].call(null, args);
			}
		};

		/**
		 * setCookieName
		 *
		 * Sets the cookie name (or sessionStorage name, if supported) used for multi-page
		 * tour persistence.
		 *
		 * @param {String} name The cookie name
		 * @returns {Object} Hopscotch
		 */
		this.setCookieName = function(name) {
			opt.cookieName = name;
			return this;
		};

		/**
		 * resetDefaultOptions
		 *
		 * Resets all configuration options to default.
		 *
		 * @returns {Object} Hopscotch
		 */
		this.resetDefaultOptions = function() {
			opt = {};
			return this;
		};

		/**
		 * resetDefaultI18N
		 *
		 * Resets all i18n.
		 *
		 * @returns {Object} Hopscotch
		 */
		this.resetDefaultI18N = function() {
			customI18N = {};
			return this;
		};

		/**
		 * hasState
		 *
		 * Returns state from a previous tour run, if it exists.
		 *
		 * @returns {String} State of previous tour run, or empty string if none exists.
		 */
		this.getState = function() {
			return utils.getState(getOption('cookieName'));
		};

		/**
		 * _configure
		 *
		 * @see this.configure
		 * @private
		 * @param options
		 * @param {Boolean} isTourOptions Should be set to true when setting options from a tour definition.
		 */
		_configure = function(options, isTourOptions) {
			var bubble,
				events = ['next', 'prev', 'start', 'end', 'show', 'error', 'close'],
				eventPropName,
				callbackProp,
				i,
				len;

			if (!opt) {
				this.resetDefaultOptions();
			}

			utils.extend(opt, options);

			if (options) {
				utils.extend(customI18N, options.i18n);
			}

			for (i = 0, len = events.length; i < len; ++i) {
				// At this point, options[eventPropName] may have changed from an array
				// to a function.
				eventPropName = 'on' + events[i].charAt(0).toUpperCase() + events[i].substring(1);
				if (options[eventPropName]) {
					this.listen(events[i],
						options[eventPropName],
						isTourOptions);
				}
			}

			bubble = getBubble(true);

			return this;
		};

		/**
		 * configure
		 *
		 * <pre>
		 * VALID OPTIONS INCLUDE...
		 *
		 * - bubbleWidth:     Number   - Default bubble width. Defaults to 280.
		 * - bubblePadding:   Number   - DEPRECATED. Default bubble padding. Defaults to 15.
		 * - smoothScroll:    Boolean  - should the page scroll smoothly to the next
		 *                               step? Defaults to TRUE.
		 * - scrollDuration:  Number   - Duration of page scroll. Only relevant when
		 *                               smoothScroll is set to true. Defaults to
		 *                               1000ms.
		 * - scrollTopMargin: NUMBER   - When the page scrolls, how much space should there
		 *                               be between the bubble/targetElement and the top
		 *                               of the viewport? Defaults to 200.
		 * - showCloseButton: Boolean  - should the tour bubble show a close (X) button?
		 *                               Defaults to TRUE.
		 * - showPrevButton:  Boolean  - should the bubble have the Previous button?
		 *                               Defaults to FALSE.
		 * - showNextButton:  Boolean  - should the bubble have the Next button?
		 *                               Defaults to TRUE.
		 * - arrowWidth:      Number   - Default arrow width. (space between the bubble
		 *                               and the targetEl) Used for bubble position
		 *                               calculation. Only use this option if you are
		 *                               using your own custom CSS. Defaults to 20.
		 * - skipIfNoElement  Boolean  - If a specified target element is not found,
		 *                               should we skip to the next step? Defaults to
		 *                               TRUE.
		 * - onNext:          Function - A callback to be invoked after every click on
		 *                               a "Next" button.
		 *
		 * - i18n:            Object   - For i18n purposes. Allows you to change the
		 *                               text of button labels and step numbers.
		 * - i18n.stepNums:   Array\<String\> - Provide a list of strings to be shown as
		 *                               the step number, based on index of array. Unicode
		 *                               characters are supported. (e.g., ['&#x4e00;',
		 *                               '&#x4e8c;', '&#x4e09;']) If there are more steps
		 *                               than provided numbers, Arabic numerals
		 *                               ('4', '5', '6', etc.) will be used as default.
		 * // =========
		 * // CALLBACKS
		 * // =========
		 * - onNext:          Function - Invoked after every click on a "Next" button.
		 * - onPrev:          Function - Invoked after every click on a "Prev" button.
		 * - onStart:         Function - Invoked when the tour is started.
		 * - onEnd:           Function - Invoked when the tour ends.
		 * - onClose:         Function - Invoked when the user closes the tour before finishing.
		 * - onError:         Function - Invoked when the specified target element doesn't exist on the page.
		 *
		 * // ====
		 * // I18N
		 * // ====
		 * i18n:              OBJECT      - For i18n purposes. Allows you to change the text
		 *                                  of button labels and step numbers.
		 * i18n.nextBtn:      STRING      - Label for next button
		 * i18n.prevBtn:      STRING      - Label for prev button
		 * i18n.doneBtn:      STRING      - Label for done button
		 * i18n.skipBtn:      STRING      - Label for skip button
		 * i18n.closeTooltip: STRING      - Text for close button tooltip
		 * i18n.stepNums:   ARRAY<STRING> - Provide a list of strings to be shown as
		 *                                  the step number, based on index of array. Unicode
		 *                                  characters are supported. (e.g., ['&#x4e00;',
		 *                                  '&#x4e8c;', '&#x4e09;']) If there are more steps
		 *                                  than provided numbers, Arabic numerals
		 *                                  ('4', '5', '6', etc.) will be used as default.
		 * </pre>
		 *
		 * @example hopscotch.configure({ scrollDuration: 1000, scrollTopMargin: 150 });
		 * @example
		 * hopscotch.configure({
     *   scrollTopMargin: 150,
     *   onStart: function() {
     *     alert("Have fun!");
     *   },
     *   i18n: {
     *     nextBtn: 'Forward',
     *     prevBtn: 'Previous'
     *     closeTooltip: 'Quit'
     *   }
     * });
		 *
		 * @param {Object} options A hash of configuration options.
		 * @returns {Object} Hopscotch
		 */
		this.configure = function(options) {
			return _configure.call(this, options, false);
		};

		/**
		 * Set the template that should be used for rendering Hopscotch bubbles.
		 * If a string, it's assumed your template is available in the
		 * hopscotch.templates namespace.
		 *
		 * @param {String|Function(obj)} The template to use for rendering.
		 * @returns {Object} The Hopscotch object (for chaining).
		 */
		this.setRenderer = function(render){
			var typeOfRender = typeof render;

			if(typeOfRender === 'string'){
				templateToUse = render;
				customRenderer = undefined;
			}
			else if(typeOfRender === 'function'){
				customRenderer = render;
			}
			return this;
		};

		/**
		 * Sets the escaping method to be used by JST templates.
		 *
		 * @param {Function} - The escape method to use.
		 * @returns {Object} The Hopscotch object (for chaining).
		 */
		this.setEscaper = function(esc){
			if (typeof esc === 'function'){
				customEscape = esc;
			}
			return this;
		};

		init.call(this, initOptions);
	};

	winHopscotch = new Hopscotch();
	context[namespace] = winHopscotch;

// Template includes, placed inside a closure to ensure we don't
// end up declaring our shim globally.
	(function(){
		var _ = {};
		/*
		 * Adapted from the Underscore.js framework. Check it out at
		 * https://github.com/jashkenas/underscore
		 */
		_.escape = function(str){
			if(customEscape){ return customEscape(str); }

			if(str == null) return '';
			return ('' + str).replace(new RegExp('[&<>"\']', 'g'), function(match){
				if(match == '&'){ return '&amp;' }
				if(match == '<'){ return '&lt;' }
				if(match == '>'){ return '&gt;' }
				if(match == '"'){ return '&quot;' }
				if(match == "'"){ return '&#x27;' }
			});
		}
		this["hopscotch"] = this["hopscotch"] || {};
		this["hopscotch"]["templates"] = this["hopscotch"]["templates"] || {};

		this["hopscotch"]["templates"]["bubble_default"] = function(obj) {
			obj || (obj = {});
			var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
			function print() { __p += __j.call(arguments, '') }
			with (obj) {


				function optEscape(str, unsafe){
					if(unsafe){
						return _.escape(str);
					}
					return str;
				}
				;
				__p += '\n<div class="hopscotch-bubble-container" style="width: ' +
					((__t = ( step.width )) == null ? '' : __t) +
					'px; padding: ' +
					((__t = ( step.padding )) == null ? '' : __t) +
					'px;">\n  ';
				if(tour.isTour){ ;
					__p += '<span class="hopscotch-bubble-number">' +
						((__t = ( i18n.stepNum )) == null ? '' : __t) +
						'</span>';
				} ;
				__p += '\n  <div class="hopscotch-bubble-content">\n    ';
				if(step.title !== ''){ ;
					__p += '<h3 class="hopscotch-title">' +
						((__t = ( optEscape(step.title, tour.unsafe) )) == null ? '' : __t) +
						'</h3>';
				} ;
				__p += '\n    ';
				if(step.content  !== ''){ ;
					__p += '<div class="hopscotch-content">' +
						((__t = ( optEscape(step.content, tour.unsafe) )) == null ? '' : __t) +
						'</div>';
				} ;
				__p += '\n  </div>\n  <div class="hopscotch-actions">\n    ';
				if(buttons.showPrev){ ;
					__p += '<button class="hopscotch-nav-button prev hopscotch-prev">' +
						((__t = ( i18n.prevBtn )) == null ? '' : __t) +
						'</button>';
				} ;
				__p += '\n    ';
				if(buttons.showCTA){ ;
					__p += '<button class="hopscotch-nav-button next hopscotch-cta">' +
						((__t = ( buttons.ctaLabel )) == null ? '' : __t) +
						'</button>';
				} ;
				__p += '\n    ';
				if(buttons.showNext){ ;
					__p += '<button class="hopscotch-nav-button next hopscotch-next">' +
						((__t = ( i18n.nextBtn )) == null ? '' : __t) +
						'</button>';
				} ;
				__p += '\n  </div>\n  ';
				if(buttons.showClose){ ;
					__p += '<a title="' +
						((__t = ( i18n.closeTooltip )) == null ? '' : __t) +
						'" href="#" class="hopscotch-bubble-close hopscotch-close">' +
						((__t = ( i18n.closeTooltip )) == null ? '' : __t) +
						'</a>';
				} ;
				__p += '\n</div>\n<div class="hopscotch-bubble-arrow-container hopscotch-arrow">\n  <div class="hopscotch-bubble-arrow-border"></div>\n  <div class="hopscotch-bubble-arrow"></div>\n</div>';

			}
			return __p
		};
	}());

}(window, 'hopscotch'));
