package cn.citycraft.GsonAgent.normal.stream;

import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Field;

import com.google.gson.stream.JsonWriter;

import cn.citycraft.GsonAgent.api.utils.Utils;

public class JsonWriterHandle extends JsonWriter implements cn.citycraft.GsonAgent.api.stream.JsonWriter {
    /*
     * From RFC 4627, "All Unicode characters may be placed within the
     * quotation marks except for the characters that must be escaped:
     * quotation mark, reverse solidus, and the control characters
     * (U+0000 through U+001F)."
     *
     * We also escape '\u2028' and '\u2029', which JavaScript interprets as
     * newline characters. This prevents eval() from failing with a syntax
     * error. http://code.google.com/p/google-gson/issues/detail?id=341
     */
    private static Field stackField, stackSizeField, indentField, separatorField;

    static {
        final Class<JsonWriter> clzz = JsonWriter.class;
        try {
            stackField = Utils.getDeclareField(clzz, "stack");
            stackSizeField = Utils.getDeclareField(clzz, "stackSize");
            indentField = Utils.getDeclareField(clzz, "indent");
            separatorField = Utils.getDeclareField(clzz, "separator");
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * The output data, containing at most one top-level array or object.
     */
    private final Writer out;
    private String deferredName;

    private boolean withoutQuotes = false;

    /**
     * Creates a new instance that writes a JSON-encoded stream to {@code out}.
     * For best performance, ensure {@link Writer} is buffered; wrapping in
     * {@link java.io.BufferedWriter BufferedWriter} if necessary.
     *
     * @param out
     */
    public JsonWriterHandle(final Writer out) {
        super(out);
        this.out = out;
    }

    /**
     * Begins encoding a new array. Each call to this method must be paired with
     * a call to {@link #endArray}.
     *
     * @return this writer.
     * @throws java.io.IOException
     */
    @Override
    public JsonWriterHandle beginArray() throws IOException {
        writeDeferredName();
        return open(EMPTY_ARRAY, "[");
    }

    /**
     * Begins encoding a new object. Each call to this method must be paired
     * with a call to {@link #endObject}.
     *
     * @return this writer.
     * @throws java.io.IOException
     */
    @Override
    public JsonWriterHandle beginObject() throws IOException {
        writeDeferredName();
        return open(EMPTY_OBJECT, "{");
    }

    /**
     * Flushes and closes this writer and the underlying {@link Writer}.
     *
     * @throws IOException
     *             if the JSON document is incomplete.
     */
    @Override
    public void close() throws IOException {
        out.close();
        final int size = this.getStackSize();
        if (size > 1 || size == 1 && this.getStack()[size - 1] != NONEMPTY_DOCUMENT) {
            throw new IOException("Incomplete document");
        }
        this.setStackSize(0);
    }

    /**
     * Ends encoding the current array.
     *
     * @return this writer.
     * @throws java.io.IOException
     */
    @Override
    public JsonWriterHandle endArray() throws IOException {
        return close(EMPTY_ARRAY, NONEMPTY_ARRAY, "]");
    }

    /**
     * Ends encoding the current object.
     *
     * @return this writer.
     * @throws java.io.IOException
     */
    @Override
    public JsonWriterHandle endObject() throws IOException {
        return close(EMPTY_OBJECT, NONEMPTY_OBJECT, "}");
    }

    /**
     * Ensures all buffered data is written to the underlying {@link Writer} and
     * flushes that writer.
     *
     * @throws java.io.IOException
     */
    @Override
    public void flush() throws IOException {
        if (this.getStackSize() == 0) {
            throw new IllegalStateException("JsonWriterHandle is closed.");
        }
        out.flush();
    }

    public JsonWriterHandle getHandle() {
        return this;
    }

    public String getIndent() {
        return Utils.invokeStringField(this, indentField);
    }

    public String getSeparator() {
        return Utils.invokeStringField(this, separatorField);
    }

    public int[] getStack() {
        return Utils.invokeIntArrayField(this, stackField);
    }

    public int getStackSize() {
        return Utils.invokeIntField(this, stackSizeField);
    }

    /**
     * Encodes the property name.
     *
     * @param name
     *            the name of the forthcoming value. May not be null.
     * @return this writer.
     * @throws java.io.IOException
     */
    @Override
    public JsonWriterHandle name(final String name) throws IOException {
        if (name == null) {
            throw new NullPointerException("name == null");
        }
        if (deferredName != null) {
            throw new IllegalStateException();
        }
        if (this.getStackSize() == 0) {
            throw new IllegalStateException("JsonWriterHandle is closed.");
        }
        deferredName = name;
        withoutQuotes = false;
        return this;
    }

    @Override
    public JsonWriterHandle nameWithoutQuotes(final String name) throws IOException {
        this.name(name);
        withoutQuotes = true;
        return this;
    }

    /**
     * Encodes {@code null}.
     *
     * @return this writer.
     * @throws java.io.IOException
     */
    @Override
    public JsonWriterHandle nullValue() throws IOException {
        if (deferredName != null) {
            if (this.getSerializeNulls()) {
                writeDeferredName();
            } else {
                deferredName = null;
                return this; // skip the name and the value
            }
        }
        beforeValue(false);
        out.write("null");
        return this;
    }

    public void setSeparator(final String separator) {
        Utils.modifyField(this, separatorField, separator);
    }

    public void setStack(final int index, final int value) {
        final int[] newStack = this.getStack();
        newStack[index] = value;
        this.setStack(newStack);
    }

    public void setStack(final int[] stack) {
        Utils.modifyField(this, stackField, stack);
    }

    public void setStackSize(final int stackSize) {
        Utils.modifyField(this, stackSizeField, stackSize);
    }

    public void stringExtend(final String value) throws IOException {
        final String[] replacements = this.isHtmlSafe() ? Utils.HTML_SAFE_REPLACEMENT_CHARS : Utils.REPLACEMENT_CHARS;
        if (!this.withoutQuotes) {
            this.out.write("\"");
        }
        int last = 0;
        final int length = value.length();
        for (int i = 0; i < length; i++) {
            final char c = value.charAt(i);
            String replacement;
            if (c < 128) {
                replacement = replacements[c];
                if (replacement == null) {
                    continue;
                }
            } else if (c == '\u2028') {
                replacement = "\\u2028";
            } else if (c == '\u2029') {
                replacement = "\\u2029";
            } else {
                continue;
            }
            if (last < i) {
                out.write(value, last, i - last);
            }
            out.write(replacement);
            last = i + 1;
        }
        if (last < length) {
            out.write(value, last, length - last);
        }
        if (!this.withoutQuotes) {
            this.out.write("\"");
        }
        withoutQuotes = false;
    }

    /**
     * Encodes {@code value}.
     *
     * @param value
     * @return this writer.
     * @throws java.io.IOException
     */
    @Override
    public JsonWriterHandle value(final boolean value) throws IOException {
        writeDeferredName();
        beforeValue(false);
        out.write(value ? "true" : "false");
        return this;
    }

    /**
     * Encodes {@code value}.
     *
     * @param value
     *            a finite value. May not be {@link Double#isNaN() NaNs} or
     *            {@link Double#isInfinite() infinities}.
     * @return this writer.
     * @throws java.io.IOException
     */
    @Override
    public JsonWriterHandle value(final double value) throws IOException {
        if (Double.isNaN(value) || Double.isInfinite(value)) {
            throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
        }
        writeDeferredName();
        beforeValue(false);
        out.append(Double.toString(value));
        return this;
    }

    /**
     * Encodes {@code value}.
     *
     * @param value
     * @return this writer.
     * @throws java.io.IOException
     */
    @Override
    public JsonWriterHandle value(final long value) throws IOException {
        writeDeferredName();
        beforeValue(false);
        out.write(Long.toString(value));
        return this;
    }

    /**
     * Encodes {@code value}.
     *
     * @param value
     *            a finite value. May not be {@link Double#isNaN() NaNs} or
     *            {@link Double#isInfinite() infinities}.
     * @return this writer.
     * @throws java.io.IOException
     */
    @Override
    public JsonWriterHandle value(final Number value) throws IOException {
        if (value == null) {
            return nullValue();
        }
        writeDeferredName();
        final String string = value.toString();
        if (!this.isLenient() && (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
            throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
        }
        beforeValue(false);
        out.append(string);
        return this;
    }

    /**
     * Encodes {@code value}.
     *
     * @param value
     *            the literal string value, or null to encode a null literal.
     * @return this writer.
     * @throws java.io.IOException
     */
    @Override
    public JsonWriterHandle value(final String value) throws IOException {
        if (value == null) {
            return nullValue();
        }
        writeDeferredName();
        beforeValue(false);
        stringExtend(value);
        return this;
    }

    /**
     * Inserts any necessary separators and whitespace before a name. Also
     * adjusts the stack to expect the name's value.
     */
    private void beforeName() throws IOException {
        final int context = peek();
        if (context == NONEMPTY_OBJECT) { // first in object
            out.write(',');
        } else if (context != EMPTY_OBJECT) { // not in an object!
            throw new IllegalStateException("Nesting problem.");
        }
        newline();
        replaceTop(DANGLING_NAME);
    }

    /**
     * Inserts any necessary separators and whitespace before a literal value,
     * inline array, or inline object. Also adjusts the stack to expect either a
     * closing bracket or another element.
     *
     * @param root
     *            true if the value is a new array or object, the two values
     *            permitted as top-level elements.
     */
    @SuppressWarnings("fallthrough")
    private void beforeValue(final boolean root) throws IOException {
        switch (peek()) {
        case NONEMPTY_DOCUMENT:
            if (!this.isLenient()) {
                throw new IllegalStateException("JSON must have only one top-level value.");
            }
            // fall-through
        case EMPTY_DOCUMENT: // first in document
            if (!this.isLenient() && !root) {
                throw new IllegalStateException("JSON must start with an array or an object.");
            }
            replaceTop(NONEMPTY_DOCUMENT);
            break;
        case EMPTY_ARRAY: // first in array
            replaceTop(NONEMPTY_ARRAY);
            newline();
            break;
        case NONEMPTY_ARRAY: // another in array
            out.append(',');
            newline();
            break;
        case DANGLING_NAME: // value for name
            out.append(this.getSeparator());
            replaceTop(NONEMPTY_OBJECT);
            break;
        default:
            throw new IllegalStateException("Nesting problem.");
        }
    }

    /**
     * Closes the current scope by appending any necessary whitespace and the
     * given bracket.
     */
    private JsonWriterHandle close(final int empty, final int nonempty, final String closeBracket) throws IOException {
        final int context = peek();
        if (context != nonempty && context != empty) {
            throw new IllegalStateException("Nesting problem.");
        }
        if (deferredName != null) {
            throw new IllegalStateException("Dangling name: " + deferredName);
        }
        this.setStackSize(this.getStackSize() - 1);
        if (context == nonempty) {
            newline();
        }
        out.write(closeBracket);
        return this;
    }

    private void newline() throws IOException {
        if (this.getIndent() == null) {
            return;
        }
        out.write("\n");
        for (int i = 1, size = this.getStackSize(); i < size; i++) {
            out.write(this.getIndent());
        }
    }

    /**
     * Enters a new scope by appending any necessary whitespace and the given
     * bracket.
     */
    private JsonWriterHandle open(final int empty, final String openBracket) throws IOException {
        beforeValue(true);
        push(empty);
        out.write(openBracket);
        return this;
    }

    /**
     * Returns the value on the top of the stack.
     */
    private int peek() {
        if (this.getStackSize() == 0) {
            throw new IllegalStateException("JsonWriterHandle is closed.");
        }
        return this.getStack()[this.getStackSize() - 1];
    }

    private void push(final int newTop) {
        int[] stacks = this.getStack();
        if (this.getStackSize() == stacks.length) {
            final int[] newStack = new int[stacks.length * 2];
            System.arraycopy(stacks, 0, newStack, 0, stacks.length);
            stacks = newStack;
            this.setStack(stacks);
        }
        this.setStack(this.getStackSize(), newTop);
        this.setStackSize(this.getStackSize() + 1);
    }

    /**
     * Replace the value on the top of the stack with the given value.
     */
    private void replaceTop(final int topOfStack) {
        this.getStack()[this.getStackSize() - 1] = topOfStack;
    }

    private void writeDeferredName() throws IOException {
        if (deferredName != null) {
            beforeName();
            stringExtend(deferredName);
            deferredName = null;
        }
    }
}
