package de.renew.simulator.api;

import java.util.Properties;

import org.apache.log4j.Logger;

import de.renew.plugin.ServiceLookupException;
import de.renew.plugin.ServiceLookupInfrastructure;
import de.renew.simulatorontology.shadow.ShadowNetSystem;
import de.renew.simulatorontology.shadow.SyntaxException;
import de.renew.simulatorontology.simulation.NoSimulationException;
import de.renew.simulatorontology.simulation.SimulationEnvironment;
import de.renew.simulatorontology.simulation.SimulationRunningException;
import de.renew.simulatorontology.simulation.Simulator;
import de.renew.simulatorontology.simulation.SimulatorExtension;

/**
 * This class serves as the static facade for the Simulator Plug-in and offers the following actions:
 * <ul>
 *     <li>Setup and termination of a simulation</li>
 *     <li>Retrieval of information regarding the current simulation mode</li>
 *     <li>The insertion of a {@link ShadowNetSystem} that should be simulated</li>
 *     <li>Setting the default net loader</li>
 * </ul>
 *
 * It can be used to set up one Renew simulation per virtual machine.
 * The simulation engine can be configured and extended in various ways
 * <p>
 * One way to set up a simulation might look as follows:
 *
 * <pre><code>
 *        // These things need to be known beforehand:
 *        String mainNet;                   // The name of the net for
 *                                          // the initial instance.
 *        ShadowNetSystem shadowNetSystem;  // All nets needed for the
 *                                          // simulation initially.
 *
 *        // Get the simulator plugin
 *        SimulatorPlugin simulatorPlugin = SimulatorPlugin.getCurrent();
 *
 *        // Allocate storage for primary net instance reference
 *        NetInstance primaryInstance = null;
 *        SimulationLockExecutor.lock();
 *        try {
 *            // Obtain fresh simulation pool thread to set up new simulation.
 *            Future<NetInstance> future = SimulationThreadPool.getNew().submitAndWait(new Callable<NetInstance>() {
 *                public NetInstance call() throws SomeException {
 *
 *                    // Use default net loader.
 *                    SimulationManager.setDefaultNetLoader();
 *
 *                    // Initialise the simulation.
 *                    SimulationManager.setupSimulation(null);
 *
 *                    // Compile and add nets.
 *                    SimulationManager.addShadowNetSystem(shadowNetSystem);
 *
 *                    // Create the initial net instance.
 *                    NetInstance primaryInstance = simulatorPlugin.createNetInstance(mainNet));
 *
 *                    // Start the simulation.
 *                    SimulationManager.getCurrentSimulator().startRun();
 *
 *                    return primaryInstance;
 *                }
 *            });
 *            primaryInstance = future.get();
 *        } catch (ExecutionException e) {
 *            ...
 *        } finally {
 *            // Release the mutual exclusion lock under any circumstances!
 *            SimulationLockExecutor.unlock();
 *        }
 * </code></pre>
 */
public final class SimulationManager {

    private static final Logger LOGGER = Logger.getLogger(SimulationManager.class);
    private static final ISimulationManager SIMULATION_MANAGER;

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

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

    /**
     * Sets up a new simulation environment by doing the following:
     * <ul>
     * <li>the simulation thread pool is reset</li>
     * <li>the set of properties is configured</li>
     * <li>all registered extensions become activated</li>
     * <li>a new simulation engine is set up</li>
     * <li>the initial net instance is created</li>
     * </ul>
     * The simulation will <b>not</b> be started, so no steps will be executed.
     * All added {@link SimulatorExtension}s will be notified.
     *
     * <p>
     * This method will automatically create a new thread if it is not called
     * from a simulation thread. The disadvantage is that exceptions are not
     * communicated.
     * </p>
     *
     * <p>
     * The behavior of the method has changed from Renew release 2.1 to 2.2. It
     * no longer automatically terminates a running simulation. Instead, an
     * exception is thrown (see below).
     * </p>
     *
     * <p>
     * Callers of this method that switch to a simulation thread by themselves
     * should use the simulator's thread pool. This method
     * automatically discards the new thread pool if simulation setup fails.
     * After successful execution of this method, the new thread pool becomes
     * the current thread pool and the calling thread belongs to the current
     * simulation.
     * </p>
     *
     * <p>
     * Access to this method is exclusive. The Java synchronized mechanism is
     * replaced by a specialized {@link de.renew.simulator.api.SimulationLockExecutor}.
     * How to achieve synchronization across multiple methods is explained there.
     * </p>
     *
     * @param properties additional properties that specify this simulation
     *        environment. These properties will override any default values
     *        from the plugin properties. May be <code>null</code>.
     * @throws SimulationRunningException if a simulation is already running.
     */
    public static void setupSimulation(Properties properties) {
        SIMULATION_MANAGER.setupSimulation(properties);
    }

