package de.renew.application;

import java.io.IOException;
import java.io.ObjectOutput;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.apache.log4j.Logger;

import de.renew.engine.searchqueue.SearchQueue;
import de.renew.engine.thread.SimulationLockExecutorProvider;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.net.IDRegistry;
import de.renew.net.INetLookup;
import de.renew.net.Net;
import de.renew.net.NetInstance;
import de.renew.net.NetLookup;
import de.renew.net.serialisation.NetSerializer;
import de.renew.plugin.IPlugin;
import de.renew.plugin.PluginAdapter;
import de.renew.plugin.PluginManager;
import de.renew.plugin.PluginProperties;
import de.renew.plugin.PropertyHelper;
import de.renew.simulator.api.ISimulationLockExecutor;
import de.renew.simulator.api.ISimulationManager;
import de.renew.simulator.api.SimulatorPropertyConstants;
import de.renew.simulatorontology.loading.NetNotFoundException;
import de.renew.simulatorontology.simulation.NoSimulationException;
import de.renew.simulatorontology.simulation.Simulator;
import de.renew.util.ClassSource;
import de.renew.util.RenewObjectOutputStream;
import de.renew.util.SingletonException;

import static de.renew.simulator.api.SimulatorPropertyConstants.MULTIPLICITY_PROP_NAME;
import static de.renew.simulator.api.SimulatorPropertyConstants.REINIT_PROP_NAME;


/**
 * This class serves as the main facade for the Renew simulator component. 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>
 * This class combines most of the functionality of the old classes
 * <code>de.renew.gui.CPNSimulation</code> and
 * <code>de.renew.application.ShadowSimulator</code>. The old subclasses of
 * <code>ShadowSimulator</code> (<code>de.renew.remote.ServerImpl</code>,
 * <code>de.renew.access.AccessControlledServerImpl</code> and
 * <code>de.renew.workflow.WorkflowServerImpl</code>) are now covered by the
 * extension interface defined along with this class.
 * </p>
 * <p>
 * This class must be used as a singleton (and in fact all constructors and
 * methods enforce this) because some parts of the simulation engine use global
 * states in static fields.
 * </p>
 * <p>
 * One way to set up a simulation might look as follows:
 *
 * <pre>
 *        // 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;
 *
 *        // Acquire mutual exclusion for operations on simulator plug-in.
 *        simulatorPlugin.lock.lock();
 *        try {
 *            // Obtain fresh simulation pool thread to set up new simulation.
 *            Future&lt;NetInstance&gt; future = SimulationThreadPool.getNew().submitAndWait(new Callable&lt;NetInstance&gt;() {
 *                public NetInstance call() throws SomeException {
 *
 *                    // Use default net loader.
 *                    simulatorPlugin.setDefaultNetLoader();
 *
 *                    // Initialise the simulation.
 *                    simulatorPlugin.setupSimulation(null);
 *
 *                    // Compile and add nets.
 *                    simulatorPlugin.insertNets(shadowNetSystem);
 *
 *                    // Create the initial net instance.
 *                    NetInstance primaryInstance = simulatorPlugin.createNetInstance(mainNet));
 *
 *                    // Start the simulation.
 *                    simulatorPlugin.getCurrentEnvironment().getSimulator().startRun();
 *
 *                    return primaryInstance;
 *                }
 *            });
 *            primaryInstance = future.get();
 *        } catch (ExecutionException e) {
 *           ...
 *        } finally {
 *                 // Release the mutual exclusion lock under any circumstances!
 *                simulatorPlugin.lock.unlock();
 *        }
 * </pre>
 * <p>
 * SimulatorPlugin.java Created: Mon Jun 2 2003
 *
 * @author Michael Duvigneau
 * @since Renew 2.0
 */
public class SimulatorPlugin extends PluginAdapter {

    /**
     * Version history:
     * 1 no header, included NetInstances and SearchQueue
     * 2 header(label, version, simulator type), NetInstances, Nets,
     *   SearchQueue - since Renew 1.2 beta 11
     * 3 small changes to ReflectionSerializer (Class[], nulls) can
     *   still read streams of version 2
     * 4 marking and search queue are now saved with time stamps
     *   incompatible change
     * 5 PlaceInstance is now an abstract class with subclasses
     *   incompatible change
     * (6) used in branch agent_serialization
     * 7 Different ID handling for net elements, introduction of remote
     *   layer - incompatible change
     * 8 Decomposed package de.renew.simulator - incompatible change
     * 9 Java version change, added assertions - incompatible change
     */
    static final int STATE_STREAM_VERSION = 9;

