/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 org.apache.thrift.protocol;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Stack;

import org.apache.thrift.TByteArrayOutputStream;
import org.apache.thrift.TException;
import org.apache.thrift.transport.TTransport;

/**
 * JSON protocol implementation for thrift.
 * This is a full-featured protocol supporting write and read.
 * Please see the C++ class header for a detailed description of the
 * protocol's wire format.
 */
public class TJSONProtocol extends TProtocol {

  /**
   * Factory for JSON protocol objects
   */
  public static class Factory implements TProtocolFactory {

    public TProtocol getProtocol(TTransport trans) {
      return new TJSONProtocol(trans);
    }

  }

  private static final byte[]  COMMA            = new byte[] { ',' };
  private static final byte[]  COLON            = new byte[] { ':' };
  private static final byte[]  LBRACE           = new byte[] { '{' };
  private static final byte[]  RBRACE           = new byte[] { '}' };
  private static final byte[]  LBRACKET         = new byte[] { '[' };
  private static final byte[]  RBRACKET         = new byte[] { ']' };
  private static final byte[]  QUOTE            = new byte[] { '"' };
  private static final byte[]  BACKSLASH        = new byte[] { '\\' };
  private static final byte[]  ZERO             = new byte[] { '0' };

  private static final byte[]  ESCSEQ           = new byte[] { '\\', 'u', '0', '0' };

  private static final long    VERSION          = 1;

  private static final byte[]  JSON_CHAR_TABLE  = {
                                                /* 0 1 2 3 4 5 6 7 8 9 A B C D E F */
                                                0, 0, 0, 0, 0, 0, 0, 0, 'b', 't', 'n', 0, 'f', 'r', 0, 0, // 0
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1
      1, 1, '"', 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2
                                                };

  private static final String  ESCAPE_CHARS     = "\"\\/bfnrt";

  private static final byte[]  ESCAPE_CHAR_VALS = {
                                                '"', '\\', '/', '\b', '\f', '\n', '\r', '\t',
                                                };

  private static final int     DEF_STRING_SIZE  = 16;

  private static final byte[]  NAME_BOOL        = new byte[] { 't', 'f' };
  private static final byte[]  NAME_BYTE        = new byte[] { 'i', '8' };
  private static final byte[]  NAME_I16         = new byte[] { 'i', '1', '6' };
  private static final byte[]  NAME_I32         = new byte[] { 'i', '3', '2' };
  private static final byte[]  NAME_I64         = new byte[] { 'i', '6', '4' };
  private static final byte[]  NAME_DOUBLE      = new byte[] { 'd', 'b', 'l' };
  private static final byte[]  NAME_STRUCT      = new byte[] { 'r', 'e', 'c' };
  private static final byte[]  NAME_STRING      = new byte[] { 's', 't', 'r' };
  private static final byte[]  NAME_MAP         = new byte[] { 'm', 'a', 'p' };
  private static final byte[]  NAME_LIST        = new byte[] { 'l', 's', 't' };
  private static final byte[]  NAME_SET         = new byte[] { 's', 'e', 't' };

  private static final TStruct ANONYMOUS_STRUCT = new TStruct();

  private static final byte[] getTypeNameForTypeID(byte typeID)
                                                               throws TException {
    switch (typeID) {
    case TType.BOOL:
      return NAME_BOOL;
    case TType.BYTE:
      return NAME_BYTE;
    case TType.I16:
      return NAME_I16;
    case TType.I32:
      return NAME_I32;
    case TType.I64:
      return NAME_I64;
    case TType.DOUBLE:
      return NAME_DOUBLE;
    case TType.STRING:
      return NAME_STRING;
    case TType.STRUCT:
      return NAME_STRUCT;
    case TType.MAP:
      return NAME_MAP;
    case TType.SET:
      return NAME_SET;
    case TType.LIST:
      return NAME_LIST;
    default:
      throw new TProtocolException(TProtocolException.NOT_IMPLEMENTED,
          "Unrecognized type");
    }
  }

