// map_functions.js
// Provide functionality for managing the map display
// Developed by Kyle Evans, 2007
// Revised by Rich Rarey
//  
// PURPOSE:
// JAVAScript containing functions that manipulate and display the coverage maps, station 'dots' and balloon popups.
//
//
// REVISIONS:
// 090217 Changed function GotResponse() to display "not available" in a field 
//        when the database returns "-1". Also added error handling on that catches
//        when the user selects a demographic category without clicking on a public station in the map.
// 090318 Made globalMinZoom a variable that is used in function updateMarkers() to limit how far the user 
//        can zoom out and still see the coverage maps.
//
// 091002 Added additional code in function showConstantFields to receive and display the lowest 
//        contour field strength for the selected station to reinforce to the user that the
//        demographic data is related to biggest coverage pattern (it holds the most people).
// 091004 Removed the display for lowest field strength after confirming that the contours are
//        concentric hollow rings that do not overlap populations (the contours MUST be summed for total
//        population demographics.
//        Build instead a lovely three-color legend that appears under the selected station's 
//        call letters and city of license.
// 091007 Fixed IE bug that prevented map from displaying at all. Solution: renamed parameter returned by map_server.php
//        from "class" to "Stationclass".
//        Removed FM frequency variables and function.
// ------------------------------------------------------------------------------------------

// Initial variables (with global map variable)
var map;
var globalMapCommercial = 1;
var activeOverlays = [];
var globalOverlayCounter = 0;
var currentStation;
// globalMinZoom is the variable that controls how far out a user can zoom and still have
// the coverage maps displayed.
// The higher the zoom is, the closer the zoom and the fewer stations will be in the viewable frame.
// Accroding to Kyle, he made the (arbitrary) decision to go with a zoom level of 7 because it seemed
// that anything smaller than that would really kill the server.  "But YMMV :-)" he says.
// We set it to 5, to see as much as the great state of Texas as possible.
var globalMinZoom = 5;
// ------------------------------------------------------------------------------------------

// Updated transmitter icons
var iconTransmitter = new GIcon();  
	iconTransmitter.image = "http://www.nprlabs.org/novatv/icons/iconTransmitter.png";
	iconTransmitter.shadow = "http://www.nprlabs.org/novatv/icons/iconTransmitter_shadow.png";
	iconTransmitter.iconSize = new GSize(14, 12);
	iconTransmitter.shadowSize = new GSize(14, 12);
	iconTransmitter.iconAnchor = new GPoint(6, 20);
	iconTransmitter.infoWindowAnchor = new GPoint(5, 1);

var iconNPRTransmitter = new GIcon();  
	iconNPRTransmitter.image = "http://www.nprlabs.org/novatv/icons/iconNPRTransmitter.png";
	iconNPRTransmitter.shadow = "http://www.nprlabs.org/novatv/icons/iconNPRTransmitter_shadow.png";
	iconNPRTransmitter.iconSize = new GSize(21, 18);
	iconNPRTransmitter.shadowSize = new GSize(21, 18);
	iconNPRTransmitter.iconAnchor = new GPoint(6, 20);
	iconNPRTransmitter.infoWindowAnchor = new GPoint(5, 1);

var points;

// ------------------------------------------------------------------------------------------
function init() {
	var xmlHttp; // Creating the xmlHttp variable
	map = new GMap2(document.getElementById("map"));  // Creating the map element
	map.addControl(new GSmallMapControl());  // Creating a control for scale
	map.addControl(new GMapTypeControl());  // Creating a control for map type (map, satellite, etc.)
	map.addControl(new GScaleControl());
	map.setCenter(new GLatLng(35.224773, -100.991943), 9);  // Initializing the center of the map

	updateMarkers();  // Call the updateMarkers method, finding and adding markers that appear within the viewable area

	// Method called when the user zooms on the map
	GEvent.addListener(map,'zoomend',function() {
		// window.alert('mapZoomed');
		updateMarkers();											  
	});

	// Method called when the user moves the viewable area of the map
	GEvent.addListener(map,'moveend',function() {
		// window.alert('mapMoved');
		updateMarkers();
	});
}

