/*
 * 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.distributed.internal.locks;

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.SystemFailure;
import org.apache.geode.distributed.LockServiceDestroyedException;
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.HighPriorityDistributionMessage;
import org.apache.geode.distributed.internal.MessageWithReply;
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.logging.log4j.LogMarker;
import org.apache.geode.internal.serialization.DeserializationContext;
import org.apache.geode.internal.serialization.SerializationContext;
import org.apache.geode.logging.internal.log4j.api.LogService;

/**
 * Synchronously releases a lock.
 *
 */
public class DLockReleaseProcessor extends ReplyProcessor21 {
  private static final Logger logger = LogService.getLogger();

  private DLockReleaseReplyMessage reply;

  protected Object objectName;

  public DLockReleaseProcessor(DistributionManager dm, InternalDistributedMember member,
      String serviceName, Object objectName) {
    super(dm, member);
    this.objectName = objectName;
  }

  /**
   * Returns true if release was acknowledged by the grantor; false means we targeted someone who is
   * not the grantor
   */
  protected boolean release(InternalDistributedMember grantor, String serviceName,
      boolean lockBatch, int lockId) {

    DistributionManager dm = getDistributionManager();
    DLockReleaseMessage msg = new DLockReleaseMessage();
    msg.processorId = getProcessorId();
    msg.serviceName = serviceName;
    msg.objectName = objectName;
    msg.lockBatch = lockBatch;
    msg.lockId = lockId;

    msg.setRecipient(grantor);
    if (grantor.equals(dm.getId())) {
      // local... don't message...
      msg.setSender(grantor);
      msg.processLocally(dm);
    } else {
      dm.putOutgoing(msg);
    }

    // keep waiting even if interrupted
    try {
      waitForRepliesUninterruptibly();
    } catch (ReplyException e) {
      e.handleCause();
    }

    if (reply == null) {
      return false;
    }
    return reply.replyCode == DLockReleaseReplyMessage.OK;
  }

  @Override
  protected boolean allowReplyFromSender() {
    return true;
  }

  @Override
  public void process(DistributionMessage msg) {
    try {
      Assert.assertTrue(msg instanceof DLockReleaseReplyMessage,
          "DLockReleaseProcessor is unable to process message of type " + msg.getClass());

      DLockReleaseReplyMessage myReply = (DLockReleaseReplyMessage) msg;
      final boolean isDebugEnabled_DLS = logger.isTraceEnabled(LogMarker.DLS_VERBOSE);
      if (isDebugEnabled_DLS) {
        logger.trace(LogMarker.DLS_VERBOSE, "Handling: {}", myReply);
      }
      reply = myReply;

      if (isDebugEnabled_DLS) {
        // grantor acknowledged release of lock...
        if (myReply.replyCode == DLockReleaseReplyMessage.OK) {
          logger.trace(LogMarker.DLS_VERBOSE, "Successfully released {} in {}", objectName,
              myReply.serviceName);
        }
        // sender denies being the grantor...
        else if (myReply.replyCode == DLockReleaseReplyMessage.NOT_GRANTOR) {
          logger.trace(LogMarker.DLS_VERBOSE,
              "{} has responded DLockReleaseReplyMessage.NOT_GRANTOR for {}", myReply.getSender(),
              myReply.serviceName);
        }
      }
    } finally {
      super.process(msg);
    }
  }

