'use strict';

var at; // The index of the current character
var ch; // The current character
var escapee = {
	'"': '"',
	'\\': '\\',
	'/': '/',
	b: '\b',
	f: '\f',
	n: '\n',
	r: '\r',
	t: '\t'
};
var text;

// Call error when something is wrong.
function error(m) {
	throw {
		name: 'SyntaxError',
		message: m,
		at: at,
		text: text
	};
}

function next(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;
}

function number() {
	// Parse a number value.
	var num;
	var str = '';

	if (ch === '-') {
		str = '-';
		next('-');
	}
	while (ch >= '0' && ch <= '9') {
		str += ch;
		next();
	}
	if (ch === '.') {
		str += '.';
		while (next() && ch >= '0' && ch <= '9') {
			str += ch;
		}
	}
	if (ch === 'e' || ch === 'E') {
		str += ch;
		next();
		if (ch === '-' || ch === '+') {
			str += ch;
			next();
		}
		while (ch >= '0' && ch <= '9') {
			str += ch;
			next();
		}
	}
	num = Number(str);
	if (!isFinite(num)) {
		error('Bad number');
	}
	return num;
}

function string() {
	// Parse a string value.
	var hex;
	var i;
	var str = '';
	var uffff;

	// When parsing for string values, we must look for " and \ characters.
	if (ch === '"') {
		while (next()) {
			if (ch === '"') {
				next();
				return str;
			} 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;
					}
					str += String.fromCharCode(uffff);
				} else if (typeof escapee[ch] === 'string') {
					str += escapee[ch];
				} else {
					break;
				}
			} else {
				str += ch;
			}
		}
	}
	error('Bad string');
}

// Skip whitespace.
function white() {
	while (ch && ch <= ' ') {
		next();
	}
}

// true, false, or null.
function word() {
	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;
		default:
			error("Unexpected '" + ch + "'");
	}
}

// Parse an array value.
function array() {
	var arr = [];

	if (ch === '[') {
		next('[');
		white();
		if (ch === ']') {
			next(']');
			return arr; // empty array
		}
		while (ch) {
			arr.push(value()); // eslint-disable-line no-use-before-define
			white();
			if (ch === ']') {
				next(']');
				return arr;
			}
			next(',');
			white();
		}
	}
	error('Bad array');
}

// Parse an object value.
function object() {
	var key;
	var obj = {};

	if (ch === '{') {
		next('{');
		white();
		if (ch === '}') {
			next('}');
			return obj; // empty object
		}
		while (ch) {
			key = string();
			white();
			next(':');
			if (Object.prototype.hasOwnProperty.call(obj, key)) {
				error('Duplicate key "' + key + '"');
			}
			obj[key] = value(); // eslint-disable-line no-use-before-define
			white();
			if (ch === '}') {
				next('}');
				return obj;
			}
			next(',');
			white();
		}
	}
	error('Bad object');
}

// Parse a JSON value. It could be an object, an array, a string, a number, or a word.
function value() {
	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.
module.exports = 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;
		var v;
		var val = holder[key];
		if (val && typeof val === 'object') {
			for (k in value) {
				if (Object.prototype.hasOwnProperty.call(val, k)) {
					v = walk(val, k);
					if (typeof v === 'undefined') {
						delete val[k];
					} else {
						val[k] = v;
					}
				}
			}
		}
		return reviver.call(holder, key, val);
	}({ '': result }, '')) : result;
};
