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

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

import org.apache.logging.log4j.Logger;

import org.apache.geode.DataSerializer;
import org.apache.geode.cache.RegionDestroyedException;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.internal.ClusterDistributionManager;
import org.apache.geode.distributed.internal.DistributionManager;
import org.apache.geode.distributed.internal.DistributionMessage;
import org.apache.geode.distributed.internal.DistributionStats;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.distributed.internal.ReplyException;
import org.apache.geode.distributed.internal.ReplyMessage;
import org.apache.geode.distributed.internal.ReplyProcessor21;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.internal.Assert;
import org.apache.geode.internal.cache.InternalRegion;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.internal.cache.RemoteOperationException;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.internal.logging.log4j.LogMarker;

/**
 * This message is used by a transaction to determine the size of a region on a remote member.
 *
 * @since GemFire 5.0
 */
public class RemoteSizeMessage extends RemoteOperationMessage {
  private static final Logger logger = LogService.getLogger();

  /**
   * Empty constructor to satisfy {@link DataSerializer} requirements
   */
  public RemoteSizeMessage() {}

  /**
   * The message sent to a member to get the size of their region
   *
   * @param recipient member to send the message to
   * @param regionPath the path to the region
   * @param processor the reply processor used to wait on the response
   */
  private RemoteSizeMessage(DistributedMember recipient, String regionPath,
      ReplyProcessor21 processor) {
    super((InternalDistributedMember) recipient, regionPath, processor);
  }

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

  /**
   * Sends a message for {@link java.util.Map#size()} ignoring any errors on send
   *
   * @param distributedMember the set of members that the size message is sent to
   * @param r the Region to get the size of
   * @return the processor used to read the returned size
   */
  public static SizeResponse send(DistributedMember distributedMember, InternalRegion r) {
    Assert.assertTrue(distributedMember != null, "RemoteSizeMessage NULL recipients set");
    SizeResponse p = new SizeResponse(r.getSystem(), distributedMember);
    RemoteSizeMessage m = new RemoteSizeMessage(distributedMember, r.getFullPath(), p);
    r.getDistributionManager().putOutgoing(m);
    return p;
  }

  @Override
  public boolean canStartRemoteTransaction() {
    return false;
  }

  @Override
  protected boolean operateOnRegion(ClusterDistributionManager dm, LocalRegion r, long startTime)
      throws RemoteOperationException {
    int size = r.size();
    SizeReplyMessage.send(getSender(), getProcessorId(), dm, size);
    return false;
  }

  @Override
  protected void appendFields(StringBuffer buff) {
    super.appendFields(buff);
  }

  public int getDSFID() {
    return R_SIZE_MESSAGE;
  }

  @Override
  public void fromData(DataInput in) throws IOException, ClassNotFoundException {
    super.fromData(in);
    DataSerializer.readArrayList(in); /* read unused data for backwards compatibility */
    in.readByte(); /* read unused data for backwards compatibility */
  }

  @Override
  public void toData(DataOutput out) throws IOException {
    super.toData(out);
    DataSerializer.writeArrayList(null, out);
    out.writeByte(0);
  }

  public static class SizeReplyMessage extends ReplyMessage {
    private int size;

    /**
     * Empty constructor to conform to DataSerializable interface
     */
    public SizeReplyMessage() {}

    private SizeReplyMessage(int processorId, int size) {
      this.processorId = processorId;
      this.size = size;
    }

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

    /** Send an ack */
    public static void send(InternalDistributedMember recipient, int processorId,
        DistributionManager dm, int size) {
      Assert.assertTrue(recipient != null, "SizeReplyMessage NULL reply message");
      SizeReplyMessage m = new SizeReplyMessage(processorId, size);
      m.setRecipient(recipient);
      dm.putOutgoing(m);
    }

    /**
     * Processes this message. This method is invoked by the receiver of the message.
     *
     * @param dm the distribution manager that is processing the message.
     */
    @Override
    public void process(final DistributionManager dm, final ReplyProcessor21 processor) {
      final long startTime = getTimestamp();
      if (logger.isTraceEnabled(LogMarker.DM_VERBOSE)) {
        logger.trace(LogMarker.DM_VERBOSE,
            "{}: process invoking reply processor with processorId: {}", this.processorId);
      }

      if (processor == null) {
        if (logger.isTraceEnabled(LogMarker.DM_VERBOSE)) {
          logger.trace(LogMarker.DM_VERBOSE, "{} processor not found", getClass().getName());
        }
        return;
      }
      processor.process(this);

      if (logger.isTraceEnabled(LogMarker.DM_VERBOSE)) {
        logger.trace(LogMarker.DM_VERBOSE, "{} processed {}", getClass().getName(), this);
      }
      dm.getStats().incReplyMessageTime(DistributionStats.getStatTime() - startTime);
    }

    @Override
    public void toData(DataOutput out) throws IOException {
      super.toData(out);
      out.writeInt(size);
    }

    @Override
    public int getDSFID() {
      return R_SIZE_REPLY_MESSAGE;
    }

    @Override
    public void fromData(DataInput in) throws IOException, ClassNotFoundException {
      super.fromData(in);
      this.size = in.readInt();
    }

    @Override
    public String toString() {
      StringBuffer sb = new StringBuffer();
      sb.append(this.getClass().getName()).append(" processorid=").append(this.processorId)
          .append(" reply to sender ").append(this.getSender()).append(" returning size=")
          .append(getSize());
      return sb.toString();
    }

    public int getSize() {
      return size;
    }
  }

  /**
   * A processor to capture the value returned by RemoteSizeMessage
   *
   * @since GemFire 5.0
   */
  public static class SizeResponse extends ReplyProcessor21 {
    private int returnValue;

    public SizeResponse(InternalDistributedSystem ds, DistributedMember recipient) {
      super(ds, (InternalDistributedMember) recipient);
    }

    // Note that this causes GEODE-4612 and should be removed
    @Override
    protected void processException(ReplyException ex) {
      logger.debug("SizeResponse ignoring exception: {}", ex.getMessage(), ex);
    }

    @Override
    public void process(DistributionMessage msg) {
      try {
        if (msg instanceof SizeReplyMessage) {
          SizeReplyMessage reply = (SizeReplyMessage) msg;
          this.returnValue = reply.getSize();
        }
      } finally {
        super.process(msg);
      }
    }

    /**
     * @return wait for and return the size
     */
    public int waitForSize() {
      try {
        waitForRepliesUninterruptibly();
      } catch (ReplyException e) {
        Throwable cause = e.getCause();
        if (cause instanceof RegionDestroyedException) {
          RegionDestroyedException rde = (RegionDestroyedException) cause;
          throw rde;
        }
        throw e;
      }
      return this.returnValue;
    }
  }

}
