/*
 * 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;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;

import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TField;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.protocol.TProtocolFactory;
import org.apache.thrift.protocol.TProtocolUtil;
import org.apache.thrift.protocol.TType;
import org.apache.thrift.transport.TMemoryInputTransport;
import org.apache.thrift.transport.TTransportException;

/**
 * Generic utility for easily deserializing objects from a byte array or Java
 * String.
 *
 */
public class TDeserializer {
  private final TProtocol protocol_;
  private final TMemoryInputTransport trans_;

  /**
   * Create a new TDeserializer that uses the TBinaryProtocol by default.
   */
  public TDeserializer() throws TTransportException {
    this(new TBinaryProtocol.Factory());
  }

  /**
   * Create a new TDeserializer. It will use the TProtocol specified by the
   * factory that is passed in.
   *
   * @param protocolFactory Factory to create a protocol
   */
  public TDeserializer(TProtocolFactory protocolFactory) throws TTransportException {
    trans_ = new TMemoryInputTransport(new TConfiguration());
    protocol_ = protocolFactory.getProtocol(trans_);
  }

  /**
   * Deserialize the Thrift object from a byte array.
   *
   * @param base The object to read into
   * @param bytes The array to read from
   */
  public void deserialize(TBase base, byte[] bytes) throws TException {
      deserialize(base, bytes, 0, bytes.length);
  }

  /**
   * Deserialize the Thrift object from a byte array.
   *
   * @param base The object to read into
   * @param bytes The array to read from
   * @param offset The offset into {@code bytes}
   * @param length The length to read from {@code bytes}
   */
  public void deserialize(TBase base, byte[] bytes, int offset, int length) throws TException {
    try {
      trans_.reset(bytes, offset, length);
      base.read(protocol_);
    } finally {
      trans_.clear();
      protocol_.reset();
    }
  }

  /**
   * Deserialize the Thrift object from a Java string, using a specified
   * character set for decoding.
   *
   * @param base The object to read into
   * @param data The string to read from
   * @param charset Valid JVM charset
   */
  public void deserialize(TBase base, String data, String charset) throws TException {
    try {
      deserialize(base, data.getBytes(charset));
    } catch (UnsupportedEncodingException uex) {
      throw new TException("JVM DOES NOT SUPPORT ENCODING: " + charset);
    } finally {
      protocol_.reset();
    }
  }

  /**
   * Deserialize only a single Thrift object (addressed by recursively using field id)
   * from a byte record.
   * @param tb The object to read into
   * @param bytes The serialized object to read from
   * @param fieldIdPathFirst First of the FieldId's that define a path tb
   * @param fieldIdPathRest The rest FieldId's that define a path tb
   * @throws TException
   */
  public void partialDeserialize(TBase tb, byte[] bytes, TFieldIdEnum fieldIdPathFirst, TFieldIdEnum ... fieldIdPathRest) throws TException {
    try {
      if (locateField(bytes, fieldIdPathFirst, fieldIdPathRest) != null) {
        // if this line is reached, iprot will be positioned at the start of tb.
        tb.read(protocol_);
      }
    } catch (Exception e) {
      throw new TException(e);
    } finally {
      trans_.clear();
      protocol_.reset();
    }
  }

  /**
   * Deserialize only a boolean field (addressed by recursively using field id)
   * from a byte record.
   * @param bytes The serialized object to read from
   * @param fieldIdPathFirst First of the FieldId's that define a path to a boolean field
   * @param fieldIdPathRest The rest FieldId's that define a path to a boolean field
   * @throws TException
   */
  public Boolean partialDeserializeBool(byte[] bytes, TFieldIdEnum fieldIdPathFirst, TFieldIdEnum ... fieldIdPathRest) throws TException {
    return (Boolean) partialDeserializeField(TType.BOOL, bytes, fieldIdPathFirst, fieldIdPathRest);
  }