  // -------------------------------------------------------------------------
  // DLockReleaseMessage
  // -------------------------------------------------------------------------
  public static class DLockReleaseMessage extends HighPriorityDistributionMessage
      implements MessageWithReply {
    /** The name of the DistributedLockService */
    protected String serviceName;

    /** The object name */
    protected Object objectName;

    /** True if objectName identifies a batch of locks */
    protected boolean lockBatch;

    /** Id of the processor that will handle replies */
    protected int processorId;

    /** Matches up this release with the original lock request */
    protected int lockId;

    // set used during processing of this msg...
    protected DLockService svc;
    protected DLockGrantor grantor;

    public DLockReleaseMessage() {}

    @Override
    public int getProcessorId() {
      return processorId;
    }

    /**
     * Processes this message - invoked on the node that is the lock grantor.
     */
    @Override
    protected void process(final ClusterDistributionManager dm) {
      boolean failed = true;
      ReplyException replyException = null;
      try {
        svc = DLockService.getInternalServiceNamed(serviceName);
        if (svc == null) {
          failed = false; // basicProcess has it's own finally-block w reply
          basicProcess(dm, false); // don't have a grantor anymore
        } else {
          executeBasicProcess(dm); // use executor
        }
        failed = false; // nothing above threw anything
      } catch (RuntimeException e) {
        replyException = new ReplyException(e);
        throw e;
      } catch (VirtualMachineError e) {
        SystemFailure.initiateFailure(e);
        replyException = new ReplyException(e);
        throw e;
      } catch (Error e) {
        SystemFailure.checkFailure();
        replyException = new ReplyException(e);
        throw e;
      } finally {
        if (failed) {
          // above code failed so now ensure reply is sent
          if (logger.isTraceEnabled(LogMarker.DLS_VERBOSE)) {
            logger.trace(LogMarker.DLS_VERBOSE, "DLockReleaseMessage.process failed for <{}>",
                this);
          }
          int replyCode = DLockReleaseReplyMessage.NOT_GRANTOR;
          DLockReleaseReplyMessage replyMsg = new DLockReleaseReplyMessage();
          replyMsg.serviceName = serviceName;
          replyMsg.replyCode = replyCode;
          replyMsg.setProcessorId(processorId);
          replyMsg.setRecipient(getSender());
          replyMsg.setException(replyException);

          if (dm.getId().equals(getSender())) {
            replyMsg.setSender(getSender());
            replyMsg.dmProcess(dm);
          } else {
            dm.putOutgoing(replyMsg);
          }
        }
      }
    }

    /** Process locally without using messaging or executor */
    protected void processLocally(final DistributionManager dm) {
      svc = DLockService.getInternalServiceNamed(serviceName);
      basicProcess(dm, true); // don't use executor
    }

    /**
     * Execute basicProcess inside Pooled Executor because grantor may not be initializing which
     * will require us to wait.
     * <p>
     * this.svc and this.grantor must be set before calling this method.
     */
    private void executeBasicProcess(final DistributionManager dm) {
      final DLockReleaseMessage msg = this;
      dm.getExecutors().getWaitingThreadPool().execute(() -> {
        if (logger.isTraceEnabled(LogMarker.DLS_VERBOSE)) {
          logger.trace(LogMarker.DLS_VERBOSE, "[executeBasicProcess] waitForGrantor {}", msg);
        }
        basicProcess(dm, true);
      });
    }

    /**
     * Perform basic processing of this message.
     * <p>
     * this.svc and this.grantor must be set before calling this method.
     */
    protected void basicProcess(final DistributionManager dm, final boolean waitForGrantor) {
      final boolean isDebugEnabled_DLS = logger.isTraceEnabled(LogMarker.DLS_VERBOSE);
      if (isDebugEnabled_DLS) {
        logger.trace(LogMarker.DLS_VERBOSE, "[basicProcess] {}", this);
      }
      int replyCode = DLockReleaseReplyMessage.NOT_GRANTOR;
      ReplyException replyException = null;
      try {
        if (svc == null || svc.isDestroyed()) {
          return;
        }

        if (waitForGrantor) {
          try {
            grantor = DLockGrantor.waitForGrantor(svc);
          } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            grantor = null;
          }
        }

        if (grantor == null || grantor.isDestroyed()) {
          return;
        }

        try {
          if (lockBatch) {
            grantor.releaseLockBatch(objectName, getSender());
            replyCode = DLockReleaseReplyMessage.OK;
          } else {
            grantor.releaseIfLocked(objectName, getSender(), lockId);
            replyCode = DLockReleaseReplyMessage.OK;
          }
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          replyCode = DLockReleaseReplyMessage.NOT_GRANTOR;
        }
      } catch (LockGrantorDestroyedException ignore) {
      } catch (LockServiceDestroyedException ignore) {
      } catch (RuntimeException e) {
        replyException = new ReplyException(e);
        if (isDebugEnabled_DLS) {
          logger.trace(LogMarker.DLS_VERBOSE, "[basicProcess] caught RuntimeException", e);
        }
      } catch (VirtualMachineError err) {
        SystemFailure.initiateFailure(err);
        // If this ever returns, rethrow the error. We're poisoned
        // now, so don't let this thread continue.
        throw err;
      } catch (Error e) {
        // Whenever you catch Error or Throwable, you must also
        // catch VirtualMachineError (see above). However, there is
        // _still_ a possibility that you are dealing with a cascading
        // error condition, so you also need to check to see if the JVM
        // is still usable:
        SystemFailure.checkFailure();
        replyException = new ReplyException(e);
        if (isDebugEnabled_DLS) {
          logger.trace(LogMarker.DLS_VERBOSE, "[basicProcess] caught Error", e);
        }
      } finally {
        DLockReleaseReplyMessage replyMsg = new DLockReleaseReplyMessage();
        replyMsg.serviceName = serviceName;
        replyMsg.replyCode = replyCode;
        replyMsg.setProcessorId(processorId);
        replyMsg.setRecipient(getSender());
        replyMsg.setException(replyException);

        if (dm.getId().equals(getSender())) {
          replyMsg.setSender(getSender());
          replyMsg.dmProcess(dm);
        } else {
          dm.putOutgoing(replyMsg);
        }
        if (grantor != null && !lockBatch) {
          // moved what was here into grantor...
          try {
            grantor.postRemoteReleaseLock(objectName);
          } catch (InterruptedException e) {
            try {
              dm.getCancelCriterion().checkCancelInProgress(e);
            } finally {
              Thread.currentThread().interrupt();
            }
          }
        } // grantor != null
        else {
          if (DLockGrantor.DEBUG_SUSPEND_LOCK && isDebugEnabled_DLS) {
            logger.trace(LogMarker.DLS_VERBOSE,
                "DLockReleaseMessage, omitted postRemoteRelease lock on " + objectName
                    + "; grantor = " + grantor + ", lockBatch = " + lockBatch + ", replyMsg = "
                    + replyMsg);
          }
        }
      }
    }

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

