/*
 * Copyright 1999-2011 Alibaba Group.
 *  
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *  
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.dubbo.common.json;

import java.io.IOException;

import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;

import com.alibaba.dubbo.common.bytecode.Wrapper;
import com.alibaba.dubbo.common.utils.Stack;

/**
 * JSON.
 *
 * @author qian.lei
 */

public class JSON {
    public static final char LBRACE = '{', RBRACE = '}';

    public static final char LSQUARE = '[', RSQUARE = ']';

    public static final char COMMA = ',', COLON = ':', QUOTE = '"';

    public static final String NULL = "null";

    static final JSONConverter DEFAULT_CONVERTER = new GenericJSONConverter();

    // state.
    public static final byte END = 0, START = 1, OBJECT_ITEM = 2, OBJECT_VALUE = 3, ARRAY_ITEM = 4;

    private static class Entry {
        byte state;
        Object value;

        Entry(byte s, Object v) {
            state = s;
            value = v;
        }
    }

    private JSON() {
    }

    @Override
    public int hashCode() {
        return super.hashCode();
    }

    /**
     * json string.
     *
     * @param obj object.
     * @return json string.
     * @throws IOException.
     */
    public static String json(Object obj) throws IOException {
        if (obj == null) return NULL;
        StringWriter sw = new StringWriter();
        try {
            json(obj, sw);
            return sw.getBuffer().toString();
        } finally {
            sw.close();
        }
    }

    /**
     * write json.
     *
     * @param obj    object.
     * @param writer writer.
     * @throws IOException.
     */
    public static void json(Object obj, Writer writer) throws IOException {
        json(obj, writer, false);
    }

    public static void json(Object obj, Writer writer, boolean writeClass) throws IOException {
        if (obj == null)
            writer.write(NULL);
        else
            json(obj, new JSONWriter(writer), writeClass);
    }

    /**
     * json string.
     *
     * @param obj        object.
     * @param properties property name array.
     * @return json string.
     * @throws IOException.
     */
    public static String json(Object obj, String[] properties) throws IOException {
        if (obj == null) return NULL;
        StringWriter sw = new StringWriter();
        try {
            json(obj, properties, sw);
            return sw.getBuffer().toString();
        } finally {
            sw.close();
        }
    }

    public static void json(Object obj, final String[] properties, Writer writer) throws IOException {
        json(obj, properties, writer, false);
    }

    /**
     * write json.
     *
     * @param obj        object.
     * @param properties property name array.
     * @param writer     writer.
     * @throws IOException.
     */
    public static void json(Object obj, final String[] properties, Writer writer, boolean writeClass) throws IOException {
        if (obj == null)
            writer.write(NULL);
        else
            json(obj, properties, new JSONWriter(writer), writeClass);
    }

    private static void json(Object obj, JSONWriter jb, boolean writeClass) throws IOException {
        if (obj == null)
            jb.valueNull();
        else
            DEFAULT_CONVERTER.writeValue(obj, jb, writeClass);
    }

    private static void json(Object obj, String[] properties, JSONWriter jb, boolean writeClass) throws IOException {
        if (obj == null) {
            jb.valueNull();
        } else {
            Wrapper wrapper = Wrapper.getWrapper(obj.getClass());

            Object value;
            jb.objectBegin();
            for (String prop : properties) {
                jb.objectItem(prop);
                value = wrapper.getPropertyValue(obj, prop);
                if (value == null)
                    jb.valueNull();
                else
                    DEFAULT_CONVERTER.writeValue(value, jb, writeClass);
            }
            jb.objectEnd();
        }
    }

    /**
     * parse json.
     *
     * @param json json source.
     * @return JSONObject or JSONArray or Boolean or Long or Double or String or null
     * @throws ParseException
     */
    public static Object parse(String json) throws ParseException {
        StringReader reader = new StringReader(json);
        try {
            return parse(reader);
        } catch (IOException e) {
            throw new ParseException(e.getMessage());
        } finally {
            reader.close();
        }
    }

    /**
     * parse json.
     *
     * @param reader reader.
     * @return JSONObject or JSONArray or Boolean or Long or Double or String or null
     * @throws IOException
     * @throws ParseException
     */
    public static Object parse(Reader reader) throws IOException, ParseException {
        return parse(reader, JSONToken.ANY);
    }

    /**
     * parse json.
     *
     * @param json json string.
     * @param type target type.
     * @return result.
     * @throws ParseException
     */
    public static <T> T parse(String json, Class<T> type) throws ParseException {
        StringReader reader = new StringReader(json);
        try {
            return parse(reader, type);
        } catch (IOException e) {
            throw new ParseException(e.getMessage());
        } finally {
            reader.close();
        }
    }

