/*
Author: Oliver Steele
Copyright: Copyright 2006 Oliver Steele.  All rights reserved.
Homepage: http://osteele.com/sources/openlaszlo/json
License: MIT License.
Version: 1.0

== JSON.stringify
function JSON.stringify(value, [options]);

Usage:
  JSON.stringify(123) // => '123'
  JSON.stringify([1,2,3]) // => '[1,2,3]'
  JSON.stringify({a:1,b:2}) // => '{"a":1,"b":2}'

== JSON.parse
function JSON.parse(string, [options]);

Parses the first JSON expression from +string+.  If
+options.allowSuffix+ is not specified (the default), the expression
must span the string or the parse produces an error.

Errors are signalled by returning undefined, and optionally invoking
+options.errorHandler+ (see below).

Usage:
  JSON.parse('123') // => 123
  JSON.parse('[1,2,3]') // => [1,2,3]
  JSON.parse('{"a":1,"b":2}') // => {a: 1, b: 2}
  JSON.parse('[') // => undefined (parse error)

+options+ can contain these properties:
* allowSuffix: allow text to follow the expression.  This can be
  used to pack multiple JSON expressions into a single string.
+ startIndex: the index into the string to begin parsing.  This is
  useful in conjunction with allowSuffix.
* errorHandler: function (message: string, index: number): void
  If an error occurs, the errorHandler is invoked with two arguments:
  an error string and a character offset.

=== Notes
JSON.parse accepts these strings don't conform to the JSON grammar:
* '1.e1' -- JSON requires digits after the decimal point
* '01' -- JSON doesn't permit leading zeros.
* '"\a"' -- JSON doesn't permit nonescape characters to be encoded

The parser does not accept unquoted object property names such as {a:
1}.  These are valid JavaScript (when the property name is a valid
identifier), but they are not in the JSON grammar.
*/

/*
Implementation notes:
* use for(;;) instead of for(in) where order matters, since Flash
  iterates backwards.
* test with obj[n] instead of obj.n, to avoid debugger warnings
 */

var JSON = {};

JSON.stringify = function (value) {
    var generator = new JSON.generator();
    return generator.generate(value);
};

JSON.parse = function (str, options) {
    var parser = new JSON.parser();
    if (arguments.length > 1)
        parser.setOptions(options);
    return parser.parse(str);
};

JSON.HEXDIGITS = "0123456789abcdef";

JSON.generator = function () {};

JSON.generator.CHARACTER_QUOTES = {
    '\\': '\\\\',
    '\"': '\\\"',
    '\b': '\\b',
    '\f': '\\f',
    '\n': '\\n',
    '\r': '\\r',
    '\t': '\\t'
};

JSON.generator.prototype.primitiveExceptions = {
	'NaN': 'null',
	'Infinity': 'null',
	'-Infinity': 'null'
};

JSON.generator.prototype.generate = function (value, options) {
    this.segments = [];
    this.appendValue(value);
    return this.segments.join('');
};

JSON.generator.prototype.appendValue = function (value) {
    switch (typeof value) {
    case 'string':
        this.appendString(value);
        break;
    case 'object':
        this.findObjectAppender(value).apply(this, [value]);
        break;
    default:
        this.appendPrimitive(value);
    }
};

JSON.generator.prototype.appendPrimitive = function (v) {
    var s = String(v);
    if (this.primitiveExceptions[s])
        s = this.primitiveExceptions[s];
    this.segments.push(s);
};

JSON.generator.prototype.appendString = function (string) {
    var segments = this.segments;
    var escapes = JSON.generator.CHARACTER_QUOTES;
    var start = 0;
    var i = 0;
    segments.push('"');
    while (i < string.length) {
        var c = string.charAt(i++);
        if (escapes[c]) {
            if (i-1 > start)
                segments.push(string.slice(start, i-1));
            segments.push(escapes[c]);
            start = i;
        } else if (c < ' ' || c > '~') {
			var n = c.charCodeAt(0);
			segments.push('\\u');
			for (var shift = 16; (shift -= 4) >= 0; )
				segments.push(JSON.HEXDIGITS.charAt((n >> shift) & 15));
			start = i;
		} // else collect into current segment
    }
    if (i > start)
        segments.push(string.slice(start, i));
    segments.push('"');
};

// This implements an ordered hash --- ordered, so that Object comes
// last
JSON.generator.prototype.objectAppenders = [
    // treat the Object types that correspond to primitives as though
    // they were the corresponding primitives, so that, for example,
    // {true, new Boolean(true)}, {true, new Boolean(true)}, and {'a',
    // new String('a')} are all equivalence classes for stringifying
    [Boolean, JSON.generator.prototype.appendPrimitive],
    [Number, JSON.generator.prototype.appendPrimitive],
    [String, JSON.generator.prototype.appendString],
    [Array, function (ar) {
        var segments = this.segments;
        segments.push("[");
        for (var i = 0; i < ar.length; i++) {
            if (i > 0) segments.push(",");
            this.appendValue(ar[i]);
        }
        segments.push("]");
    }],
    // This must come last, since it's a superclass
    [Object, function (object) {
        var segments = this.segments;
        segments.push("{");
        var count = 0;
        for (var key in object) {
            if (count++) segments.push(",");
            this.appendValue(key);
            segments.push(":");
            this.appendValue(object[key]);
        }
        segments.push("}");
    }]];

