'use strict';

var escapedChars = {
    'b': '\b',
    'f': '\f',
    'n': '\n',
    'r': '\r',
    't': '\t',
    '"': '"',
    '/': '/',
    '\\': '\\'
};

var A_CODE = 'a'.charCodeAt();


function parse(source) {
    var pointers = {};
    var line = 0;
    var column = 0;
    var pos = 0;
    return {
        data: _parse('', true),
        pointers: pointers
    };

    function _parse(ptr, topLevel) {
        whitespace();
        var data;
        map(ptr, 'value');
        var char = getChar();
        switch (char) {
            case 't':
                read('rue');
                data = true;
                break;
            case 'f':
                read('alse');
                data = false;
                break;
            case 'n':
                read('ull');
                data = null;
                break;
            case '"':
                data = parseString();
                break;
            case '[':
                data = parseArray(ptr);
                break;
            case '{':
                data = parseObject(ptr);
                break;
            default:
                backChar();
                if ('-0123456789'.indexOf(char) >= 0)
                    data = parseNumber();
                else
                    unexpectedToken();
        }
        map(ptr, 'valueEnd');
        whitespace();
        if (topLevel && pos < source.length) unexpectedToken();
        return data;
    }

    function whitespace() {
        loop: while (pos < source.length) {
            switch (source[pos]) {
                case ' ':
                    column++;
                    break;
                case '\t':
                    column += 4;
                    break;
                case '\r':
                    column = 0;
                    break;
                case '\n':
                    column = 0;
                    line++;
                    break;
                default:
                    break loop;
            }
            pos++;
        }
    }

    function parseString() {
        var str = '';
        var char;
        while (true) {
            char = getChar();
            if (char == '"') {
                break;
            } else if (char == '\\') {
                char = getChar();
                if (char in escapedChars)
                    str += escapedChars[char];
                else if (char == 'u')
                    str += getCharCode();
                else
                    wasUnexpectedToken();
            } else {
                str += char;
            }
        }
        return str;
    }

    function parseNumber() {
        var numStr = '';
        if (source[pos] == '-') numStr += getChar();

        numStr += source[pos] == '0' ? getChar() : getDigits();

        if (source[pos] == '.')
            numStr += getChar() + getDigits();

        if (source[pos] == 'e' || source[pos] == 'E') {
            numStr += getChar();
            if (source[pos] == '+' || source[pos] == '-') numStr += getChar();
            numStr += getDigits();
        }

        return +numStr;
    }

    function parseArray(ptr) {
        whitespace();
        var arr = [];
        var i = 0;
        if (getChar() == ']') return arr;
        backChar();

        while (true) {
            var itemPtr = ptr + '/' + i;
            arr.push(_parse(itemPtr));
            whitespace();
            var char = getChar();
            if (char == ']') break;
            if (char != ',') wasUnexpectedToken();
            whitespace();
            i++;
        }
        return arr;
    }

    function parseObject(ptr) {
        whitespace();
        var obj = {};
        if (getChar() == '}') return obj;
        backChar();

        while (true) {
            var loc = getLoc();
            if (getChar() != '"') wasUnexpectedToken();
            var key = parseString();
            var propPtr = ptr + '/' + escapeJsonPointer(key);
            mapLoc(propPtr, 'key', loc);
            map(propPtr, 'keyEnd');
            whitespace();
            if (getChar() != ':') wasUnexpectedToken();
            whitespace();
            obj[key] = _parse(propPtr);
            whitespace();
            var char = getChar();
            if (char == '}') break;
            if (char != ',') wasUnexpectedToken();
            whitespace();
        }
        return obj;
    }

    function read(str) {
        for (var i = 0; i < str.length; i++)
            if (getChar() !== str[i]) wasUnexpectedToken();
    }

    function getChar() {
        checkUnexpectedEnd();
        var char = source[pos];
        pos++;
        column++; // new line?
        return char;
    }

    function backChar() {
        pos--;
        column--;
    }

    function getCharCode() {
        var count = 4;
        var code = 0;
        while (count--) {
            code <<= 4;
            var char = getChar().toLowerCase();
            if (char >= 'a' && char <= 'f')
                code += char.charCodeAt() - A_CODE + 10;
            else if (char >= '0' && char <= '9')
                code += +char;
            else
                wasUnexpectedToken();
        }
        return String.fromCharCode(code);
    }

    function getDigits() {
        var digits = '';
        while (source[pos] >= '0' && source[pos] <= '9')
            digits += getChar();

        if (digits.length) return digits;
        checkUnexpectedEnd();
        unexpectedToken();
    }

    function map(ptr, prop) {
        mapLoc(ptr, prop, getLoc());
    }

    function mapLoc(ptr, prop, loc) {
        pointers[ptr] = pointers[ptr] || {};
        pointers[ptr][prop] = loc;
    }

    function getLoc() {
        return {
            line: line,
            column: column,
            pos: pos
        };
    }

    function unexpectedToken() {
        throw new SyntaxError('Unexpected token ' + source[pos] + ' in JSON at position ' + pos);
    }

    function wasUnexpectedToken() {
        backChar();
        unexpectedToken();
    }

    function checkUnexpectedEnd() {
        if (pos >= source.length)
            throw new SyntaxError('Unexpected end of JSON input');
    }
};