  /**
   * Deserialize only a byte field (addressed by recursively using field id)
   * from a byte record.
   * @param bytes The serialized object to read from
   * @param fieldIdPathFirst First of the FieldId's that define a path to a byte field
   * @param fieldIdPathRest The rest FieldId's that define a path to a byte field
   * @throws TException
   */
  public Byte partialDeserializeByte(byte[] bytes, TFieldIdEnum fieldIdPathFirst, TFieldIdEnum ... fieldIdPathRest) throws TException {
    return (Byte) partialDeserializeField(TType.BYTE, bytes, fieldIdPathFirst, fieldIdPathRest);
  }

  /**
   * Deserialize only a double field (addressed by recursively using field id)
   * from a byte record.
   * @param bytes The serialized object to read from
   * @param fieldIdPathFirst First of the FieldId's that define a path to a double field
   * @param fieldIdPathRest The rest FieldId's that define a path to a double field
   * @throws TException
   */
  public Double partialDeserializeDouble(byte[] bytes, TFieldIdEnum fieldIdPathFirst, TFieldIdEnum ... fieldIdPathRest) throws TException {
    return (Double) partialDeserializeField(TType.DOUBLE, bytes, fieldIdPathFirst, fieldIdPathRest);
  }

  /**
   * Deserialize only an i16 field (addressed by recursively using field id)
   * from a byte record.
   * @param bytes The serialized object to read from
   * @param fieldIdPathFirst First of the FieldId's that define a path to an i16 field
   * @param fieldIdPathRest The rest FieldId's that define a path to an i16 field
   * @throws TException
   */
  public Short partialDeserializeI16(byte[] bytes, TFieldIdEnum fieldIdPathFirst, TFieldIdEnum ... fieldIdPathRest) throws TException {
    return (Short) partialDeserializeField(TType.I16, bytes, fieldIdPathFirst, fieldIdPathRest);
  }

  /**
   * Deserialize only an i32 field (addressed by recursively using field id)
   * from a byte record.
   * @param bytes The serialized object to read from
   * @param fieldIdPathFirst First of the FieldId's that define a path to an i32 field
   * @param fieldIdPathRest The rest FieldId's that define a path to an i32 field
   * @throws TException
   */
  public Integer partialDeserializeI32(byte[] bytes, TFieldIdEnum fieldIdPathFirst, TFieldIdEnum ... fieldIdPathRest) throws TException {
    return (Integer) partialDeserializeField(TType.I32, bytes, fieldIdPathFirst, fieldIdPathRest);
  }

  /**
   * Deserialize only an i64 field (addressed by recursively using field id)
   * from a byte record.
   * @param bytes The serialized object to read from
   * @param fieldIdPathFirst First of the FieldId's that define a path to an i64 field
   * @param fieldIdPathRest The rest FieldId's that define a path to an i64 field
   * @throws TException
   */
  public Long partialDeserializeI64(byte[] bytes, TFieldIdEnum fieldIdPathFirst, TFieldIdEnum ... fieldIdPathRest) throws TException {
    return (Long) partialDeserializeField(TType.I64, bytes, fieldIdPathFirst, fieldIdPathRest);
  }

  /**
   * Deserialize only a string field (addressed by recursively using field id)
   * from a byte record.
   * @param bytes The serialized object to read from
   * @param fieldIdPathFirst First of the FieldId's that define a path to a string field
   * @param fieldIdPathRest The rest FieldId's that define a path to a string field
   * @throws TException
   */
  public String partialDeserializeString(byte[] bytes, TFieldIdEnum fieldIdPathFirst, TFieldIdEnum ... fieldIdPathRest) throws TException {
    return (String) partialDeserializeField(TType.STRING, bytes, fieldIdPathFirst, fieldIdPathRest);
  }

  /**
   * Deserialize only a binary field (addressed by recursively using field id)
   * from a byte record.
   * @param bytes The serialized object to read from
   * @param fieldIdPathFirst First of the FieldId's that define a path to a binary field
   * @param fieldIdPathRest The rest FieldId's that define a path to a binary field
   * @throws TException
   */
  public ByteBuffer partialDeserializeByteArray(byte[] bytes, TFieldIdEnum fieldIdPathFirst, TFieldIdEnum ... fieldIdPathRest) throws TException {
    // TType does not have binary, so we use the arbitrary num 100
    return (ByteBuffer) partialDeserializeField((byte)100, bytes, fieldIdPathFirst, fieldIdPathRest);
  }