    /**
     * parse json
     *
     * @param reader json source.
     * @param type   target type.
     * @return result.
     * @throws IOException
     * @throws ParseException
     */
    @SuppressWarnings("unchecked")
    public static <T> T parse(Reader reader, Class<T> type) throws IOException, ParseException {
        return (T) parse(reader, new J2oVisitor(type, DEFAULT_CONVERTER), JSONToken.ANY);
    }

    /**
     * parse json.
     *
     * @param json  json string.
     * @param types target type array.
     * @return result.
     * @throws ParseException
     */
    public static Object[] parse(String json, Class<?>[] types) throws ParseException {
        StringReader reader = new StringReader(json);
        try {
            return (Object[]) parse(reader, types);
        } catch (IOException e) {
            throw new ParseException(e.getMessage());
        } finally {
            reader.close();
        }
    }

    /**
     * parse json.
     *
     * @param reader json source.
     * @param types  target type array.
     * @return result.
     * @throws IOException
     * @throws ParseException
     */
    public static Object[] parse(Reader reader, Class<?>[] types) throws IOException, ParseException {
        return (Object[]) parse(reader, new J2oVisitor(types, DEFAULT_CONVERTER), JSONToken.LSQUARE);
    }

    /**
     * parse json.
     *
     * @param json    json string.
     * @param handler handler.
     * @return result.
     * @throws ParseException
     */
    public static Object parse(String json, JSONVisitor handler) throws ParseException {
        StringReader reader = new StringReader(json);
        try {
            return parse(reader, handler);
        } catch (IOException e) {
            throw new ParseException(e.getMessage());
        } finally {
            reader.close();
        }
    }

    /**
     * parse json.
     *
     * @param reader  json source.
     * @param handler handler.
     * @return resule.
     * @throws IOException
     * @throws ParseException
     */
    public static Object parse(Reader reader, JSONVisitor handler) throws IOException, ParseException {
        return parse(reader, handler, JSONToken.ANY);
    }

    private static Object parse(Reader reader, int expect) throws IOException, ParseException {
        JSONReader jr = new JSONReader(reader);
        JSONToken token = jr.nextToken(expect);

        byte state = START;
        Object value = null, tmp;
        Stack<Entry> stack = new Stack<Entry>();

        do {
            switch (state) {
                case END:
                    throw new ParseException("JSON source format error.");
                case START: {
                    switch (token.type) {
                        case JSONToken.NULL:
                        case JSONToken.BOOL:
                        case JSONToken.INT:
                        case JSONToken.FLOAT:
                        case JSONToken.STRING: {
                            state = END;
                            value = token.value;
                            break;
                        }
                        case JSONToken.LSQUARE: {
                            state = ARRAY_ITEM;
                            value = new JSONArray();
                            break;
                        }
                        case JSONToken.LBRACE: {
                            state = OBJECT_ITEM;
                            value = new JSONObject();
                            break;
                        }
                        default:
                            throw new ParseException("Unexcepted token expect [ VALUE or '[' or '{' ] get '" + JSONToken.token2string(token.type) + "'");
                    }
                    break;
                }
                case ARRAY_ITEM: {
                    switch (token.type) {
                        case JSONToken.COMMA:
                            break;
                        case JSONToken.NULL:
                        case JSONToken.BOOL:
                        case JSONToken.INT:
                        case JSONToken.FLOAT:
                        case JSONToken.STRING: {
                            ((JSONArray) value).add(token.value);
                            break;
                        }
                        case JSONToken.RSQUARE: // end of array.
                        {
                            if (stack.isEmpty()) {
                                state = END;
                            } else {
                                Entry entry = stack.pop();
                                state = entry.state;
                                value = entry.value;
                            }
                            break;
                        }
                        case JSONToken.LSQUARE: // array begin.
                        {
                            tmp = new JSONArray();
                            ((JSONArray) value).add(tmp);
                            stack.push(new Entry(state, value));

                            state = ARRAY_ITEM;
                            value = tmp;
                            break;
                        }
                        case JSONToken.LBRACE: // object begin.
                        {
                            tmp = new JSONObject();
                            ((JSONArray) value).add(tmp);
                            stack.push(new Entry(state, value));

                            state = OBJECT_ITEM;
                            value = tmp;
                            break;
                        }
                        default:
                            throw new ParseException("Unexcepted token expect [ VALUE or ',' or ']' or '[' or '{' ] get '" + JSONToken.token2string(token.type) + "'");
                    }
                    break;
                }
                case OBJECT_ITEM: {
                    switch (token.type) {
                        case JSONToken.COMMA:
                            break;
                        case JSONToken.IDENT: // item name.
                        {
                            stack.push(new Entry(OBJECT_ITEM, (String) token.value));
                            state = OBJECT_VALUE;
                            break;
                        }
                        case JSONToken.NULL: {
                            stack.push(new Entry(OBJECT_ITEM, "null"));
                            state = OBJECT_VALUE;
                            break;
                        }
                        case JSONToken.BOOL:
                        case JSONToken.INT:
                        case JSONToken.FLOAT:
                        case JSONToken.STRING: {
                            stack.push(new Entry(OBJECT_ITEM, token.value.toString()));
                            state = OBJECT_VALUE;
                            break;
                        }
                        case JSONToken.RBRACE: // end of object.
                        {
                            if (stack.isEmpty()) {
                                state = END;
                            } else {
                                Entry entry = stack.pop();
                                state = entry.state;
                                value = entry.value;
                            }
                            break;
                        }
                        default:
                            throw new ParseException("Unexcepted token expect [ IDENT or VALUE or ',' or '}' ] get '" + JSONToken.token2string(token.type) + "'");
                    }
                    break;
                }
                case OBJECT_VALUE: {
                    switch (token.type) {
                        case JSONToken.COLON:
                            break;
                        case JSONToken.NULL:
                        case JSONToken.BOOL:
                        case JSONToken.INT:
                        case JSONToken.FLOAT:
                        case JSONToken.STRING: {
                            ((JSONObject) value).put((String) stack.pop().value, token.value);
                            state = OBJECT_ITEM;
                            break;
                        }
                        case JSONToken.LSQUARE: // array begin.
                        {
                            tmp = new JSONArray();
                            ((JSONObject) value).put((String) stack.pop().value, tmp);
                            stack.push(new Entry(OBJECT_ITEM, value));

                            state = ARRAY_ITEM;
                            value = tmp;
                            break;
                        }
                        case JSONToken.LBRACE: // object begin.
                        {
                            tmp = new JSONObject();
                            ((JSONObject) value).put((String) stack.pop().value, tmp);
                            stack.push(new Entry(OBJECT_ITEM, value));

                            state = OBJECT_ITEM;
                            value = tmp;
                            break;
                        }
                        default:
                            throw new ParseException("Unexcepted token expect [ VALUE or '[' or '{' ] get '" + JSONToken.token2string(token.type) + "'");
                    }
                    break;
                }
                default:
                    throw new ParseException("Unexcepted state.");
            }
        }
        while ((token = jr.nextToken()) != null);
        stack.clear();
        return value;
    }