    /**
     * The header identification string for all saved state streams.
     */
    static final String STATE_STREAM_LABEL = "RenewState";

    /**
     * The name of the main package of this class.
     */
    private static final String MAIN_PACKAGE_NAME = "de.renew.simulator";

    /**
     * Name for the {@link SimulationControlCommand} when registering and removing
     * the command at {@link PluginManager#addCLCommand addCLCommand} and
     * {@link PluginManager#removeCLCommand removeCLCommand}
     */
    private static final String SIMULATION_CONTROL_COMMAND = "simulator";

    /**
     * Name for the {@link StartSimulationCommand} when registering and removing
     * the command at {@link PluginManager#addCLCommand addCLCommand} and
     * {@link PluginManager#removeCLCommand removeCLCommand}
     */
    private static final String SIMULATION_START_COMMAND = "startsimulator";

    /**
     * The logger for this class.
     */
    private static final Logger LOGGER = Logger.getLogger(SimulatorPlugin.class);

    /**
     * Used to synchronise access to the static <code>singleton</code> variable.
     */
    private static final Object SINGLETON_LOCK = new Object();

    private static final ISimulationLockExecutor LOCK_EXECUTOR =
        SimulationLockExecutorProvider.provider();

    /**
     * Holds a reference to the one and only SimulatorPlugin instance. Set by
     * the {@link #SimulatorPlugin constructor} and reset by the method
     * {@link #cleanup}. To check whether the current object is still the
     * current singleton instance, call {@link #checkSingleton} at each public
     * method entry point.
     */
    private static SimulatorPlugin _singleton = null;

    private final ISimulationManager _simulationManager;

    /**
     * The {@link INetLookup} used to find known nets.
     */
    private final INetLookup _netLookup = new NetLookup(LOCK_EXECUTOR);

    /**
     * Remembers whether classReinit mode was active in the last run.
     */
    private boolean _previousClassReinit = false;

    /**
     * Creates an instance of the simulator component facade. There can exist at
     * most one instance at any time, additional constructor calls will throw an
     * exception. To get rid of the instance, call {@link #cleanup}. Any
     * subsequent calls to methods of the old instance will raise runtime
     * exceptions.
     *
     * @param props a <code>PluginProperties</code> value
     * @throws SingletonException if another singleton instance exists.
     */
    public SimulatorPlugin(PluginProperties props) {
        super(props);
        synchronized (SINGLETON_LOCK) {
            if (_singleton != null) {
                throw new SingletonException("At most one instance of SimulatorPlugin is allowed.");
            }
            _singleton = this;
        }
        _simulationManager = SimulationManagerProvider.provider();
    }

    /**
     * This method <b>must</b> be called at the entry point of all public
     * methods of this object to ensure that this instance is still the
     * <code>SimulatorPlugin</code> singleton instance.
     *
     * @throws SingletonException if this instance is not the
     *         <code>SimulatorPlugin</code> singleton instance anymore.
     */
    private void checkSingleton() {
        if (_singleton != this) {
            throw new SingletonException();
        }
    }

    /**
     * Initialises this plugin after all dependencies to other plugins have been
     * fulfilled.
     *
     * @throws SingletonException if this instance is not the
     *         <code>SimulatorPlugin</code> singleton instance anymore.
     */
    @Override
    public void init() {
        checkSingleton();
        // addExtension(new RemoteExtension());
        PluginManager.getInstance().addCLCommand(
            SIMULATION_START_COMMAND,
            new StartSimulationCommand(
                _simulationManager, this, LOCK_EXECUTOR,
                new SimulationStateRestorerImpl(LOCK_EXECUTOR, _simulationManager)));
        PluginManager.getInstance().addCLCommand(
            SIMULATION_CONTROL_COMMAND, new SimulationControlCommand(_simulationManager));
    }

