/*
 * Hibernate Search, full-text search for your domain model
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.search.testsupport.concurrency;

import java.util.concurrent.TimeUnit;

import junit.framework.AssertionFailedError;

/**
 * A utility allowing to re-execute a piece of code in the current thread until it "works".
 * <p>
 * Meant to be used with lambdas.
 *
 * @see #pollAssertion(ThrowingRunnable)
 *
 * @author Yoann Rodiere
 */
public final class Poller {

	public static Poller milliseconds(long timeoutMs, long stepMs) {
		return create( timeoutMs, stepMs, TimeUnit.MILLISECONDS );
	}

	public static Poller nanoseconds(long timeoutNanos, long stepNanos) {
		return create( timeoutNanos, stepNanos, TimeUnit.NANOSECONDS );
	}

	private static Poller create(long timeoutValue, long stepValue, TimeUnit timeUnit) {
		return new Poller( timeUnit.toNanos( timeoutValue ), timeUnit.toNanos( stepValue ) );
	}

	private final long timeoutValueNanos;
	private final long stepValueNanos;

	private Poller(long timeoutValueNanos, long stepValueNanos) {
		this.timeoutValueNanos = timeoutValueNanos;
		this.stepValueNanos = stepValueNanos;
	}

	/**
	 * Run the given statement repeatedly until it doesn't throw any {@link AssertionError},
	 * or throw the {@link AssertionError} if the timeout is reached.
	 */
	public <E extends Exception> void pollAssertion(ThrowingRunnable<E> runnable) throws E {
		long initialNanoTime = System.nanoTime();
		long deadline = initialNanoTime + timeoutValueNanos;

		int nbOfFailedAttempts = 0;

		AssertionError assertionError;
		do {
			sleepStep();

			try {
				runnable.run();
				return; // Don't wait any more: the assertion passed, we're good to go.
			}
			catch (AssertionError e) {
				/*
				 * Only keep the very last assertion error, because it's probably the most relevant:
				 * the runnable may perform multiple assertions, and for instance the first assertion
				 * might fail for 2s, then be okay, the second assertion may become okay after 3s, etc.
				 * In the end what we want to know is which assertions was failing when we timed out.
				 */
				assertionError = e;
				++nbOfFailedAttempts;
			}
		} while ( System.nanoTime() < deadline );

		long timeSpentNanos = System.nanoTime() - initialNanoTime;
		throw newAssertionPollingError( assertionError, nbOfFailedAttempts, timeSpentNanos );
	}

	private void sleepStep() {
		try {
			TimeUnit.NANOSECONDS.sleep( stepValueNanos );
		}
		catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			throw new IllegalStateException( "Interrupted while waiting for an assertion to pass.", e );
		}
	}

	public interface ThrowingRunnable<E extends Exception> {
		void run() throws E;
	}

	private AssertionError newAssertionPollingError(AssertionError lastAssertionError, int nbOfFailedAttempts, long timeSpentNanos) {
		AssertionError error = new AssertionFailedError(
				"Assertion failed even after " + nbOfFailedAttempts + " attempts in " + timeSpentNanos + "ns : "
				+ lastAssertionError.getMessage()
				);
		error.initCause( lastAssertionError );
		return error;
	}
}
