
var json = json || {};
var text = text || require('./text');

json.TextReader = class {

    static open(data) {
        const decoder = text.Decoder.open(data);
        let state = 'start';
        for (let i = 0; i < 0x1000; i++) {
            const c = decoder.decode();
            if (c === undefined || c === '\0') {
                if (i === 0) {
                    return null;
                }
                break;
            }
            if (c <= ' ') {
                if (c !== ' ' && c !== '\n' && c !== '\r' && c !== '\t') {
                    return null;
                }
                continue;
            }
            switch (state) {
                case 'start':
                    if (c === '#') {
                        state = 'comment';
                        break;
                    }
                    if (c === '[') {
                        state = 'list';
                        break;
                    }
                    if (c === '{') {
                        state = 'object';
                        break;
                    }
                    if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') {
                        state = '';
                        break;
                    }
                    return null;
                case 'list':
                    if (c === '"' || c === '-' || c === '+' || c === '{' || c === '[' || (c >= '0' && c <= '9')) {
                        state = '';
                        break;
                    }
                    return null;
                case 'object':
                    if (c != '"') {
                        return null;
                    }
                    state = '';
                    continue;
            }
        }
        return new json.TextReader(data);
    }

    constructor(data) {
        this._data = data;
        this._escape = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' };
    }

    read() {
        const decoder = text.Decoder.open(this._data);
        const stack = [];
        this._decoder = decoder;
        this._position = 0;
        this._char = decoder.decode();
        this._whitespace();
        let obj = undefined;
        let first = true;
        for (;;) {
            if (Array.isArray(obj)) {
                this._whitespace();
                let c = this._char;
                if (c === ']') {
                    this._next();
                    this._whitespace();
                    if (stack.length > 0) {
                        obj = stack.pop();
                        first = false;
                        continue;
                    }
                    if (this._char !== undefined) {
                        this._unexpected();
                    }
                    return obj;
                }
                if (!first) {
                    if (this._char !== ',') {
                        this._unexpected();
                    }
                    this._next();
                    this._whitespace();
                    c = this._char;
                }
                first = false;
                switch (c) {
                    case '{': {
                        this._next();
                        stack.push(obj);
                        const item = {};
                        obj.push(item);
                        obj = item;
                        first = true;
                        break;
                    }
                    case '[': {
                        this._next();
                        stack.push(obj);
                        const item = [];
                        obj.push(item);
                        obj = item;
                        first = true;
                        break;
                    }
                    default: {
                        obj.push(c === '"' ? this._string() : this._literal());
                        break;
                    }
                }
            }
            else if (obj instanceof Object) {
                this._whitespace();
                let c = this._char;
                if (c === '}') {
                    this._next();
                    this._whitespace();
                    if (stack.length > 0) {
                        obj = stack.pop();
                        first = false;
                        continue;
                    }
                    if (this._char !== undefined) {
                        this._unexpected();
                    }
                    return obj;
                }
                if (!first) {
                    if (this._char !== ',') {
                        this._unexpected();
                    }
                    this._next();
                    this._whitespace();
                    c = this._char;
                }
                first = false;
                if (c === '"') {
                    const key = this._string();
                    switch (key) {
                        case '__proto__':
                        case 'constructor':
                        case 'prototype':
                            throw new json.Error("Invalid key '" + key + "'" + this._location());
                    }
                    this._whitespace();
                    if (this._char !== ':') {
                        this._unexpected();
                    }
                    this._next();
                    this._whitespace();
                    c = this._char;
                    switch (c) {
                        case '{': {
                            this._next();
                            stack.push(obj);
                            const value = {};
                            obj[key] = value;
                            obj = value;
                            first = true;
                            break;
                        }
                        case '[': {
                            this._next();
                            stack.push(obj);
                            const value = [];
                            obj[key] = value;
                            obj = value;
                            first = true;
                            break;
                        }
                        default: {
                            obj[key] = c === '"' ? this._string() : this._literal();
                            break;
                        }
                    }
                    this._whitespace();
                    continue;
                }
                this._unexpected();
            }
            else {
                const c = this._char;
                switch (c) {
                    case '{': {
                        this._next();
                        this._whitespace();
                        obj = {};
                        first = true;
                        break;
                    }
                    case '[': {
                        this._next();
                        this._whitespace();
                        obj = [];
                        first = true;
                        break;
                    }
                    default: {
                        const value = c === '"' ? this._string() : c >= '0' && c <= '9' ? this._number() : this._literal();
                        this._whitespace();
                        if (this._char !== undefined) {
                            this._unexpected();
                        }
                        return value;
                    }
                }
            }
        }
    }

    _next() {
        if (this._char === undefined) {
            this._unexpected();
        }
        this._position = this._decoder.position;
        this._char = this._decoder.decode();
    }

    _whitespace() {
        while (this._char === ' ' || this._char === '\n' || this._char === '\r' || this._char === '\t') {
            this._next();
        }
    }

    _literal() {
        const c = this._char;
        if (c >= '0' && c <= '9') {
            return this._number();
        }
        switch (c) {
            case 't': this._expect('true'); return true;
            case 'f': this._expect('false'); return false;
            case 'n': this._expect('null'); return null;
            case 'N': this._expect('NaN'); return NaN;
            case 'I': this._expect('Infinity'); return Infinity;
            case '-': return this._number();
        }
        this._unexpected();
    }

    _number() {
        let value = '';
        if (this._char === '-') {
            value = '-';
            this._next();
        }
        if (this._char === 'I') {
            this._expect('Infinity');
            return -Infinity;
        }
        const c = this._char;
        if (c < '0' || c > '9') {
            this._unexpected();
        }
        value += c;
        this._next();
        if (c === '0') {
            const n = this._char;
            if (n >= '0' && n <= '9') {
                this._unexpected();
            }
        }
        while (this._char >= '0' && this._char <= '9') {
            value += this._char;
            this._next();
        }
        if (this._char === '.') {
            value += '.';
            this._next();
            const n = this._char;
            if (n < '0' || n > '9') {
                this._unexpected();
            }
            while (this._char >= '0' && this._char <= '9') {
                value += this._char;
                this._next();
            }
        }
        if (this._char === 'e' || this._char === 'E') {
            value += this._char;
            this._next();
            const s = this._char;
            if (s === '-' || s === '+') {
                value += this._char;
                this._next();
            }
            const c = this._char;
            if (c < '0' || c > '9') {
                this._unexpected();
            }
            value += this._char;
            this._next();
            while (this._char >= '0' && this._char <= '9') {
                value += this._char;
                this._next();
            }
        }
        return +value;
    }

    _string() {
        let value = '';
        this._next();
        while (this._char != '"') {
            if (this._char === '\\') {
                this._next();
                if (this._char === 'u') {
                    this._next();
                    let uffff = 0;
                    for (let i = 0; i < 4; i ++) {
                        const hex = parseInt(this._char, 16);
                        if (!isFinite(hex)) {
                            this._unexpected();
                        }
                        this._next();
                        uffff = uffff * 16 + hex;
                    }
                    value += String.fromCharCode(uffff);
                }
                else if (this._escape[this._char]) {
                    value += this._escape[this._char];
                    this._next();
                }
                else {
                    this._unexpected();
                }
            }
            else if (this._char < ' ') {
                this._unexpected();
            }
            else {
                value += this._char;
                this._next();
            }
        }
        this._next();
        return value;
    }

    _expect(value) {
        for (let i = 0; i < value.length; i++) {
            if (value[i] !== this._char) {
                this._unexpected();
            }
            this._next();
        }
    }

    _unexpected() {
        let c = this._char;
        if (c === undefined) {
            throw new json.Error('Unexpected end of JSON input.');
        }
        else if (c === '"') {
            c = 'string';
        }
        else if ((c >= '0' && c <= '9') || c === '-') {
            c = 'number';
        }
        else {
            if (c < ' ' || c > '\x7F') {
                const name = Object.keys(this._escape).filter((key) => this._escape[key] === c);
                c = (name.length === 1) ? '\\' + name : '\\u' + ('000' + c.charCodeAt(0).toString(16)).slice(-4);
            }
            c = "token '" + c + "'";
        }
        throw new json.Error('Unexpected ' + c + this._location());
    }

    _location() {
        let line = 1;
        let column = 1;
        this._decoder.position = 0;
        let c;
        do {
            if (this._decoder.position === this._position) {
                return ' at ' + line.toString() + ':' + column.toString() + '.';
            }
            c = this._decoder.decode();
            if (c === '\n') {
                line++;
                column = 1;
            }
            else {
                column++;
            }
        }
        while (c !== undefined);
        return ' at ' + line.toString() + ':' + column.toString() + '.';
    }
};

