/*
 * 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.util.Date;

import org.apache.geode.internal.InternalDataSerializer;
import org.apache.geode.internal.cache.GemFireCacheImpl;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.pdx.PdxInstance;
import org.apache.geode.pdx.PdxInstanceFactory;

/**
 * PdxInstances created with this factory can never be deserialized but you can access their fields
 * just like any other pdx.
 * <p>
 * The current implementation of this interface is meant for internal use only. The way it defines a
 * PdxType is expensive since it can never figure out it is already defined without doing an
 * expensive check in the type registry. We should optimize this before making this a public
 * feature.
 */
public class PdxInstanceFactoryImpl implements PdxInstanceFactory {

  private final PdxWriterImpl writer;

  private boolean created = false;

  private PdxInstanceFactoryImpl(String name, boolean expectDomainClass) {
    PdxOutputStream pdxOutputStream = new PdxOutputStream();
    PdxType pdxType = new PdxType(name, expectDomainClass);
    InternalCache internalCache = GemFireCacheImpl
        .getForPdx("PDX registry is unavailable because the Cache has been closed.");
    TypeRegistry pdxRegistry = internalCache.getPdxRegistry();
    this.writer = new PdxWriterImpl(pdxType, pdxRegistry, pdxOutputStream);
  }

  public static PdxInstanceFactory newCreator(String name, boolean expectDomainClass) {
    return new PdxInstanceFactoryImpl(name, expectDomainClass);
  }

  @Override
  public PdxInstance create() {
    if (this.created) {
      throw new IllegalStateException("The create method can only be called once.");
    }
    this.created = true;
    this.writer.completeByteStreamGeneration();
    return this.writer.makePdxInstance();
  }

  @Override
  public PdxInstanceFactory writeChar(String fieldName, char value) {
    this.writer.writeChar(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeBoolean(String fieldName, boolean value) {
    this.writer.writeBoolean(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeByte(String fieldName, byte value) {
    this.writer.writeByte(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeShort(String fieldName, short value) {
    this.writer.writeShort(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeInt(String fieldName, int value) {
    this.writer.writeInt(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeLong(String fieldName, long value) {
    this.writer.writeLong(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeFloat(String fieldName, float value) {
    this.writer.writeFloat(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeDouble(String fieldName, double value) {
    this.writer.writeDouble(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeDate(String fieldName, Date value) {
    this.writer.writeDate(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeString(String fieldName, String value) {
    this.writer.writeString(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeObject(String fieldName, Object value) {
    return writeObject(fieldName, value, false);
  }

  @Override
  public PdxInstanceFactory writeBooleanArray(String fieldName, boolean[] value) {
    this.writer.writeBooleanArray(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeCharArray(String fieldName, char[] value) {
    this.writer.writeCharArray(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeByteArray(String fieldName, byte[] value) {
    this.writer.writeByteArray(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeShortArray(String fieldName, short[] value) {
    this.writer.writeShortArray(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeIntArray(String fieldName, int[] value) {
    this.writer.writeIntArray(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeLongArray(String fieldName, long[] value) {
    this.writer.writeLongArray(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeFloatArray(String fieldName, float[] value) {
    this.writer.writeFloatArray(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeDoubleArray(String fieldName, double[] value) {
    this.writer.writeDoubleArray(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeStringArray(String fieldName, String[] value) {
    this.writer.writeStringArray(fieldName, value);
    return this;
  }

  @Override
  public PdxInstanceFactory writeObjectArray(String fieldName, Object[] value) {
    return writeObjectArray(fieldName, value, false);
  }

  @Override
  public PdxInstanceFactory writeArrayOfByteArrays(String fieldName, byte[][] value) {
    this.writer.writeArrayOfByteArrays(fieldName, value);
    return this;
  }

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

  @Override
  public PdxInstanceFactory markIdentityField(String fieldName) {
    this.writer.markIdentityField(fieldName);
    return this;
  }

  @Override
  public PdxInstanceFactory writeObject(String fieldName, Object value, boolean checkPortability) {
    if (InternalDataSerializer.is662SerializationEnabled()) {
      boolean alreadyInProgress = InternalDataSerializer.isPdxSerializationInProgress();
      if (!alreadyInProgress) {
        InternalDataSerializer.setPdxSerializationInProgress(true);
        try {
          this.writer.writeObject(fieldName, value, checkPortability);
        } finally {
          InternalDataSerializer.setPdxSerializationInProgress(false);
        }
      } else {
        this.writer.writeObject(fieldName, value, checkPortability);
      }
    } else {
      this.writer.writeObject(fieldName, value, checkPortability);
    }

    return this;
  }

  @Override
  public PdxInstanceFactory writeObjectArray(String fieldName, Object[] value,
      boolean checkPortability) {
    if (InternalDataSerializer.is662SerializationEnabled()) {
      boolean alreadyInProgress = InternalDataSerializer.isPdxSerializationInProgress();
      if (!alreadyInProgress) {
        InternalDataSerializer.setPdxSerializationInProgress(true);
        try {
          this.writer.writeObjectArray(fieldName, value, checkPortability);
        } finally {
          InternalDataSerializer.setPdxSerializationInProgress(false);
        }
      } else {
        this.writer.writeObjectArray(fieldName, value, checkPortability);
      }
    } else {
      this.writer.writeObjectArray(fieldName, value, checkPortability);
    }
    return this;
  }

  @Override
  public <CT, VT extends CT> PdxInstanceFactory writeField(String fieldName, VT fieldValue,
      Class<CT> fieldType, boolean checkPortability) {
    if (InternalDataSerializer.is662SerializationEnabled()) {
      boolean alreadyInProgress = InternalDataSerializer.isPdxSerializationInProgress();
      if (!alreadyInProgress) {
        InternalDataSerializer.setPdxSerializationInProgress(true);
        try {
          this.writer.writeField(fieldName, fieldValue, fieldType, checkPortability);
        } finally {
          InternalDataSerializer.setPdxSerializationInProgress(false);
        }
      } else {
        this.writer.writeField(fieldName, fieldValue, fieldType, checkPortability);
      }
    } else {
      this.writer.writeField(fieldName, fieldValue, fieldType, checkPortability);
    }
    return this;
  }

  public static PdxInstance createPdxEnum(String className, String enumName, int enumOrdinal,
      InternalCache internalCache) {
    if (className == null) {
      throw new IllegalArgumentException("className must not be null");
    }
    if (enumName == null) {
      throw new IllegalArgumentException("enumName must not be null");
    }
    TypeRegistry tr = internalCache.getPdxRegistry();
    EnumInfo ei = new EnumInfo(className, enumName, enumOrdinal);
    return ei.getPdxInstance(tr.defineEnum(ei));
  }

}
