/*
 * 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.internal.cache.tier.sockets;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.logging.log4j.Logger;

import org.apache.geode.DataSerializer;
import org.apache.geode.InternalGemFireException;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.internal.DM;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.internal.DataSerializableFixedID;
import org.apache.geode.internal.InternalDataSerializer;
import org.apache.geode.internal.Version;
import org.apache.geode.internal.cache.versions.DiskVersionTag;
import org.apache.geode.internal.cache.versions.VersionSource;
import org.apache.geode.internal.cache.versions.VersionTag;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.internal.logging.log4j.LogMarker;

/**
 * A refinement of ObjectPartList that adds per-entry versionTags and has its own serialized form to
 * allow key lists when there are no object components.
 * <P>
 * This class also implements Externalizable so that it can be serialized as part of a
 * PutAllPartialResultException.
 *
 * @since GemFire 7.0
 */

public class VersionedObjectList extends ObjectPartList implements Externalizable {
  private static final Logger logger = LogService.getLogger();
  private static final long serialVersionUID = -8384671357532347892L;

  ArrayList<VersionTag> versionTags;

  private boolean regionIsVersioned;

  /** a flag telling us that after transmission of the list all objects should remain serialized */
  private boolean serializeValues;

  /**
   * add a new entry to the list
   *
   * @param key the entry's key
   * @param versionTag the version tag for the entry, or null if there is no version information
   *        available
   */
  public void addKeyAndVersion(Object key, VersionTag versionTag) {
    if (logger.isTraceEnabled(LogMarker.VERSIONED_OBJECT_LIST)) {
      logger.trace(LogMarker.VERSIONED_OBJECT_LIST, "VersionedObjectList.addKeyAndVersion({}; {})",
          key, versionTag);
    }
    if (this.objects.size() > 0) {
      throw new IllegalStateException("attempt to add key/version to a list containing objects");
    }
    this.keys.add(key);
    if (this.regionIsVersioned) {
      // Assert.assertTrue(versionTag != null);
      this.versionTags.add(versionTag);
    }
  }

  @Override
  public void addPart(Object key, Object value, byte objectType, VersionTag versionTag) {
    if (logger.isTraceEnabled(LogMarker.VERSIONED_OBJECT_LIST)) {
      logger.trace(LogMarker.VERSIONED_OBJECT_LIST, "addPart({}; {}; {}; {}", key, value,
          objectType, versionTag);
    }
    super.addPart(key, value, objectType, versionTag);
    if (this.regionIsVersioned) {
      int tagsSize = this.versionTags.size();
      if (keys != null && (tagsSize != this.keys.size() - 1)) {
        // this should not happen - either all or none of the entries should have tags
        throw new InternalGemFireException();
      }
      if (this.objects != null && (this.objects.size() > 0)
          && (tagsSize != this.objects.size() - 1)) {
        // this should not happen - either all or none of the entries should have tags
        throw new InternalGemFireException();
      }
      // Assert.assertTrue(versionTag != null);
      this.versionTags.add(versionTag);
    }
  }

  /**
   * add a versioned "map" entry to the list
   *
   * @param key
   * @param value
   * @param versionTag
   */
  public void addObject(Object key, Object value, VersionTag versionTag) {
    addPart(key, value, OBJECT, versionTag);
  }

  // public methods

  public VersionedObjectList() {
    super();
    this.versionTags = new ArrayList();
  }

  public VersionedObjectList(boolean serializeValues) {
    this();
    this.serializeValues = serializeValues;
  }

  public VersionedObjectList(int maxSize, boolean hasKeys, boolean regionIsVersioned) {
    this(maxSize, hasKeys, regionIsVersioned, false);
  }

  public VersionedObjectList(int maxSize, boolean hasKeys, boolean regionIsVersioned,
      boolean serializeValues) {
    super(maxSize, hasKeys);
    if (regionIsVersioned) {
      this.versionTags = new ArrayList(maxSize);
      this.regionIsVersioned = true;
    } else {
      this.versionTags = new ArrayList();
    }
    this.serializeValues = serializeValues;
  }

