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

import org.apache.logging.log4j.Logger;
import org.apache.geode.internal.cache.tier.sockets.CommBufferPool;
import org.apache.geode.internal.logging.LogService;

/**
 * TXSynchronizationThread manages beforeCompletion and afterCompletion calls on behalf of a client
 * cache. The thread should be instantiated with a Runnable that invokes beforeCompletion behavior.
 * Then you must invoke runSecondRunnable() with another Runnable that invokes afterCompletion
 * behavior.
 * 
 * @since GemFire 6.6
 */
public class TXSynchronizationRunnable implements Runnable {
  private static final Logger logger = LogService.getLogger();

  private Runnable firstRunnable;
  private final Object firstRunnableSync = new Object();
  private boolean firstRunnableCompleted;

  private Runnable secondRunnable;
  private final Object secondRunnableSync = new Object();
  private boolean secondRunnableCompleted;

  private boolean abort;
  private final CommBufferPool commBufferPool;

  public TXSynchronizationRunnable(Runnable beforeCompletion, final CommBufferPool commBufferPool) {
    this.firstRunnable = beforeCompletion;
    this.commBufferPool = commBufferPool;
  }

  public void run() {
    commBufferPool.setTLCommBuffer();
    try {
      doSynchronizationOps();
    } finally {
      commBufferPool.releaseTLCommBuffer();
    }
  }

  private void doSynchronizationOps() {
    synchronized (this.firstRunnableSync) {
      try {
        this.firstRunnable.run();
      } finally {
        if (logger.isTraceEnabled()) {
          logger.trace("beforeCompletion notification completed");
        }
        this.firstRunnableCompleted = true;
        this.firstRunnable = null;
        this.firstRunnableSync.notifyAll();
      }
    }
    synchronized (this.secondRunnableSync) {
      // TODO there should be a transaction timeout that keeps this thread
      // from sitting around forever in the event the client goes away
      final boolean isTraceEnabled = logger.isTraceEnabled();
      while (this.secondRunnable == null && !this.abort) {
        try {
          if (isTraceEnabled) {
            logger.trace("waiting for afterCompletion notification");
          }
          this.secondRunnableSync.wait(1000);
        } catch (InterruptedException ignore) {
          // eat the interrupt and check for exit conditions
        }
        if (this.secondRunnable == null) {
          InternalCache cache = GemFireCacheImpl.getInstance();
          if (cache == null || cache.getCancelCriterion().isCancelInProgress()) {
            return;
          }
        }
      }
      if (isTraceEnabled) {
        logger.trace("executing afterCompletion notification");
      }
      try {
        if (!this.abort) {
          this.secondRunnable.run();
        }
      } finally {
        if (isTraceEnabled) {
          logger.trace("afterCompletion notification completed");
        }
        this.secondRunnableCompleted = true;
        this.secondRunnable = null;
        this.secondRunnableSync.notifyAll();
      }
    }
  }

  /**
   * wait for the initial beforeCompletion step to finish
   */
  public void waitForFirstExecution() {
    synchronized (this.firstRunnableSync) {
      while (!this.firstRunnableCompleted) {
        try {
          this.firstRunnableSync.wait(1000);
        } catch (InterruptedException ignore) {
          // eat the interrupt and check for exit conditions
        }
        // we really need the Cache Server's cancel criterion here, not the cache's
        // but who knows how to get it?
        InternalCache cache = GemFireCacheImpl.getInstance();
        if (cache == null) {
          return;
        }
        cache.getCancelCriterion().checkCancelInProgress(null);
      }
    }
  }

  /**
   * run the afterCompletion portion of synchronization. This method schedules execution of the
   * given runnable and then waits for it to finish running
   */
  public void runSecondRunnable(Runnable r) {
    synchronized (this.secondRunnableSync) {
      this.secondRunnable = r;
      this.secondRunnableSync.notifyAll();
      while (!this.secondRunnableCompleted && !this.abort) {
        try {
          this.secondRunnableSync.wait(1000);
        } catch (InterruptedException ignore) {
          // eat the interrupt and check for exit conditions
        }
        // we really need the Cache Server's cancel criterion here, not the cache's
        // but who knows how to get it?
        InternalCache cache = GemFireCacheImpl.getInstance();
        if (cache == null) {
          return;
        }
        cache.getCancelCriterion().checkCancelInProgress(null);
      }
    }
  }

  /**
   * stop waiting for an afterCompletion to arrive and just exit
   */
  public void abort() {
    synchronized (this.secondRunnableSync) {
      this.abort = true;
    }
  }
}