  private static final byte getTypeIDForTypeName(byte[] name)
                                                             throws TException {
    byte result = TType.STOP;
    if (name.length > 1) {
      switch (name[0]) {
      case 'd':
        result = TType.DOUBLE;
        break;
      case 'i':
        switch (name[1]) {
        case '8':
          result = TType.BYTE;
          break;
        case '1':
          result = TType.I16;
          break;
        case '3':
          result = TType.I32;
          break;
        case '6':
          result = TType.I64;
          break;
        }
        break;
      case 'l':
        result = TType.LIST;
        break;
      case 'm':
        result = TType.MAP;
        break;
      case 'r':
        result = TType.STRUCT;
        break;
      case 's':
        if (name[1] == 't') {
          result = TType.STRING;
        }
        else if (name[1] == 'e') {
          result = TType.SET;
        }
        break;
      case 't':
        result = TType.BOOL;
        break;
      }
    }
    if (result == TType.STOP) {
      throw new TProtocolException(TProtocolException.NOT_IMPLEMENTED,
          "Unrecognized type");
    }
    return result;
  }

  // Base class for tracking JSON contexts that may require inserting/reading
  // additional JSON syntax characters
  // This base context does nothing.
  protected class JSONBaseContext {
    /**
     * @throws TException
     */
    protected void write() throws TException {}

    /**
     * @throws TException
     */
    protected void read() throws TException {}

    protected boolean escapeNum() {
      return false;
    }
  }

  // Context for JSON lists. Will insert/read commas before each item except
  // for the first one
  protected class JSONListContext extends JSONBaseContext {
    private boolean first_ = true;

    protected void write() throws TException {
      if (first_) {
        first_ = false;
      } else {
        trans_.write(COMMA);
      }
    }

    protected void read() throws TException {
      if (first_) {
        first_ = false;
      } else {
        readJSONSyntaxChar(COMMA);
      }
    }
  }

  // Context for JSON records. Will insert/read colons before the value portion
  // of each record pair, and commas before each key except the first. In
  // addition, will indicate that numbers in the key position need to be
  // escaped in quotes (since JSON keys must be strings).
  protected class JSONPairContext extends JSONBaseContext {
    private boolean first_ = true;
    private boolean colon_ = true;

    protected void write() throws TException {
      if (first_) {
        first_ = false;
        colon_ = true;
      } else {
        trans_.write(colon_ ? COLON : COMMA);
        colon_ = !colon_;
      }
    }

    protected void read() throws TException {
      if (first_) {
        first_ = false;
        colon_ = true;
      } else {
        readJSONSyntaxChar(colon_ ? COLON : COMMA);
        colon_ = !colon_;
      }
    }

    protected boolean escapeNum() {
      return colon_;
    }
  }

  // Holds up to one byte from the transport
  protected class LookaheadReader {

    private boolean hasData_;
    private byte[]  data_ = new byte[1];

    // Return and consume the next byte to be read, either taking it from the
    // data buffer if present or getting it from the transport otherwise.
    protected byte read() throws TException {
      if (hasData_) {
        hasData_ = false;
      }
      else {
        trans_.readAll(data_, 0, 1);
      }
      return data_[0];
    }

    // Return the next byte to be read without consuming, filling the data
    // buffer if it has not been filled already.
    protected byte peek() throws TException {
      if (!hasData_) {
        trans_.readAll(data_, 0, 1);
      }
      hasData_ = true;
      return data_[0];
    }
  }

  // Stack of nested contexts of type JSONBaseContext that we may be in
  private Stack           contextStack_ = new Stack();

  // Current context that we are in
  private JSONBaseContext context_      = new JSONBaseContext();

  // Reader that manages a 1-byte buffer
  private LookaheadReader reader_       = new LookaheadReader();

  // Push a new JSON context onto the stack.
  private void pushContext(JSONBaseContext c) {
    contextStack_.push(context_);
    context_ = c;
  }

  // Pop the last JSON context off the stack
  private void popContext() {
    context_ = (JSONBaseContext)contextStack_.pop();
  }

