package com.avaje.ebeaninternal.server.transaction;

import com.avaje.ebeaninternal.api.SpiEbeanServer;
import com.avaje.ebeaninternal.server.cluster.BinaryMessage;
import com.avaje.ebeaninternal.server.cluster.BinaryMessageList;
import com.avaje.ebeaninternal.server.core.PersistRequest;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.id.IdBinder;

import java.io.DataInput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * Wraps the information representing a Inserted Updated or Deleted Bean.
 * <p>
 * This information is broadcast across the cluster so that remote BeanListeners
 * are notified of the inserts updates and deletes that occurred.
 * </p>
 * <p>
 * You control it the data is broadcast and what data is broadcast by the
 * BeanListener.getClusterData() method. It is guessed that often just the Id
 * property or perhaps a few properties in a Map will be broadcast to reduce the
 * size of data sent around the network.
 * </p>
 */
public class BeanPersistIds {

  private final BeanDescriptor<?> beanDescriptor;

  private final String descriptorId;

  private List<Object> insertIds;
  private List<Object> updateIds;
  private List<Object> deleteIds;

  /**
   * Create the payload.
   */
  public BeanPersistIds(BeanDescriptor<?> desc) {
    this.beanDescriptor = desc;
    this.descriptorId = desc.getDescriptorId();
  }

  public static BeanPersistIds readBinaryMessage(SpiEbeanServer server, DataInput dataInput) throws IOException {

    String descriptorId = dataInput.readUTF();
    BeanDescriptor<?> desc = server.getBeanDescriptorById(descriptorId);
    BeanPersistIds bp = new BeanPersistIds(desc);
    bp.read(dataInput);
    return bp;
  }

  private void read(DataInput dataInput) throws IOException {

    IdBinder idBinder = beanDescriptor.getIdBinder();

    int iudType = dataInput.readInt();
    List<Object> idList = readIdList(dataInput, idBinder);
    switch (iudType) {
      case 0:
        insertIds = idList;
        break;
      case 1:
        updateIds = idList;
        break;
      case 2:
        deleteIds = idList;
        break;

      default:
        throw new RuntimeException("Invalid iudType " + iudType);
    }
  }

  /**
   * Write the contents into a BinaryMessage form.
   * <p>
   * For a RemoteBeanPersist with a large number of id's note that this is
   * broken up into many BinaryMessages each with a maximum of 100 ids. This
   * enables the contents of a large RemoteTransactionEvent to be split up
   * across multiple Packets.
   * </p>
   */
  void writeBinaryMessage(BinaryMessageList msgList) throws IOException {

    writeIdList(beanDescriptor, 0, insertIds, msgList);
    writeIdList(beanDescriptor, 1, updateIds, msgList);
    writeIdList(beanDescriptor, 2, deleteIds, msgList);
  }

  private List<Object> readIdList(DataInput dataInput, IdBinder idBinder) throws IOException {

    int count = dataInput.readInt();
    if (count < 1) {
      return null;
    }
    List<Object> idList = new ArrayList<Object>(count);
    for (int i = 0; i < count; i++) {
      idList.add(idBinder.readData(dataInput));
    }
    return idList;
  }

  /**
   * Write a BinaryMessage containing the descriptorId, iudType and list of Id
   * values.
   * <p>
   * Note that a given BinaryMessage has a maximum of 100 Ids. This is due to
   * the limit of UDP packet sizes. We break up the RemoteBeanPersist into
   * potentially many smaller BinaryMessages which may be put into multiple
   * Packets.
   * </p>
   */
  private void writeIdList(BeanDescriptor<?> desc, int iudType, List<Object> idList, BinaryMessageList msgList) throws IOException {

    IdBinder idBinder = desc.getIdBinder();

    int count = idList == null ? 0 : idList.size();
    if (count > 0) {
      int loop = 0;
      int i = 0;
      int eof = idList.size();
      do {
        ++loop;
        int endOfLoop = Math.min(eof, loop * 100);

        BinaryMessage m = new BinaryMessage(endOfLoop * 4 + 20);

        DataOutputStream os = m.getOs();
        os.writeInt(BinaryMessage.TYPE_BEANIUD);
        os.writeUTF(descriptorId);
        os.writeInt(iudType);
        os.writeInt(count);

        for (; i < endOfLoop; i++) {
          idBinder.writeData(os, idList.get(i));
        }

        os.flush();
        msgList.add(m);

      } while (i < eof);
    }
  }

  public String toString() {
    StringBuilder sb = new StringBuilder();
    if (beanDescriptor != null) {
      sb.append(beanDescriptor.getFullName());
    } else {
      sb.append("descId:").append(descriptorId);
    }
    if (insertIds != null) {
      sb.append(" insertIds:").append(insertIds);
    }
    if (updateIds != null) {
      sb.append(" updateIds:").append(updateIds);
    }
    if (deleteIds != null) {
      sb.append(" deleteIds:").append(deleteIds);
    }
    return sb.toString();
  }

  void addId(PersistRequest.Type type, Serializable id) {
    switch (type) {
      case INSERT:
        addInsertId(id);
        break;
      case UPDATE:
        addUpdateId(id);
        break;
      case DELETE:
      case SOFT_DELETE:
        addDeleteId(id);
        break;

      default:
        break;
    }
  }

  private void addInsertId(Serializable id) {
    if (insertIds == null) {
      insertIds = new ArrayList<Object>();
    }
    insertIds.add(id);
  }

  private void addUpdateId(Serializable id) {
    if (updateIds == null) {
      updateIds = new ArrayList<Object>();
    }
    updateIds.add(id);
  }

  private void addDeleteId(Serializable id) {
    if (deleteIds == null) {
      deleteIds = new ArrayList<Object>();
    }
    deleteIds.add(id);
  }

  public BeanDescriptor<?> getBeanDescriptor() {
    return beanDescriptor;
  }

  List<Object> getDeleteIds() {
    return deleteIds;
  }

  /**
   * Notify the cache of this event that came from another server in the cluster.
   */
  void notifyCacheAndListener() {

    // any change invalidates the query cache
    beanDescriptor.queryCacheClear();

    if (updateIds != null) {
      for (int i = 0; i < updateIds.size(); i++) {
        Object id = updateIds.get(i);
        beanDescriptor.cacheHandleDeleteById(id);
      }
    }
    if (deleteIds != null) {
      for (int i = 0; i < deleteIds.size(); i++) {
        Object id = deleteIds.get(i);
        beanDescriptor.cacheHandleDeleteById(id);
      }
    }
  }
}