  /**
   * replace null membership IDs in version tags with the given member ID. VersionTags received from
   * a server may have null IDs because they were operations performed by that server. We transmit
   * them as nulls to cut costs, but have to do the swap on the receiving end (in the client)
   *
   * @param sender
   */
  public void replaceNullIDs(DistributedMember sender) {
    for (VersionTag versionTag : versionTags) {
      if (versionTag != null) {
        versionTag.replaceNullIDs((InternalDistributedMember) sender);
      }
    }
  }

  @Override
  public void addObjectPartForAbsentKey(Object key, Object value) {
    addPart(key, value, KEY_NOT_AT_SERVER, null);
  }

  /**
   * Add a part for a destroyed or missing entry. If the version tag is not null this represents a
   * tombstone.
   *
   * @param key
   * @param value
   * @param version
   */
  public void addObjectPartForAbsentKey(Object key, Object value, VersionTag version) {
    addPart(key, value, KEY_NOT_AT_SERVER, version);
  }

  @Override
  public void addAll(ObjectPartList other) {
    if (logger.isTraceEnabled(LogMarker.VERSIONED_OBJECT_LIST)) {
      logger.trace(LogMarker.VERSIONED_OBJECT_LIST, "VOL.addAll(other={}; this={}", other, this);
    }
    int myTypeArrayLength = this.hasKeys ? this.keys.size() : this.objects.size();
    int otherTypeArrayLength = other.hasKeys ? other.keys.size() : other.objects.size();
    super.addAll(other);
    VersionedObjectList vother = (VersionedObjectList) other;
    this.regionIsVersioned |= vother.regionIsVersioned;
    this.versionTags.addAll(vother.versionTags);
    if (myTypeArrayLength > 0 || otherTypeArrayLength > 0) {
      int newSize = myTypeArrayLength + otherTypeArrayLength;
      if (this.objectTypeArray != null) {
        newSize = Math.max(newSize, this.objectTypeArray.length);
        if (this.objectTypeArray.length < newSize) { // need more room
          byte[] temp = this.objectTypeArray;
          this.objectTypeArray = new byte[newSize];
          System.arraycopy(temp, 0, this.objectTypeArray, 0, temp.length);
        }
      } else {
        this.objectTypeArray = new byte[newSize];
      }
      if (other.objectTypeArray != null) {
        System.arraycopy(other.objectTypeArray, 0, this.objectTypeArray, myTypeArrayLength,
            otherTypeArrayLength);
      }
    }
  }

  public List<VersionTag> getVersionTags() {
    return Collections.unmodifiableList(this.versionTags);
  }

  public boolean hasVersions() {
    return this.versionTags.size() > 0;
  }

  /** clear the version tags from this list */
  public void clearVersions() {
    this.versionTags = new ArrayList<VersionTag>(Math.max(50, this.size()));
  }

  /**
   * Add a version, assuming that the key list has already been established and we are now filling
   * in version information.
   *
   * @param tag the version tag to add
   */
  public void addVersion(VersionTag tag) {
    this.versionTags.add(tag);
  }

  /**
   * save the current key/tag pairs in the given map
   *
   * @param vault
   */
  public void saveVersions(Map<Object, VersionTag> vault) {
    Iterator it = this.iterator();
    while (it.hasNext()) {
      Entry e = it.next();
      if (e.getVersionTag() != null || !vault.containsKey(e.getKey())) {
        // bug 51850: There could be duplicated keys in removeAll. If non-singlehop, the returned
        // version tags from the each primary bucket will be conflated here.
        vault.put(e.getKey(), e.getVersionTag());
      }
    }
  }

  /**
   *
   * @return whether the source region had concurrency checks enabled
   */
  public boolean regionIsVersioned() {
    return this.regionIsVersioned;
  }