  /**
   * Constructor
   */
  public TJSONProtocol(TTransport trans) {
    super(trans);
  }

  public void reset() {
    contextStack_.clear();
    context_ = new JSONBaseContext();
    reader_ = new LookaheadReader();
  }

  // Temporary buffer used by several methods
  private byte[] tmpbuf_ = new byte[4];

  // Read a byte that must match b[0]; otherwise an exception is thrown.
  // Marked protected to avoid synthetic accessor in JSONListContext.read
  // and JSONPairContext.read
  protected void readJSONSyntaxChar(byte[] b) throws TException {
    byte ch = reader_.read();
    if (ch != b[0]) {
      throw new TProtocolException(TProtocolException.INVALID_DATA,
          "Unexpected character:" + (char)ch);
    }
  }

  // Convert a byte containing a hex char ('0'-'9' or 'a'-'f') into its
  // corresponding hex value
  private static final byte hexVal(byte ch) throws TException {
    if ((ch >= '0') && (ch <= '9')) {
      return (byte)((char)ch - '0');
    }
    else if ((ch >= 'a') && (ch <= 'f')) {
      return (byte)((char)ch - 'a' + 10);
    }
    else {
      throw new TProtocolException(TProtocolException.INVALID_DATA,
          "Expected hex character");
    }
  }

  // Convert a byte containing a hex value to its corresponding hex character
  private static final byte hexChar(byte val) {
    val &= 0x0F;
    if (val < 10) {
      return (byte)((char)val + '0');
    }
    else {
      return (byte)((char)(val - 10) + 'a');
    }
  }

  private static boolean isHighSurrogate(char c) {
    return c >= '\uD800' && c <= '\uDBFF';
  }

  private static boolean isLowSurrogate(char c) {
    return c >= '\uDC00' && c <= '\uDFFF';
  }

  private static byte[] toUTF8(int codepoint) {
    final int[] FIRST_BYTE_MASK = { 0, 0xc0, 0xe0, 0xf0 };
    int length = 0;
    if (codepoint <= 0x7f) length = 1;
    else if (codepoint <= 0x7ff) length = 2;
    else if (codepoint <= 0xffff) length = 3;
    else if (codepoint <= 0x1fffff) length = 4;
    else throw new RuntimeException("Code point over U+1FFFFF is not supported");

    byte[] bytes = new byte[length];
    switch (length) {
    case 4:
      bytes[3] = (byte)((codepoint & 0x3f) | 0x80);
      codepoint >>= 6;
    case 3:
      bytes[2] = (byte)((codepoint & 0x3f) | 0x80);
      codepoint >>= 6;
    case 2:
      bytes[1] = (byte)((codepoint & 0x3f) | 0x80);
      codepoint >>= 6;
    case 1:
      bytes[0] = (byte)(codepoint | FIRST_BYTE_MASK[length - 1]);
    }

    return bytes;
  }

  private static byte[] toUTF8(int high, int low) {
    int codepoint = (1 << 16) + ((high & 0x3ff) << 10);
    codepoint += low & 0x3ff;
    return toUTF8(codepoint);
  }

  // Write the bytes in array buf as a JSON characters, escaping as needed
  private void writeJSONString(byte[] b) throws TException {
    context_.write();
    trans_.write(QUOTE);
    int len = b.length;
    for (int i = 0; i < len; i++) {
      if ((b[i] & 0x00FF) >= 0x30) {
        if (b[i] == BACKSLASH[0]) {
          trans_.write(BACKSLASH);
          trans_.write(BACKSLASH);
        }
        else {
          trans_.write(b, i, 1);
        }
      }
      else {
        tmpbuf_[0] = JSON_CHAR_TABLE[b[i]];
        if (tmpbuf_[0] == 1) {
          trans_.write(b, i, 1);
        }
        else if (tmpbuf_[0] > 1) {
          trans_.write(BACKSLASH);
          trans_.write(tmpbuf_, 0, 1);
        }
        else {
          trans_.write(ESCSEQ);
          tmpbuf_[0] = hexChar((byte)(b[i] >> 4));
          tmpbuf_[1] = hexChar(b[i]);
          trans_.write(tmpbuf_, 0, 2);
        }
      }
    }
    trans_.write(QUOTE);
  }

