package de.renew.gui;

import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import de.renew.api.SimulationStateLoader;
import de.renew.application.IllegalCompilerException;
import de.renew.application.SimulatorExtensionAdapter;
import de.renew.application.SimulatorPlugin;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.net.NetInstance;
import de.renew.remote.NetInstanceAccessor;
import de.renew.remote.RemotePlugin;
import de.renew.shadowcompiler.ShadowCompilerFactory;
import de.renew.shadowcompiler.ShadowNetSystemCompiler;
import de.renew.shadowcompiler.ShadowNetSystemCreator;
import de.renew.simulator.api.SimulationManager;
import de.renew.simulator.api.SimulatorExtensions;
import de.renew.simulatorontology.loading.NetNotFoundException;
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;


public class CPNSimulation {
    private static final org.apache.log4j.Logger logger =
        org.apache.log4j.Logger.getLogger(CPNSimulation.class);
    protected ShadowNetSystem netSystem;
    private final boolean sequentialOnly;
    private BreakpointManager breakpointManager = null;
    private final CPNDrawingLoader drawingLoader;
    private final SimulatorPlugin simulatorPlugin;
    public Exception lastSyntaxException;
    public boolean inGuiSetup;
    private SimulatorGuiCompilationExtension compilationExtension;

    /**
     * Creates an instance of CPNSimulation.
     * <p>
     * The object has nearly-singleton character. It is mandatory to call {
     * {@link #dispose()} before the object is released.
     * </p>
     *
     * @param onlySequential
     *            - documents whether simulations should be regarded as
     *            sequential. This class does not use the value directly. It
     *            seems that the parameter might be obsolete as there exists one
     *            constructor call only with a fixed value of <code>false</code>
     *            : {@link ModeReplacement#getSimulation()}.
     * @param loader
     *            - a reference to the central loader and manager of
     *            CPNDrawings. Needed to compile all drawings on demand.
     */
    public CPNSimulation(boolean onlySequential, CPNDrawingLoader loader) {
        this.simulatorPlugin = SimulatorPlugin.getCurrent();
        this.sequentialOnly = onlySequential;
        this.drawingLoader = loader;
        this.inGuiSetup = false;
        this.lastSyntaxException = null;
        newNetSystem();
        this.compilationExtension = new SimulatorGuiCompilationExtension(this);
        SimulatorExtensions.addExtension(compilationExtension);
    }

    public boolean isStrictlySequential() {
        return sequentialOnly;
    }

    public void buildAllShadows() {
        newNetSystem();
        boolean again = true;
        while (again) {
            again = false;
            Iterator<CPNDrawing> drawings = drawingLoader.loadedDrawings();

            try {
                // Build new shadows of all nets and all elements.
                // Do everything in a fine order:
                while (drawings.hasNext()) {
                    CPNDrawing currentDrawing = drawings.next();
                    logger.debug(
                        "CPNSimulation: Building shadow for drawing " + currentDrawing + ".");
                    currentDrawing.buildShadow(netSystem);
                }
            } catch (ConcurrentModificationException e) {
                logger.error(
                    "CPNSimulation.buildAllShadows(): Concurrent modification. Redo from start...");
                again = true;
            }
        }
    }

    /**
     * Creates a new ShadowNetSystem, sets the information about compiler and
     * net loader, and updates the private instance variable {@link #netSystem}.
     **/
    protected void newNetSystem() {
        try {
            simulatorPlugin.possiblySetupClassSource(simulatorPlugin.getProperties());
        } catch (IllegalStateException e) {
            logger.warn(
                "CPNSimulation: Cannot configure classReinit mode while a simulation is running.");
        }

        ShadowCompilerFactory compilerFactory =
            ModeReplacement.getInstance().getDefaultCompilerFactory();
        if (compilerFactory == null) {
            logger.warn("CPNSimulation: cannot start, got no compiler.");
        }

        netSystem = ShadowNetSystemCreator.createWithCompiler(compilerFactory);

        logger.debug("CPNSimulation: New net system created.");
    }

    public ShadowNetSystem getNetSystem() {
        // Make sure that all shadows are correctly set up.
        buildAllShadows();
        // Return the shadow net system.
        return netSystem;
    }

    public CPNDrawingLoader getDrawingLoader() {
        return drawingLoader;
    }

    public void setBreakpointManager(BreakpointManager newBreakpointManager) {
        BreakpointManager oldBreakpointManager = breakpointManager;
        breakpointManager = newBreakpointManager;
        if (oldBreakpointManager != null) {
            SimulatorExtensions.removeExtension(oldBreakpointManager);
        }
        SimulatorExtensions.addExtension(newBreakpointManager);
    }

    /**
     * Returns the actual breakpoint manager. Attention: May be
     * <code>null</code>!
     */
    public BreakpointManager getBreakpointManager() {
        return breakpointManager;
    }

    public SimulatorPlugin getSimulatorPlugin() {
        return simulatorPlugin;
    }

