package de.renew.simulator.api;

import java.util.function.Supplier;

import org.apache.log4j.Logger;

import de.renew.plugin.ServiceLookupException;
import de.renew.plugin.ServiceLookupInfrastructure;

/**
 * This class is responsible for managing the simulation lock and performing actions with it.
 * Manages the lock that is used to synchronize access to all method calls
 * that operate on the simulation state. Since all these methods include
 * convenience wrapping and locking, users of this plugin need not worry about
 * it unless they want to ensure atomic execution of multiple operations.
 * <p>
 * When using the lock it must be acquired <em>before</em> wrapping
 * execution within a simulation thread of the simulation thread pool. It is
 * also strongly recommended to include the unlock statement in a
 * try-finally block.
 * Alternatively, the utility methods {@link #runWithLock(Runnable)} and {@link #supplyWithLock(Supplier)} can be used,
 * Example:
 * </p>
 *
 * <pre><code>
 * try {
 *     Future future = SimulationThreadPool.getCurrent().submitAndWait((Callable) () -> {
 *        ...
 *     });
 *     return future.get();
 * } catch (ExecutionException e) {
 *    ...
 * } finally {
 *     SimulationLockExecutor.unlock();
 * }
 * </code></pre>
 */
public final class SimulationLockExecutor {

    private static final ISimulationLockExecutor EXECUTOR;
    private static final Logger LOGGER = Logger.getLogger(SimulationLockExecutor.class);

    static {
        try {
            EXECUTOR = ServiceLookupInfrastructure.getInstance()
                .getFirstServiceProvider(ISimulationLockExecutor.class);
        } catch (ServiceLookupException e) {
            LOGGER.error(
                "Could not find a service provider for " + ISimulationLockExecutor.class + ": ", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Private, as no instances of this class should exist.
     */
    private SimulationLockExecutor() {}

    /**
     * Locks the simulation lock.  If the lock is already locked, the caller has to wait.
     */
    public static void lock() {
        EXECUTOR.lock();
    }

    /**
     * Unlocks the simulation lock.
     */
    public static void unlock() {
        EXECUTOR.unlock();
    }

    /**
     * Runs the given runnable. Before running, the simulation lock is locked.
     * Afterwards, the lock is unlocked again.
     *
     * @param runnable the runnable to run
     */
    public static void runWithLock(Runnable runnable) {
        EXECUTOR.runWithLock(runnable);
    }

    /**
     * Runs the given supplier and returns its result. Before running, the simulation lock is locked.
     * Afterwards, the lock is unlocked again.
     *
     * @param supplier the supplier to run
     * @param <T> the result's type
     * @return the supplier's result
     */
    public static <T> T supplyWithLock(Supplier<T> supplier) {
        return EXECUTOR.supplyWithLock(supplier);
    }
}