  // Write out number as a JSON value. If the context dictates so, it will be
  // wrapped in quotes to output as a JSON string.
  private void writeJSONInteger(long num) throws TException {
    context_.write();
    String str = Long.toString(num);
    boolean escapeNum = context_.escapeNum();
    if (escapeNum) {
      trans_.write(QUOTE);
    }
    try {
      byte[] buf = str.getBytes("UTF-8");
      trans_.write(buf);
    } catch (UnsupportedEncodingException uex) {
      throw new TException("JVM DOES NOT SUPPORT UTF-8");
    }
    if (escapeNum) {
      trans_.write(QUOTE);
    }
  }

  // Write out a double as a JSON value. If it is NaN or infinity or if the
  // context dictates escaping, write out as JSON string.
  private void writeJSONDouble(double num) throws TException {
    context_.write();
    String str = Double.toString(num);
    boolean special = false;
    switch (str.charAt(0)) {
    case 'N': // NaN
    case 'I': // Infinity
      special = true;
      break;
    case '-':
      if (str.charAt(1) == 'I') { // -Infinity
        special = true;
      }
      break;
    }

    boolean escapeNum = special || context_.escapeNum();
    if (escapeNum) {
      trans_.write(QUOTE);
    }
    try {
      byte[] b = str.getBytes("UTF-8");
      trans_.write(b, 0, b.length);
    } catch (UnsupportedEncodingException uex) {
      throw new TException("JVM DOES NOT SUPPORT UTF-8");
    }
    if (escapeNum) {
      trans_.write(QUOTE);
    }
  }

  // Write out contents of byte array b as a JSON string with base-64 encoded
  // data
  private void writeJSONBase64(byte[] b, int offset, int length) throws TException {
    context_.write();
    trans_.write(QUOTE);
    int len = length;
    int off = offset;
    while (len >= 3) {
      // Encode 3 bytes at a time
      TBase64Utils.encode(b, off, 3, tmpbuf_, 0);
      trans_.write(tmpbuf_, 0, 4);
      off += 3;
      len -= 3;
    }
    if (len > 0) {
      // Encode remainder
      TBase64Utils.encode(b, off, len, tmpbuf_, 0);
      trans_.write(tmpbuf_, 0, len + 1);
    }
    trans_.write(QUOTE);
  }

  private void writeJSONObjectStart() throws TException {
    context_.write();
    trans_.write(LBRACE);
    pushContext(new JSONPairContext());
  }

  private void writeJSONObjectEnd() throws TException {
    popContext();
    trans_.write(RBRACE);
  }

  private void writeJSONArrayStart() throws TException {
    context_.write();
    trans_.write(LBRACKET);
    pushContext(new JSONListContext());
  }

  private void writeJSONArrayEnd() throws TException {
    popContext();
    trans_.write(RBRACKET);
  }

  public void writeMessageBegin(TMessage message) throws TException {
    writeJSONArrayStart();
    writeJSONInteger(VERSION);
    try {
      byte[] b = message.name.getBytes("UTF-8");
      writeJSONString(b);
    } catch (UnsupportedEncodingException uex) {
      throw new TException("JVM DOES NOT SUPPORT UTF-8");
    }
    writeJSONInteger(message.type);
    writeJSONInteger(message.seqid);
  }

  public void writeMessageEnd() throws TException {
    writeJSONArrayEnd();
  }

  public void writeStructBegin(TStruct struct) throws TException {
    writeJSONObjectStart();
  }

  public void writeStructEnd() throws TException {
    writeJSONObjectEnd();
  }

  public void writeFieldBegin(TField field) throws TException {
    writeJSONInteger(field.id);
    writeJSONObjectStart();
    writeJSONString(getTypeNameForTypeID(field.type));
  }