    public void syntaxCheckOnly() throws SyntaxException {
        // Make sure to recreate all shadows, so that changes of
        // the drawings are incorporated.
        buildAllShadows();

        // Compile and flatten the lookup table.
        ShadowNetSystemCompiler.getInstance().compile(netSystem);
    }

    /**
     * Start a new simulation. Return the initially created NetInstance, if the
     * creation of the initial instance was successful, or null, if no initial
     * creation took place.
     *
     * @param mainNet
     *            the name of the net to create the first net instance from.
     *
     * @throws SyntaxException
     *             if the compiler found any errors in any net drawing.
     *
     * @throws NetNotFoundException
     *             if no net with the given name <code>mainNet</code> exists.
     */
    public NetInstanceAccessor initSimulation(String mainNet)
        throws SyntaxException, NetNotFoundException, NoSimulationException
    {
        // Remember that we are causing the current simulation setup.
        // Actually a concurrent setup from other sources might interfere
        // until we acquire the SimulatorPlugin lock in the try block below.
        // However, there won't be much damage unless our
        // SimulatorGuiCompilationExtension is called before we could call
        // buildAllShadows - then we might insert an empty, outdated, or
        // partially built sns into the simulation.
        inGuiSetup = true;
        lastSyntaxException = null;
        logger.trace("CPNSimulation: initializing simulation...");
        buildAllShadows();
        try {
            // Set the simulation up
            SimulationManager.setDefaultNetLoader();
            try {
                logger.trace("CPNSimulation: calling setupSimulation...");
                SimulationManager.setupSimulation(null);
                logger.trace("CPNSimulation: finished setupSimulation...");
            } catch (SimulationRunningException e) {
                logger.warn("Simulation already running, inserting nets anyway");
            }

            // The simulation setup should trigger our
            // SimulatorGuiCompilationExtension.
            // If something goes wrong, terminate the simulation.
            if (lastSyntaxException != null) {
                logger.trace(
                    "CPNSimulation: forwarding (rethrowing) exception: " + lastSyntaxException);
                SimulationManager.terminateSimulation();
                if (lastSyntaxException instanceof SyntaxException syntaxException) {
                    throw syntaxException;
                } else if (lastSyntaxException instanceof IllegalCompilerException ice) {
                    throw ice;
                } else {
                    throw new RuntimeException(
                        "Unexpected Exception type: " + lastSyntaxException, lastSyntaxException);
                }
            }

            logger.trace("CPNSimulation: instantiating net " + mainNet + "...");
            NetInstanceAccessor primaryInstance = null;

            if (RemotePlugin.getInstance() != null) {
                primaryInstance = RemotePlugin.getInstance()
                    .wrapInstance(simulatorPlugin.createNetInstance(mainNet));
            }
            if (primaryInstance == null) {
                logger.trace("CPNSimulation: instantiation of net " + mainNet + " failed.");
                SimulationManager.terminateSimulation();
            }
            return primaryInstance;
        } catch (NoSimulationException e) {
            logger.info("CPNSimulation: Simulation terminated externally.");
            throw e;
        } catch (NetNotFoundException | RuntimeException | Error e) {
            logger.trace("CPNSimulation: catching and rethrowing exception: " + e);
            SimulationManager.terminateSimulation();
            throw e;
        } finally {
            inGuiSetup = false;
        }
    }

    /**
     * Writes all currently known <code>NetInstance</code>s, <code>Net</code>s,
     * the <code>SearchQueue</code> contents and some additional information to
     * the stream. The written information is sufficient to continue the
     * simulation from the same state after deserialization.
     * <p>
     * <b>Side effect:</b> Any open <code>BindingSelectionFrame</code> will be
     * closed on execution of this method!
     * </p>
     * <p>
     * With Renew 2.0, most of this method's functionality has moved to the
     * <code>SimulatorPlugin</code>.
     * </p>
     *
     * @param output
     *            target stream
     *
     * @see SimulatorPlugin#saveState
     **/
    public void saveState(java.io.ObjectOutput output) throws java.io.IOException {
        // We have to ensure that the simulation is stopped.
        // Yes, the simulator plugin enforces this, too, but we
        // need to freeze the state before we collect the local
        // net instances and close the selection frame.
        synchronized (simulatorPlugin) {
            SimulationManager.getCurrentSimulator().stopRun();

            // Close the BindingSelectionFrame (otherwise it would
            // be stored with its transition).
            BindingSelectionFrame.close();

            // Collect all local instances from open drawings.
            NetInstance[] instances = CPNInstanceDrawing.getAllLocalInstances();

            // Let the simulator plugin save the state.
            simulatorPlugin.saveState(output, instances);
        }
    }

