/*
 * 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.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.geode.DataSerializable;
import org.apache.geode.DataSerializer;
import org.apache.geode.internal.InternalDataSerializer;
import org.apache.geode.internal.Version;
import org.apache.geode.internal.cache.tier.sockets.OldClientSupportService;
import org.apache.geode.pdx.PdxFieldAlreadyExistsException;
import org.apache.geode.pdx.internal.AutoSerializableManager.AutoClassInfo;

public class PdxType implements DataSerializable {

  private static final long serialVersionUID = -1950047949756115279L;

  private int cachedHash = 0;

  private int typeId;
  private String className;
  private boolean noDomainClass;
  /**
   * Will be set to true if any fields on this type have been deleted.
   *
   * @since GemFire 8.1
   */
  private boolean hasDeletedField;

  /**
   * A count of the total number of variable length field offsets.
   */
  private int vlfCount;

  private final ArrayList<PdxField> fields = new ArrayList<PdxField>();

  private final transient Map<String, PdxField> fieldsMap = new HashMap<String, PdxField>();
  private transient volatile SortedSet<PdxField> sortedIdentityFields;

  public PdxType() {
    // for deserialization
  }

  public PdxType(String name, boolean expectDomainClass) {
    this.className = name;
    this.noDomainClass = !expectDomainClass;
    swizzleGemFireClassNames();
  }

  public PdxType(PdxType copy) {
    this.typeId = copy.typeId;
    this.className = copy.className;
    this.noDomainClass = copy.noDomainClass;
    this.vlfCount = copy.vlfCount;
    for (PdxField ft : copy.fields) {
      addField(ft);
    }
  }

  private static final byte NO_DOMAIN_CLASS_BIT = 1;
  private static final byte HAS_DELETED_FIELD_BIT = 2;

  private void swizzleGemFireClassNames() {
    OldClientSupportService svc = InternalDataSerializer.getOldClientSupportService();
    if (svc != null) {
      this.className = svc.processIncomingClassName(this.className);
    }
  }

  public void fromData(DataInput in) throws IOException, ClassNotFoundException {
    this.className = DataSerializer.readString(in);
    swizzleGemFireClassNames();
    {
      byte bits = in.readByte();
      this.noDomainClass = (bits & NO_DOMAIN_CLASS_BIT) != 0;
      this.hasDeletedField = (bits & HAS_DELETED_FIELD_BIT) != 0;
    }

    this.typeId = in.readInt();
    this.vlfCount = in.readInt();

    int arrayLen = InternalDataSerializer.readArrayLength(in);

    for (int i = 0; i < arrayLen; i++) {
      PdxField vft = new PdxField();
      vft.fromData(in);
      addField(vft);
    }
  }

  public void toData(DataOutput out) throws IOException {
    DataSerializer.writeString(this.className, out);
    {
      // pre 8.1 we wrote a single boolean
      // 8.1 and after we write a byte whose bits are:
      // 1: noDomainClass
      // 2: hasDeletedField
      byte bits = 0;
      if (this.noDomainClass) {
        bits |= NO_DOMAIN_CLASS_BIT;
      }
      // Note that this code attempts to only set the HAS_DELETED_FIELD_BIT
      // if serializing for 8.1 or later.
      // But in some cases 8.1 serialized data may be sent to a pre 8.1 member.
      // In that case if this bit is set it will cause the pre 8.1 member
      // to set noDomainClass to true.
      // For this reason the pdx delete-field command should only be used after
      // all member have been upgraded to 8.1 or later.
      Version sourceVersion = InternalDataSerializer.getVersionForDataStream(out);
      if (sourceVersion.compareTo(Version.GFE_81) >= 0) {
        if (this.hasDeletedField) {
          bits |= HAS_DELETED_FIELD_BIT;
        }
      }
      out.writeByte(bits);
    }

    out.writeInt(this.typeId);
    out.writeInt(this.vlfCount);

    InternalDataSerializer.writeArrayLength(this.fields.size(), out);

    for (int i = 0; i < this.fields.size(); i++) {

      PdxField vft = this.fields.get(i);
      vft.toData(out);
    }
  }

  @Override
  public int hashCode() {
    int hash = cachedHash;
    if (hash == 0) {
      hash = 1;
      hash = hash * 31 + this.className.hashCode();
      for (PdxField field : this.fields) {
        hash = hash * 31 + field.hashCode();
      }
      if (hash == 0) {
        hash = 1;
      }
      cachedHash = hash;
    }
    return cachedHash;
  }

  @Override
  public boolean equals(Object other) {
    if (other == this) {
      return true;
    }
    if (other == null || !(other instanceof PdxType)) {
      return false;
    }
    // Note: do not compare type id in equals
    PdxType otherVT = (PdxType) other;
    if (!(this.className.equals(otherVT.className))) {
      return false;
    }
    if (this.noDomainClass != otherVT.noDomainClass) {
      return false;
    }
    if (otherVT.fields.size() != this.fields.size() || otherVT.vlfCount != this.vlfCount) {
      return false;
    }
    for (int i = 0; i < this.fields.size(); i++) {
      if (!this.fields.get(i).equals(otherVT.fields.get(i))) {
        return false;
      }
    }
    return true;
  }

  /**
   * Return true if two pdx types have same class name and the same fields but, unlike equals, field
   * order does not matter. Note a type that expects a domain class can be compatible with one that
   * does not expect a domain class.
   *
   * @param other the other pdx type
   * @return true if two pdx types are compatible.
   */
  public boolean compatible(PdxType other) {
    if (other == null)
      return false;
    if (!getClassName().equals(other.getClassName())) {
      return false;
    }

    Collection<PdxField> myFields = getSortedFields();
    Collection<PdxField> otherFields = other.getSortedFields();

    return myFields.equals(otherFields);
  }

  public int getVariableLengthFieldCount() {
    return this.vlfCount;
  }

  public String getClassName() {
    return this.className;
  }

  public boolean getNoDomainClass() {
    return this.noDomainClass;
  }

  public int getTypeId() {
    return this.typeId;
  }

  public int getDSId() {
    return this.typeId >> 24 & 0xFF;
  }

  public int getTypeNum() {
    return this.typeId & 0x00FFFFFF;
  }

  public void setTypeId(int tId) {
    this.typeId = tId;
  }

  /*
   * This method is use to create Pdxtype for which classname is not available; while creating
   * PdxInstance
   */
  public void setClassName(String className) {
    this.className = className;
  }

  public void addField(PdxField ft) {
    if (this.fieldsMap.put(ft.getFieldName(), ft) != null) {
      throw new PdxFieldAlreadyExistsException(
          "The field \"" + ft.getFieldName() + "\" already exists.");
    }
    this.fields.add(ft);
  }

  public void initialize(PdxWriterImpl writer) {
    this.vlfCount = writer.getVlfCount();
    int size = this.fields.size();
    int fixedLenFieldOffset = 0;
    boolean seenVariableLenType = false;
    for (int i = 0; i < size; i++) {
      PdxField vft = this.fields.get(i);
      // System.out.println(i + ": " + vft);
      if (vft.isVariableLengthType()) {
        if (seenVariableLenType) {
          vft.setVlfOffsetIndex(vft.getVarLenFieldSeqId());
        } else {
          vft.setRelativeOffset(fixedLenFieldOffset);
          vft.setVlfOffsetIndex(-1);
        }
        seenVariableLenType = true;
      } else if (seenVariableLenType) {
        PdxField tmp = null;
        int minusOffset = vft.getFieldType().getWidth();
        for (int j = (i + 1); j < size; j++) {
          tmp = this.fields.get(j);
          if (tmp.isVariableLengthType()) {
            break;
          } else {
            minusOffset += tmp.getFieldType().getWidth();
          }
        }
        if (tmp != null && tmp.isVariableLengthType()) {
          vft.setRelativeOffset(-minusOffset);
          vft.setVlfOffsetIndex(tmp.getVarLenFieldSeqId());
        } else {
          vft.setRelativeOffset(-minusOffset);
          vft.setVlfOffsetIndex(-1); // From the byte after the last field data byte
        }
      } else {
        vft.setRelativeOffset(fixedLenFieldOffset);
        fixedLenFieldOffset += vft.getFieldType().getWidth();
      }
    }
    // no longer mark identity fields implicitly. Fixes bug 42976.

    // System.out.println("Printing the position array:");
    // for (int i = 0; i < this.positionArray.length; i++) {
    // System.out.println("[" + i + "][0]=" + this.positionArray[i][0] + ", ["
    // + i + "][1]=" + this.positionArray[i][1]);
    // }
  }

  public PdxField getPdxField(String fieldName) {
    PdxField result = this.fieldsMap.get(fieldName);
    if (result != null && result.isDeleted()) {
      result = null;
    }
    return result;
  }

  public List<PdxField> getFields() {
    return Collections.unmodifiableList(this.fields);
  }

  public PdxField getPdxFieldByIndex(int index) {
    return this.fields.get(index);
  }

  public int getFieldCount() {
    return this.fields.size();
  }

  public int getUndeletedFieldCount() {
    if (!getHasDeletedField()) {
      return 0;
    }
    int result = this.fields.size();
    for (PdxField f : this.fields) {
      if (f.isDeleted()) {
        result--;
      }
    }
    return result;
  }

  public String toFormattedString() {
    StringBuffer sb = new StringBuffer("PdxType[");
    sb.append("dsid=").append(getDSId());
    sb.append(", typenum=").append(getTypeNum());
    sb.append("\n        name=").append(this.className);
    sb.append("\n        fields=[");
    for (PdxField vft : fields) {
      sb.append("\n        ");
      sb.append(/* vft.getFieldName() + ":" + vft.getTypeId() */ vft.toString());
    }
    sb.append("]]");
    return sb.toString();
  }

  public String toString() {
    StringBuffer sb = new StringBuffer("PdxType[");
    sb.append("dsid=").append(getDSId());
    sb.append(",typenum=").append(getTypeNum());
    sb.append(",name=").append(this.className);
    sb.append(",fields=[");
    for (PdxField vft : fields) {
      sb.append(/* vft.getFieldName() + ":" + vft.getTypeId() */ vft.toString()).append(", ");
    }
    sb.append("]]");
    return sb.toString();
  }

  /**
   *
   * @param readFields the fields that have been read
   * @return a List of fields that have not been read (may be empty).
   */
  public List<Integer> getUnreadFieldIndexes(List<String> readFields) {
    ArrayList<Integer> result = new ArrayList<Integer>();
    for (PdxField ft : this.fields) {
      if (!ft.isDeleted() && !readFields.contains(ft.getFieldName())) {
        result.add(ft.getFieldIndex());
      }
    }
    return result;
  }

  /**
   * Return true if the this type has a field that the other type does not have.
   *
   * @param other the type we are comparing to
   * @return true if the this type has a field that the other type does not have.
   */
  public boolean hasExtraFields(PdxType other) {
    for (PdxField ft : this.fields) {
      if (!ft.isDeleted() && other.getPdxField(ft.getFieldName()) == null) {
        return true;
      }
    }
    return false;
  }

  // Result does not include deleted fields
  public SortedSet<PdxField> getSortedIdentityFields() {
    if (this.sortedIdentityFields == null) {
      TreeSet<PdxField> sortedSet = new TreeSet<PdxField>();
      for (PdxField field : fields) {
        if (field.isIdentityField() && !field.isDeleted()) {
          sortedSet.add(field);
        }
      }
      // If we don't find any marked identity fields, use all of the fields.
      if (sortedSet.isEmpty()) {
        for (PdxField field : fields) {
          if (!field.isDeleted()) {
            sortedSet.add(field);
          }
        }
      }
      this.sortedIdentityFields = sortedSet;
    }
    return this.sortedIdentityFields;
  }

  // Result does not include deleted fields
  public Collection<PdxField> getSortedFields() {
    TreeSet<PdxField> sortedSet = new TreeSet<PdxField>();
    for (PdxField pf : this.fields) {
      if (!pf.isDeleted()) {
        sortedSet.add(pf);
      }
    }
    return new ArrayList<PdxField>(sortedSet);
  }

  // Result does not include deleted fields
  public List<String> getFieldNames() {
    ArrayList<String> result = new ArrayList<String>(this.fields.size());
    for (PdxField f : this.fields) {
      if (!f.isDeleted()) {
        result.add(f.getFieldName());
      }
    }
    return Collections.unmodifiableList(result);
  }

  /**
   * Used to optimize auto deserialization
   */
  private final transient AtomicReference<AutoClassInfo> autoClassInfo =
      new AtomicReference<AutoClassInfo>();

  public void setAutoInfo(AutoClassInfo autoClassInfo) {
    this.autoClassInfo.set(autoClassInfo);
  }

  public AutoClassInfo getAutoInfo(Class<?> c) {
    AutoClassInfo ci = this.autoClassInfo.get();
    if (ci != null) {
      Class<?> lastClassAutoSerialized = ci.getInfoClass();
      if (c.equals(lastClassAutoSerialized)) {
        return ci;
      } else {
        if (lastClassAutoSerialized == null) {
          this.autoClassInfo.compareAndSet(ci, null);
        }
      }
    }
    return null;
  }

  public void toStream(PrintStream printStream, boolean printFields) {
    printStream.print("  ");
    printStream.print(getClassName());
    printStream.print(": ");
    printStream.print("id=");
    printStream.print(getTypeNum());
    if (getDSId() != 0) {
      printStream.print(" dsId=");
      printStream.print(getDSId());
    }
    printStream.println();
    if (printFields) {
      for (PdxField field : this.fields) {
        field.toStream(printStream);
      }
    }
  }

  public boolean getHasDeletedField() {
    return this.hasDeletedField;
  }

  public void setHasDeletedField(boolean b) {
    this.hasDeletedField = b;
  }
}