function stringify(data, _, whitespace) {
    if (!validType(data)) return;
    var wsLine = 0;
    var wsPos, wsColumn;
    switch (typeof whitespace) {
        case 'number':
            var len = whitespace > 10 ? 10 : whitespace < 0 ? 0 : Math.floor(whitespace);
            whitespace = len && repeat(len, ' ');
            wsPos = len;
            wsColumn = len;
            break;
        case 'string':
            whitespace = whitespace.slice(0, 10);
            wsPos = 0;
            wsColumn = 0;
            for (var j = 0; j < whitespace.length; j++) {
                var char = whitespace[j];
                switch (char) {
                    case ' ':
                        wsColumn++;
                        break;
                    case '\t':
                        wsColumn += 4;
                        break;
                    case '\r':
                        wsColumn = 0;
                        break;
                    case '\n':
                        wsColumn = 0;
                        wsLine++;
                        break;
                    default:
                        throw new Error('whitespace characters not allowed in JSON');
                }
                wsPos++;
            }
            break;
        default:
            whitespace = undefined;
    }

    var json = '';
    var pointers = {};
    var line = 0;
    var column = 0;
    var pos = 0;
    _stringify(data, 0, '');
    return {
        json: json,
        pointers: pointers
    };

    function _stringify(_data, lvl, ptr) {
        map(ptr, 'value');
        switch (typeof _data) {
            case 'number':
            case 'boolean':
                out('' + _data);
                break;
            case 'string':
                out(quoted(_data));
                break;
            case 'object':
                if (_data === null)
                    out('null');
                else if (typeof _data.toJSON == 'function')
                    out(quoted(_data.toJSON()));
                else if (Array.isArray(_data))
                    stringifyArray();
                else
                    stringifyObject();
        }
        map(ptr, 'valueEnd');

        function stringifyArray() {
            if (_data.length) {
                out('[');
                var itemLvl = lvl + 1;
                for (var i = 0; i < _data.length; i++) {
                    if (i) out(',');
                    indent(itemLvl);
                    var item = validType(_data[i]) ? _data[i] : null;
                    var itemPtr = ptr + '/' + i;
                    _stringify(item, itemLvl, itemPtr);
                }
                indent(lvl);
                out(']');
            } else {
                out('[]');
            }
        }

        function stringifyObject() {
            var keys = Object.keys(_data);
            if (keys.length) {
                out('{');
                var propLvl = lvl + 1;
                for (var i = 0; i < keys.length; i++) {
                    var key = keys[i];
                    var value = _data[key];
                    if (validType(value)) {
                        if (i) out(',');
                        var propPtr = ptr + '/' + escapeJsonPointer(key);
                        indent(propLvl);
                        map(propPtr, 'key');
                        out(quoted(key));
                        map(propPtr, 'keyEnd');
                        out(':');
                        if (whitespace) out(' ');
                        _stringify(value, propLvl, propPtr);
                    }
                }
                indent(lvl);
                out('}');
            } else {
                out('{}');
            }
        }
    }

    function out(str) {
        column += str.length;
        pos += str.length;
        json += str;
    }

    function indent(lvl) {
        if (whitespace) {
            json += '\n' + repeat(lvl, whitespace);
            line++;
            column = 0;
            while (lvl--) {
                if (wsLine) {
                    line += wsLine;
                    column = wsColumn;
                } else {
                    column += wsColumn;
                }
                pos += wsPos;
            }
            pos += 1; // \n character
        }
    }

    function map(ptr, prop) {
        pointers[ptr] = pointers[ptr] || {};
        pointers[ptr][prop] = {
            line: line,
            column: column,
            pos: pos
        };
    }

    function repeat(n, str) {
        return Array(n + 1).join(str);
    }
};


var VALID_TYPES = ['number', 'boolean', 'string', 'object'];

function validType(data) {
    return VALID_TYPES.indexOf(typeof data) >= 0;
}


var ESC_QUOTE = /"|\\/g;
var ESC_B = /[\b]/g;
var ESC_F = /\f/g;
var ESC_N = /\n/g;
var ESC_R = /\r/g;
var ESC_T = /\t/g;

function quoted(str) {
    str = str.replace(ESC_QUOTE, '\\$&')
        .replace(ESC_F, '\\f')
        .replace(ESC_B, '\\b')
        .replace(ESC_N, '\\n')
        .replace(ESC_R, '\\r')
        .replace(ESC_T, '\\t');
    return '"' + str + '"';
}


var ESC_0 = /~/g;
var ESC_1 = /\//g;

function escapeJsonPointer(str) {
    return str.replace(ESC_0, '~0')
        .replace(ESC_1, '~1');
}