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

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.apache.logging.log4j.Logger;

import org.apache.geode.DataSerializer;
import org.apache.geode.distributed.internal.DM;
import org.apache.geode.distributed.internal.DistributionManager;
import org.apache.geode.distributed.internal.DistributionMessage;
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.InternalDataSerializer;
import org.apache.geode.internal.NanoTimer;
import org.apache.geode.internal.cache.ForceReattemptException;
import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.internal.logging.log4j.LogMarker;

public class FetchPartitionDetailsMessage extends PartitionMessage {

  private static final Logger logger = LogService.getLogger();

  private volatile boolean internal;
  private LoadProbe loadProbe;
  private boolean fetchOfflineMembers;

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

  private FetchPartitionDetailsMessage(Set<InternalDistributedMember> recipients, int regionId,
      ReplyProcessor21 processor, boolean internal, boolean fetchOfflineMembers, LoadProbe probe) {
    super(recipients, regionId, processor);
    this.internal = internal;
    this.fetchOfflineMembers = fetchOfflineMembers;
    this.loadProbe = probe;
  }

  /**
   * Sends a message to fetch {@link org.apache.geode.cache.partition.PartitionMemberInfo
   * PartitionMemberDetails} for the specified <code>PartitionedRegion</code>.
   * 
   * @param recipients the members to fetch PartitionMemberDetails from
   * @param region the PartitionedRegion to fetch member details for
   * @param fetchOfflineMembers
   * @return the processor used to fetch the PartitionMemberDetails
   */
  public static FetchPartitionDetailsResponse send(Set<InternalDistributedMember> recipients,
      PartitionedRegion region, boolean internal, boolean fetchOfflineMembers, LoadProbe probe) {

    Assert.assertTrue(recipients != null && !recipients.isEmpty(),
        "FetchPartitionDetailsMessage NULL recipient");

    FetchPartitionDetailsResponse response =
        new FetchPartitionDetailsResponse(region.getSystem(), recipients, region);
    FetchPartitionDetailsMessage msg = new FetchPartitionDetailsMessage(recipients,
        region.getPRId(), response, internal, fetchOfflineMembers, probe);

    /* Set<InternalDistributedMember> failures = */
    region.getDistributionManager().putOutgoing(msg);

    region.getPrStats().incPartitionMessagesSent();
    return response;
  }

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

  @Override
  public boolean isSevereAlertCompatible() {
    // allow forced-disconnect processing for all cache op messages
    return true;
  }

  @Override
  protected boolean operateOnPartitionedRegion(DistributionManager dm, PartitionedRegion region,
      long startTime) throws ForceReattemptException {

    PartitionMemberInfoImpl details = (PartitionMemberInfoImpl) region.getRedundancyProvider()
        .buildPartitionMemberDetails(this.internal, this.loadProbe);
    OfflineMemberDetails offlineDetails;
    if (this.internal && this.fetchOfflineMembers) {
      offlineDetails = region.getRedundancyProvider().fetchOfflineMembers();
    } else {
      offlineDetails = new OfflineMemberDetailsImpl(new Set[0]);
    }
    region.getPrStats().endPartitionMessagesProcessing(startTime);
    FetchPartitionDetailsReplyMessage.send(getSender(), getProcessorId(), details, dm,
        offlineDetails, null);

    // Unless there was an exception thrown, this message handles sending the
    // response
    return false;
  }

  @Override
  protected void appendFields(StringBuilder buff) {
    super.appendFields(buff);
    buff.append("; internal=").append(this.internal);
  }

  public int getDSFID() {
    return PR_FETCH_PARTITION_DETAILS_MESSAGE;
  }

  @Override
  public void fromData(DataInput in) throws IOException, ClassNotFoundException {
    super.fromData(in);
    this.internal = in.readBoolean();
    this.fetchOfflineMembers = in.readBoolean();
    this.loadProbe = (LoadProbe) DataSerializer.readObject(in);
  }

  @Override
  public void toData(DataOutput out) throws IOException {
    super.toData(out);
    out.writeBoolean(this.internal);
    out.writeBoolean(this.fetchOfflineMembers);
    DataSerializer.writeObject(loadProbe, out);
  }

  public static class FetchPartitionDetailsReplyMessage extends ReplyMessage {

    static final byte NO_PARTITION = 0;
    static final byte OK = 1;
    static final byte OK_INTERNAL = 2;

    private long configuredMaxMemory;
    private long size;
    private int bucketCount;
    private int primaryCount;
    private PRLoad prLoad;
    private long[] bucketSizes;
    private OfflineMemberDetails offlineDetails;

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

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

    private FetchPartitionDetailsReplyMessage(int processorId, PartitionMemberInfoImpl details,
        OfflineMemberDetails offlineDetails, ReplyException re) {
      this.processorId = processorId;

      this.configuredMaxMemory = details.getConfiguredMaxMemory();
      this.size = details.getSize();
      this.bucketCount = details.getBucketCount();
      this.primaryCount = details.getPrimaryCount();
      this.prLoad = details.getPRLoad();
      this.bucketSizes = details.getBucketSizes();
      this.offlineDetails = offlineDetails;

      setException(re);
    }

    /**
     * Send an ack
     * 
     * @param offlineDetails
     */
    public static void send(InternalDistributedMember recipient, int processorId,
        PartitionMemberInfoImpl details, DM dm, OfflineMemberDetails offlineDetails,
        ReplyException re) {
      Assert.assertTrue(recipient != null, "FetchPartitionDetailsReplyMessage NULL recipient");
      FetchPartitionDetailsReplyMessage m =
          new FetchPartitionDetailsReplyMessage(processorId, details, offlineDetails, re);
      m.setRecipient(recipient);
      dm.putOutgoing(m);
    }

