/*******************************************************************************
 * Copyright (c) 2000, 2016 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.internal;

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

public final class PendingSyncExec {
	private final Semaphore semaphore = new Semaphore(0);

	private Thread operation;

	private final Runnable runnable;

	// Accessed by multiple threads. Synchronize on "this" before accessing.
	private boolean hasFinishedRunning;

	public PendingSyncExec(Runnable runnable) {
		this.runnable = runnable;
	}

	/**
	 * Attempts to acquire this semaphore. Returns true if it was successfully
	 * acquired, and false otherwise.
	 */
	private boolean acquire(long delay) throws InterruptedException {
		return semaphore.tryAcquire(delay, TimeUnit.MILLISECONDS);
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof PendingSyncExec)) {
			return false;
		}
		return (runnable == ((PendingSyncExec) obj).runnable);
	}

	public Thread getOperationThread() {
		return operation;
	}

	public void run() {
		// Clear the interrupted flag. The blocked thread may have been
		// periodically interrupting the UI thread in
		// order to interrupt other tasks in the queue and cause this job to
		// start execution. (Note that it will
		// continue to try to interrupt this runnable if it takes too long to
		// run, until we set the hasFinishedRunning
		// flag).
		Thread.interrupted();
		try {
			if (runnable != null) {
				runnable.run();
			}
		} finally {
			// Record the fact that this pending syncExec has finished
			// execution, to prevent the calling thread from
			// interrupting this thread.
			synchronized (this) {
				hasFinishedRunning = true;
			}
			// The calling thread may have still interrupted this operation up
			// until the point where we flipped the
			// hasFinishedRunning flag.
			Thread.interrupted();
			semaphore.release();
		}
	}

	public void waitUntilExecuted(UILockListener lockListener) throws InterruptedException {
		// even if the UI was not blocked earlier, it might become blocked
		// before it can serve the asyncExec to do the pending work
		while (!acquire(1000)) {
			if (lockListener.isUIWaiting()) {
				synchronized (this) {
					if (!hasFinishedRunning) {
						lockListener.interruptUI(runnable);
					}
				}
			}
		}
	}

	@Override
	public int hashCode() {
		return Objects.hashCode(runnable);
	}

	public void setOperationThread(Thread operation) {
		this.operation = operation;
	}

	// for debug only
	@Override
	public String toString() {
		return "PendingSyncExec(" + runnable + ")"; //$NON-NLS-1$ //$NON-NLS-2$
	}
}