// ------------------------------------------------------------------------------------------
// Method called when a user clicks on a quickview
function getQuickview() {
	clearActiveOverlays();
	var selectedQuickview = document.getElementById("quickviewOptions").value;  // Get the index of the selected quickview
	var quickLatitude = Col0[selectedQuickview];  // Look up the latitude
	var quickLongitude = Col1[selectedQuickview];  // and longitude
	var quickZoom = Col2[selectedQuickview];  // and zoom of the selected quickview index
	map.setCenter(new GLatLng(quickLatitude, quickLongitude), quickZoom);  // Set the center
//	map.setZoom(quickZoom);  // and the zoom at the returned values
//	window.alert('getQuickview');
//	updateMarkers();  // and recalculate the viewable markers
}

// ------------------------------------------------------------------------------------------
// Method called to update markers in the viewable area
function updateMarkers() { 
	// window.alert('updateMarkers');
// Only update markers if the map is at a closer zoom level (controls the server load)
// On original source currentZoom had to be > 7 to reduce server workload.
	var currentZoom = map.getZoom();  
	if(currentZoom > globalMinZoom) {
		var i = 0;
		map.clearOverlays();  // Remove the existing points 
	
		var bounds = map.getBounds(); 	// Get the boundary for the data 
		var southWest = bounds.getSouthWest();  // Southwest boundary
		var northEast = bounds.getNorthEast();  // Northwest boundary
		// Format the boundary values for the xmlhttp request
		var getVars = 'ne=' + northEast.toUrlValue() + '&sw=' + southWest.toUrlValue();
		// GLog.writeUrl(url);  // Log the URL for testing 
		// Retrieve viewable points
		var request = GXmlHttp.create();  // Create an xmlHttp request
		request.open('GET', 'map_server.php?'+getVars, true);  // Open the page with the boundaries as input variables

        // Use an anonymous function, aka "closures", to process the return data from the called php script.
		request.onreadystatechange = function() { 
			if (request.readyState == 4) { // When the request has been completed
				document.getElementById('sidebar-list').innerHTML = '';  // Reset the sidebar list by setting the inner HTML to null
				var jscript = request.responseText;  // Read the response text
				points = json_parse(jscript);
				for (i = 0; i < points.points.length; i++) {
					var point = new GLatLng(points.points[i].lat,points.points[i].lng);  // Create the GLatLng object for the point
					// Create the point using the GLatLng point object and information in the viewable point array					
					try {
					var marker = createMarker(point,
											  points.points[i].city,
											  points.points[i].ptfpID,
											  points.points[i].service,
											  points.points[i].channel,
											  points.points[i].Stationclass,
											  points.points[i].callsign,
											  points.points[i].fileNumber,
											  points.points[i].licensee,
											  points.points[i].status);  
					} catch (e) {
						window.alert(i+" = Number. Error:"+ e.name);
					}		
				} 
			} 
	    } 
		request.send(null);  // Send a null request when the function has completed

		// Redraw the active layers on the map
		if(globalOverlayCounter != 0) {
			for(i = 0; i <= globalOverlayCounter; i++){
			//ERROR HERE USING IE (any version)
				try {
					map.addOverlay(activeOverlays[i].geoXml);	
				} catch (e) {
					// catch all thrown errors and print error type
					//alert("Problem at globalOverlayCounter. i = "+ i + " Error:" + e.name + e.description); 
				 } 
			}
		}
		updateActiveOverlays();
	}
	else {
		map.clearOverlays();  // Remove the existing points if the map is beyond the appropriate zoom level
	}
}


// ------------------------------------------------------------------------------------------
// This is a helper method to dynamically control mapping of commercial stations
function checkCommercial() {
	if(document.myForm.commOff.checked == true) {  // Check if commercial mapping is set to 'off'
		globalMapCommercial = 0;   // Set global variable to zero
	//	window.alert('checkCommercial1');
		updateMarkers();  // Refresh the map - the change is caught in the createMarker method
	}
	else {
		globalMapCommercial = 1;  // Set global variable to one
	//	window.alert('checkComercial2');
		updateMarkers();  // Refresh the map
	}
}