    @Override
    public void process(final DM dm, final ReplyProcessor21 processor) {
      final long startTime = getTimestamp();
      if (logger.isTraceEnabled(LogMarker.DM)) {
        logger.trace(LogMarker.DM,
            "FetchPartitionDetailsReplyMessage process invoking reply processor with processorId: {}",
            this.processorId);
      }

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

      if (logger.isTraceEnabled(LogMarker.DM)) {
        logger.trace(LogMarker.DM, "{} processed {}", processor, this);
      }
      dm.getStats().incReplyMessageTime(NanoTimer.getTime() - startTime);
    }

    InternalPartitionDetails unmarshalPartitionMemberDetails() {
      if (this.configuredMaxMemory == 0) {
        return null;
      } else {
        if (this.prLoad == null) {
          return new PartitionMemberInfoImpl(getSender(), this.configuredMaxMemory, this.size,
              this.bucketCount, this.primaryCount);
        } else {
          return new PartitionMemberInfoImpl(getSender(), this.configuredMaxMemory, this.size,
              this.bucketCount, this.primaryCount, this.prLoad, this.bucketSizes);
        }
      }
    }

    @Override
    public void toData(DataOutput out) throws IOException {
      super.toData(out);
      if (this.configuredMaxMemory == 0) {
        out.writeByte(NO_PARTITION);
      } else {
        if (this.prLoad == null) {
          out.writeByte(OK);
        } else {
          out.writeByte(OK_INTERNAL);
        }

        out.writeLong(this.configuredMaxMemory);
        out.writeLong(this.size);
        out.writeInt(this.bucketCount);
        out.writeInt(this.primaryCount);
        if (this.prLoad != null) {
          InternalDataSerializer.invokeToData(this.prLoad, out);
          DataSerializer.writeLongArray(this.bucketSizes, out);
          InternalDataSerializer.invokeToData(offlineDetails, out);
        }
      }
    }

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

    @Override
    public void fromData(DataInput in) throws IOException, ClassNotFoundException {
      super.fromData(in);
      byte flag = in.readByte();
      if (flag != NO_PARTITION) {
        this.configuredMaxMemory = in.readLong();
        this.size = in.readLong();
        this.bucketCount = in.readInt();
        this.primaryCount = in.readInt();
        if (flag == OK_INTERNAL) {
          this.prLoad = PRLoad.createFromDataInput(in);
          this.bucketSizes = DataSerializer.readLongArray(in);
          this.offlineDetails = new OfflineMemberDetailsImpl();
          InternalDataSerializer.invokeFromData(this.offlineDetails, in);
        }
      }
    }

    @Override
    public String toString() {
      StringBuffer sb = new StringBuffer();
      sb.append("FetchPartitionDetailsReplyMessage ").append("processorid=")
          .append(this.processorId).append(" reply to sender ").append(this.getSender())
          .append(" returning configuredMaxMemory=").append(this.configuredMaxMemory)
          .append(" size=").append(this.size).append(" bucketCount=").append(this.bucketCount)
          .append(" primaryCount=").append(this.primaryCount).append(" prLoad=").append(this.prLoad)
          .append(" bucketSizes=").append(Arrays.toString(this.bucketSizes));
      return sb.toString();
    }
  }

  /**
   * A processor to capture the value returned by
   * {@link org.apache.geode.internal.cache.partitioned.FetchPartitionDetailsMessage.FetchPartitionDetailsReplyMessage}
   * 
   */
  public static class FetchPartitionDetailsResponse extends PartitionResponse {

    private final Set<InternalPartitionDetails> allDetails =
        new HashSet<InternalPartitionDetails>();
    private OfflineMemberDetails offlineDetails;

    final PartitionedRegion partitionedRegion;

    public FetchPartitionDetailsResponse(InternalDistributedSystem ds,
        Set<InternalDistributedMember> recipients, PartitionedRegion theRegion) {
      super(ds, recipients);
      partitionedRegion = theRegion;
    }

    @Override
    public void process(DistributionMessage msg) {
      try {
        if (msg instanceof FetchPartitionDetailsReplyMessage) {
          FetchPartitionDetailsReplyMessage reply = (FetchPartitionDetailsReplyMessage) msg;
          InternalPartitionDetails details = reply.unmarshalPartitionMemberDetails();
          if (details != null) {
            synchronized (allDetails) {
              this.allDetails.add(details);
              // This just picks the offline details from the last member to return
              this.offlineDetails = reply.offlineDetails;
            }
            if (logger.isTraceEnabled(LogMarker.DM)) {
              logger.debug("FetchPartitionDetailsResponse return details is {}", details);
            }
          } else if (logger.isTraceEnabled(LogMarker.DM)) {
            logger.debug("FetchPartitionDetailsResponse ignoring null details");
          }
        }
      } finally {
        super.process(msg);
      }
    }

    /**
     * Ignore any incoming exception from other VMs, we just want an acknowledgement that the
     * message was processed.
     */
    @Override
    protected void processException(ReplyException ex) {
      logger.debug("FetchPartitionDetailsResponse ignoring exception {}", ex.getMessage(), ex);
    }

    /**
     * @return set of all PartitionMemberDetails
     */
    public Set<InternalPartitionDetails> waitForResponse() {
      waitForRepliesUninterruptibly();
      synchronized (allDetails) {
        return this.allDetails;
      }
    }

    public OfflineMemberDetails getOfflineMembers() {
      return offlineDetails;
    }
  }

}
