/* -*- Mode: java; tab-width: 4; indent-tabs-mode: 1; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Rhino code, released
 * May 6, 1999.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1997-1999
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Norris Boyd
 *   Raphael Speyer
 *   Hannes Wallnoefer
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL"), in which
 * case the provisions of the GPL are applicable instead of those above. If
 * you wish to allow use of your version of this file only under the terms of
 * the GPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replacing
 * them with the notice and other provisions required by the GPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the GPL.
 *
 * ***** END LICENSE BLOCK ***** */

package org.mozilla.javascript.json;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptRuntime;

import java.util.ArrayList;
import java.util.List;

/**
 * This class converts a stream of JSON tokens into a JSON value.
 *
 * See ECMA 15.12.
 * @author Raphael Speyer
 * @author Hannes Wallnoefer
 */
public class JsonParser {

    private Context cx;
    private Scriptable scope;

    private int pos;
    private int length;
    private String src;

    public JsonParser(Context cx, Scriptable scope) {
        this.cx = cx;
        this.scope = scope;
    }

    public synchronized Object parseValue(String json) throws ParseException {
        if (json == null) {
            throw new ParseException("Input string may not be null");
        }
        pos = 0;
        length = json.length();
        src = json;
        Object value = readValue();
        consumeWhitespace();
        if (pos < length) {
            throw new ParseException("Expected end of stream at char " + pos);
        }
        return value;
    }

    private Object readValue() throws ParseException {
        consumeWhitespace();
        while (pos < length) {
            char c = src.charAt(pos++);
            switch (c) {
                case '{':
                    return readObject();
                case '[':
                    return readArray();
                case 't':
                    return readTrue();
                case 'f':
                    return readFalse();
                case '"':
                    return readString();
                case 'n':
                    return readNull();
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case '0':
                case '-':
                    return readNumber(c);
                default:
                    throw new ParseException("Unexpected token: " + c);
            }
        }
        throw new ParseException("Empty JSON string");
    }

    private Object readObject() throws ParseException {
        Scriptable object = cx.newObject(scope);
        String id;
        Object value;
        boolean needsComma = false;
        consumeWhitespace();
        while (pos < length) {
            char c = src.charAt(pos++);
            switch(c) {
                case '}':
                    return object;
                case ',':
                    if (!needsComma) {
                        throw new ParseException("Unexpected comma in object literal");
                    }
                    needsComma = false;
                    break;
                case '"':
                    if (needsComma) {
                        throw new ParseException("Missing comma in object literal");
                    }
                    id = readString();
                    consume(':');
                    value = readValue();

                    long index = ScriptRuntime.indexFromString(id);
                    if (index < 0) {
                      object.put(id, object, value);
                    } else {
                      object.put((int)index, object, value);
                    }

                    needsComma = true;
                    break;
                default:
                    throw new ParseException("Unexpected token in object literal");
            }
            consumeWhitespace();
        }
        throw new ParseException("Unterminated object literal");
    }

    private Object readArray() throws ParseException {
        List<Object> list = new ArrayList<Object>();
        boolean needsComma = false;
        consumeWhitespace();
        while (pos < length) {
            char c = src.charAt(pos);
            switch(c) {
                case ']':
                    pos += 1;
                    return cx.newArray(scope, list.toArray());
                case ',':
                    if (!needsComma) {
                        throw new ParseException("Unexpected comma in array literal");
                    }
                    needsComma = false;
                    pos += 1;
                    break;
                default:
                    if (needsComma) {
                        throw new ParseException("Missing comma in array literal");
                    }
                    list.add(readValue());
                    needsComma = true;
            }
            consumeWhitespace();
        }
        throw new ParseException("Unterminated array literal");
    }