function directToStation(inputLatitude, inputLongitude) {
//	currentCenterLat = inputLatitude;
//	currentCenterLon = inputLongitude;
//	window.alert('directToStation => ' + inputLatitude + inputLongitude);
	map.setCenter(new GLatLng(inputLatitude, inputLongitude), 8);  // Set the center
	// window.alert('1');
	// map.setZoom(8);  // and the zoom at the returned values
	// window.alert('2');
	// updateMarkers();  // and recalculate the viewable markers
}

// ------------------------------------------------------------------------------------------
function createMarker(point,city,ptfpID,service,frequency,stationClass,callsign,fileNumber,licensee,status) {
	if(ptfpID == 1) {
		// window.alert('createMarker ===> ' + callsign);
	}
	// Create a marker for the input point based on its PTFP status and service
	if(ptfpID > 0) {
		var marker = new GMarker(point, iconNPRTransmitter);  // NPR Station
	} else {
		var marker = new GMarker(point, iconTransmitter);
	}

	// Fill the sidebar content for PTFP station
	if(ptfpID > 0) {
		var listItem = document.createElement('li');  // Create the list element
		// var listItemLink = listItem.appendChild(document.createElement('a'));
		// listItemLink.href = "#"
		var isChecked = isTheStationActive(callsign);
		listItem.innerHTML = "<span class='style1'>" + callsign + " - " + frequency + " - " + service + " - " + status + "</span><br><span class='style3'>" + city + "<br><b>Station Coverage Overlay - </b></span><span class='style1'><input type='checkbox' id='" + callsign + "' onClick='toggleGeoXML(this.id, this.checked)'" + isChecked + "><br><hr></span>";
	
		var focusPoint = function() { 
			var markerHTML = '<div class="style1">' + callsign + '</div><div class="style3">Channel - ' + frequency + '</div><div class="style3">Status - ' + status + '</div><div class="style3">File Number - ' + fileNumber + '</div>';  /* Maybe some noncommercial image goes here */ 
			marker.openInfoWindowHtml(markerHTML); 
			showUser(callsign); // Passes the currently selected point into the showUser method, requesting relevant information for the bottom panel
			newStationSelected();
			getConstantFields(callsign);
			currentStation = callsign;
			return false;
		}; 

		GEvent.addListener(marker, 'click', focusPoint);
		// listItemLink.onclick = focusPoint;
		document.getElementById('sidebar-list').appendChild(listItem);	
		map.addOverlay(marker);
	}

	if(globalMapCommercial > 0) {
		if(ptfpID < 1) {
			/*
			var listItem = document.createElement('li');
			var listItemLink = listItem.appendChild(document.createElement('a'));
			listItemLink.href = "#"
			listItemLink.innerHTML = "<span class='style1'>" + callsign + "</span><br><span class='style3'>Frequency - " + frequency + "<br> Class - " + stationClass + "</span>";
			*/
			var focusPoint = function() { 
				var markerHTML = '<div class="style1">' + callsign + '</div><div class="style3">Frequency - ' + frequency + '</div><div class="style3">Class - ' + stationClass + '</div><div class="style3">File Number - ' + fileNumber + '</div>'; 
				marker.openInfoWindowHtml(markerHTML); 
				/* This is where the AJAX call to the table will go */
				newStationSelected();
				showUser(callsign); // Passes the currently selected point into the showUser method, recalling relevant information for the bottom panel
				return false;
			}; 
			GEvent.addListener(marker, 'click', focusPoint);
			// listItemLink.onclick = focusPoint;
			// document.getElementById('sidebar-list').appendChild(listItem);	
			map.addOverlay(marker);
		}
	}
} 