  /**
   * add some versionless keys
   */
  public void addAllKeys(Collection<?> keys) {
    if (!this.hasKeys) {
      this.hasKeys = true;
      this.keys = new ArrayList(keys);
    } else {
      this.keys.addAll(keys);
    }
  }

  @Override
  public void reinit(int maxSize) {
    super.reinit(maxSize);
    this.versionTags.clear();
  }

  /** sets the keys for this partlist */
  public VersionedObjectList setKeys(List newKeys) {
    this.keys = newKeys;
    this.hasKeys = (this.keys != null);
    return this;
  }

  public void clearObjects() {
    this.objects = Collections.emptyList();
    this.objectTypeArray = new byte[0];
  }

  @Override
  public void clear() {
    super.clear();
    this.versionTags.clear();
  }

  public void processVersionTags(InternalDistributedMember sender) {

  }

  private static Version[] serializationVersions = new Version[] {Version.GFE_80};

  @Override
  public Version[] getSerializationVersions() {
    return serializationVersions;
  }

  public void toDataPre_GFE_8_0_0_0(DataOutput out) throws IOException {
    getCanonicalIDs();
    toData(out);
  }

  public void fromDataPre_GFE_8_0_0_0(DataInput in) throws IOException, ClassNotFoundException {
    fromData(in);
  }

  /*
   * for backward compatibility we need to make sure the IDs in the version tags aren't the partial
   * IDs sent in serialized tags but the full tags. See bug #50063
   */
  private void getCanonicalIDs() {
    if (this.versionTags != null) {
      DM dm = InternalDistributedSystem.getConnectedInstance().getDistributionManager();
      if (dm != null) {
        for (VersionTag tag : this.versionTags) {
          if (tag != null) {
            tag.setCanonicalIDs(dm);
          }
        }
      }
    }
  }

  static final byte FLAG_NULL_TAG = 0;
  static final byte FLAG_FULL_TAG = 1;
  static final byte FLAG_TAG_WITH_NEW_ID = 2;
  static final byte FLAG_TAG_WITH_NUMBER_ID = 3;

  @Override
  public void toData(DataOutput out) throws IOException {
    toData(out, 0, this.regionIsVersioned ? this.versionTags.size() : size(), true, true);
  }