    /**
     * Adds net templates based on the shadow nets in the given {@code ShadowNetSystem} to the current
     * simulation.
     * <p>
     * When this method is called on a fresh simulation setup, the given net
     * system's information about shadow net loader and compiler are extracted
     * and kept for the simulation lifetime. In this case, the given {@code ShadowNetSystem}
     * <i>must</i> be configured with a {@code ShadowNetCompiler}. However,
     * setting a {@code ShadowNetLoader} is optional.
     * </p>
     *
     * <p>
     * Access to this method is exclusive. The Java synchronized mechanism is
     * replaced by a specialized {@link de.renew.simulator.api.SimulationLockExecutor}.
     * How to achieve synchronization across multiple methods is explained there.
     * </p>
     *
     * @param shadowNetSystem holds all nets to be compiled into this simulation
     *        environment. The state of the net system will change during
     *        the insertion process: nets are marked as compiled, and the
     *        default shadow net loader is configured (optional).
     *
     * @throws NoSimulationException if there is no simulation set up.
     * @throws SyntaxException if an error occurs during the compilation process.
     * @throws NullPointerException if the {@code netSystem} is {@code null}.
     */
    public static void addShadowNetSystem(ShadowNetSystem shadowNetSystem)
        throws SyntaxException, NoSimulationException
    {
        SIMULATION_MANAGER.addShadowNetSystem(shadowNetSystem);
    }

    /**
     * Returns the currently configured {@code Simulator}, if a simulation is running.
     * Otherwise, {@code null} is returned.
     *
     * @return the currently configured {@code Simulator}, if a simulation is running. Otherwise, {@code null}
     */
    public static Simulator getCurrentSimulator() {
        return SIMULATION_MANAGER.getCurrentSimulator();
    }

    /**
     * Returns the properties used in the current simulation, if a simulation is running.
     * Otherwise, {@code null} is returned.
     *
     * @return the properties used in this simulation, if a simulation is running. Otherwise, {@code null}
     **/
    public static Properties getSimulationProperties() {
        return SIMULATION_MANAGER.getSimulationProperties();
    }

    /**
     * Returns the current simulation environment, if a simulation has been set
     * up.
     * <p>
     * Do not expect that the data provided here has any guaranteed life span -
     * if you want to be informed about the termination of the simulation, write
     * and register a {@link SimulatorExtension}.
     * </p>
     *
     * @return a <code>SimulationEnvironment</code> object describing the actual
     *         simulation setup.
     */
    public static SimulationEnvironment getCurrentEnvironment() {
        return SIMULATION_MANAGER.getCurrentEnvironment();
    }

    /**
     * Terminates the current simulation. If no simulation has been set up,
     * nothing happens.
     *
     * <p>
     * Access to this method is exclusive. The Java synchronized mechanism is
     * replaced by a specialized {@link de.renew.simulator.api.SimulationLockExecutor}.
     * How to achieve synchronization across multiple methods is explained there.
     * </p>
     */
    public static void terminateSimulation() {
        SIMULATION_MANAGER.terminateSimulation();
    }

    /**
     * Returns whether the simulation is currently active. If this method returns
     * {@code true}, a simulation has been set up
     * <i>and</i> is still active (see documentation of {@link Simulator#isActive}).
     * <p>
     * This method will automatically create a new thread if it is not called
     * from a simulation thread
     *
     * @return {@code true}, if the simulation is active (see above), {@code false} otherwise.
     */
    public static boolean isSimulationActive() {
        return SIMULATION_MANAGER.isSimulationActive();
    }

    /**
     * Configure a default net loader to be used in the next
     * simulation setup. This net loader will be connected to the plugin's
     * shadow net system.
     */
    public static void setDefaultNetLoader() {
        SIMULATION_MANAGER.setDefaultNetLoader();
    }
}
