/*
 * 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.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;

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

/**
 * JSON Writer.
 * <p>
 * w.objectBegin().objectItem("name").valueString("qianlei").objectEnd() = {name:"qianlei"}.
 *
 * @author qian.lei
 */

public class JSONWriter {
    private static final byte UNKNOWN = 0, ARRAY = 1, OBJECT = 2, OBJECT_VALUE = 3;

    private static class State {
        private byte type;
        private int itemCount = 0;

        State(byte t) {
            type = t;
        }
    }

    private Writer mWriter;

    private State mState = new State(UNKNOWN);

    private Stack<State> mStack = new Stack<State>();

    public JSONWriter(Writer writer) {
        mWriter = writer;
    }

    public JSONWriter(OutputStream is, String charset) throws UnsupportedEncodingException {
        mWriter = new OutputStreamWriter(is, charset);
    }

    /**
     * object begin.
     *
     * @return this.
     * @throws IOException.
     */
    public JSONWriter objectBegin() throws IOException {
        beforeValue();

        mWriter.write(JSON.LBRACE);
        mStack.push(mState);
        mState = new State(OBJECT);
        return this;
    }

    /**
     * object end.
     *
     * @return this.
     * @throws IOException.
     */
    public JSONWriter objectEnd() throws IOException {
        mWriter.write(JSON.RBRACE);
        mState = mStack.pop();
        return this;
    }

    /**
     * object item.
     *
     * @param name name.
     * @return this.
     * @throws IOException.
     */
    public JSONWriter objectItem(String name) throws IOException {
        beforeObjectItem();

        mWriter.write(JSON.QUOTE);
        mWriter.write(escape(name));
        mWriter.write(JSON.QUOTE);
        mWriter.write(JSON.COLON);
        return this;
    }

    /**
     * array begin.
     *
     * @return this.
     * @throws IOException.
     */
    public JSONWriter arrayBegin() throws IOException {
        beforeValue();

        mWriter.write(JSON.LSQUARE);
        mStack.push(mState);
        mState = new State(ARRAY);
        return this;
    }

    /**
     * array end, return array value.
     *
     * @return this.
     * @throws IOException.
     */
    public JSONWriter arrayEnd() throws IOException {
        mWriter.write(JSON.RSQUARE);
        mState = mStack.pop();
        return this;
    }

    /**
     * value.
     *
     * @return this.
     * @throws IOException.
     */
    public JSONWriter valueNull() throws IOException {
        beforeValue();

        mWriter.write(JSON.NULL);
        return this;
    }

    /**
     * value.
     *
     * @param value value.
     * @return this.
     * @throws IOException
     */
    public JSONWriter valueString(String value) throws IOException {
        beforeValue();

        mWriter.write(JSON.QUOTE);
        mWriter.write(escape(value));
        mWriter.write(JSON.QUOTE);
        return this;
    }

    /**
     * value.
     *
     * @param value value.
     * @return this.
     * @throws IOException
     */
    public JSONWriter valueBoolean(boolean value) throws IOException {
        beforeValue();

        mWriter.write(value ? "true" : "false");
        return this;
    }

    /**
     * value.
     *
     * @param value value.
     * @return this.
     * @throws IOException
     */
    public JSONWriter valueInt(int value) throws IOException {
        beforeValue();

        mWriter.write(String.valueOf(value));
        return this;
    }

    /**
     * value.
     *
     * @param value value.
     * @return this.
     * @throws IOException
     */
    public JSONWriter valueLong(long value) throws IOException {
        beforeValue();

        mWriter.write(String.valueOf(value));
        return this;
    }

    /**
     * value.
     *
     * @param value value.
     * @return this.
     * @throws IOException
     */
    public JSONWriter valueFloat(float value) throws IOException {
        beforeValue();

        mWriter.write(String.valueOf(value));
        return this;
    }

    /**
     * value.
     *
     * @param value value.
     * @return this.
     * @throws IOException
     */
    public JSONWriter valueDouble(double value) throws IOException {
        beforeValue();

        mWriter.write(String.valueOf(value));
        return this;
    }

    private void beforeValue() throws IOException {
        switch (mState.type) {
            case ARRAY:
                if (mState.itemCount++ > 0)
                    mWriter.write(JSON.COMMA);
                return;
            case OBJECT:
                throw new IOException("Must call objectItem first.");
            case OBJECT_VALUE:
                mState.type = OBJECT;
                return;
        }
    }

    private void beforeObjectItem() throws IOException {
        switch (mState.type) {
            case OBJECT_VALUE:
                mWriter.write(JSON.NULL);
            case OBJECT:
                mState.type = OBJECT_VALUE;
                if (mState.itemCount++ > 0)
                    mWriter.write(JSON.COMMA);
                return;
            default:
                throw new IOException("Must call objectBegin first.");
        }
    }

    private static final String[] CONTROL_CHAR_MAP = new String[]{
            "\\u0000", "\\u0001", "\\u0002", "\\u0003", "\\u0004", "\\u0005", "\\u0006", "\\u0007",
            "\\b", "\\t", "\\n", "\\u000b", "\\f", "\\r", "\\u000e", "\\u000f",
            "\\u0010", "\\u0011", "\\u0012", "\\u0013", "\\u0014", "\\u0015", "\\u0016", "\\u0017",
            "\\u0018", "\\u0019", "\\u001a", "\\u001b", "\\u001c", "\\u001d", "\\u001e", "\\u001f"
    };

    private static String escape(String str) {
        if (str == null)
            return str;
        int len = str.length();
        if (len == 0)
            return str;

        char c;
        StringBuilder sb = null;
        for (int i = 0; i < len; i++) {
            c = str.charAt(i);
            if (c < ' ') // control char.
            {
                if (sb == null) {
                    sb = new StringBuilder(len << 1);
                    sb.append(str, 0, i);
                }
                sb.append(CONTROL_CHAR_MAP[c]);
            } else {
                switch (c) {
                    case '\\':
                    case '/':
                    case '"':
                        if (sb == null) {
                            sb = new StringBuilder(len << 1);
                            sb.append(str, 0, i);
                        }
                        sb.append('\\').append(c);
                        break;
                    default:
                        if (sb != null)
                            sb.append(c);
                }
            }
        }
        return sb == null ? str : sb.toString();
    }
}