  public void writeFieldEnd() throws TException {
    writeJSONObjectEnd();
  }

  public void writeFieldStop() {}

  public void writeMapBegin(TMap map) throws TException {
    writeJSONArrayStart();
    writeJSONString(getTypeNameForTypeID(map.keyType));
    writeJSONString(getTypeNameForTypeID(map.valueType));
    writeJSONInteger(map.size);
    writeJSONObjectStart();
  }

  public void writeMapEnd() throws TException {
    writeJSONObjectEnd();
    writeJSONArrayEnd();
  }

  public void writeListBegin(TList list) throws TException {
    writeJSONArrayStart();
    writeJSONString(getTypeNameForTypeID(list.elemType));
    writeJSONInteger(list.size);
  }

  public void writeListEnd() throws TException {
    writeJSONArrayEnd();
  }

  public void writeSetBegin(TSet set) throws TException {
    writeJSONArrayStart();
    writeJSONString(getTypeNameForTypeID(set.elemType));
    writeJSONInteger(set.size);
  }

  public void writeSetEnd() throws TException {
    writeJSONArrayEnd();
  }

  public void writeBool(boolean b) throws TException {
    writeJSONInteger(b ? (long)1 : (long)0);
  }

  public void writeByte(byte b) throws TException {
    writeJSONInteger(b);
  }

  public void writeI16(short i16) throws TException {
    writeJSONInteger(i16);
  }

  public void writeI32(int i32) throws TException {
    writeJSONInteger(i32);
  }

  public void writeI64(long i64) throws TException {
    writeJSONInteger(i64);
  }

  public void writeDouble(double dub) throws TException {
    writeJSONDouble(dub);
  }

  public void writeString(String str) throws TException {
    try {
      byte[] b = str.getBytes("UTF-8");
      writeJSONString(b);
    } catch (UnsupportedEncodingException uex) {
      throw new TException("JVM DOES NOT SUPPORT UTF-8");
    }
  }

  public void writeBinary(byte[] bin) throws TException {
    writeJSONBase64(bin, 0, bin.length);
  }

  /**
   * Reading methods.
   */

  // Read in a JSON string, unescaping as appropriate.. Skip reading from the
  // context if skipContext is true.
  private TByteArrayOutputStream readJSONString(boolean skipContext)
                                                                    throws TException {
    TByteArrayOutputStream arr = new TByteArrayOutputStream(DEF_STRING_SIZE);
    int highSurrogate = 0;
    if (!skipContext) {
      context_.read();
    }
    readJSONSyntaxChar(QUOTE);
    while (true) {
      byte ch = reader_.read();
      if (ch == QUOTE[0]) {
        break;
      }
      if (ch == ESCSEQ[0]) {
        ch = reader_.read();
        if (ch == ESCSEQ[1]) {
          trans_.readAll(tmpbuf_, 0, 4);
          short cu = (short)(
              ((short)hexVal(tmpbuf_[0]) << 12) +
              ((short)hexVal(tmpbuf_[1]) << 8) +
              ((short)hexVal(tmpbuf_[2]) << 4) +
              (short)hexVal(tmpbuf_[3]));
          try {
            if (isHighSurrogate((char)cu)) {
              if (highSurrogate != 0) {
                throw new TProtocolException(TProtocolException.INVALID_DATA,
                    "Expected low surrogate char");
              }
              highSurrogate = cu;
            }
            else if (isLowSurrogate((char)cu)) {
              if (highSurrogate == 0) {
                throw new TProtocolException(TProtocolException.INVALID_DATA,
                    "Expected high surrogate char");
              }

              arr.write(toUTF8(highSurrogate, cu));
              highSurrogate = 0;
            }
            else {
              arr.write(toUTF8(cu));
            }
            continue;
          }
          catch (UnsupportedEncodingException ex) {
            throw new TProtocolException(TProtocolException.NOT_IMPLEMENTED,
                "JVM does not support UTF-8");
          }
          catch (IOException ex) {
            throw new TProtocolException(TProtocolException.INVALID_DATA,
                "Invalid unicode sequence");
          }
        }
        else {
          int off = ESCAPE_CHARS.indexOf(ch);
          if (off == -1) {
            throw new TProtocolException(TProtocolException.INVALID_DATA,
                "Expected control char");
          }
          ch = ESCAPE_CHAR_VALS[off];
        }
      }
      arr.write(ch);
    }

    if (highSurrogate != 0) {
      throw new TProtocolException(TProtocolException.INVALID_DATA,
          "Expected low surrogate char");
    }
    return arr;
  }