  void toData(DataOutput out, int startIndex, int numEntries, boolean sendKeys, boolean sendObjects)
      throws IOException {
    int flags = 0;
    boolean hasObjects = false;
    boolean hasTags = false;
    if (sendKeys && this.hasKeys) {
      flags |= 0x01;
    }
    if (sendObjects && !this.objects.isEmpty()) {
      flags |= 0x02;
      hasObjects = true;
    }
    if (this.versionTags.size() > 0) {
      flags |= 0x04;
      hasTags = true;
      for (VersionTag tag : this.versionTags) {
        if (tag != null) {
          if (tag instanceof DiskVersionTag) {
            flags |= 0x20;
          }
          break;
        }
      }
    }
    if (this.regionIsVersioned) {
      flags |= 0x08;
    }
    if (this.serializeValues) {
      flags |= 0x10;
    }
    if (logger.isTraceEnabled(LogMarker.VERSIONED_OBJECT_LIST)) {
      logger.trace(LogMarker.VERSIONED_OBJECT_LIST,
          "serializing {} with flags 0x{} startIndex={} numEntries={}", this,
          Integer.toHexString(flags), startIndex, numEntries);
    }
    out.writeByte(flags);
    if (sendKeys && hasKeys) {
      int numToWrite = numEntries;
      if (numToWrite + startIndex > this.keys.size()) {
        numToWrite = Math.max(0, this.keys.size() - startIndex);
      }
      InternalDataSerializer.writeUnsignedVL(numToWrite, out);
      int index = startIndex;
      for (int i = 0; i < numToWrite; i++, index++) {
        DataSerializer.writeObject(this.keys.get(index), out);
      }
    }
    if (sendObjects && hasObjects) {
      int numToWrite = numEntries;
      if (numToWrite + startIndex > this.objects.size()) {
        numToWrite = Math.max(0, this.objects.size() - startIndex);
      }
      InternalDataSerializer.writeUnsignedVL(numToWrite, out);
      int idx = 0;
      int index = startIndex;
      for (int i = 0; i < numToWrite; i++, index++) {
        writeObject(this.objects.get(index), idx++, out);
      }
    }
    if (hasTags) {
      int numToWrite = numEntries;
      if (numToWrite + startIndex > this.versionTags.size()) {
        numToWrite = Math.max(0, this.versionTags.size() - startIndex);
      }
      InternalDataSerializer.writeUnsignedVL(numToWrite, out);
      Map<VersionSource, Integer> ids = new HashMap<VersionSource, Integer>(numToWrite);
      int idCount = 0;
      int index = startIndex;
      for (int i = 0; i < numToWrite; i++, index++) {
        VersionTag tag = this.versionTags.get(index);
        if (tag == null) {
          out.writeByte(FLAG_NULL_TAG);
        } else {
          VersionSource id = tag.getMemberID();
          if (id == null) {
            out.writeByte(FLAG_FULL_TAG);
            InternalDataSerializer.invokeToData(tag, out);
          } else {
            Integer idNumber = ids.get(id);
            if (idNumber == null) {
              out.writeByte(FLAG_TAG_WITH_NEW_ID);
              idNumber = Integer.valueOf(idCount++);
              ids.put(id, idNumber);
              InternalDataSerializer.invokeToData(tag, out);
            } else {
              out.writeByte(FLAG_TAG_WITH_NUMBER_ID);
              tag.toData(out, false);
              tag.setMemberID(id);
              InternalDataSerializer.writeUnsignedVL(idNumber, out);
            }
          }
        }
      }
    }
  }

  @Override
  public void fromData(DataInput in) throws IOException, ClassNotFoundException {
    final boolean isDebugEnabled_VOL = logger.isTraceEnabled(LogMarker.VERSIONED_OBJECT_LIST);
    int flags = in.readByte();
    this.hasKeys = (flags & 0x01) == 0x01;
    boolean hasObjects = (flags & 0x02) == 0x02;
    boolean hasTags = (flags & 0x04) == 0x04;
    this.regionIsVersioned = (flags & 0x08) == 0x08;
    this.serializeValues = (flags & 0x10) == 0x10;
    boolean persistent = (flags & 0x20) == 0x20;
    if (isDebugEnabled_VOL) {
      logger.trace(LogMarker.VERSIONED_OBJECT_LIST,
          "deserializing a VersionedObjectList with flags 0x{}", Integer.toHexString(flags));
    }
    if (this.hasKeys) {
      int size = (int) InternalDataSerializer.readUnsignedVL(in);
      this.keys = new ArrayList(size);
      if (isDebugEnabled_VOL) {
        logger.trace(LogMarker.VERSIONED_OBJECT_LIST, "reading {} keys", size);
      }
      for (int i = 0; i < size; i++) {
        this.keys.add(DataSerializer.readObject(in));
      }
    }
    if (hasObjects) {
      int size = (int) InternalDataSerializer.readUnsignedVL(in);
      if (isDebugEnabled_VOL) {
        logger.trace(LogMarker.VERSIONED_OBJECT_LIST, "reading {} objects", size);
      }
      this.objects = new ArrayList(size);
      this.objectTypeArray = new byte[size];
      for (int i = 0; i < size; i++) {
        readObject(i, in);
      }
    } else {
      this.objects = new ArrayList();
    }
    if (hasTags) {
      int size = (int) InternalDataSerializer.readUnsignedVL(in);
      if (isDebugEnabled_VOL) {
        logger.trace(LogMarker.VERSIONED_OBJECT_LIST, "reading {} version tags", size);
      }
      this.versionTags = new ArrayList<VersionTag>(size);
      List<VersionSource> ids = new ArrayList<VersionSource>(size);
      for (int i = 0; i < size; i++) {
        byte entryType = in.readByte();
        switch (entryType) {
          case FLAG_NULL_TAG:
            this.versionTags.add(null);
            break;
          case FLAG_FULL_TAG:
            this.versionTags.add(VersionTag.create(persistent, in));
            break;
          case FLAG_TAG_WITH_NEW_ID:
            VersionTag tag = VersionTag.create(persistent, in);
            ids.add(tag.getMemberID());
            this.versionTags.add(tag);
            break;
          case FLAG_TAG_WITH_NUMBER_ID:
            tag = VersionTag.create(persistent, in);
            int idNumber = (int) InternalDataSerializer.readUnsignedVL(in);
            tag.setMemberID(ids.get(idNumber));
            this.versionTags.add(tag);
            break;
        }
      }
    } else {
      this.versionTags = new ArrayList<VersionTag>();
    }
  }