    private String readString() throws ParseException {
        StringBuilder b = new StringBuilder();
        while (pos < length) {
            char c = src.charAt(pos++);
            if (c <= '\u001F') {
                throw new ParseException("String contains control character");
            }
            switch(c) {
                case '\\':
                    if (pos >= length) {
                        throw new ParseException("Unterminated string");
                    }
                    c = src.charAt(pos++);
                    switch (c) {
                        case '"':
                            b.append('"');
                            break;
                        case '\\':
                            b.append('\\');
                            break;
                        case '/':
                            b.append('/');
                            break;
                        case 'b':
                            b.append('\b');
                            break;
                        case 'f':
                            b.append('\f');
                            break;
                        case 'n':
                            b.append('\n');
                            break;
                        case 'r':
                            b.append('\r');
                            break;
                        case 't':
                            b.append('\t');
                            break;
                        case 'u':
                            if (length - pos < 5) {
                                throw new ParseException("Invalid character code: \\u" + src.substring(pos));
                            }
                            try {
                                b.append((char) Integer.parseInt(src.substring(pos, pos + 4), 16));
                                pos += 4;
                            } catch (NumberFormatException nfx) {
                                throw new ParseException("Invalid character code: " + src.substring(pos, pos + 4));
                            }
                            break;
                        default:
                            throw new ParseException("Unexcpected character in string: '\\" + c + "'");
                    }
                    break;
                case '"':
                    return b.toString();
                default:
                    b.append(c);
                    break;
            }
        }
        throw new ParseException("Unterminated string literal");
    }

    private Number readNumber(char first) throws ParseException {
        StringBuilder b = new StringBuilder();
        b.append(first);
        while (pos < length) {
            char c = src.charAt(pos);
            if (!Character.isDigit(c)
                    && c != '-'
                    && c != '+'
                    && c != '.'
                    && c != 'e'
                    && c != 'E') {
                break;
            }
            pos += 1;
            b.append(c);
        }
        String num = b.toString();
        int numLength = num.length();
        try {
            // check for leading zeroes
            for (int i = 0; i < numLength; i++) {
                char c = num.charAt(i);
                if (Character.isDigit(c)) {
                    if (c == '0'
                            && numLength > i + 1
                            && Character.isDigit(num.charAt(i + 1))) {
                        throw new ParseException("Unsupported number format: " + num);
                    }
                    break;
                }
            }
            final double dval = Double.parseDouble(num);
            final int ival = (int)dval;
            if (ival == dval) {
                return Integer.valueOf(ival);
            } else {
                return Double.valueOf(dval);
            }
        } catch (NumberFormatException nfe) {
            throw new ParseException("Unsupported number format: " + num);
        }
    }

    private Boolean readTrue() throws ParseException {
        if (length - pos < 3
                || src.charAt(pos) != 'r'
                || src.charAt(pos + 1) != 'u'
                || src.charAt(pos + 2) != 'e') {
            throw new ParseException("Unexpected token: t");
        }
        pos += 3;
        return Boolean.TRUE;
    }

    private Boolean readFalse() throws ParseException {
        if (length - pos < 4
                || src.charAt(pos) != 'a'
                || src.charAt(pos + 1) != 'l'
                || src.charAt(pos + 2) != 's'
                || src.charAt(pos + 3) != 'e') {
            throw new ParseException("Unexpected token: f");
        }
        pos += 4;
        return Boolean.FALSE;
    }

    private Object readNull() throws ParseException {
        if (length - pos < 3
                || src.charAt(pos) != 'u'
                || src.charAt(pos + 1) != 'l'
                || src.charAt(pos + 2) != 'l') {
            throw new ParseException("Unexpected token: n");
        }
        pos += 3;
        return null;
    }

    private void consumeWhitespace() {
        while (pos < length) {
            char c = src.charAt(pos);
            switch (c) {
                case ' ':
                case '\t':
                case '\r':
                case '\n':
                    pos += 1;
                    break;
                default:
                    return;
            }
        }
    }

    private void consume(char token) throws ParseException {
        consumeWhitespace();
        if (pos >= length) {
            throw new ParseException("Expected " + token + " but reached end of stream");
        }
        char c = src.charAt(pos++);
        if (c == token) {
            return;
        } else {
            throw new ParseException("Expected " + token + " found " + c);
        }
    }

    public static class ParseException extends Exception {
        ParseException(String message) {
            super(message);
        }

        ParseException(Exception cause) {
            super(cause);
        }
    }

}