  // Return true if the given byte could be a valid part of a JSON number.
  private boolean isJSONNumeric(byte b) {
    switch (b) {
    case '+':
    case '-':
    case '.':
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
    case 'E':
    case 'e':
      return true;
    }
    return false;
  }

  // Read in a sequence of characters that are all valid in JSON numbers. Does
  // not do a complete regex check to validate that this is actually a number.
  private String readJSONNumericChars() throws TException {
    StringBuffer strbuf = new StringBuffer();
    while (true) {
      byte ch = reader_.peek();
      if (!isJSONNumeric(ch)) {
        break;
      }
      strbuf.append((char)reader_.read());
    }
    return strbuf.toString();
  }

  // Read in a JSON number. If the context dictates, read in enclosing quotes.
  private long readJSONInteger() throws TException {
    context_.read();
    if (context_.escapeNum()) {
      readJSONSyntaxChar(QUOTE);
    }
    String str = readJSONNumericChars();
    if (context_.escapeNum()) {
      readJSONSyntaxChar(QUOTE);
    }
    try {
      return Long.valueOf(str).longValue();
    } catch (NumberFormatException ex) {
      throw new TProtocolException(TProtocolException.INVALID_DATA,
          "Bad data encounted in numeric data");
    }
  }

  // Read in a JSON double value. Throw if the value is not wrapped in quotes
  // when expected or if wrapped in quotes when not expected.
  private double readJSONDouble() throws TException {
    context_.read();
    if (reader_.peek() == QUOTE[0]) {
      TByteArrayOutputStream arr = readJSONString(true);
      try {
        double dub = Double.valueOf(arr.toString("UTF-8")).doubleValue();
        if (!context_.escapeNum() && !Double.isNaN(dub) &&
            !Double.isInfinite(dub)) {
          // Throw exception -- we should not be in a string in this case
          throw new TProtocolException(TProtocolException.INVALID_DATA,
              "Numeric data unexpectedly quoted");
        }
        return dub;
      } catch (UnsupportedEncodingException ex) {
        throw new TException("JVM DOES NOT SUPPORT UTF-8");
      }
    }
    else {
      if (context_.escapeNum()) {
        // This will throw - we should have had a quote if escapeNum == true
        readJSONSyntaxChar(QUOTE);
      }
      try {
        return Double.valueOf(readJSONNumericChars()).doubleValue();
      } catch (NumberFormatException ex) {
        throw new TProtocolException(TProtocolException.INVALID_DATA,
            "Bad data encounted in numeric data");
      }
    }
  }

  // Read in a JSON string containing base-64 encoded data and decode it.
  private byte[] readJSONBase64() throws TException {
    TByteArrayOutputStream arr = readJSONString(false);
    byte[] b = arr.get();
    int len = arr.len();
    int off = 0;
    int size = 0;
    // Ignore padding
    int bound = len >= 2 ? len - 2 : 0;
    for (int i = len - 1; i >= bound && b[i] == '='; --i) {
      --len;
    }
    while (len >= 4) {
      // Decode 4 bytes at a time
      TBase64Utils.decode(b, off, 4, b, size); // NB: decoded in place
      off += 4;
      len -= 4;
      size += 3;
    }
    // Don't decode if we hit the end or got a single leftover byte (invalid
    // base64 but legal for skip of regular string type)
    if (len > 1) {
      // Decode remainder
      TBase64Utils.decode(b, off, len, b, size); // NB: decoded in place
      size += len - 1;
    }
    // Sadly we must copy the byte[] (any way around this?)
    byte[] result = new byte[size];
    System.arraycopy(b, 0, result, 0, size);
    return result;
  }