    private static Object parse(Reader reader, JSONVisitor handler, int expect) throws IOException, ParseException {
        JSONReader jr = new JSONReader(reader);
        JSONToken token = jr.nextToken(expect);

        Object value = null;
        int state = START, index = 0;
        Stack<int[]> states = new Stack<int[]>();
        boolean pv = false;

        handler.begin();
        do {
            switch (state) {
                case END:
                    throw new ParseException("JSON source format error.");
                case START: {
                    switch (token.type) {
                        case JSONToken.NULL: {
                            value = token.value;
                            state = END;
                            pv = true;
                            break;
                        }
                        case JSONToken.BOOL: {
                            value = token.value;
                            state = END;
                            pv = true;
                            break;
                        }
                        case JSONToken.INT: {
                            value = token.value;
                            state = END;
                            pv = true;
                            break;
                        }
                        case JSONToken.FLOAT: {
                            value = token.value;
                            state = END;
                            pv = true;
                            break;
                        }
                        case JSONToken.STRING: {
                            value = token.value;
                            state = END;
                            pv = true;
                            break;
                        }
                        case JSONToken.LSQUARE: {
                            handler.arrayBegin();
                            state = ARRAY_ITEM;
                            break;
                        }
                        case JSONToken.LBRACE: {
                            handler.objectBegin();
                            state = OBJECT_ITEM;
                            break;
                        }
                        default:
                            throw new ParseException("Unexcepted token expect [ VALUE or '[' or '{' ] get '" + JSONToken.token2string(token.type) + "'");
                    }
                    break;
                }
                case ARRAY_ITEM: {
                    switch (token.type) {
                        case JSONToken.COMMA:
                            break;
                        case JSONToken.NULL: {
                            handler.arrayItem(index++);
                            handler.arrayItemValue(index, token.value, true);
                            break;
                        }
                        case JSONToken.BOOL: {
                            handler.arrayItem(index++);
                            handler.arrayItemValue(index, token.value, true);
                            break;
                        }
                        case JSONToken.INT: {
                            handler.arrayItem(index++);
                            handler.arrayItemValue(index, token.value, true);
                            break;
                        }
                        case JSONToken.FLOAT: {
                            handler.arrayItem(index++);
                            handler.arrayItemValue(index, token.value, true);
                            break;
                        }
                        case JSONToken.STRING: {
                            handler.arrayItem(index++);
                            handler.arrayItemValue(index, token.value, true);
                            break;
                        }
                        case JSONToken.LSQUARE: {
                            handler.arrayItem(index++);
                            states.push(new int[]{state, index});

                            index = 0;
                            state = ARRAY_ITEM;
                            handler.arrayBegin();
                            break;
                        }
                        case JSONToken.LBRACE: {
                            handler.arrayItem(index++);
                            states.push(new int[]{state, index});

                            index = 0;
                            state = OBJECT_ITEM;
                            handler.objectBegin();
                            break;
                        }
                        case JSONToken.RSQUARE: {
                            if (states.isEmpty()) {
                                value = handler.arrayEnd(index);
                                state = END;
                            } else {
                                value = handler.arrayEnd(index);
                                int[] tmp = states.pop();
                                state = tmp[0];
                                index = tmp[1];

                                switch (state) {
                                    case ARRAY_ITEM: {
                                        handler.arrayItemValue(index, value, false);
                                        break;
                                    }
                                    case OBJECT_ITEM: {
                                        handler.objectItemValue(value, false);
                                        break;
                                    }
                                }
                            }
                            break;
                        }
                        default:
                            throw new ParseException("Unexcepted token expect [ VALUE or ',' or ']' or '[' or '{' ] get '" + JSONToken.token2string(token.type) + "'");
                    }
                    break;
                }
                case OBJECT_ITEM: {
                    switch (token.type) {
                        case JSONToken.COMMA:
                            break;
                        case JSONToken.IDENT: {
                            handler.objectItem((String) token.value);
                            state = OBJECT_VALUE;
                            break;
                        }
                        case JSONToken.NULL: {
                            handler.objectItem("null");
                            state = OBJECT_VALUE;
                            break;
                        }
                        case JSONToken.BOOL:
                        case JSONToken.INT:
                        case JSONToken.FLOAT:
                        case JSONToken.STRING: {
                            handler.objectItem(token.value.toString());
                            state = OBJECT_VALUE;
                            break;
                        }
                        case JSONToken.RBRACE: {
                            if (states.isEmpty()) {
                                value = handler.objectEnd(index);
                                state = END;
                            } else {
                                value = handler.objectEnd(index);
                                int[] tmp = states.pop();
                                state = tmp[0];
                                index = tmp[1];

                                switch (state) {
                                    case ARRAY_ITEM: {
                                        handler.arrayItemValue(index, value, false);
                                        break;
                                    }
                                    case OBJECT_ITEM: {
                                        handler.objectItemValue(value, false);
                                        break;
                                    }
                                }
                            }
                            break;
                        }
                        default:
                            throw new ParseException("Unexcepted token expect [ IDENT or VALUE or ',' or '}' ] get '" + JSONToken.token2string(token.type) + "'");
                    }
                    break;
                }
                case OBJECT_VALUE: {
                    switch (token.type) {
                        case JSONToken.COLON:
                            break;
                        case JSONToken.NULL: {
                            handler.objectItemValue(token.value, true);
                            state = OBJECT_ITEM;
                            break;
                        }
                        case JSONToken.BOOL: {
                            handler.objectItemValue(token.value, true);
                            state = OBJECT_ITEM;
                            break;
                        }
                        case JSONToken.INT: {
                            handler.objectItemValue(token.value, true);
                            state = OBJECT_ITEM;
                            break;
                        }
                        case JSONToken.FLOAT: {
                            handler.objectItemValue(token.value, true);
                            state = OBJECT_ITEM;
                            break;
                        }
                        case JSONToken.STRING: {
                            handler.objectItemValue(token.value, true);
                            state = OBJECT_ITEM;
                            break;
                        }
                        case JSONToken.LSQUARE: {
                            states.push(new int[]{OBJECT_ITEM, index});

                            index = 0;
                            state = ARRAY_ITEM;
                            handler.arrayBegin();
                            break;
                        }
                        case JSONToken.LBRACE: {
                            states.push(new int[]{OBJECT_ITEM, index});

                            index = 0;
                            state = OBJECT_ITEM;
                            handler.objectBegin();
                            break;
                        }
                        default:
                            throw new ParseException("Unexcepted token expect [ VALUE or '[' or '{' ] get '" + JSONToken.token2string(token.type) + "'");
                    }
                    break;
                }
                default:
                    throw new ParseException("Unexcepted state.");
            }
        }
        while ((token = jr.nextToken()) != null);
        states.clear();
        return handler.end(value, pv);
    }
}