// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.sdk;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import org.chromium.sdk.util.MethodIsBlockingException;

/**
 * Convenient implementation of {@code SyncCallback}. Client may create one,
 * then call asynchronous command, and finally wait on blocking method
 * {@code #tryAcquire()} or {@link #acquireDefault}.
 * <p>
 * Class uses symbolic parameter {@link RelayOk} in its methods that suggests that
 * user should first call some asynchronous method, then pass it to the acquire method.
 */
public class CallbackSemaphore implements SyncCallback {
  public static final long OPERATION_TIMEOUT_MS = 120000;

  private final Semaphore sem = new Semaphore(0);
  private Exception savedException;

  /**
   * Tries to acquire semaphore with some reasonable default timeout.
   * @param relayOk symbolic return value from the asynchronous operation that we are waiting for
   * @return false if {@code #OPERATION_TIMEOUT_MS} was exceeded and we gave up
   * @throws MethodIsBlockingException if called from a callback
   */
  public boolean tryAcquireDefault(RelayOk relayOk) throws MethodIsBlockingException {
    return tryAcquire(OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
  }

  /**
   * @param relayOk symbolic return value from the asynchronous operation that we are waiting for
   */
  public void acquireDefault(RelayOk relayOk) throws MethodIsBlockingException {
    boolean res = tryAcquireDefault(relayOk);
    if (!res) {
      throw new RuntimeException("Failed to acquire semaphore (timeout)");
    }
  }

  /**
   * Tries to acquire the semaphore. This method blocks until the semaphore is
   * released; typically release call comes from a worker thread of
   * org.chromium.sdk, the same thread that may call all other callbacks.
   * It is vital not to call this method from any callback of org.chromium.sdk,
   * because it's a sure deadlock.
   * To prevent, this the method declares throwing
   * {@code MethodIsBlockingException} which is symbolically thrown whenever
   * someone violates this rule (i.e. invokes this method from a callback).
   * Though currently nobody actually throws it, such declarations help to
   * track blocking methods.
   * @return false if {@code timeout} was exceeded and we gave up
   * @throws MethodIsBlockingException if called from a callback
   */
  public boolean tryAcquire(long timeout, TimeUnit unit) throws MethodIsBlockingException {
    boolean res;
    try {
      res = sem.tryAcquire(timeout, unit);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    if (savedException != null) {
      throw new RuntimeException("Exception occured in callback", savedException);
    }
    return res;
  }
  /**
   * Implementation of {@code SyncCallback#callbackDone(RuntimeException)}.
   */
  @Override
  public void callbackDone(RuntimeException e) {
    if (e == null) {
      savedException = null;
    } else {
      savedException = new Exception("Exception saved from callback", e);
    }
    sem.release();
  }
}