/*
 * 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.geode.pdx.internal;

import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Date;

import org.apache.geode.InternalGemFireException;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.internal.DSCODE;
import org.apache.geode.internal.HeapDataOutputStream;
import org.apache.geode.internal.InternalDataSerializer;
import org.apache.geode.internal.cache.GemFireCacheImpl;
import org.apache.geode.internal.tcp.ByteBufferInputStream.ByteSource;
import org.apache.geode.pdx.FieldType;
import org.apache.geode.pdx.PdxFieldAlreadyExistsException;
import org.apache.geode.pdx.PdxFieldDoesNotExistException;
import org.apache.geode.pdx.PdxInstance;
import org.apache.geode.pdx.PdxSerializable;
import org.apache.geode.pdx.PdxSerializationException;
import org.apache.geode.pdx.PdxUnreadFields;
import org.apache.geode.pdx.PdxWriter;
import org.apache.geode.pdx.internal.AutoSerializableManager.AutoClassInfo;

/**
 * A new instance of this class is created for each (nested) instance of {@link PdxSerializable}.
 * But it may share the underlying instance of {@link HeapDataOutputStream} with other instances of
 * this class.
 *
 * @since GemFire 6.6
 * @see InternalDataSerializer#basicWriteObject(Object, java.io.DataOutput, boolean)
 */
public class PdxWriterImpl implements PdxWriter {

  public static final byte TYPE_ID_SIZE = DataSize.INTEGER_SIZE;
  public static final byte HEADER_SIZE = TYPE_ID_SIZE + DataSize.INTEGER_SIZE + DataSize.BYTE_SIZE;
  public static final int EXPAND_SIZE = 32; // used for number of offsets array

  /**
   * tr is no longer final because it is initialized late when using a PdxSerializer.
   */
  private TypeRegistry tr;

  private final Object pdx;
  private final PdxOutputStream os;
  private final AutoClassInfo aci;

  /**
   * Offsets to the variable length fields.
   */
  private int[] vlfOffsets;

  /**
   * The number of variable length fields that need an offset. The first VLF does not need an
   * offset.
   */
  private int vlfCount = 0;

  private boolean hasSeenFirstVlf = false;

  /**
   * The offset into the hdos to the header.
   */
  protected final int headerOffset;

  private PdxUnreadData unreadData;

  private PdxType existingType;
  private PdxType newType;
  private int fieldId = -1;

  /**
   * If true then extra validation is done to detect if mistakes have been made in the way PdxWriter
   * is used. Currently this will cause PdxSerializationException to be thrown if the number, names,
   * or types of fields are changed or if different identity fields are marked on the same instance
   * of a class. This property should only be set when debugging new code since it will slow down
   * pdx serialization.
   */
  private static final boolean sysPropDoExtraPdxValidation =
      Boolean.getBoolean(DistributionConfig.GEMFIRE_PREFIX + "validatePdxWriters");

  private boolean doExtraValidation = sysPropDoExtraPdxValidation;

  public PdxWriterImpl(TypeRegistry tr, Object pdx, PdxOutputStream out) {
    this.tr = tr;
    this.pdx = pdx;
    this.os = out;
    this.headerOffset = this.os.size();
    this.aci = null;
  }

  PdxWriterImpl(PdxType pdxType, PdxOutputStream out) {
    this.tr = null;
    this.pdx = null;
    this.os = out;
    this.existingType = pdxType;
    this.headerOffset = this.os.size();
    this.aci = null;
  }

  PdxWriterImpl(PdxType pt, TypeRegistry tr, PdxOutputStream out) {
    this.tr = tr;
    this.pdx = null;
    this.os = out;
    this.newType = pt;
    this.headerOffset = this.os.size();
    this.aci = null;
  }

  public PdxWriterImpl(TypeRegistry tr, Object pdx, AutoClassInfo aci, PdxOutputStream os) {
    this.tr = tr;
    this.pdx = pdx;
    this.os = os;
    this.headerOffset = this.os.size();
    this.aci = aci;
  }

  private boolean fieldsWritten() {
    return this.fieldId >= 0;
  }