  private void readJSONObjectStart() throws TException {
    context_.read();
    readJSONSyntaxChar(LBRACE);
    pushContext(new JSONPairContext());
  }

  private void readJSONObjectEnd() throws TException {
    readJSONSyntaxChar(RBRACE);
    popContext();
  }

  private void readJSONArrayStart() throws TException {
    context_.read();
    readJSONSyntaxChar(LBRACKET);
    pushContext(new JSONListContext());
  }

  private void readJSONArrayEnd() throws TException {
    readJSONSyntaxChar(RBRACKET);
    popContext();
  }

  public TMessage readMessageBegin() throws TException {
    readJSONArrayStart();
    if (readJSONInteger() != VERSION) {
      throw new TProtocolException(TProtocolException.BAD_VERSION,
          "Message contained bad version.");
    }
    String name;
    try {
      name = readJSONString(false).toString("UTF-8");
    } catch (UnsupportedEncodingException ex) {
      throw new TException("JVM DOES NOT SUPPORT UTF-8");
    }
    byte type = (byte)readJSONInteger();
    int seqid = (int)readJSONInteger();
    return new TMessage(name, type, seqid);
  }

  public void readMessageEnd() throws TException {
    readJSONArrayEnd();
  }

  public TStruct readStructBegin() throws TException {
    readJSONObjectStart();
    return ANONYMOUS_STRUCT;
  }

  public void readStructEnd() throws TException {
    readJSONObjectEnd();
  }

  public TField readFieldBegin() throws TException {
    byte ch = reader_.peek();
    byte type;
    short id = 0;
    if (ch == RBRACE[0]) {
      type = TType.STOP;
    }
    else {
      id = (short)readJSONInteger();
      readJSONObjectStart();
      type = getTypeIDForTypeName(readJSONString(false).get());
    }
    return new TField("", type, id);
  }

  public void readFieldEnd() throws TException {
    readJSONObjectEnd();
  }

  public TMap readMapBegin() throws TException {
    readJSONArrayStart();
    byte keyType = getTypeIDForTypeName(readJSONString(false).get());
    byte valueType = getTypeIDForTypeName(readJSONString(false).get());
    int size = (int)readJSONInteger();
    readJSONObjectStart();
    return new TMap(keyType, valueType, size);
  }

  public void readMapEnd() throws TException {
    readJSONObjectEnd();
    readJSONArrayEnd();
  }

  public TList readListBegin() throws TException {
    readJSONArrayStart();
    byte elemType = getTypeIDForTypeName(readJSONString(false).get());
    int size = (int)readJSONInteger();
    return new TList(elemType, size);
  }

  public void readListEnd() throws TException {
    readJSONArrayEnd();
  }

  public TSet readSetBegin() throws TException {
    readJSONArrayStart();
    byte elemType = getTypeIDForTypeName(readJSONString(false).get());
    int size = (int)readJSONInteger();
    return new TSet(elemType, size);
  }

  public void readSetEnd() throws TException {
    readJSONArrayEnd();
  }

  public boolean readBool() throws TException {
    return (readJSONInteger() == 0 ? false : true);
  }

  public byte readByte() throws TException {
    return (byte)readJSONInteger();
  }

  public short readI16() throws TException {
    return (short)readJSONInteger();
  }

  public int readI32() throws TException {
    return (int)readJSONInteger();
  }

  public long readI64() throws TException {
    return readJSONInteger();
  }

  public double readDouble() throws TException {
    return readJSONDouble();
  }

  public String readString() throws TException {
    try {
      return readJSONString(false).toString("UTF-8");
    } catch (UnsupportedEncodingException ex) {
      throw new TException("JVM DOES NOT SUPPORT UTF-8");
    }
  }

  public byte[] readBinary() throws TException {
    return readJSONBase64();
  }

}