    /**
     * Restores a simulation saved by <code>saveState()</code>. This method
     * mostly delegates to {@link SimulationStateLoader#loadState}.
     * It additionally tries to reopen the explicitly named net instances within
     * <code>CPNInstanceDrawing</code>s.
     * <p>
     * With Renew 2.0, most of this method's functionality has moved to the
     * <code>SimulatorPlugin</code>.
     * </p>
     *
     * @param input source stream
     */
    public void loadState(java.io.ObjectInput input)
        throws java.io.IOException, ClassNotFoundException, SimulationRunningException
    {
        NetInstance[] instances = SimulationStateLoader.loadState(input, null);

        // Now try to open CPNInstanceDrawings
        // for all net instances which were loaded.
        NetInstanceAccessor currentInstance;
        CPNApplication editor = GuiPlugin.getCurrent().getGui();
        RemotePlugin remote = RemotePlugin.getInstance();
        for (NetInstance instance : instances) {
            currentInstance = remote.wrapInstance(instance);
            if (editor != null) {
                editor.openInstanceDrawing(currentInstance);
            }
        }
    }

    public boolean isSimulationActive() {
        return SimulationManager.isSimulationActive();
    }

    public void simulationTerminate() {
        SimulationManager.terminateSimulation();
    }

    /**
     * To be called after an external firing of a transition was initiated.
     **/
    public void simulationRefresh() {
        SimulationThreadPool.getCurrent().submitAndWait(() -> {
            Simulator simulator = SimulationManager.getCurrentSimulator();
            if (simulator != null) {
                simulator.refresh();
            }
            return null;
        });
    }

    public void simulationStop() {
        SimulationThreadPool.getCurrent().submitAndWait(() -> {
            Simulator simulator = SimulationManager.getCurrentSimulator();
            if (simulator != null) {
                simulator.stopRun();
            }
            return null;
        });
    }

    public void simulationRun() {
        SimulationThreadPool.getCurrent().submitAndWait(new Callable<>() {
            @Override
            public Object call() {
                Simulator simulator = SimulationManager.getCurrentSimulator();

                // To avoid simulation concurrent to serialization
                synchronized (this) {
                    if (breakpointManager != null) {
                        breakpointManager.clearLog();
                    }
                    if (simulator != null) {
                        try {
                            simulator.startRun();
                        } catch (RuntimeException e) {
                            logger
                                .warn("Caught exception while trying to start simulation: " + e, e);
                            throw e;
                        }
                    }
                }
                return null;
            }
        });
    }

    public int simulationStep() {
        Future<Integer> future = SimulationThreadPool.getCurrent().submitAndWait(new Callable<>() {
            @Override
            public Integer call() {
                Simulator simulator = SimulationManager.getCurrentSimulator();

                // To avoid simulation concurrent to serialization
                synchronized (this) {
                    if (breakpointManager != null) {
                        breakpointManager.clearLog();
                    }
                    if (simulator != null) {
                        return simulator.step();
                    } else {
                        return Simulator.STATUS_DISABLED;
                    }
                }
            }
        });
        try {
            return future.get();
        } catch (InterruptedException e) {
            logger.info("Net step aborted");
        } catch (ExecutionException e) {
            logger.error("Exception during execution of simulation step: " + e);
        }
        return Simulator.STATUS_DISABLED;
    }

    public void dispose() {
        SimulatorExtensions.removeExtension(compilationExtension);
        compilationExtension = null;
    }

    private static class SimulatorGuiCompilationExtension extends SimulatorExtensionAdapter {
        private final CPNSimulation sim;

        public SimulatorGuiCompilationExtension(CPNSimulation sim) {
            this.sim = sim;
            logger.trace("SimGuiCompilationExt created.");
        }

        @Override
        public void simulationSetup(SimulationEnvironment env) {
            super.simulationSetup(env);
            ShadowNetSystem netSystem;
            if (sim.inGuiSetup) {
                logger.trace("SimGuiCompilationExt: setup called within guiSetup.");
                netSystem = sim.netSystem; // SNS has already been prepared
            } else {
                logger.trace("SimGuiCompilationExt: setup called concurrently.");
                netSystem = sim.getNetSystem(); // trigger full SNS generation
            }
            try {
                SimulationManager.addShadowNetSystem(netSystem);
                logger.trace("SimGuiCompilationExt: finished initial compilation.");
            } catch (NoSimulationException e) {
                logger.error("This is absurd: no simulation during simulation setup", e);
            } catch (SyntaxException e) {
                if (sim.inGuiSetup) {
                    // Store the exception because the user expects feedback.
                    sim.lastSyntaxException = e;
                } else {
                    logger.warn(
                        "SyntaxException in editor nets during non-gui simulation setup:\n"
                            + e.getMessage(),
                        e);
                }
            } catch (IllegalCompilerException e) {
                if (sim.inGuiSetup) {
                    // Store the exception because the user expects feedback.
                    sim.lastSyntaxException = e;
                } else {
                    logger.warn(
                        "IllegalCompilerException in editor nets during non-gui simulation setup:\n"
                            + e.getMessage(),
                        e);
                }
            }
        }
    }
}