  private void beforeFieldWrite() {
    ++this.fieldId;
    if (this.fieldId > 0) {
      // already wrote first field
      return;
    }
    initialize();
  }

  private void initialize() {
    writeHeader();
    if (this.existingType != null) {
      // PdxInstance is using us to flush its dirty fields
      return;
    }
    if (definingNewPdxType()) {
      // PdxInstanceFactoryImpl is using us
      return;
    }
    PdxUnreadData ud = initUnreadData();
    if (ud == null && this.pdx != null) {
      if (this.aci != null) {
        this.existingType = aci.getSerializedType();
      } else {
        this.existingType = this.tr.getExistingType(this.pdx);
      }
    } else if (ud != null) {
      this.existingType = ud.getSerializedType();
    }

    if (this.existingType != null) {
      int c = this.existingType.getVariableLengthFieldCount();
      if (c > 0) {
        this.vlfOffsets = new int[c];
      }
    } else if (this.pdx != null) {
      this.newType = new PdxType(this.pdx.getClass().getName(), true);
    }
  }

  private boolean unreadDataInitialized = false;

  PdxUnreadData initUnreadData() {
    if (this.unreadDataInitialized) {
      return this.unreadData;
    }
    this.unreadDataInitialized = true;
    if (this.tr == null) {
      // We are being PdxSerializer serialized.
      // Now is the time to initialize tr.
      this.tr = GemFireCacheImpl.getForPdx("Could not access Pdx registry").getPdxRegistry();
    }
    PdxUnreadData ud = this.unreadData;
    if (ud == null && this.pdx != null) {
      ud = this.tr.getUnreadData(this.pdx);
      this.unreadData = ud;
    }
    return ud;
  }

  public PdxWriter writeChar(String fieldName, char value) {
    updateMetaData(fieldName, FieldType.CHAR);
    this.os.writeChar(value);
    return this;
  }

  public void writeChar(char value) {
    beforeFieldWrite();
    this.os.writeChar(value);
  }

  public PdxWriter writeBoolean(String fieldName, boolean value) {
    updateMetaData(fieldName, FieldType.BOOLEAN);
    this.os.writeByte((value) ? 0x1 : 0x0);
    return this;
  }

  public void writeBoolean(boolean value) {
    beforeFieldWrite();
    this.os.writeByte((value) ? 0x1 : 0x0);
  }

  public PdxWriter writeByte(String fieldName, byte value) {
    updateMetaData(fieldName, FieldType.BYTE);
    this.os.writeByte(value);
    return this;
  }

  public void writeByte(byte value) {
    beforeFieldWrite();
    this.os.writeByte(value);
  }

  public PdxWriter writeShort(String fieldName, short value) {
    updateMetaData(fieldName, FieldType.SHORT);
    this.os.writeShort(value);
    return this;
  }

  public void writeShort(short value) {
    beforeFieldWrite();
    this.os.writeShort(value);
  }

  public PdxWriter writeInt(String fieldName, int value) {
    updateMetaData(fieldName, FieldType.INT);
    this.os.writeInt(value);
    return this;
  }

  public void writeInt(int value) {
    beforeFieldWrite();
    this.os.writeInt(value);
  }

  public PdxWriter writeLong(String fieldName, long value) {
    updateMetaData(fieldName, FieldType.LONG);
    this.os.writeLong(value);
    return this;
  }

  public void writeLong(long value) {
    beforeFieldWrite();
    this.os.writeLong(value);
  }

  public PdxWriter writeFloat(String fieldName, float value) {
    updateMetaData(fieldName, FieldType.FLOAT);
    this.os.writeFloat(value);
    return this;
  }

  public void writeFloat(float value) {
    beforeFieldWrite();
    this.os.writeFloat(value);
  }

  public PdxWriter writeDouble(String fieldName, double value) {
    updateMetaData(fieldName, FieldType.DOUBLE);
    this.os.writeDouble(value);
    return this;
  }

  public void writeDouble(double value) {
    beforeFieldWrite();
    this.os.writeDouble(value);
  }