    @Override
    public void toData(DataOutput out,
        SerializationContext context) throws IOException {
      super.toData(out, context);
      DataSerializer.writeString(serviceName, out);
      DataSerializer.writeObject(objectName, out);
      out.writeBoolean(lockBatch);
      out.writeInt(processorId);
      out.writeInt(lockId);
    }

    @Override
    public void fromData(DataInput in,
        DeserializationContext context) throws IOException, ClassNotFoundException {
      super.fromData(in, context);
      serviceName = DataSerializer.readString(in);
      objectName = DataSerializer.readObject(in);
      lockBatch = in.readBoolean();
      processorId = in.readInt();
      lockId = in.readInt();
    }

    @Override
    public String toString() {
      return "DLockReleaseMessage for " + serviceName + ", "
          + objectName + "; processorId=" + processorId
          + "; lockBatch=" + lockBatch + "; lockId=" + lockId;
    }
  } // DLockReleaseMessage

  // -------------------------------------------------------------------------
  // DLockReleaseReplyMessage
  // -------------------------------------------------------------------------
  public static class DLockReleaseReplyMessage extends ReplyMessage {

    static final int NOT_GRANTOR = 0;
    static final int OK = 1;

    /** Name of service to release the lock in; for toString only */
    protected String serviceName;

    /** OK or NOT_GRANTOR for the service */
    protected int replyCode = NOT_GRANTOR;

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

    @Override
    public void fromData(DataInput in,
        DeserializationContext context) throws IOException, ClassNotFoundException {
      super.fromData(in, context);
      serviceName = DataSerializer.readString(in);
      replyCode = in.readInt();
    }

    @Override
    public void toData(DataOutput out,
        SerializationContext context) throws IOException {
      super.toData(out, context);
      DataSerializer.writeString(serviceName, out);
      out.writeInt(replyCode);
    }

    @Override
    public String toString() {
      StringBuilder buff = new StringBuilder();
      buff.append("DLockReleaseReplyMessage");
      buff.append(" (serviceName=");
      buff.append(serviceName);
      buff.append("; replyCode=");
      switch (replyCode) {
        case NOT_GRANTOR:
          buff.append("NOT_GRANTOR");
          break;
        case OK:
          buff.append("OK");
          break;
        default:
          buff.append(replyCode);
          break;
      }
      buff.append("; sender=");
      buff.append(getSender());
      buff.append("; processorId=");
      buff.append(super.processorId);
      buff.append(")");
      return buff.toString();
    }
  } // DLockReleaseReplyMessage
}