// ------------------------------------------------------------------------------------------
// This method toggles on and off the KML file (essentially the ITM coverage)
function toggleGeoXML(id, checked) {
  var i = 0;

//	window.alert('toggleGeoXML');

  if (checked) {
    var geoXml = new GGeoXml('http://www.nprlabs.org/TV6stationcoverage/' + id + '.kml');  // create a GGeoXml object from the properly formed URL
	// window.alert('looking at http://www.echobloom.com/sandbox/nprlabs/stationcoverage/' + id + '.kml')
	activeOverlays[globalOverlayCounter] = {'id':id, 'geoXml':geoXml};
	globalOverlayCounter = globalOverlayCounter + 1;
	// stationCoverageOverlay[id].geoXml = geoXml;  // Add a reference to this object in the appropriate record's attributes
    map.addOverlay(geoXml);  // Add the overlay to the map
	updateActiveOverlays();
  } 
  /* Iterate through the activeOverlays layer, looking for the appropriate id match, then remove the geoXml reference */
  else {
    for(i = 0; i <= globalOverlayCounter; i++) {
		if(activeOverlays[i].id == id){
			map.removeOverlay(activeOverlays[i].geoXml);
			activeOverlays.splice(i,1);
			globalOverlayCounter = globalOverlayCounter - 1;  // I think this would be better handled by activeOverlays.length();
			updateActiveOverlays();
		}
	}
  }
	updateMarkers();
}

function isTheStationActive(inputCallsign) { 
	returnValue = '';
	if(activeOverlays.length) {
		for(i = 0; i < globalOverlayCounter; i++) {
			if(activeOverlays[i].id == inputCallsign){
				returnValue = 'CHECKED';
			}
		}
	}

	return returnValue;
}

// ------------------------------------------------------------------------------------------
// Refill the HTML element for the active overlays (these boxes will always be checked)
function updateActiveOverlays(){
	// alert(globalOverlayCounter);
	document.getElementById('overlays-list').innerHTML = '';
// iterate through the activeOverlays variable
	if(globalOverlayCounter != 0) {
		for (i = 0; i < globalOverlayCounter; i++) {
			var activeOverlay = document.createElement('li');  // Create the list element for the overlay
			// activeOverlay.innerHTML = activeOverlays[i].id;
			activeOverlay.innerHTML = "<span class='style1'><input type='checkbox' id='" + activeOverlays[i].id + "' onClick='toggleGeoXML(this.id, this.checked)' CHECKED> - " + activeOverlays[i].id + "<br><hr></span>";
			document.getElementById('overlays-list').appendChild(activeOverlay);
		}
	}

}

// ------------------------------------------------------------------------------------------
function clearActiveOverlays(){
// reinitialize the activeOverlays variable
// call updateActiveOverlays
	if(globalOverlayCounter != 0) {
		for(i = 0; i <= globalOverlayCounter; i++) {
				map.removeOverlay(activeOverlays[i].geoXml);
			activeOverlays.splice(i,1);
			globalOverlayCounter = globalOverlayCounter - 1;  // I think this would be better handled by activeOverlays.length();
		}
	}
	
	updateActiveOverlays();

}

// ------------------------------------------------------------------------------------------
function showUser(str) { 
	// var request = GXmlHttp.create();
	xmlHttp2=GetXmlHttpObject() // Creating the xmlHTTP object, with all relevant type-checking

	var url="map_selectedStation.php"; // Identifying the PHP page that handles the database call
	url=url+"?q="+str; // Forming the URL to include the relevant callsign
	url=url+"&sid="+Math.random(); // Adding a random number to prevent the server from using a cached file
	xmlHttp2.onreadystatechange = stateChanged;  // When there's a result from the query, call the stateChanged method and write it to the screen 
	xmlHttp2.open("GET",url,true); // Opens the XMLHTTP object with the given URL
	xmlHttp2.send(null); // Executing the query
}

// ------------------------------------------------------------------------------------------
function stateChanged()  { 
	if (xmlHttp2.readyState==4 || xmlHttp2.readyState=="complete") { 
		// If the state has changed, then replace the text of the div element with the returned information
		// We also add an ID tag to hold our custom legend for the selected station.
		document.getElementById("stationHeader").innerHTML=xmlHttp2.responseText+'<div id="stationLegend"></div>';
	} 
}

// ------------------------------------------------------------------------------------------
// Creating the XMLHTTP object with all relevant type-checking and cross-browser compatability
function GetXmlHttpObject() {
	var xmlHttp=null;
	try {
		xmlHttp = new XMLHttpRequest(); // Firefox, Opera 8.0+, Safari
	}
	catch (e) {
		try {
			xmlHttp = new ActiveXObject("Msxml2.XMLHTTP"); // Internet Explorer
		}
		catch (e) {
			xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
		}
	}
	return xmlHttp;
}

