package de.renew.application;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
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.simulator.ISimulatorFactory;
import de.renew.engine.simulator.SimulatorEventQueue;
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.loading.NetLoader;
import de.renew.plugin.PluginManager;
import de.renew.plugin.PropertyHelper;
import de.renew.shadowcompiler.DefaultCompiledNetLoader;
import de.renew.shadowcompiler.DefaultShadowNetLoader;
import de.renew.shadowcompiler.SNSFinder;
import de.renew.shadowcompiler.SequentialOnlyExtension;
import de.renew.shadowcompiler.ShadowLookup;
import de.renew.shadowcompiler.ShadowNetSystemCompiler;
import de.renew.simulator.api.IFinderRegistration;
import de.renew.simulator.api.ISimulationLockExecutor;
import de.renew.simulator.api.ISimulationManager;
import de.renew.simulator.api.ISimulatorExtensions;
import de.renew.simulatorontology.loading.NetNotFoundException;
import de.renew.simulatorontology.shadow.ShadowCompilationResult;
import de.renew.simulatorontology.shadow.ShadowNetLoader;
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 is responsible for managing the simulation environment (see {@link ISimulationManager}).
 * It is provided to the interface module through the {@link SimulationManagerProvider}.
 * The facade for this class is {@link de.renew.simulator.api.SimulationManager}.
 */
class SimulationManagerImpl implements ISimulationManager {
    /**
     * The name of the property to get the priority of the simulator thread
     */
    private static final String PRIORITY_PROP_NAME = "de.renew.simulatorPriority";

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

    private final ISimulatorExtensions _simulatorExtensions;
    private final IFinderRegistration _finderRegistration;
    private final ISimulatorFactory _simulatorFactory;
    private final INetLookup _netLookup;
    private final ISimulationLockExecutor _simulationLockExecutor;

    /**
     * Holds all information about the current simulation environment.
     */
    private SimulationEnvironment _currentSimulation;

    /**
     * Holds a reference to the SimulationThreadPool
     */
    private SimulationThreadPool _simulationThreadPool;

    /**
     * Flags that the current simulation is virgin, that no nets have been added
     * yet.
     */
    private boolean _virginSimulation;

    /**
     * Holds the net loader to use in the next simulation setup.
     */
    private NetLoader _nextNetLoader;

    /**
     * Creates a new {@code SimulationManagerImpl}. The given dependencies will be used to manage the simulation.
     *
     * @param simulatorExtensions is needed to retrieve and notify the registered {@link SimulatorExtension} instances
     * @param finderRegistration is needed to register and retrieve net loaders and finders
     * @param simulatorFactory is needed to create the {@link Simulator} to use for the simulation
     * @param netLookup is needed to retrieve and manage the known nets for this simulation
     * @param simulationLockExecutor is needed to make all methods mutually exclusive
     */
    SimulationManagerImpl(
        ISimulatorExtensions simulatorExtensions, IFinderRegistration finderRegistration,
        ISimulatorFactory simulatorFactory, INetLookup netLookup,
        ISimulationLockExecutor simulationLockExecutor)
    {
        _simulationThreadPool = SimulationThreadPool.getCurrent();
        _simulatorExtensions = simulatorExtensions;
        _finderRegistration = finderRegistration;
        _simulatorFactory = simulatorFactory;
        _netLookup = netLookup;
        _simulationLockExecutor = simulationLockExecutor;
        _finderRegistration.registerDefaultNetFinder(new SNSFinder());
        setDefaultNetLoader();
    }