    /**
     * Writes the given {@code NetInstance}s as well as all known
     * {@code Net}s and the {@code SearchQueue} contents and some
     * additional information to the stream. The written information is
     * sufficient to continue the simulation from the same state after
     * deserialization.
     * <p>
     * If the given {@code ObjectOutput} is a <b>
     * {@link de.renew.util.RenewObjectOutputStream}</b>, its feature of cutting
     * down the recursion depth by delaying the serialization of some fields
     * will be used.
     * </p>
     *
     * <p>
     * Access to this method is exclusive. The Java synchronized mechanism is
     * replaced by a specialized {@link de.renew.util.Lock#lock()}. How to achieve
     * synchronization across multiple methods is explained there.
     * </p>
     *
     * @param output target stream (see note about
     *        {@code RenewObjectOutputStream} above).
     * @param instances an array of net instances to be explicitly included in the
     *        saved state (e.g. instances displayed to the user).
     * @throws IOException if an error occurs during the serialisation to the output
     *         stream.
     * @throws NoSimulationException if there is no simulation set up.
     * @throws SingletonException if this object is not the simulator plugin singleton
     *         instance anymore.
     */
    public void saveState(final ObjectOutput output, final NetInstance[] instances)
        throws IOException
    {
        checkSingleton();

        LOCK_EXECUTOR.lock();
        try {
            Future<Object> future = SimulationThreadPool.getCurrent().submitAndWait(() -> {
                if (!_simulationManager.isSimulationSetup()) {
                    throw new NoSimulationException();
                }
                _simulationManager.getCurrentSimulator().stopRun();

                // Use the domain trace feature of the
                // RenewObjectOutputStream, if available.
                RenewObjectOutputStream rOut = null;
                if (output instanceof RenewObjectOutputStream) {
                    rOut = (RenewObjectOutputStream) output;

                }
                if (rOut != null) {
                    rOut.beginDomain(SimulatorPlugin.class);
                }

                // Write the header, which contains:
                // - label
                // - file format version number
                // - type of simulator used
                output.writeObject(STATE_STREAM_LABEL);
                output.writeInt(STATE_STREAM_VERSION);
                output.writeInt(
                    PropertyHelper.getIntProperty(
                        _simulationManager.getSimulationProperties(), MULTIPLICITY_PROP_NAME));

                // First part: save all NetInstances explicitly
                // named.
                // They are not necessarily sufficient to describe
                // the
                // simulation state completely.
                output.writeInt(instances.length);
                for (NetInstance instance : instances) {
                    output.writeObject(instance);
                }

                // If a RenewObjectOutputStream is used, write
                // all delayed fields NOW.
                if (rOut != null) {
                    rOut.writeDelayedObjects();

                }

                // Second part: save all Nets currently known by
                // the INetLookup#isKnown() lookup mechanism.
                // This ensures that the static part of all compiled
                // nets will be available on deserialization.
                new NetSerializer(LOCK_EXECUTOR, new NetLookup()).saveAllKnownNets(output);

                // Third part: add all entries from the SearchQueue.
                // These entries alone are sufficient to describe
                // the
                // current simulation state completely.
                // But this information does not necessarily
                // include
                // all
                // nets possibly required by future simulation
                // steps.
                SearchQueue.saveQueue(output);

                // Last part: Restore the ID registry with its
                // well-known
                // IDs for every token.
                IDRegistry.save(output);

                if (rOut != null) {
                    rOut.endDomain(SimulatorPlugin.class);
                }
                return null;
            });
            future.get();
        } catch (InterruptedException e) {
            LOGGER.error("Timeout while waiting for simulation thread to finish", e);
        } catch (ExecutionException e) {
            Throwable t = e.getCause();
            if (t instanceof IOException exc) {
                throw exc;
            } else if (t instanceof NoSimulationException exc) {
                throw exc;
            } else if (t instanceof RuntimeException exc) {
                throw exc;
            } else if (t instanceof Error exc) {
                throw exc;
            } else {
                LOGGER.error("Simulation thread threw an exception", e);
            }
        } finally {
            LOCK_EXECUTOR.unlock();
        }
    }

