// jscs:disable
// jshint ignore: start

/* global console, jsonView */

/*
 * ViewJSON
 * Version 1.0
 * A Google Chrome extension to display JSON in a user-friendly format
 *
 * This is a chromeified version of the JSONView Firefox extension by Ben Hollis:
 * http://jsonview.com
 * http://code.google.com/p/jsonview
 *
 * Also based on the XMLTree Chrome extension by Moonty & alan.stroop
 * https://chrome.google.com/extensions/detail/gbammbheopgpmaagmckhpjbfgdfkpadb
 *
 * port by Jamie Wilkinson (@jamiew) | http://jamiedubs.com | http://github.com/jamiew
 * MIT license / copyfree (f) F.A.T. Lab http://fffff.at
 * Speed Project Approved: 2h
 */

function collapse( evt ) {
	var collapser = evt.target;
	var target    = collapser.parentNode.getElementsByClassName( 'collapsible' );
	if ( ! target.length ) {
		return;
	}
	target = target[0];
	if ( target.style.display === 'none' ) {
		var ellipsis = target.parentNode.getElementsByClassName( 'ellipsis' )[0];
		target.parentNode.removeChild( ellipsis );
		target.style.display = '';
	} else {
		target.style.display = 'none';
		var ellipsis         = document.createElement( 'span' );
		ellipsis.className   = 'ellipsis';
		ellipsis.innerHTML   = ' &hellip; ';
		target.parentNode.insertBefore( ellipsis, target );
	}
	collapser.innerHTML = (collapser.innerHTML === '-') ? '+' : '-';
}

function addCollapser( item ) {
	// This mainly filters out the root object (which shouldn't be collapsible).
	if ( item.nodeName !== 'LI' ) {
		return;
	}
	var collapser       = document.createElement( 'div' );
	collapser.className = 'collapser';
	collapser.innerHTML = '-';
	collapser.addEventListener( 'click', collapse, false );
	item.insertBefore( collapser, item.firstChild );
}