// ------------------------------------------------------------------------------------------
function getConstantFields(str) { 
	
	xmlHttp=GetXmlHttpObject()
	if (xmlHttp==null) {
		alert ("Browser does not support HTTP Request");
		return
	}
	var url="map_getConstantFields.php";
	url=url+"?q="+str;
	url=url+"&sid="+Math.random();
	xmlHttp.onreadystatechange = showConstantFields; 
	xmlHttp.open("GET",url,true);
	xmlHttp.send(null);
}

// ------------------------------------------------------------------------------------------
function showConstantFields()  { 
	if (xmlHttp.readyState==4 || xmlHttp.readyState=="complete") { 

	var jscript = xmlHttp.responseText;  // Read the response text
	var result;  // Initialize the result variable

	eval(jscript);  // Parse the responseText, essentially creating the variable array 'points'
	for (i in result) {  // Iterate through the viewable point array
		var totalPop_1990 = result[i].totalPop_1990;
		var totalPop_2000 = result[i].totalPop_2000;
		var totalPop_2007 = result[i].totalPop_2007;
		var HHIncome_1990 = result[i].HHIncome_1990;
		var HHIncome_2000 = result[i].HHIncome_2000;
		var HHIncome_2007 = result[i].HHIncome_2007;
		var MinFieldStrength = result[i].MinFieldStrength;
	
	}
	// Setting the values in the table
	document.getElementById("Row1Col1").innerHTML=totalPop_1990;
	document.getElementById("Row1Col2").innerHTML=totalPop_2000;
	document.getElementById("Row1Col3").innerHTML=totalPop_2007;
	document.getElementById("Row2Col1").innerHTML=HHIncome_1990;
	document.getElementById("Row2Col2").innerHTML=HHIncome_2000;
	document.getElementById("Row2Col3").innerHTML=HHIncome_2007;
	
	// This section determines the contour values to create a custom legend
	// for the user's benefit. There are six sets of contour plots, each set has three
	// contour values, a low, medium and high.
	// By using the found MinFieldStrength, we can determine which set of number to use,
	// and hand-code the other two field contour strengths.
	var MedFieldStrength=0;
	var HighFieldStrength=0;
	// Possible MinFieldStrength values: 41, 36, 28, 64, 56, 47. Any other value is an error.
	if (MinFieldStrength==41) {
		MedFieldStrength=61;
		HighFieldStrength=81;
	} else if (MinFieldStrength==36) {
		MedFieldStrength=56;
		HighFieldStrength=76;
	} else if (MinFieldStrength==28) {
		MedFieldStrength=35;
		HighFieldStrength=45;
	} else if (MinFieldStrength==64) {
		MedFieldStrength=77;
		HighFieldStrength=80;
	} else if (MinFieldStrength==56) {		
		MedFieldStrength=76;
		HighFieldStrength=96;
	} else if (MinFieldStrength==47) {
		MedFieldStrength=67;
		HighFieldStrength=87;
	} else {
		MedFieldStrength="-1";
		HighFieldStrength="-1";
	}

    // This is the HTML that creates a pretty legend. We cleverly insert our field values into the string and send it to the web page.
	var myStationLegend="<table border='0' cellspacing='2' cellpadding='2'><tr><td bgcolor='#FFFF66' class='subHeadingText'>&nbsp;"+HighFieldStrength+"dB&micro; &nbsp;</td><td bgcolor='#33FF66' class='subHeadingText'>&nbsp;"+MedFieldStrength+"dB&micro; &nbsp;</td><td bgcolor='#F2726F' class='subHeadingText'>&nbsp;"+MinFieldStrength+"dB&micro; &nbsp;</td></tr></table>";




	document.getElementById("stationLegend").innerHTML=myStationLegend;
	
	

	} 
}

// ------------------------------------------------------------------------------------------
function getRow(field,target){

	xmlHttp3=GetXmlHttpObject()
	var url="map_retrieveField.php";
	url=url+"?var="+field+"&call="+currentStation+"&target="+target;
	// url=url+"&sid="+Math.random();
	xmlHttp3.onreadystatechange = gotResponse; 
	xmlHttp3.open("GET",url,true);
	xmlHttp3.send(null);

}