  private void writeObject(Object value, int index, DataOutput out) throws IOException {
    byte objectType = this.objectTypeArray[index];
    if (logger.isTraceEnabled(LogMarker.VERSIONED_OBJECT_LIST)) {
      logger.trace(LogMarker.VERSIONED_OBJECT_LIST, "writing object {} of type {}: {}", index,
          objectType, value);
    }
    out.writeByte(objectType);
    if (objectType == OBJECT && value instanceof byte[]) {
      if (this.serializeValues) {
        DataSerializer.writeByteArray((byte[]) value, out);
      } else {
        out.write((byte[]) value);
      }
    } else if (objectType == EXCEPTION) {
      // write exception as byte array so native clients can skip it
      DataSerializer.writeByteArray(CacheServerHelper.serialize(value), out);
      // write the exception string for native clients
      DataSerializer.writeString(value.toString(), out);
    } else {
      if (this.serializeValues) {
        DataSerializer.writeObjectAsByteArray(value, out);
      } else {
        DataSerializer.writeObject(value, out);
      }
    }
  }


  private void readObject(int index, DataInput in) throws IOException, ClassNotFoundException {
    Object value;
    this.objectTypeArray[index] = in.readByte();
    if (logger.isTraceEnabled(LogMarker.VERSIONED_OBJECT_LIST)) {
      logger.trace(LogMarker.VERSIONED_OBJECT_LIST, "reading object {} of type {}", index,
          objectTypeArray[index]);
    }
    boolean isException = this.objectTypeArray[index] == EXCEPTION;
    if (isException) {
      byte[] exBytes = DataSerializer.readByteArray(in);
      value = CacheServerHelper.deserialize(exBytes);
      // ignore the exception string meant for native clients
      DataSerializer.readString(in);
    } else if (this.serializeValues) {
      value = DataSerializer.readByteArray(in);
    } else {
      value = DataSerializer.readObject(in);
    }
    this.objects.add(value);
  }