    @Override
    public void setupSimulation(Properties properties) {
        Runnable action = () -> SimulationThreadPool.getNew().executeAndWait(() -> {
            // Check that no simulation is running. The old behavior
            // of terminating a simulation (Renew 2.1) was in conflict
            // with the SimulationThreadPool (introduced Renew 2.2).
            // Terminating a simulation is an asynchronous process
            // in contrast to the immediate replacement of a current
            // thread pool during setup. It would be probable that
            // some pending events of the old simulation would be
            // executed within the new thread pool.
            if (isSimulationSetup()) {
                SimulationThreadPool.discardNew();
                throw new SimulationRunningException();
            }

            restartThreadPool();

            // Combine the plugin properties with the specified
            // properties from this method call. Disconnect the
            // active property set from the plugin properties by
            // copying all entries.
            final Properties activeProperties = new Properties();
            activeProperties.putAll(SimulatorPlugin.getCurrent().getProperties());
            if (properties != null) {
                activeProperties.putAll(properties);
            }
            int maxPriority = PropertyHelper
                .getIntProperty(activeProperties, PRIORITY_PROP_NAME, Thread.NORM_PRIORITY);
            if (_simulationThreadPool.getMaxPriority() != maxPriority) {
                _simulationThreadPool.setMaxPriority(maxPriority);
            }
            SimulatorEventQueue.initialize();
            LOGGER.debug("SimulationManagerImpl: Setting up simulation.");

            // Configure class reloading, if requested.
            SimulatorPlugin.getCurrent().possiblySetupClassSource(activeProperties);

            // Ensure that all old nets have been forgotten and
            // set the new net loader.
            _netLookup.forgetAllNets();
            _netLookup.setNetLoader(_nextNetLoader);
            if (_nextNetLoader instanceof DelayedDelegationNetLoader) {
                LOGGER.debug("SimulationManagerImpl: Creating default shadow net loader.");
                _finderRegistration.setNetLoader(new DefaultShadowNetLoader(activeProperties));
                LOGGER.debug("SimulationManagerImpl: Configuring delayed net loader.");
                ((DelayedDelegationNetLoader) _nextNetLoader).setNetLoader(
                    new DefaultCompiledNetLoader(
                        this, _netLookup, _finderRegistration.getNetLoader()));
            }


            // Create the simulation engine with respect to the
            // current properties.
            Simulator simulator = _simulatorFactory.createSimulator(activeProperties);

            SimulatorExtension[] extensions =
                _simulatorExtensions.getSimulationExtensions().toArray(new SimulatorExtension[0]);
            // Store all information in a new simulation environment.
            _currentSimulation = new SimulationEnvironment(simulator, extensions, activeProperties);
            _virginSimulation = true;


            // Inform all active extensions about the simulation setup.
            for (SimulatorExtension activeExtension : extensions) {
                activeExtension.simulationSetup(_currentSimulation);
            }

            // Register this as exit blocker as long as the simulation
            // is
            // active.
            PluginManager.getInstance().blockExit(SimulatorPlugin.getCurrent());
        });

        _simulationLockExecutor.runWithLock(action);
    }

