/*
jQuery.wizard v1.0.0
https://github.com/kflorence/jquery-wizard/
An asynchronous form wizard that supports branching.

Requires:
 - jQuery 1.3.2+
 - jQuery UI widget 1.8.0+

Copyright (c) 2011 Kyle Florence
Dual licensed under the MIT and GPLv2 licenses.
*/

(function( $, undefined ) {

var excludesFilter,
	count = 0,
	selector = {},
	className = {},

	// Reference to commonly used methods
	aps = Array.prototype.slice,

	// Used to normalize function arguments that can be either
	// an array of values or a single value
	arr = function( obj ) {
		return $.isArray( obj ) ? obj : [ obj ];
	},

	// Commonly used strings
	id = "id",
	form = "form",
	click = "click",
	submit = "submit",
	disabled = "disabled",
	wizard = "wizard",

	def = "default",
	num = "number",
	obj = "object",
	str = "string",
	bool = "boolean",

	// Events
	afterBackward = "afterBackward",
	afterDestroy = "afterDestroy",
	afterForward = "afterForward",
	afterSelect = "afterSelect",
	beforeBackward = "beforeBackward",
	beforeDestroy = "beforeDestroy",
	beforeForward = "beforeForward",
	beforeSelect = "beforeSelect",
	beforeSubmit = "beforeSubmit";

// Generate selectors and class names for common wizard elements
$.each( "branch form header step wrapper".split( " " ), function() {
	selector[ this ] = "." + ( className[ this ] = wizard + "-" + this );
});

$.widget( "kf." + wizard, {
	version: "1.0.0",
	options: {
		animations: {
			show: {
				options: {
					duration: 0
				},
				properties: {
					opacity: "show"
				}
			},
			hide: {
				options: {
					duration: 0
				},
				properties: {
					opacity: "hide"
				}
			}
		},
		backward: ".backward",
		branches: ".branch",
		disabled: false,
		enableSubmit: false,
		forward: ".forward",
		header: ":header:first",
		initialStep: 0,
		stateAttribute: "data-state",
		stepClasses: {
			current: "current",
			exclude: "exclude",
			stop: "stop",
			submit: "submit",
			unidirectional: "unidirectional"
		},
		steps: ".step",
		submit: ":submit",
		transitions: {},
		unidirectional: false,

		/* callbacks */
		afterBackward: null,
		afterDestroy: null,
		afterForward: null,
		afterSelect: null,
		beforeBackward: null,
		beforeDestroy: null,
		beforeForward: null,
		beforeSelect: null,
		create: null
	},

	_create: function() {
		var $form, $header,
			self = this,
			o = self.options,
			$element = self.element,
			$steps = $element.find( o.steps ),
			$stepsWrapper = $steps.eq( 0 ).parent();

		if ( $element[ 0 ].elements ) {
			$form = $element;

		// If element isn't form, look inside and outside element
		} else if ( !( $form = $element.find( form ) ).length ) {
			$form = $element.closest( form );
		}

		// If header isn't found in element, look in form scope
		if ( !( $header = $element.find( o.header ) ).length ) {
			$header = $form.find( o.header );
		}

		self.elements = {
			form: $form.addClass( className.form ),
			submit: $form.find( o.submit ),
			forward: $form.find( o.forward ),
			backward: $form.find( o.backward ),
			header: $header.addClass( className.header ),
			steps: $element.find( o.steps ).hide().addClass( className.step ),
			branches: $element.find( o.branches ).add( $stepsWrapper ).addClass( className.branch ),
			stepsWrapper: $stepsWrapper.addClass( className.wrapper ),
			wizard: $element.addClass( wizard )
		};

		if ( !$stepsWrapper.attr( id ) ) {

			// stepsWrapper must have an ID as it also functions as the default branch
			$stepsWrapper.attr( id, wizard + "-" + ( ++count ) );
		}

		self.elements.forward.click(function( event ) {
			event.preventDefault();
			self.forward( event );
		});

		self.elements.backward.click(function( event ) {
			event.preventDefault();
			self.backward( event );
		});

		self._currentState = {
			branchesActivated: [],
			stepsActivated: []
		};

		self._stepCount = self.elements.steps.length;
		self._lastStepIndex = self._stepCount - 1;

		// Cache branch labels for quick access later
		self._branchLabels = [];
		self.elements.steps.each(function( i ) {
			self._branchLabels[ i ] = $( this ).parent().attr( id );
		});

		// Called in the context of jQuery's .filter() method in _state()
		self._excludesFilter = function() {
			return !$( this ).hasClass( o.stepClasses.exclude );
		};

		// Add default transition function if one wasn't defined
		if ( !o.transitions[ def ] ) {
			o.transitions[ def ] = function( step ) {
				return self.stepIndex( step.nextAll( selector.step ) );
			};
		}

		// Select initial step
		self.select.apply( self, arr( o.initialStep ) );
	},

	_fastForward: function( toIndex, relative, callback ) {
		var i = 0,
			self = this,
			stepIndex = self._currentState.stepIndex,
			stepsTaken = [ stepIndex ];

		if ( $.isFunction( relative ) ) {
			callback = relative;
			relative = undefined;
		}

		(function next() {
			self._transition( stepIndex, function( step, branch ) {
				if ( ( stepIndex = self.stepIndex( step, branch ) ) === -1 ) {
					throw new Error( '[_fastForward]: Invalid step "' + step + '"' );

				} else if ( $.inArray( stepIndex, stepsTaken ) >= 0 ) {
					throw new Error( '[_fastForward]: Recursion detected on step "' + step + '"' );

				} else {
					stepsTaken.push( stepIndex );

					if ( stepIndex === self._lastStepIndex ||
						( relative ? ++i : stepIndex ) === toIndex ) {
						callback.call( self, stepIndex, stepsTaken );

					} else {
						next();
					}
				}
			});
		})();
	},

	_find: function( needles, haystack, wrap ) {
		var element, i, l, needle, type,
			found = [],
			$haystack = haystack instanceof jQuery ? haystack : $( haystack );

		function matchElement( i, current ) {
			if ( current === needle ) {
				element = current;

				// Break from .each loop
				return false;
			}
		}

		if ( needles !== null && $haystack.length ) {
			needles = arr( needles );

			for ( i = 0, l = needles.length; i < l; i++ ) {
				element = null;
				needle = needles[ i ];
				type = typeof needle;

				if ( type === num ) {
					element = $haystack.get( needle );

				} else if ( type === str ) {
					element = document.getElementById( needle.replace( '#', '' ) );

				} else if ( type === obj ) {
					if ( needle instanceof jQuery && needle.length ) {
						needle = needle[ 0 ];
					}

					if ( needle.nodeType ) {
						$haystack.each( matchElement );
					}
				}

				if ( element ) {
					found.push( element );
				}
			}
		}

		// Returns a jQuery object by default. If the wrap argument is
		// false, it will return an array of elements instead.
		return wrap === false ? found : $( found );
	},

	_move: function( step, branch, relative, history, callback ) {
		var self = this,
			current = self._currentState;

		if ( typeof branch === bool ) {
			callback = history;
			history = relative;
			relative = branch;
			branch = undefined;
		}

		function move( stepIndex, stepsTaken ) {
			callback.call( self, stepIndex, $.isArray( history ) ?
				history : history !== false ? stepsTaken : undefined );
		}

		if ( relative === true ) {
			if ( step > 0 ) {
				self._fastForward( step, relative, move );

			} else {
				callback.call( self, current.stepsActivated[
					// Normalize to zero if negative
					Math.max( 0, step + ( current.stepsActivated.length - 1 ) ) ] );
			}

		// Don't attempt to move to invalid steps
		} else if ( ( step = self.stepIndex( step, branch ) ) !== -1 ) {
			if ( step > current.stepIndex ) {
				self._fastForward( step, move );

			} else {
				move.call( self, step );
			}
		}
	},

	_state: function( stepIndex, stepsTaken ) {
		if ( !this.isValidStepIndex( stepIndex ) ) {
			return null;
		}

		var o = this.options,
			state = $.extend( true, {}, this._currentState );

		// stepsTaken must be an array of at least one step
		stepsTaken = arr( stepsTaken || stepIndex );

		state.step = this.elements.steps.eq( stepIndex );
		state.branch = state.step.parent();
		state.branchStepCount = state.branch.children( selector.step ).length;
		state.isMovingForward = stepIndex > state.stepIndex;
		state.stepIndexInBranch = state.branch.children( selector.step ).index( state.step );

		var branchLabel, indexOfBranch, indexOfStep,
			i = 0,
			l = stepsTaken.length;

		for ( ; i < l; i++ ) {
			stepIndex = stepsTaken[ i ];
			branchLabel = this._branchLabels[ stepIndex ];

			// Going forward
			if ( !state.stepIndex || state.stepIndex < stepIndex ) {

				// No duplicate steps
				if ( $.inArray( stepIndex, state.stepsActivated ) < 0 ) {
					state.stepsActivated.push( stepIndex );

					// No duplicate branch labels
					if ( $.inArray( branchLabel, state.branchesActivated ) < 0 ) {
						state.branchesActivated.push( branchLabel );
					}
				}

			// Going backward
			} else if ( state.stepIndex > stepIndex ) {
				indexOfBranch = $.inArray( branchLabel, state.branchesActivated ) + 1;
				indexOfStep = $.inArray( stepIndex, state.stepsActivated ) + 1;

				// Don't remove initial branch
				if ( indexOfBranch > 0 ) {
					state.branchesActivated.splice( indexOfBranch,
							// IE requires this argument
							state.branchesActivated.length - 1 );
				}

				// Don't remove the initial step
				if ( indexOfStep > 0 ) {
					state.stepsActivated.splice( indexOfStep,
							// IE requires this argument
							state.stepsActivated.length - 1 );
				}
			}

			state.stepIndex = stepIndex;
			state.branchLabel = branchLabel;
		}

		// Steps completed: the number of steps we have visited
		state.stepsComplete = Math.max( 0, this._find(
			state.stepsActivated, this.elements.steps
		).filter( this._excludesFilter ).length - 1 );

		// Steps possible: the number of steps in all of the branches we have visited
		state.stepsPossible = Math.max( 0, this._find(
			state.branchesActivated, this.elements.branches
		).children( selector.step ).filter( this._excludesFilter ).length - 1 );

		$.extend( state, {
			branchLabel: this._branchLabels[ stepIndex ],
			isFirstStep: stepIndex === 0,
			isFirstStepInBranch: state.stepIndexInBranch === 0,
			isLastStep: stepIndex === this._lastStepIndex,
			isLastStepInBranch: state.stepIndexInBranch === state.branchStepCount - 1,
			percentComplete: ( 100 * state.stepsComplete / state.stepsPossible ),
			stepsRemaining: ( state.stepsPossible - state.stepsComplete )
		});

		return state;
	},

	_transition: function( step, branch, action ) {
		var self = this;

		// args: action
		// step becomes the current step
		if ( $.isFunction( step ) ) {
			action = step;
			step = self._currentState.stepIndex;
			branch = undefined;

		// args: step, action
		} else if ( $.isFunction( branch ) ) {
			action = branch;
			branch = undefined;
		}

		var response,
			o = self.options,
			$step = self.step( step, branch ),
			stateName = $step.attr( o.stateAttribute ),
			transitionFunc = stateName ? o.transitions[ stateName ] : o.transitions[ def ];

		if ( $.isFunction( transitionFunc ) ) {
			response = transitionFunc.call( self, $step, function() {
				return action.apply( self, aps.call( arguments ) );
			});

		} else {
			response = stateName;
		}

		// A response of 'undefined' or 'false' will halt immediate action
		// waiting instead for the transition function to handle the call
		if ( response !== undefined && response !== false ) {

			// Response could be array like [ step, branch ]
			action.apply( self, arr( response ) );
		}

		// The immediate response
		return response;
	},

	_update: function( event, state ) {
		var current = this._currentState,
			o = this.options;

		if ( current.step ) {
			if ( o.disabled || !state ||
				state.stepIndex === current.stepIndex ||
				!this._trigger( beforeSelect, event, state ) ||
				( state.isMovingForward && !this._trigger( beforeForward, event, state ) ) ||
				( !state.isMovingForward && !this._trigger( beforeBackward, event, state ) ) ) {

				return;
			}

			current.step.removeClass( o.stepClasses.current )
				.animate( o.animations.hide.properties,
					// Fixes #3583 - http://bugs.jquery.com/ticket/3583
					$.extend( {}, o.animations.hide.options ) );
		}

		// Note that this does not affect the value of 'current'
		this._currentState = state;

		state.step.addClass( o.stepClasses.current )
			.animate( o.animations.show.properties,
				// Fixes #3583 - http://bugs.jquery.com/ticket/3583
				$.extend( {}, o.animations.show.options ) );

		if ( state.isFirstStep || o.unidirectional ||
			state.step.hasClass( o.stepClasses.unidirectional ) ) {
			this.elements.backward.attr( disabled, true );

		} else {
			this.elements.backward.removeAttr( disabled );
		}

		if ( ( state.isLastStepInBranch && !state.step.attr( o.stateAttribute ) ) ||
			state.step.hasClass( o.stepClasses.stop ) ) {
			this.elements.forward.attr( disabled, true );

		} else {
			this.elements.forward.removeAttr( disabled );
		}

		if ( o.enableSubmit || state.step.hasClass( o.stepClasses.submit ) ) {
			this.elements.submit.removeAttr( disabled );

		} else {
			this.elements.submit.attr( disabled, true );
		}

		if ( current.step ) {
			this._trigger( afterSelect, event, state );
			this._trigger( state.isMovingForward ? afterForward : afterBackward, event, state );
		}
	},

	backward: function( event, howMany ) {
		if ( typeof event === num ) {
			howMany = event;
			event = undefined;

		}

		if ( howMany === undefined ) {
			howMany = 1;
		}

		if ( this._currentState.isFirstStep || typeof howMany !== num ) {
			return;
		}

		this._move( -howMany, true, false, function( stepIndex, stepsTaken ) {
			this._update( event, this._state( stepIndex, stepsTaken ) );
		});
	},

	branch: function( branch ) {
		return arguments.length ?
			this._find( branch, this.elements.branches ) : this._currentState.branch;
	},

	branches: function( branch ) {
		return arguments.length ?
			this.branch( branch ).children( selector.branch ) : this.elements.branches;
	},

	branchesActivated: function() {
		return this._find( this._currentState.branchesActivated, this.elements.branches );
	},

	destroy: function() {
		var $elements = this.elements;

		if ( !this._trigger( beforeDestroy, null, this.state() ) ) {
			return;
		}

		this.element.removeClass( wizard );

		$elements.form.removeClass( className.form );
		$elements.header.removeClass( className.header );
		$elements.steps.show().removeClass( className.step );
		$elements.stepsWrapper.removeClass( className.wrapper );
		$elements.branches.removeClass( className.branch );

		$.Widget.prototype.destroy.call( this );

		this._trigger( afterDestroy );
	},

	form: function() {
		return this.elements.form;
	},

	forward: function( event, howMany, history ) {
		if ( typeof event === num ) {
			history = howMany;
			howMany = event;
			event = undefined;

		}

		if ( howMany === undefined ) {
			howMany = 1;
		}

		if ( this._currentState.isLastStep || typeof howMany !== num ) {
			return;
		}

		this._move( howMany, true, history, function( stepIndex, stepsTaken ) {
			this._update( event, this._state( stepIndex, stepsTaken ) );
		});
	},

	isValidStep: function( step, branch ) {
		return this.isValidStepIndex( this.stepIndex( step, branch ) );
	},

	isValidStepIndex: function( stepIndex ) {
		return typeof stepIndex === num && stepIndex >= 0 && stepIndex <= this._lastStepIndex;
	},

	stepCount: function() {
		return this._stepCount;
	},

	select: function( event, step, branch, relative, history ) {

		// args: step, branch, relative, history
		if ( !( event instanceof $.Event ) ) {
			history = relative;
			relative = branch;
			branch = step;
			step = event;
			event = undefined;
		}

		if ( step === undefined ) {
			return;
		}

		// args: [ step, branch ], relative, history
		if ( $.isArray( step ) ) {
			history = relative;
			relative = branch;
			branch = step[ 1 ];
			step = step[ 0 ];

		// args: step, relative, history
		} else if ( typeof branch === bool ) {
			history = relative;
			relative = branch;
			branch = undefined;

		// args: step, history
		} else if ( $.isArray( branch ) ) {
			history = branch;
			branch = undefined;
		}

		this._move( step, branch, relative, history, function( stepIndex, stepsTaken ) {
			this._update( event, this._state( stepIndex, stepsTaken ) );
		});
	},

	state: function( step, branch, stepsTaken ) {
		if ( !arguments.length ) {
			return this._currentState;
		}

		// args: [ step, branch ], stepsTaken
		if ( $.isArray( step ) ) {
			stepsTaken = branch;
			branch = step[ 1 ];
			step = step[ 0 ];

		// args: step, stepsTaken
		} else if ( $.isArray( branch ) ) {
			stepsTaken = branch;
			branch = undefined;
		}

		return this._state( this.stepIndex( step, branch ), stepsTaken );
	},

	step: function( step, branch ) {
		if ( !arguments.length ) {
			return this._currentState.step;
		}

		// args: [ step, branch ]
		if ( $.isArray( step ) ) {
			branch = step[ 1 ];
			step = step[ 0 ];
		}

		var $step,
			type = typeof step;

		// Searching for a step by index
		if ( type === num ) {
			$step = this._find( step,
				// Search within branch, if defined, otherwise search all steps
				branch !== undefined ? this.steps( branch ) : this.elements.steps );

		// Searching for a step or branch by string ID, DOM element or jQuery object
		} else {
			$step = this._find( step, this.elements.steps.add( this.elements.branches ) );

			if ( $step && $step.hasClass( className.branch ) ) {

				// If a branch is found, the arguments are essentially flip-flopped
				$step = this._find( branch || 0, this.steps( $step ) );
			}
		}

		return $step;
	},

	stepIndex: function( step, branch, relative ) {
		if ( !arguments.length ) {
			return this._currentState.stepIndex;
		}

		var $step;

		// args: [ step, branch ], relative
		if ( $.isArray( step ) ) {
			relative = branch;
			branch = step[ 1 ];
			step = step[ 0 ];

		// args: step, relative
		} else if ( typeof branch === bool ) {
			relative = branch;
			branch = undefined;
		}

		return ( $step = this.step( step, branch ) ) ?
			// The returned index can be relative to a branch, or to all steps
			( relative ? $step.siblings( selector.step ).andSelf() : this.elements.steps ).index( $step )
			: -1;
	},

	steps: function( branch ) {
		return arguments.length ?
			this.branch( branch ).children( selector.step ) : this.elements.steps;
	},

	stepsActivated: function() {
		return this._find( this._currentState.stepsActivated, this.elements.steps );
	},

	submit: function() {
		this.elements.form.submit();
	}
});

})( jQuery );