json.BinaryReader = class {

    static open(data) {
        const buffer = data instanceof Uint8Array ? data : data.peek();
        return new json.BinaryReader(buffer);
    }

    constructor(buffer) {
        this._buffer = buffer;
    }

    read() {
        const buffer = this._buffer;
        const length = buffer.length;
        const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
        const asciiDecoder = new TextDecoder('ascii');
        const utf8Decoder = new TextDecoder('utf-8');
        let position = 0;
        const skip = (offset) => {
            position += offset;
            if (position > length) {
                throw new json.Error('Expected ' + (position + length) + ' more bytes. The file might be corrupted. Unexpected end of file.', true);
            }
        };
        const header = () => {
            const start = position;
            skip(4);
            const size = view.getInt32(start, 4);
            if (size < 5 || start + size > length || buffer[start + size - 1] != 0x00) {
                throw new json.Error('Invalid file size.', true);
            }
        };
        header();
        const stack = [];
        let obj = {};
        for (;;) {
            skip(1);
            const type = buffer[position - 1];
            if (type == 0x00) {
                if (stack.length === 0) {
                    break;
                }
                obj = stack.pop();
                continue;
            }
            const start = position;
            position = buffer.indexOf(0x00, start) + 1;
            const key = asciiDecoder.decode(buffer.subarray(start, position - 1));
            let value = null;
            switch (type) {
                case 0x01: { // float64
                    const start = position;
                    skip(8);
                    value = view.getFloat64(start, true);
                    break;
                }
                case 0x02: { // string
                    skip(4);
                    const size = view.getInt32(position - 4, true);
                    const start = position;
                    skip(size);
                    value = utf8Decoder.decode(buffer.subarray(start, position - 1));
                    if (buffer[position - 1] != '0x00') {
                        throw new json.Error('String missing terminal 0.', true);
                    }
                    break;
                }
                case 0x03: { // object
                    header();
                    value = {};
                    break;
                }
                case 0x04: { // array
                    header();
                    value = [];
                    break;
                }
                case 0x05: { // bytes
                    const start = position;
                    skip(5);
                    const size = view.getInt32(start, true);
                    const subtype = buffer[start + 4];
                    if (subtype !== 0x00) {
                        throw new json.Error("Unknown binary subtype '" + subtype + "'.", true);
                    }
                    skip(size);
                    value = buffer.subarray(start + 5, position);
                    break;
                }
                case 0x08: { // boolean
                    skip(1);
                    value = buffer[position - 1];
                    if (value > 1) {
                        throw new json.Error("Invalid boolean value '" + value + "'.", true);
                    }
                    value = value === 1 ? true : false;
                    break;
                }
                case 0x0A:
                    value = null;
                    break;
                case 0x10: {
                    const start = position;
                    skip(4);
                    value = view.getInt32(start, true);
                    break;
                }
                case 0x11: { // uint64
                    const start = position;
                    skip(8);
                    value = view.getUint64(start, true).toNumber();
                    break;
                }
                case 0x12: { // int64
                    const start = position;
                    skip(8);
                    value = view.getInt64(start, true).toNumber();
                    break;
                }
                default:
                    throw new json.Error("Unknown value type '" + type + "'.", true);
            }
            if (Array.isArray(obj))  {
                if (obj.length !== parseInt(key, 10)) {
                    throw new json.Error("Invalid array index '" + key + "'.", true);
                }
                obj.push(value);
            }
            else {
                switch (key) {
                    case '__proto__':
                    case 'constructor':
                    case 'prototype':
                        throw new json.Error("Invalid key '" + key + "' at " + position.toString() + "'.", true);
                }
                obj[key] = value;
            }
            if (type === 0x03 || type === 0x04) {
                stack.push(obj);
                obj = value;
            }
        }
        if (position !== length) {
            throw new json.Error("Unexpected data at '" + position.toString() + "'.", true);
        }
        return obj;
    }
};

json.Error = class extends Error {

    constructor(message, binary) {
        super(message);
        this.name = binary ? 'BSON Error' : 'JSON Error';
    }
};

if (typeof module !== 'undefined' && typeof module.exports === 'object') {
    module.exports.TextReader = json.TextReader;
    module.exports.BinaryReader = json.BinaryReader;
}