JSON.generator.prototype.findObjectAppender = function (object) {
    if (object == null)
        return function (object) {this.segments.push("null")};
    for (var i = 0; i < this.objectAppenders.length; i++) {
        var entry = this.objectAppenders[i];
        if (object instanceof entry[0])
            return entry[1];
    }
    // program error
};


JSON.parser = function () {
    this.errorHandler = null;
};

JSON.parser.WHITESPACE = " \t\n\r\f";

JSON.parser.CHARACTER_UNQUOTES = {
    'b': '\b',
    'f': '\f',
    'n': '\n',
    'r': '\r',
    't': '\t'
};

JSON.parser.prototype.setOptions = function (options) {
	if (options['errorHandler'])
		this.errorHandler = options.errorHandler;
};

JSON.parser.prototype.parse = function (str) {
    this.string = str;
    this.index = 0;
	this.message = null;
    var value = this.readValue();
	if (!this.message && this.next())
		value = this.error("extra characters at the end of the string");
    if (this.message && this.errorHandler)
        this.errorHandler(this.message, this.index);
    return value;
};

JSON.parser.prototype.error = function (message) {
	this.message = message;
	return undefined;
}
    
JSON.parser.prototype.readValue = function () {
    var c = this.next();
    var fn = c && this.table[c];
    if (fn)
        return fn.apply(this);
    var keywords = {'true': true, 'false': false, 'null': null};
    var s = this.string;
    var i = this.index-1;
    for (var w in keywords) {
        if (s.slice(i, i+w.length) == w) {
            this.index = i+w.length;
            return keywords[w];
        }
    }
    if (c) return this.error("invalid character: '" + c + "'");
	return this.error("empty expression");
}

JSON.parser.prototype.table = {
    '{': function () {
        var o = {};
        var c;
		var count = 0;
        while ((c = this.next()) != '}') {
            if (count) {
				if (c != ',')
					this.error("missing ','");
			} else if (c == ',') {
				return this.error("extra ','");
			} else
				--this.index;
            var k = this.readValue();
            if (typeof k == "undefined") return undefined;
            if (this.next() != ':') return this.error("missing ':'");
            var v = this.readValue();
            if (typeof v == "undefined") return undefined;
            o[k] = v;
			count++;
        }
        return o;
    },
    '[': function () {
        var ar = [];
        var c;
        while ((c = this.next()) != ']') {
            if (!c) return this.error("unmatched '['");
            if (ar.length) {
				if (c != ',')
					this.error("missing ','");
			} else if (c == ',') {
				return this.error("extra ','");
			} else
				--this.index;
            var n = this.readValue();
            if (typeof n == "undefined") return undefined;
            ar.push(n);
        }
        return ar;
    },
    '"': function () {
        var s = this.string;
        var i = this.index;
        var start = i;
        var segments = [];
        var c;
        while ((c = s.charAt(i++)) != '"') {
            //if (i == s.length) return this.error("unmatched '\"'");
			if (!c) return this.error("umatched '\"'");
            if (c == '\\') {
                if (start < i-1)
                    segments.push(s.slice(start, i-1));
                c = s.charAt(i++);
				if (c == 'u') {
					var code = 0;
					start = i;
					while (i < start+4) {
						c = s.charAt(i++);
						var n = JSON.HEXDIGITS.indexOf(c.toLowerCase());
						if (n < 0) return this.error("invalid unicode digit: '"+c+"'");
						code = code * 16 + n;
					}
					segments.push(String.fromCharCode(code));
				} else
					segments.push(JSON.parser.CHARACTER_UNQUOTES[c] || c);
                start = i;
            }
        }
        if (start < i-1)
            segments.push(s.slice(start, i-1));
        this.index = i;
        return segments.length == 1 ? segments[0] : segments.join('');
    },
	// Also any digit.  The statement that follows this table
	// definition fills in the digits.
    '-': function () {
        var s = this.string;
        var i = this.index;
        var start = i-1;
        var state = 'int';
        var permittedSigns = '-';
        var transitions = {
            'int+.': 'frac',
            'int+e': 'exp',
            'frac+e': 'exp'
        };
        do {
            var c = s.charAt(i++);
			if (!c) break;
            if ('0' <= c && c <= '9') continue;
            if (permittedSigns.indexOf(c) >= 0) {
                permittedSigns = '';
                continue;
            }
            state = transitions[state+'+'+c.toLowerCase()];
            if (state == 'exp') permittedSigns = '+-';
        } while (state);
        this.index = --i;
		s = s.slice(start, i)
		if (s == '-') return this.error("invalid number");
        return Number(s);
    }
};
// copy table['_'] to each of table[i] | i <- '0'..'9':
(function (table) {
    for (var i = 0; i <= 9; i++)
        table[String(i)] = table['-'];
})(JSON.parser.prototype.table);

// return the next non-whitespace character, or undefined
JSON.parser.prototype.next = function () {
    var s = this.string;
    var i = this.index;
    do {
        if (i == s.length) return undefined;
        var c = s.charAt(i++);
    } while (JSON.parser.WHITESPACE.indexOf(c) >= 0);
    this.index = i;
    return c;
};