    @Override
    public void addShadowNetSystem(ShadowNetSystem shadowNetSystem)
        throws SyntaxException, NoSimulationException
    {
        _simulationLockExecutor.lock();
        try {
            Future<Void> future = SimulationThreadPool.getCurrent().submitAndWait(() -> {
                ShadowLookup lookup;
                if (!isSimulationActive()) {
                    throw new NoSimulationException();
                }

                Objects.requireNonNull(shadowNetSystem, "Missing shadow net system.");

                // Apply default net loader if requested.
                ShadowNetLoader netLoader = shadowNetSystem.getNetLoader();
                if ((netLoader == null) && (_finderRegistration.getNetLoader() != null)) {
                    LOGGER.debug(
                        "SimulationManagerImpl: Applying default shadow net loader to net system.");
                    shadowNetSystem.setNetLoader(_finderRegistration.getNetLoader());
                }

                // Compile nets.
                if (_virginSimulation) {
                    LOGGER.debug("SimulationManagerImpl: Compiling first net system.");
                    lookup = ShadowNetSystemCompiler.getInstance().compile(shadowNetSystem);
                } else {
                    LOGGER.debug("SimulationManagerImpl: Adding another net system.");
                    lookup = ShadowNetSystemCompiler.getInstance().compileMore(shadowNetSystem);
                }
                LOGGER.debug("SimulationManagerImpl: Compilation result lookup: " + lookup);

                // Check whether compilation result fits into the current simulation
                // with respect to sequential step requirements.
                SequentialOnlyExtension seqEx = SequentialOnlyExtension.lookup(lookup);
                boolean sequentialOnly = seqEx.getSequentialOnly();
                if (sequentialOnly && !_currentSimulation.getSimulator().isSequential()) {
                    throw new SyntaxException("Some nets need a sequential simulator.");
                    // TODO: add error objects by asking seqEx
                }

                // Now we are sure that the nets can be added to the simulation
                // Unset the virgin flag.
                _virginSimulation = false;

                // Insert all compiled nets into the running simulation.
                // This call must be done before the extensions are notified, as an infinite loop could be caused otherwise.
                lookup.makeNetsKnown();

                // Inform all active extensions about the new
                // nets.
                SimulatorExtension[] activeExtensions = _currentSimulation.getExtensions();
                ShadowCompilationResult result = new DefaultShadowCompilationResult(lookup);
                for (SimulatorExtension activeExtension : activeExtensions) {
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug(
                            SimulatorPlugin.class.getName() + ": Active Extension compile net "
                                + activeExtension.toString());
                    }
                    activeExtension.netsCompiled(result);
                }
                return null;
            });

            future.get();
        } catch (InterruptedException e) {
            LOGGER.info("Simulation ended");
        } catch (ExecutionException e) {
            Throwable t = e.getCause();
            if (t instanceof SyntaxException 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 {
            _simulationLockExecutor.unlock();
        }
    }

    @Override
    public Simulator getCurrentSimulator() {
        if (isSimulationSetup()) {
            return _currentSimulation.getSimulator();
        }
        return null;
    }

    @Override
    public Properties getSimulationProperties() {
        if (isSimulationSetup()) {
            return _currentSimulation.getProperties();
        }
        return null;
    }

    @Override
    public SimulationEnvironment getCurrentEnvironment() {
        return _currentSimulation;
    }

    @Override
    public void terminateSimulation() {
        Runnable action = () -> SimulationThreadPool.getCurrent().executeAndWait(() -> {
            if (_currentSimulation == null) {
                return;
            }
            LOGGER.debug("SimulationManagerImpl: Stopping simulation.");

            SimulatorExtension[] exts = _currentSimulation.getExtensions();
            for (SimulatorExtension ext : exts) {
                ext.simulationTerminating();
            }

            // Stop the engine.
            _currentSimulation.getSimulator().terminateRun();

            exts = _currentSimulation.getExtensions();
            for (SimulatorExtension ext : exts) {
                ext.simulationTerminated();
            }

            // Go back to simulation time 0 and
            // clear all outstanding search requests.
            // SearchQueue.reset(0);
            SearchQueue.reset(0);
            // Clear all token IDs that might be
            // still registered.
            IDRegistry.reset();

            // Forget all net structures.
            _netLookup.forgetAllNets();

            // Retract our exit blocker because the simulation is
            // over.
            PluginManager.getInstance().exitOk(SimulatorPlugin.getCurrent());

            // We should not clean the thread pool here.
            // Simulation is terminated asynchronously.
            // There still may be requests for a simulation thread.
            // SimulationThreadPool.getCurrent().cleanup();
            _currentSimulation = null;
            _finderRegistration.setNetLoader(null);
        });

        _simulationLockExecutor.runWithLock(action);
    }

    @Override
    public boolean isSimulationActive() {
        return isSimulationSetup() && _currentSimulation.getSimulator().isActive();
    }

    @Override
    public boolean isSimulationSetup() {
        return _currentSimulation != null && _currentSimulation.getSimulator() != null;
    }

    @Override
    public synchronized void setDefaultNetLoader() {
        setNetLoader(new DelayedDelegationNetLoader());
    }

    @Override
    public void cleanup() {
        setNetLoader(null);
        SimulationThreadPool.cleanup();
        _simulatorExtensions.cleanup();
    }

    private void setNetLoader(NetLoader netLoader) {
        LOGGER.debug("SimulationManagerImpl: Configuring net loader " + netLoader + ".");
        _nextNetLoader = netLoader;
    }

    private void restartThreadPool() {
        SimulationThreadPool.cleanup();
        _simulationThreadPool = SimulationThreadPool.getSimulationThreadPool();
    }

    private static final class DefaultShadowCompilationResult implements ShadowCompilationResult {
        private final ShadowLookup _lookup;

        DefaultShadowCompilationResult(ShadowLookup lookup) {
            _lookup = lookup;
        }

        @Override
        public boolean containsNewlyCompiledNets() {
            return _lookup.containsNewlyCompiledNets();
        }

        @Override
        public Collection<String> allNewlyCompiledNetNames() {
            List<String> result = new ArrayList<>();
            _lookup.allNewlyCompiledNetNames().forEachRemaining(result::add);
            return result;
        }
    }

    /**
     * This net loader serves as a placeholder. It denies all
     * <code>loadNet()</code> requests until the real net loader has been
     * configured.
     */
    private static final class DelayedDelegationNetLoader implements NetLoader {
        private NetLoader _netLoader = null;

        public void setNetLoader(NetLoader netLoader) {
            _netLoader = netLoader;
        }

        @Override
        public Net loadNet(final String netName) throws NetNotFoundException {
            if (_netLoader == null) {
                throw new NetNotFoundException("No net loader configured.");
            } else {
                return _netLoader.loadNet(netName);
            }
        }
    }
}
