// Test for equality any JavaScript type.
// Author: Philippe Rathé <prathe@gmail.com>
QUnit.equiv = (function() {

	// Stack to decide between skip/abort functions
	var callers = [];

	// Stack to avoiding loops from circular referencing
	var parents = [];
	var parentsB = [];

	function useStrictEquality( b, a ) {

		/*jshint eqeqeq:false */
		if ( b instanceof a.constructor || a instanceof b.constructor ) {

			// To catch short annotation VS 'new' annotation of a declaration. e.g.:
			// `var i = 1;`
			// `var j = new Number(1);`
			return a == b;
		} else {
			return a === b;
		}
	}

	function compareConstructors( a, b ) {
		var getProto = Object.getPrototypeOf || function( obj ) {

			/*jshint proto: true */
			return obj.__proto__;
		};
		var protoA = getProto( a );
		var protoB = getProto( b );

		// Comparing constructors is more strict than using `instanceof`
		if ( a.constructor === b.constructor ) {
			return true;
		}

		// Ref #851
		// If the obj prototype descends from a null constructor, treat it
		// as a null prototype.
		if ( protoA && protoA.constructor === null ) {
			protoA = null;
		}
		if ( protoB && protoB.constructor === null ) {
			protoB = null;
		}

		// Allow objects with no prototype to be equivalent to
		// objects with Object as their constructor.
		if ( ( protoA === null && protoB === Object.prototype ) ||
				( protoB === null && protoA === Object.prototype ) ) {
			return true;
		}

		return false;
	}

	var callbacks = {
		"string": useStrictEquality,
		"boolean": useStrictEquality,
		"number": useStrictEquality,
		"null": useStrictEquality,
		"undefined": useStrictEquality,
		"symbol": useStrictEquality,

		"nan": function( b ) {
			return isNaN( b );
		},

		"date": function( b, a ) {
			return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
		},

		"regexp": function( b, a ) {
			return QUnit.objectType( b ) === "regexp" &&

				// The regex itself
				a.source === b.source &&

				// And its modifiers
				a.global === b.global &&

				// (gmi) ...
				a.ignoreCase === b.ignoreCase &&
				a.multiline === b.multiline &&
				a.sticky === b.sticky;
		},

		// - skip when the property is a method of an instance (OOP)
		// - abort otherwise,
		// initial === would have catch identical references anyway
		"function": function() {
			var caller = callers[ callers.length - 1 ];
			return caller !== Object && typeof caller !== "undefined";
		},

		"array": function( b, a ) {
			var i, j, len, loop, aCircular, bCircular;

			// b could be an object literal here
			if ( QUnit.objectType( b ) !== "array" ) {
				return false;
			}

			len = a.length;
			if ( len !== b.length ) {
				// safe and faster
				return false;
			}

			// Track reference to avoid circular references
			parents.push( a );
			parentsB.push( b );
			for ( i = 0; i < len; i++ ) {
				loop = false;
				for ( j = 0; j < parents.length; j++ ) {
					aCircular = parents[ j ] === a[ i ];
					bCircular = parentsB[ j ] === b[ i ];
					if ( aCircular || bCircular ) {
						if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
							loop = true;
						} else {
							parents.pop();
							parentsB.pop();
							return false;
						}
					}
				}
				if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
					parents.pop();
					parentsB.pop();
					return false;
				}
			}
			parents.pop();
			parentsB.pop();
			return true;
		},

		"set": function( b, a ) {
			var aArray, bArray;

			// `b` could be any object here
			if ( QUnit.objectType( b ) !== "set" ) {
				return false;
			}

			aArray = [];
			a.forEach( function( v ) {
				aArray.push( v );
			});
			bArray = [];
			b.forEach( function( v ) {
				bArray.push( v );
			});

			return innerEquiv( bArray, aArray );
		},

		"map": function( b, a ) {
			var aArray, bArray;

			// `b` could be any object here
			if ( QUnit.objectType( b ) !== "map" ) {
				return false;
			}

			aArray = [];
			a.forEach( function( v, k ) {
				aArray.push( [ k, v ] );
			});
			bArray = [];
			b.forEach( function( v, k ) {
				bArray.push( [ k, v ] );
			});

			return innerEquiv( bArray, aArray );
		},

		"object": function( b, a ) {
			var i, j, loop, aCircular, bCircular;

			// Default to true
			var eq = true;
			var aProperties = [];
			var bProperties = [];

			if ( compareConstructors( a, b ) === false ) {
				return false;
			}

			// Stack constructor before traversing properties
			callers.push( a.constructor );

			// Track reference to avoid circular references
			parents.push( a );
			parentsB.push( b );

			// Be strict: don't ensure hasOwnProperty and go deep
			for ( i in a ) {
				loop = false;
				for ( j = 0; j < parents.length; j++ ) {
					aCircular = parents[ j ] === a[ i ];
					bCircular = parentsB[ j ] === b[ i ];
					if ( aCircular || bCircular ) {
						if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
							loop = true;
						} else {
							eq = false;
							break;
						}
					}
				}
				aProperties.push( i );
				if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
					eq = false;
					break;
				}
			}

			parents.pop();
			parentsB.pop();

			// Unstack, we are done
			callers.pop();

			for ( i in b ) {

				// Collect b's properties
				bProperties.push( i );
			}

			// Ensures identical properties name
			return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
		}
	};

	function typeEquiv( a, b ) {
		var prop = QUnit.objectType( a );
		return callbacks[ prop ]( b, a );
	}

	// The real equiv function
	function innerEquiv() {
		var args = [].slice.apply( arguments );
		if ( args.length < 2 ) {

			// End transition
			return true;
		}

		return ( (function( a, b ) {
			if ( a === b ) {

				// Catch the most you can
				return true;
			} else if ( a === null || b === null || typeof a === "undefined" ||
					typeof b === "undefined" ||
					QUnit.objectType( a ) !== QUnit.objectType( b ) ) {

				// Don't lose time with error prone cases
				return false;
			} else {
				return typeEquiv( a, b );
			}

		// Apply transition with (1..n) arguments
		}( args[ 0 ], args[ 1 ] ) ) &&
			innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) );
	}

	return innerEquiv;
}());