// ------------------------------------------------------------------------------------------
function gotResponse()  { 
	if (xmlHttp3.readyState==4 || xmlHttp3.readyState=="complete") { 
	
	var sMessage = "No public station is selected on the map.\n\nPlease click on a public station on the map FIRST, \nthen select a demographic category from the pull-down table.";
	
	// Read the response text
	var jscript = xmlHttp3.responseText;  
  	
	// Initialize the result variable
	var result;
	// Initialize the field text variables
	var field_1990;
	var field_2000;
	var field_2007;
	
	// Our default text if there's no data in the database for a field
	var sNotAvailable = "<em>not available</em>";
	
	// Evaluate the responseText as if it was a command, which creates variable array 'points'.
	// If the user has NOT selected a public station, our data will be bad, so we'll
	// pop up an alert message and exit the function gracefully.
	try {
		eval(jscript);  
	}
	catch(e) {
		alert(sMessage);
		return;
	}
	
	// Iterate through the viewable point array.
	// If we get a -1, that means there's no data
	// for that field, and we'll substitute "not avail" text.
	for (i in result) {  
			// 1990 Census field
			if (result[i].field_1990 > -1) {
				field_1990 = result[i].field_1990;
			}
			else {
				field_1990 = sNotAvailable;
			}
			
		 	// 2000 Census field
			if (result[i].field_1990 > -1) {
				field_2000 = result[i].field_2000;
			}
			else {
				field_2000 = sNotAvailable;
			}
			
			// 2007 [estimated] census field
			if (result[i].field_2007 > -1) {
				field_2007 = result[i].field_2007;
			}
			else {
				field_2007 = sNotAvailable;
			}
			var target = result[i].target;
		}

	// just some scratch to show inputs and outputs
	// Put the field name into the targets
	cell1="Row"+target+"Col1";
	cell2="Row"+target+"Col2";
	cell3="Row"+target+"Col3";
	document.getElementById(cell1).innerHTML=field_1990;
	document.getElementById(cell2).innerHTML=field_2000;
	document.getElementById(cell3).innerHTML=field_2007;
	}
}

// ------------------------------------------------------------------------------------------
function newStationSelected() {
	// document.getElementById("Row0Col1").innerHTML="&nbsp;";
	document.getElementById("Row1Col1").innerHTML="-";
	document.getElementById("Row1Col2").innerHTML="-";
	document.getElementById("Row1Col3").innerHTML="-";
	document.getElementById("Row2Col1").innerHTML="-";
	document.getElementById("Row2Col2").innerHTML="-";
	document.getElementById("Row2Col3").innerHTML="-";
	document.getElementById("Row3Col1").innerHTML="-";
	document.getElementById("Row3Col2").innerHTML="-";
	document.getElementById("Row3Col3").innerHTML="-";
	
}

// ------------------------------------------------------------------------------------------
window.onload = init;
// END map_functions.js -------------------------------------

// JSON PARSE FUNCTIONALITY