  public PdxWriter writeDate(String fieldName, Date date) {
    if (date != null && !Date.class.equals(date.getClass())) {
      // fix for bug 43717
      throw new IllegalArgumentException(
          "writeDate only accepts instances of Date. Subclasses are not supported. Use writeObject for subclasses of Date.");
    }
    updateMetaData(fieldName, FieldType.DATE);
    this.os.writeDate(date);
    return this;
  }

  public void writeDate(Date date) {
    if (date != null && !Date.class.equals(date.getClass())) {
      // fix for bug 43717
      throw new IllegalArgumentException(
          "writeDate only accepts instances of Date. Subclasses are not supported. Use writeObject for subclasses of Date.");
    }
    beforeFieldWrite();
    this.os.writeDate(date);
  }

  public PdxWriter writeString(String fieldName, String value) {
    markVariableField();
    updateMetaData(fieldName, FieldType.STRING);
    this.os.writeString(value);
    return this;
  }

  public void writeString(String value) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeString(value);
  }

  public PdxWriter writeObject(String fieldName, Object object) {
    return writeObject(fieldName, object, false);
  }

  public void writeObject(Object object) {
    writeObject(object, false);
  }

  public PdxWriter writeObject(String fieldName, Object object, boolean onlyPortableObjects) {
    markVariableField();
    updateMetaData(fieldName, FieldType.OBJECT);
    this.os.writeObject(object, onlyPortableObjects);
    return this;
  }

  public void writeObject(Object object, boolean onlyPortableObjects) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeObject(object, onlyPortableObjects);
  }

  public PdxWriter writeBooleanArray(String fieldName, boolean[] array) {
    markVariableField();
    updateMetaData(fieldName, FieldType.BOOLEAN_ARRAY);
    this.os.writeBooleanArray(array);
    return this;
  }

  public void writeBooleanArray(boolean[] array) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeBooleanArray(array);
  }

  public PdxWriter writeCharArray(String fieldName, char[] array) {
    markVariableField();
    updateMetaData(fieldName, FieldType.CHAR_ARRAY);
    this.os.writeCharArray(array);
    return this;
  }

  public void writeCharArray(char[] array) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeCharArray(array);
  }

  public PdxWriter writeByteArray(String fieldName, byte[] array) {
    markVariableField();
    updateMetaData(fieldName, FieldType.BYTE_ARRAY);
    this.os.writeByteArray(array);
    return this;
  }

  public void writeByteArray(byte[] array) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeByteArray(array);
  }

  public PdxWriter writeShortArray(String fieldName, short[] array) {
    markVariableField();
    updateMetaData(fieldName, FieldType.SHORT_ARRAY);
    this.os.writeShortArray(array);
    return this;
  }

  public void writeShortArray(short[] array) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeShortArray(array);
  }

  public PdxWriter writeIntArray(String fieldName, int[] array) {
    markVariableField();
    updateMetaData(fieldName, FieldType.INT_ARRAY);
    this.os.writeIntArray(array);
    return this;
  }

  public void writeIntArray(int[] array) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeIntArray(array);
  }

  public PdxWriter writeLongArray(String fieldName, long[] array) {
    markVariableField();
    updateMetaData(fieldName, FieldType.LONG_ARRAY);
    this.os.writeLongArray(array);
    return this;
  }

  public void writeLongArray(long[] array) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeLongArray(array);
  }

  public PdxWriter writeFloatArray(String fieldName, float[] array) {
    markVariableField();
    updateMetaData(fieldName, FieldType.FLOAT_ARRAY);
    this.os.writeFloatArray(array);
    return this;
  }

  public void writeFloatArray(float[] array) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeFloatArray(array);
  }

  public PdxWriter writeDoubleArray(String fieldName, double[] array) {
    markVariableField();
    updateMetaData(fieldName, FieldType.DOUBLE_ARRAY);
    this.os.writeDoubleArray(array);
    return this;
  }

  public void writeDoubleArray(double[] array) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeDoubleArray(array);
  }

  public PdxWriter writeStringArray(String fieldName, String[] array) {
    markVariableField();
    updateMetaData(fieldName, FieldType.STRING_ARRAY);
    this.os.writeStringArray(array);
    return this;
  }

  public void writeStringArray(String[] array) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeStringArray(array);
  }

  public PdxWriter writeObjectArray(String fieldName, Object[] array) {
    return writeObjectArray(fieldName, array, false);
  }

  public void writeObjectArray(Object[] array) {
    writeObjectArray(array, false);
  }

  public PdxWriter writeObjectArray(String fieldName, Object[] array, boolean onlyPortableObjects) {
    markVariableField();
    updateMetaData(fieldName, FieldType.OBJECT_ARRAY);
    this.os.writeObjectArray(array, onlyPortableObjects);
    return this;
  }

  public void writeObjectArray(Object[] array, boolean onlyPortableObjects) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeObjectArray(array, onlyPortableObjects);
  }

  public PdxWriter writeArrayOfByteArrays(String fieldName, byte[][] array) {
    markVariableField();
    updateMetaData(fieldName, FieldType.ARRAY_OF_BYTE_ARRAYS);
    this.os.writeArrayOfByteArrays(array);
    return this;
  }

  public void writeArrayOfByteArrays(byte[][] array) {
    markVariableField();
    beforeFieldWrite();
    this.os.writeArrayOfByteArrays(array);
  }

  private boolean alreadyGenerated = false;

  /**
   * Must be invoked only after {@link PdxSerializable#toData(PdxWriter)}
   *
   * @return total number of bytes serialized for this pdx
   */
  public int completeByteStreamGeneration() {
    if (!this.alreadyGenerated) {
      this.alreadyGenerated = true;
      if (!fieldsWritten()) {
        initialize();
      }
      writeUnreadData();
      appendOffsets();
      int typeId;
      if (definingNewPdxType()) {
        this.newType.initialize(this);
        if (this.unreadData != null && !this.unreadData.isEmpty()) {
          // We created a new type that had unreadData.
          // In this case we don't define a local type
          // but we do set the serialized type.
          typeId = this.tr.defineType(newType);
          this.unreadData.setSerializedType(newType);
        } else {
          this.newType = this.tr.defineLocalType(this.pdx, newType);
          typeId = this.newType.getTypeId();
        }
      } else {
        if (doExtraValidation()) {
          int fieldCount = this.fieldId + 1;
          if (this.existingType.getFieldCount() != fieldCount) {
            throw new PdxSerializationException("Expected the number of fields for class "
                + this.existingType.getClassName() + " to be " + this.existingType.getFieldCount()
                + " but instead it was " + fieldCount);
          }
        }
        typeId = this.existingType.getTypeId();
      }

      // Now write length of the byte stream (does not include bytes for DSCODE and the length
      // itself.)
      long bits = ((long) getCurrentOffset()) << 32 | (0x00000000FFFFFFFFL & typeId); // fixes 45005
      this.lu.update(bits);
    } // !alreadyGenerated

    return getCurrentOffset() + 1; // +1 for DSCODE.PDX
  }

  /**
   * Returns the pdx type that can be used by the auto serializer to always serialize this class.
   */
  public PdxType getAutoPdxType() {
    if (this.unreadData != null && !this.unreadData.isEmpty()) {
      return null;
    }
    completeByteStreamGeneration();
    if (definingNewPdxType()) {
      return this.newType;
    } else {
      return this.existingType;
    }
  }

  public PdxType getPdxType() {
    return newType;
  }

  /**
   * @return the offset to the byte of the first field
   */
  private int getBaseOffset() {
    return this.headerOffset + DataSize.BYTE_SIZE + (DataSize.INTEGER_SIZE * 2);
  }

  private int getCurrentOffset() {
    return this.os.size() - getBaseOffset();
  }

  /**
   * Must be invoked only after {@link PdxSerializable#toData(PdxWriter)}
   */
  private void appendOffsets() {
    int fieldDataSize = getCurrentOffset();
    // Take the list of offsets and append it in reverse order.
    byte sizeOfOffset = getSizeOfOffset(this.vlfCount, fieldDataSize);
    for (int i = (this.vlfCount - 1); i >= 0; i--) {
      switch (sizeOfOffset) {
        case 1:
          this.os.write((byte) this.vlfOffsets[i]);
          break;
        case 2:
          this.os.writeShort((short) this.vlfOffsets[i]);
          break;
        case 4:
          this.os.writeInt(this.vlfOffsets[i]);
          break;
        default:
          break;
      }
    }
  }

  /**
   * This is required while writing the byte stream.
   *
   * @param offsetCount Number of offsets to appended in this byte stream.
   * @param size Size of the byte stream (excluding DSCODE, length int and the offsets.)
   * @return size of each offset
   */
  public static byte getSizeOfOffset(int offsetCount, int size) {
    if (offsetCount < 0 || size < 0) {
      throw new InternalGemFireException(
          "Values cannot be negative. offsetCount: " + offsetCount + ", size: " + size + " bytes");
    }

    if (((offsetCount * DataSize.BYTE_SIZE) + size) <= PdxReaderImpl.MAX_UNSIGNED_BYTE) {
      return DataSize.BYTE_SIZE;
    } else if (((offsetCount * DataSize.SHORT_SIZE) + size) <= PdxReaderImpl.MAX_UNSIGNED_SHORT) {
      return DataSize.SHORT_SIZE;
    } else {
      return DataSize.INTEGER_SIZE;
    }
  }

  public void sendTo(DataOutput out) throws IOException {
    this.os.sendTo(out);
  }

  public byte[] toByteArray() {
    return this.os.toByteArray();
  }

  private void markVariableField() {
    if (!this.hasSeenFirstVlf) {
      this.hasSeenFirstVlf = true;
    } else {
      ensureVlfCapacity();
      this.vlfOffsets[this.vlfCount] = getCurrentOffset();
      this.vlfCount++;
    }
  }

  /**
   * Make sure we have room to add a VLF offset.
   */
  private void ensureVlfCapacity() {
    int vlfOffsetsCapacity = 0;
    if (this.vlfOffsets != null) {
      vlfOffsetsCapacity = this.vlfOffsets.length;
    }
    if (this.vlfCount == vlfOffsetsCapacity) {
      int[] tmp = new int[vlfOffsetsCapacity + EXPAND_SIZE];
      for (int i = 0; i < vlfOffsetsCapacity; i++) {
        tmp[i] = this.vlfOffsets[i];
      }
      this.vlfOffsets = tmp;
    }
  }

  // only needed when creating a new type
  int getVlfCount() {
    return this.vlfCount;
  }

  public <CT, VT extends CT> PdxWriter writeField(String fieldName, VT fieldValue,
      Class<CT> fieldType) {
    return writeField(fieldName, fieldValue, fieldType, false);
  }

  public <CT, VT extends CT> PdxWriter writeField(String fieldName, VT fieldValue,
      Class<CT> fieldType, boolean onlyPortableObjects) {
    if (fieldType.equals(boolean.class)) {
      boolean v = false;
      if (fieldValue != null) {
        v = (Boolean) fieldValue;
      }
      writeBoolean(fieldName, v);
    } else if (fieldType.equals(byte.class)) {
      byte v = 0;
      if (fieldValue != null) {
        v = (Byte) fieldValue;
      }
      writeByte(fieldName, v);
    } else if (fieldType.equals(char.class)) {
      char v = 0;
      if (fieldValue != null) {
        v = (Character) fieldValue;
      }
      writeChar(fieldName, v);
    } else if (fieldType.equals(short.class)) {
      short v = 0;
      if (fieldValue != null) {
        v = (Short) fieldValue;
      }
      writeShort(fieldName, v);
    } else if (fieldType.equals(int.class)) {
      int v = 0;
      if (fieldValue != null) {
        v = (Integer) fieldValue;
      }
      writeInt(fieldName, v);
    } else if (fieldType.equals(long.class)) {
      long v = 0;
      if (fieldValue != null) {
        v = (Long) fieldValue;
      }
      writeLong(fieldName, v);
    } else if (fieldType.equals(float.class)) {
      float v = 0.0f;
      if (fieldValue != null) {
        v = (Float) fieldValue;
      }
      writeFloat(fieldName, v);
    } else if (fieldType.equals(double.class)) {
      double v = 0.0;
      if (fieldValue != null) {
        v = (Double) fieldValue;
      }
      writeDouble(fieldName, v);
    } else if (fieldType.equals(String.class)) {
      writeString(fieldName, (String) fieldValue);
    } else if (fieldType.isArray()) {
      if (fieldType.equals(boolean[].class)) {
        writeBooleanArray(fieldName, (boolean[]) fieldValue);
      } else if (fieldType.equals(byte[].class)) {
        writeByteArray(fieldName, (byte[]) fieldValue);
      } else if (fieldType.equals(char[].class)) {
        writeCharArray(fieldName, (char[]) fieldValue);
      } else if (fieldType.equals(short[].class)) {
        writeShortArray(fieldName, (short[]) fieldValue);
      } else if (fieldType.equals(int[].class)) {
        writeIntArray(fieldName, (int[]) fieldValue);
      } else if (fieldType.equals(long[].class)) {
        writeLongArray(fieldName, (long[]) fieldValue);
      } else if (fieldType.equals(float[].class)) {
        writeFloatArray(fieldName, (float[]) fieldValue);
      } else if (fieldType.equals(double[].class)) {
        writeDoubleArray(fieldName, (double[]) fieldValue);
      } else if (fieldType.equals(String[].class)) {
        writeStringArray(fieldName, (String[]) fieldValue);
      } else if (fieldType.equals(byte[][].class)) {
        writeArrayOfByteArrays(fieldName, (byte[][]) fieldValue);
      } else {
        writeObjectArray(fieldName, (Object[]) fieldValue, onlyPortableObjects);
      }
    } else if (fieldType.equals(Date.class)) {
      writeDate(fieldName, (Date) fieldValue);
    } else {
      writeObject(fieldName, fieldValue, onlyPortableObjects);
    }
    return this;
  }

  private void writeUnreadData() {
    if (this.unreadData != null) {
      this.unreadData.sendTo(this);
    }
  }

  public void writeRawField(PdxField ft, ByteSource data) {
    if (ft.isVariableLengthType()) {
      markVariableField();
    }
    updateMetaData(ft);
    this.os.write(data);
  }

  public void writeRawField(PdxField ft, byte[] data) {
    if (ft.isVariableLengthType()) {
      markVariableField();
    }
    updateMetaData(ft);
    this.os.write(data, 0, data.length);
  }

  void writeField(PdxField f, Object value) {
    switch (f.getFieldType()) {
      case CHAR:
        writeChar(null, (Character) value);
        break;
      case BOOLEAN:
        writeBoolean(null, (Boolean) value);
        break;
      case BYTE:
        writeByte(null, (Byte) value);
        break;
      case SHORT:
        writeShort(null, (Short) value);
        break;
      case INT:
        writeInt(null, (Integer) value);
        break;
      case FLOAT:
        writeFloat(null, (Float) value);
        break;
      case DOUBLE:
        writeDouble(null, (Double) value);
        break;
      case LONG:
        writeLong(null, (Long) value);
        break;
      case DATE:
        writeDate(null, (Date) value);
        break;
      case STRING:
        writeString(null, (String) value);
        break;
      case BOOLEAN_ARRAY:
        writeBooleanArray(null, (boolean[]) value);
        break;
      case CHAR_ARRAY:
        writeCharArray(null, (char[]) value);
        break;
      case BYTE_ARRAY:
        writeByteArray(null, (byte[]) value);
        break;
      case SHORT_ARRAY:
        writeShortArray(null, (short[]) value);
        break;
      case INT_ARRAY:
        writeIntArray(null, (int[]) value);
        break;
      case LONG_ARRAY:
        writeLongArray(null, (long[]) value);
        break;
      case FLOAT_ARRAY:
        writeFloatArray(null, (float[]) value);
        break;
      case DOUBLE_ARRAY:
        writeDoubleArray(null, (double[]) value);
        break;
      case STRING_ARRAY:
        writeStringArray(null, (String[]) value);
        break;
      case ARRAY_OF_BYTE_ARRAYS:
        writeArrayOfByteArrays(null, (byte[][]) value);
        break;
      case OBJECT_ARRAY:
        writeObjectArray(null, (Object[]) value);
        break;
      case OBJECT:
        writeObject(null, value);
        break;
      default:
        throw new InternalGemFireException("Unhandled field type " + f.getFieldType());
    }
  }

  private HeapDataOutputStream.LongUpdater lu;

  private void writeHeader() {
    this.os.write(DSCODE.PDX);
    this.lu = this.os.reserveLong(); // dummy length and type id
  }

  public boolean definingNewPdxType() {
    return this.newType != null;
  }

  // used by unit tests
  public void setDoExtraValidation(boolean v) {
    this.doExtraValidation = v;
  }

  private boolean doExtraValidation() {
    return this.doExtraValidation;
  }

  public PdxWriter markIdentityField(String fieldName) {
    if (definingNewPdxType()) {
      PdxField ft = this.newType.getPdxField(fieldName);
      if (ft == null) {
        throw new PdxFieldDoesNotExistException(
            "Field " + fieldName + " must be written before calling markIdentityField");
      }
      ft.setIdentityField(true);
    } else if (doExtraValidation()) {
      PdxField ft = this.existingType.getPdxField(fieldName);
      if (ft == null) {
        throw new PdxFieldDoesNotExistException(
            "Field " + fieldName + " must be written before calling markIdentityField");
      } else if (!ft.isIdentityField()) {
        throw new PdxSerializationException("Expected field " + fieldName
            + " to not be marked as an identity field since it was not for the first serialization");
      }
    }
    return this;
  }

  public PdxWriter writeUnreadFields(PdxUnreadFields unread) {
    if (fieldsWritten()) {
      throw new PdxFieldAlreadyExistsException(
          "writeUnreadFields must be called before any other fields are written.");
    }
    this.unreadData = (PdxUnreadData) unread;
    return this;
  }

  private void updateMetaData(String fieldName, FieldType type) {
    updateMetaData(fieldName, type, false);
  }

  private void updateMetaData(String fieldName, FieldType type, boolean isIdentityField) {
    beforeFieldWrite();
    if (definingNewPdxType()) {
      PdxField ft = new PdxField(fieldName, this.fieldId, this.vlfCount, type, isIdentityField);
      this.newType.addField(ft);
    } else if (doExtraValidation()) {
      PdxField ft = this.existingType.getPdxField(fieldName);
      if (ft == null) {
        throw new PdxSerializationException("Did not expect field " + fieldName
            + " to be serialized since it was not the first time this class was serialized.");
      }
      if (this.fieldId != ft.getFieldIndex()) {
        throw new PdxSerializationException(
            "Detected that the order in which the fields are serialized changed since the first time this class was serialized.");
      }
      if (!ft.getFieldType().equals(type)) {
        throw new PdxSerializationException("Expected field " + fieldName + " to be of type "
            + ft.getFieldType() + " not of type " + type);
      }
    }
  }

  private void updateMetaData(PdxField ft) {
    updateMetaData(ft.getFieldName(), ft.getFieldType(), ft.isIdentityField());
  }

  PdxInstance makePdxInstance() {
    ByteBuffer bb = this.os.toByteBuffer();
    bb.get(); // skip PDX DSCODE
    int len = bb.getInt();
    bb.getInt(); // skip PDX type
    PdxType pt = this.newType;
    if (pt == null) {
      pt = this.existingType;
    }
    return new PdxInstanceImpl(pt, new PdxInputStream(bb), len);
  }

  public static boolean isPdx(byte[] valueBytes) {
    if (valueBytes == null || valueBytes.length < 1) {
      return false;
    }
    return valueBytes[0] == DSCODE.PDX;
  }

  public int position() {
    return this.os.size();
  }

}