    /**
     * Creates a net instance within the current simulation.
     *
     * <p>
     * Access to this method is exclusive. The Java synchronized mechanism is
     * replaced by a specialized {@link de.renew.util.Lock#lock()}. How to achieve
     * synchronization across multiple methods is explained there.
     * </p>
     *
     * @param net the name of the net template for the net instance to build. If
     *        {@code null}, then no net instance will be created.
     * @return the created netInstance. Returns {@code null} if the
     *         instance creation failed.
     * @throws NetNotFoundException if the instance creation failed because no net with the given
     *         name could be found.
     * @throws NoSimulationException if there is no simulation set up.
     * @throws SingletonException if this object is not the simulator plugin singleton
     *         instance anymore.
     */
    public NetInstance createNetInstance(final String net)
        throws NetNotFoundException, NoSimulationException
    {
        checkSingleton();

        LOCK_EXECUTOR.lock();
        try {
            Future<NetInstance> future = SimulationThreadPool.getCurrent().submitAndWait(() -> {
                NetInstance netInstance = null;
                if (!_simulationManager.isSimulationSetup()) {
                    throw new NoSimulationException();
                }
                Simulator currentSimulator = _simulationManager.getCurrentSimulator();

                // Create the first net instance
                if (net != null) {
                    Net netTemplate = _netLookup.findForName(net);
                    netInstance = netTemplate.getInstantiator()
                        .buildInstance(currentSimulator.nextStepIdentifier());
                    currentSimulator.refresh();
                }
                return netInstance;
            });

            return future.get();
        } catch (InterruptedException e) {
            LOGGER.info("Creation of NetInstances was aborted");
        } catch (ExecutionException e) {
            Throwable t = e.getCause();
            if (t instanceof NetNotFoundException exc) {
                throw exc;
            } else if (t instanceof NoSimulationException exc) {
                throw exc;
            } else if (t instanceof RuntimeException exc) {
                throw exc;
            } else if (t instanceof Error exc) {
                throw exc;
            } else {
                LOGGER.error("Simulation thread threw an exception", e);
            }
        } finally {
            LOCK_EXECUTOR.unlock();
        }

        // We should never return nothing but some error occurred before.
        return null;

    }

    /**
     * Stops any running simulation and clears all data used by the simulator.
     * If the cleanup was successful (returns <code>true</code>) this
     * <code>SimulatorPlugin</code> instance is rendered useless and all future
     * method calls will throw <code>SingletonException</code>s.
     *
     * @return <code>true</code>, if the cleanup was successfully finished and
     *         this object has lost its singleton status. <br>
     *         <code>false</code>, if the cleanup failed. This object is still
     *         the singleton for any simulator access.
     * @throws SingletonException if this object has already lost the singleton instance status
     *         before.
     */
    @Override
    public synchronized boolean cleanup() {

        synchronized (SINGLETON_LOCK) {
            checkSingleton();
            _simulationManager.terminateSimulation();
            _simulationManager.cleanup();
            // if (result=false) return immediately before releasing
            // the singleton!
            _singleton = null;
        }
        PluginManager.getInstance().removeCLCommand(SIMULATION_START_COMMAND);
        PluginManager.getInstance().removeCLCommand(SIMULATION_CONTROL_COMMAND);

        return true;
    }

    /**
     * Describe <code>canShutDown</code> method here.
     *
     * @return a <code>boolean</code> value
     * @throws SingletonException if this object is not the simulator plugin singleton
     *         instance anymore.
     */
    @Override
    public boolean canShutDown() {
        checkSingleton();
        return true;
    }

    /**
     * Executes the given {@link Callable} in a simulation thread and waits for
     * its computation to finish.
     * <p>
     * This facade method just delegates to the current
     * {@link SimulationThreadPool} instance.  For the caller's convenience, the
     * resulting {@link Future} is unpacked immediately.  If an
     * {@link ExecutionException} occurs, the causing exception is unpacked or
     * converted into a {@link RuntimeException}.  If there is no simulation
     * running, this method exits with a {@link NoSimulationException}.
     * </p>
     *
     * @param <T> the type of the computation
     * @param task the computation to be executed in a simulation thread.
     * @return the computation result.
     * @throws SingletonException if this object is not the simulator
     *         plugin singleton instance anymore.
     * @throws NoSimulationException if there is no simulation set up.
     * @throws InterruptedException if the current thread is interrupted
     * @see SimulationThreadPool#submitAndWait(Callable)
     **/
    public <T> T submitAndWait(Callable<T> task) throws InterruptedException {
        checkSingleton();
        LOCK_EXECUTOR.lock();
        try {
            if (!_simulationManager.isSimulationSetup()) {
                throw new NoSimulationException();
            }
            Future<T> futureObj = SimulationThreadPool.getCurrent().submitAndWait(task);
            return futureObj.get();
        } catch (ExecutionException e) {
            Throwable t = e.getCause();
            if (t instanceof NoSimulationException exc) {
                throw exc;
            } else if (t instanceof RuntimeException exc) {
                throw exc;
            } else if (t instanceof Error exc) {
                throw exc;
            } else {
                LOGGER.error("Simulation thread threw an exception: " + t, e);
                throw new RuntimeException(e);
            }
        } finally {
            LOCK_EXECUTOR.unlock();
        }
    }