  public void writeExternal(ObjectOutput out) throws IOException {
    toData(out);
  }

  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    fromData(in);
  }

  @Override
  public int getDSFID() {
    return DataSerializableFixedID.VERSIONED_OBJECT_LIST;
  }

  @Override
  public String toString() {
    StringBuilder desc = new StringBuilder();
    desc.append("VersionedObjectList(regionVersioned=").append(regionIsVersioned)
        .append("; hasKeys=").append(hasKeys).append("; keys=")
        .append(keys == null ? "null" : String.valueOf(keys.size())).append("; objects=")
        .append(objects.size()).append("; isobject=")
        .append(objectTypeArray == null ? "null" : objectTypeArray.length).append("; ccEnabled=")
        .append(this.regionIsVersioned).append("; versionTags=")
        .append(String.valueOf(versionTags.size())).append(")\n");
    Iterator entries = this.iterator();
    while (entries.hasNext()) {
      desc.append(entries.next()).append("\n");
    }
    return desc.toString();
  }

  public Set keySet() {
    if (!this.hasKeys) {
      return Collections.EMPTY_SET;
    } else {
      return new HashSet(this.keys);
    }
  }

  public class Entry implements Map.Entry {
    int index;

    Entry(int idx) {
      this.index = idx;
    }

    public Object getKey() {
      if (hasKeys) {
        return keys.get(index);
      }
      return null;
    }

    public Object getObject() {
      return objects.get(index);
    }

    public VersionTag getVersionTag() {
      if (index < versionTags.size()) {
        return versionTags.get(index);
      }
      return null;
    }

    public byte getObjectType() {
      return objectTypeArray[index];
    }

    public boolean isKeyNotOnServer() {
      return objectTypeArray[index] == KEY_NOT_AT_SERVER;
    }

    public boolean isBytes() {
      return objectTypeArray[index] == BYTES;
    }

    public boolean isException() {
      return objectTypeArray[index] == EXCEPTION;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.util.Map.Entry#getValue()
     */
    public Object getValue() {
      if (index < objects.size()) {
        return objects.get(index);
      } else {
        return null;
      }
    }

    /*
     * (non-Javadoc)
     *
     * @see java.util.Map.Entry#setValue(java.lang.Object)
     */
    public Object setValue(Object value) {
      Object result = objects.get(index);
      objects.set(index, value);
      return result;
    }

    @Override
    public String toString() {
      return "[key=" + getKey() + ",value=" + getValue() + ",v=" + getVersionTag() + "]";
    }

  }

  public Iterator iterator() {
    return new Iterator();
  }

  public class Iterator implements java.util.Iterator {
    int index = 0;
    int size = size();

    /*
     * (non-Javadoc)
     *
     * @see java.util.Iterator#hasNext()
     */
    public boolean hasNext() {
      return index < size;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.util.Iterator#next()
     */
    public Entry next() {
      return new Entry(index++);
    }

    /*
     * (non-Javadoc)
     *
     * @see java.util.Iterator#remove()
     */
    public void remove() {
      throw new UnsupportedOperationException();
    }

  }

  /**
   * Chunker is used to send a VersionedObjectList to a client in pieces. It works by pretending to
   * be a VersionedObjectList during serialization and writing only a portion of the list in each
   * invocation of toData().
   *
   *
   */
  public static class Chunker implements DataSerializableFixedID {
    private int index = 0;
    private VersionedObjectList list;
    private int chunkSize;
    private boolean sendKeys;
    private boolean sendObjects;

    public Chunker(VersionedObjectList list, int chunkSize, boolean sendKeys, boolean sendObjects) {
      this.list = list;
      this.chunkSize = chunkSize;
      this.sendKeys = sendKeys;
      this.sendObjects = sendObjects;
    }

    @Override
    public int getDSFID() {
      return DataSerializableFixedID.VERSIONED_OBJECT_LIST;
    }

    @Override
    public void toData(DataOutput out) throws IOException {
      int startIndex = index;
      this.index += this.chunkSize;
      this.list.toData(out, startIndex, chunkSize, sendKeys, sendObjects);
    }

    @Override
    public void fromData(DataInput in) throws IOException, ClassNotFoundException {
      throw new IOException("this fromData method should never be invoked");
    }

    public void toDataPre_GFE_8_0_0_0(DataOutput out) throws IOException {
      if (this.index == 0) {
        this.list.getCanonicalIDs();
      }
      toData(out);
    }

    // when deserialized a VersionedObjectList is created, not a Chunker, so this method isn't
    // needed
    // public void fromDataPre_GFE_8_0_0_0(DataInput in) throws IOException, ClassNotFoundException
    // {
    // fromData(in);
    // }

    @Override
    public Version[] getSerializationVersions() {
      return this.list.getSerializationVersions();
    }
  }

}