function jsonView( id, target ) {
	this.debug = false;
	if ( id.indexOf( '#' ) !== - 1 ) {
		this.idType = 'id';
		this.id     = id.replace( '#', '' );
	} else if ( id.indexOf( '.' ) !== - 1 ) {
		this.idType = 'class';
		this.id     = id.replace( '.', '' );
	} else {
		if ( this.debug ) {
			console.log( 'Can\'t find that element' );
		}
		return;
	}

	this.data = document.getElementById( this.id ).innerHTML;
	if ( typeof (target) !== undefined ) {
		if ( target.indexOf( '#' ) !== - 1 ) {
			this.targetType = 'id';
			this.target     = target.replace( '#', '' );
		} else if ( id.indexOf( '.' ) !== - 1 ) {
			this.targetType = 'class';
			this.target     = target.replace( '.', '' );
		} else {
			if ( this.debug ) {
				console.log( 'Can\'t find the target element' );
			}
			return;
		}
	}
	// Note: now using "*.json*" URI matching rather than these page regexes -- save CPU cycles!
	// var is_json = /^\s*(\{.*\})\s*$/.test(this.data);
	// var is_jsonp = /^.*\(\s*(\{.*\})\s*\)$/.test(this.data);
	// if(is_json || is_jsonp){
	// Our manifest specifies that we only do URLs matching '.json', so attempt to sanitize any HTML
	// added by Chrome's "text/plain" or "text/html" handlers.
	if ( /^\<pre.*\>(.*)\<\/pre\>$/.test( this.data ) ) {
		if ( this.debug ) {
			console.log( 'JSONView: data is wrapped in <pre>...</pre>, stripping HTML...' );
		}
		this.data = this.data.replace( /<(?:.|\s)*?>/g, '' ); // Aggressively strip HTML.
	}
	// Test if what remains is JSON or JSONp.
	var json_regex   = /^\s*([\[\{].*[\}\]])\s*$/; // Ghetto, but it works.
	var jsonp_regex  = /^[\s\u200B\uFEFF]*([\w$\[\]\.]+)[\s\u200B\uFEFF]*\([\s\u200B\uFEFF]*([\[{][\s\S]*[\]}])[\s\u200B\uFEFF]*\);?[\s\u200B\uFEFF]*$/;
	var jsonp_regex2 = /([\[\{][\s\S]*[\]\}])\)/; // more liberal support... this allows us to pass the jsonp.json & jsonp2.json tests.
	var is_json      = json_regex.test( this.data );
	var is_jsonp     = jsonp_regex.test( this.data );
	if ( this.debug ) {
		console.log( 'JSONView: is_json=' + is_json + ' is_jsonp=' + is_jsonp );
	}
	if ( is_json || is_jsonp ) {
		if ( this.debug ) {
			console.log( 'JSONView: sexytime!' );
		}
		// JSONFormatter json->HTML prototype straight from Firefox JSONView
		// For reference: http://code.google.com/p/jsonview.
		function JSONFormatter() {
			// No magic required.
		}

		JSONFormatter.prototype = {
			htmlEncode: function( t ) {
				return t != null ? t.toString().replace( /&/g, '&amp;' ).replace( /"/g, '&quot;' ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' ) : '';
			}, decorateWithSpan: function( value, className ) {
				return '<span class="' + className + '">' + this.htmlEncode( value ) + '</span>';
			}, // Convert a basic JSON datatype (number, string, boolean, null, object, array) into an HTML fragment.
			valueToHTML: function( value ) {
				var valueType = typeof value;
				var output    = '';
				if ( value === null ) {
					output += this.decorateWithSpan( 'null', 'null' );
				} else if ( value && value.constructor === Array ) {
					output += this.arrayToHTML( value );
				} else if ( valueType === 'object' ) {
					output += this.objectToHTML( value );
				} else if ( valueType === 'number' ) {
					output += this.decorateWithSpan( value, 'num' );
				} else if ( valueType === 'string' ) {
					if ( /^(http|https):\/\/[^\s]+$/.test( value ) ) {
						output += '<a href="' + value + '">' + this.htmlEncode( value ) + '</a>';
					} else {
						output += this.decorateWithSpan( '"' + value + '"', 'string' );
					}
				} else if ( valueType === 'boolean' ) {
					output += this.decorateWithSpan( value, 'bool' );
				}
				return output;
			}, // Convert an array into an HTML fragment
			arrayToHTML: function( json ) {
				var output      = '[<ul class="array collapsible">';
				var hasContents = false;
				for ( var prop in json ) {
					hasContents = true;
					output     += '<li>';
					output     += this.valueToHTML( json[prop] );
					output     += '</li>';
				}
				output += '</ul>]';
				if ( ! hasContents ) {
					output = '[ ]';
				}
				return output;
			}, // Convert a JSON object to an HTML fragment
			objectToHTML: function( json ) {
				var output      = '{<ul class="obj collapsible">';
				var hasContents = false;
				for ( var prop in json ) {
					hasContents = true;
					output     += '<li>';
					output     += '<span class="prop">' + this.htmlEncode( prop ) + '</span>: ';
					output     += this.valueToHTML( json[prop] );
					output     += '</li>';
				}
				output += '</ul>}';
				if ( ! hasContents ) {
					output = '{ }';
				}
				return output;
			}, // Convert a whole JSON object into a formatted HTML document.
			jsonToHTML: function( json, callback, uri ) {
				var output = '';
				if ( callback ) {
					output += '<div class="callback">' + callback + ' (</div>';
					output += '<div id="json">';
				} else {
					output += '<div id="json">';
				}
				output += this.valueToHTML( json );
				output += '</div>';
				if ( callback ) {
					output += '<div class="callback">)</div>';
				}
				return this.toHTML( output, uri );
			}, // Produce an error document for when parsing fails.
			errorPage: function( error, data, uri ) {
				// var output = '<div id="error">' + this.stringbundle.GetStringFromName('errorParsing') + '</div>';
				// output += '<h1>' + this.stringbundle.GetStringFromName('docContents') + ':</h1>';.
				var output = '<div id="error">Error parsing JSON: ' + error.message + '</div>';
				output    += '<h1>' + error.stack + ':</h1>';
				output    += '<div id="json">' + this.htmlEncode( data ) + '</div>';
				return this.toHTML( output, uri + ' - Error' );
			}, // Wrap the HTML fragment in a full document. Used by jsonToHTML and errorPage.
			toHTML: function( content ) {
				return content;
			}
		};
		// Sanitize & output -- all magic from JSONView Firefox.
		this.jsonFormatter = new JSONFormatter();
		// This regex attempts to match a JSONP structure:
		// * Any amount of whitespace (including unicode nonbreaking spaces) between the start of the file and the callback name.
		// * Callback name (any valid JavaScript function name according to ECMA-262 Edition 3 spec).
		// * Any amount of whitespace (including unicode nonbreaking spaces).
		// * Open parentheses.
		// * Any amount of whitespace (including unicode nonbreaking spaces).
		// * Either { or [, the only two valid characters to start a JSON string.
		// * Any character, any number of times.
		// * Either } or ], the only two valid closing characters of a JSON string.
		// * Any amount of whitespace (including unicode nonbreaking spaces).
		// * A closing parenthesis, an optional semicolon, and any amount of whitespace (including unicode nonbreaking spaces) until the end of the file.
		// This will miss anything that has comments, or more than one callback, or requires modification before use.
		var outputDoc = '';
		// text = text.match(jsonp_regex)[1]; .
		var cleanData        = '', callback = '';
		var callback_results = jsonp_regex.exec( this.data );
		if ( callback_results && callback_results.length === 3 ) {
			if ( this.debug ) {
				console.log( 'THIS IS JSONp' );
			}
			callback  = callback_results[1];
			cleanData = callback_results[2];
		} else {
			if ( this.debug ) {
				console.log( 'Vanilla JSON' );
			}
			cleanData = this.data;
		}
		if ( this.debug ) {
			console.log( cleanData );
		}
		// Covert, and catch exceptions on failure.
		try {
			// var jsonObj = this.nativeJSON.decode(cleanData); .
			var jsonObj = JSON.parse( cleanData );
			if ( jsonObj ) {
				outputDoc = this.jsonFormatter.jsonToHTML( jsonObj, callback );
			} else {
				throw 'There was no object!';
			}
		} catch ( e ) {
			if ( this.debug ) {
				console.log( e );
			}
			outputDoc = this.jsonFormatter.errorPage( e, this.data );
		}
		var links = '<style type="text/css">.jsonViewOutput .prop{font-weight:700;}.jsonViewOutput .null{color:red;}.jsonViewOutput .string{color:green;}.jsonViewOutput .collapser{position:absolute;left:-1em;cursor:pointer;}.jsonViewOutput li{position:relative;}.jsonViewOutput li:after{content:\',\';}.jsonViewOutput li:last-child:after{content:\'\';}.jsonViewOutput #error{-moz-border-radius:8px;border:1px solid #970000;background-color:#F7E8E8;margin:.5em;padding:.5em;}.jsonViewOutput .errormessage{font-family:monospace;}.jsonViewOutput #json{font-family:monospace;font-size:1.1em;}.jsonViewOutput ul{list-style:none;margin:0 0 0 2em;padding:0;}.jsonViewOutput h1{font-size:1.2em;}.jsonViewOutput .callback + #json{padding-left:1em;}.jsonViewOutput .callback{font-family:monospace;color:#A52A2A;}.jsonViewOutput .bool,.jsonViewOutput .num{color:blue;}</style>';
		if ( this.targetType !== undefined ) {
			this.idType = this.targetType;
			this.id     = this.target;
		}
		var el;
		if ( this.idType === 'class' ) {
			el = document.getElementsByClassName( this.id );
			if ( el ) {
				el.className += el.className ? ' jsonViewOutput' : 'jsonViewOutput';
				el.innerHTML  = links + outputDoc;
			}
		} else if ( this.idType === 'id' ) {
			el = document.getElementById( this.id );
			if ( el ) {
				el.className += el.className ? ' jsonViewOutput' : 'jsonViewOutput';
				el.innerHTML  = links + outputDoc;
			}
			el.innerHTML = links + outputDoc;
		}
		var items = document.getElementsByClassName( 'collapsible' );
		var len   = items.length;

		for ( var i = 0; i < len; i ++ ) {
			addCollapser( items[i].parentNode );
		}
	} else {
		// console.log("JSONView: this is not json, not formatting."); .
	}
}