    /**
     * Make sure that the user-level classes get reloaded for the next compiler
     * run, if the class reinit mode has been configured. This method is called
     * automatically by {@link de.renew.simulator.api.SimulationManager#setupSimulation}.
     * <p>
     * This method will automatically create a new thread if it is not called
     * from a simulation thread
     *
     * <p>
     * Access to this method is exclusive. The Java synchronized mechanism is
     * replaced by a specialized {@link de.renew.util.Lock#lock()}. How to achieve
     * synchronization across multiple methods is explained there.
     * </p>
     *
     * @param props the configuration to extract the {@link SimulatorPropertyConstants#REINIT_PROP_NAME}
     *        property from.
     * @throws IllegalStateException if simulation is already running
     * @throws IllegalStateException if there is an active simulation.
     * @throws SingletonException if this object is not the simulator plugin
     *         singleton instance anymore.
     */
    public void possiblySetupClassSource(final Properties props) throws IllegalStateException {
        checkSingleton();

        LOCK_EXECUTOR.lock();
        try {
            Future<Object> result = SimulationThreadPool.getCurrent().submitAndWait(() -> {
                if (_simulationManager.isSimulationActive()) {
                    throw new IllegalStateException(
                        "Reconfiguration of class source is "
                            + "not allowed while a simulation is running.");
                }
                boolean classReinit = PropertyHelper.getBoolProperty(props, REINIT_PROP_NAME);
                if (classReinit) {
                    if (!_previousClassReinit) {
                        LOGGER.info("Using classReinit mode.");
                    } else {
                        LOGGER.debug("SimulatorPlugin: Re-initialising class loader.");
                    }

                    // In Renew 2.x SelectiveClassLoader was
                    // replaced by
                    // BottomClassLoader.
                    //
                    // SelectiveClassLoader classLoader = new
                    // SelectiveClassLoader();
                    // classLoader.setSelectors(new String[] {
                    // "de.renew.util.ReloadableDeserializerImpl",
                    // "-java",
                    // "-collections.", "-CH.ifa.draw.",
                    // "-de.renew.",
                    // "-de.uni_hamburg.fs." });
                    ClassLoader classLoader = PluginManager.getInstance().getNewBottomClassLoader();
                    ClassSource.setClassLoader(classLoader);
                } else if (_previousClassReinit) {
                    LOGGER.debug("classReinit mode disabled.");
                    ClassSource.setClassLoader(null);
                }
                _previousClassReinit = classReinit;

                return null;
            });

            result.get();
        } catch (InterruptedException e) {
            LOGGER.error("Timeout while waiting for simulation thread to finish", e);
        } catch (ExecutionException e) {
            Throwable t = e.getCause();
            if (t instanceof IllegalStateException exc) {
                throw exc;
            } else if (t instanceof RuntimeException exc) {
                throw exc;
            } else if (t instanceof Error exc) {
                throw exc;
            } else {
                LOGGER.error("Simulation thread threw an exception", e);
            }
        } finally {
            LOCK_EXECUTOR.unlock();
        }
    }

    /**
     * Provides a reference to the current Renew Simulator plugin instance. The
     * instance is queried from the plugin management system. So the result will
     * be <code>null</code>, if the simulator plugin is not activated.
     *
     * @return the active simulator plugin instance, if there is any. Returns
     *         <code>null</code> otherwise.
     */
    public static SimulatorPlugin getCurrent() {
        for (IPlugin plugin : PluginManager.getInstance().getPluginsProviding(MAIN_PACKAGE_NAME)) {
            if (plugin instanceof SimulatorPlugin) {
                return (SimulatorPlugin) plugin;
            }
        }
        return _singleton;
    }
}