  /**
   * Deserialize only the id of the field set in a TUnion (addressed by recursively using field id)
   * from a byte record.
   * @param bytes The serialized object to read from
   * @param fieldIdPathFirst First of the FieldId's that define a path to a TUnion
   * @param fieldIdPathRest The rest FieldId's that define a path to a TUnion
   * @throws TException
   */
  public Short partialDeserializeSetFieldIdInUnion(byte[] bytes, TFieldIdEnum fieldIdPathFirst, TFieldIdEnum ... fieldIdPathRest)  throws TException {
    try {
      TField field = locateField(bytes, fieldIdPathFirst, fieldIdPathRest);
      if (field != null){
        protocol_.readStructBegin(); // The Union
        return protocol_.readFieldBegin().id; // The field set in the union
      }
      return null;
    } catch (Exception e) {
      throw new TException(e);
    } finally {
      trans_.clear();
      protocol_.reset();
    }
  }

  private Object partialDeserializeField(byte ttype, byte[] bytes, TFieldIdEnum fieldIdPathFirst, TFieldIdEnum ... fieldIdPathRest) throws TException {
    try {
      TField field = locateField(bytes, fieldIdPathFirst, fieldIdPathRest);
      if (field != null) {
        if (ttype == field.type) {
          // if this point is reached, iprot will be positioned at the start of
          // the field
          switch (ttype) {
          case TType.BOOL:
            return protocol_.readBool();
          case TType.BYTE:
            return protocol_.readByte();
          case TType.DOUBLE:
            return protocol_.readDouble();
          case TType.I16:
            return protocol_.readI16();
          case TType.I32:
            return protocol_.readI32();
          case TType.I64:
            return protocol_.readI64();
          case TType.STRING:
            return protocol_.readString();
          default:
            return null;
          }
        }
        // hack to differentiate between string and binary
        if (ttype == 100 && field.type == TType.STRING) {
          return protocol_.readBinary();
        }
      }
      return null;
    } catch (Exception e) {
      throw new TException(e);
    } finally {
      trans_.clear();
      protocol_.reset();
    }
  }

  private TField locateField(byte[] bytes, TFieldIdEnum fieldIdPathFirst, TFieldIdEnum ... fieldIdPathRest) throws TException {
    trans_.reset(bytes);

    TFieldIdEnum[] fieldIdPath = new TFieldIdEnum[fieldIdPathRest.length + 1];
    fieldIdPath[0] = fieldIdPathFirst;
    System.arraycopy(fieldIdPathRest, 0, fieldIdPath, 1, fieldIdPathRest.length);

    // index into field ID path being currently searched for
    int curPathIndex = 0;

    // this will be the located field, or null if it is not located
    TField field = null;

    protocol_.readStructBegin();

    while (curPathIndex < fieldIdPath.length) {
      field = protocol_.readFieldBegin();
      // we can stop searching if we either see a stop or we go past the field
      // id we're looking for (since fields should now be serialized in asc
      // order).
      if (field.type == TType.STOP || field.id > fieldIdPath[curPathIndex].getThriftFieldId()) {
        return null;
      }

      if (field.id != fieldIdPath[curPathIndex].getThriftFieldId()) {
        // Not the field we're looking for. Skip field.
        TProtocolUtil.skip(protocol_, field.type);
        protocol_.readFieldEnd();
      } else {
        // This field is the next step in the path. Step into field.
        curPathIndex++;
        if (curPathIndex < fieldIdPath.length) {
          protocol_.readStructBegin();
        }
      }
    }
    return field;
  }

  /**
   * Deserialize the Thrift object from a Java string, using the default JVM
   * charset encoding.
   *
   * @param base The object to read into
   * @param data The string to read from
   */
  public void fromString(TBase base, String data) throws TException {
    deserialize(base, data.getBytes());
  }
}