json_parse = (function () {

// This is a function that can parse a JSON text, producing a JavaScript
// data structure. It is a simple, recursive descent parser. It does not use
// eval or regular expressions, so it can be used as a model for implementing
// a JSON parser in other languages.

// We are defining the function inside of another function to avoid creating
// global variables.

    var at,     // The index of the current character
        ch,     // The current character
        escapee = {
            '"':  '"',
            '\\': '\\',
            '/':  '/',
            b:    '\b',
            f:    '\f',
            n:    '\n',
            r:    '\r',
            t:    '\t'
        },
        text,

        error = function (m) {

// Call error when something is wrong.

            throw {
                name:    'SyntaxError',
                message: m,
                at:      at,
                text:    text
            };
        },

        next = function (c) {

// If a c parameter is provided, verify that it matches the current character.

            if (c && c !== ch) {
                error("Expected '" + c + "' instead of '" + ch + "'");
            }

// Get the next character. When there are no more characters,
// return the empty string.

            ch = text.charAt(at);
            at += 1;
            return ch;
        },

        number = function () {

// Parse a number value.

            var number,
                string = '';

            if (ch === '-') {
                string = '-';
                next('-');
            }
            while (ch >= '0' && ch <= '9') {
                string += ch;
                next();
            }
            if (ch === '.') {
                string += '.';
                while (next() && ch >= '0' && ch <= '9') {
                    string += ch;
                }
            }
            if (ch === 'e' || ch === 'E') {
                string += ch;
                next();
                if (ch === '-' || ch === '+') {
                    string += ch;
                    next();
                }
                while (ch >= '0' && ch <= '9') {
                    string += ch;
                    next();
                }
            }
            number = +string;
            if (isNaN(number)) {
                error("Bad number");
            } else {
                return number;
            }
        },

        string = function () {

// Parse a string value.

            var hex,
                i,
                string = '',
                uffff;

// When parsing for string values, we must look for " and \ characters.

            if (ch === '"') {
                while (next()) {
                    if (ch === '"') {
                        next();
                        return string;
                    } else if (ch === '\\') {
                        next();
                        if (ch === 'u') {
                            uffff = 0;
                            for (i = 0; i < 4; i += 1) {
                                hex = parseInt(next(), 16);
                                if (!isFinite(hex)) {
                                    break;
                                }
                                uffff = uffff * 16 + hex;
                            }
                            string += String.fromCharCode(uffff);
                        } else if (typeof escapee[ch] === 'string') {
                            string += escapee[ch];
                        } else {
                            break;
                        }
                    } else {
                        string += ch;
                    }
                }
            }
            error("Bad string");
        },

        white = function () {

// Skip whitespace.

            while (ch && ch <= ' ') {
                next();
            }
        },

        word = function () {

// true, false, or null.

            switch (ch) {
            case 't':
                next('t');
                next('r');
                next('u');
                next('e');
                return true;
            case 'f':
                next('f');
                next('a');
                next('l');
                next('s');
                next('e');
                return false;
            case 'n':
                next('n');
                next('u');
                next('l');
                next('l');
                return null;
            }
            error("Unexpected '" + ch + "'");
        },

        value,  // Place holder for the value function.

        array = function () {

// Parse an array value.

            var array = [];

            if (ch === '[') {
                next('[');
                white();
                if (ch === ']') {
                    next(']');
                    return array;   // empty array
                }
                while (ch) {
                    array.push(value());
                    white();
                    if (ch === ']') {
                        next(']');
                        return array;
                    }
                    next(',');
                    white();
                }
            }
            error("Bad array");
        },

        object = function () {

// Parse an object value.

            var key,
                object = {};

            if (ch === '{') {
                next('{');
                white();
                if (ch === '}') {
                    next('}');
                    return object;   // empty object
                }
                while (ch) {
                    key = string();
                    white();
                    next(':');
                    if (Object.hasOwnProperty.call(object, key)) {
                        error('Duplicate key "' + key + '"');
                    }
                    object[key] = value();
                    white();
                    if (ch === '}') {
                        next('}');
                        return object;
                    }
                    next(',');
                    white();
                }
            }
            error("Bad object");
        };

    value = function () {

// Parse a JSON value. It could be an object, an array, a string, a number,
// or a word.

        white();
        switch (ch) {
        case '{':
            return object();
        case '[':
            return array();
        case '"':
            return string();
        case '-':
            return number();
        default:
            return ch >= '0' && ch <= '9' ? number() : word();
        }
    };

// Return the json_parse function. It will have access to all of the above
// functions and variables.

    return function (source, reviver) {
        var result;

        text = source;
        at = 0;
        ch = ' ';
        result = value();
        white();
        if (ch) {
            error("Syntax error");
        }

// If there is a reviver function, we recursively walk the new structure,
// passing each name/value pair to the reviver function for possible
// transformation, starting with a temporary root object that holds the result
// in an empty key. If there is not a reviver function, we simply return the
// result.

        return typeof reviver === 'function' ? (function walk(holder, key) {
            var k, v, value = holder[key];
            if (value && typeof value === 'object') {
                for (k in value) {
                    if (Object.hasOwnProperty.call(value, k)) {
                        v = walk(value, k);
                        if (v !== undefined) {
                            value[k] = v;
                        } else {
                            delete value[k];
                        }
                    }
                }
            }
            return reviver.call(holder, key, value);
        }({'': result}, '')) : result;
    };
}());