package de.renew.application;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.PrintStream;
import java.io.StreamCorruptedException;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.apache.log4j.Logger;

import de.renew.database.SetupHelper.SimulationState;
import de.renew.database.TransactionSource;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.net.NetInstance;
import de.renew.plugin.command.CLCommand;
import de.renew.simulator.api.ISimulationLockExecutor;
import de.renew.simulator.api.ISimulationManager;
import de.renew.simulator.api.SimulatorPropertyConstants;
import de.renew.simulatorontology.shadow.ShadowNetSystem;
import de.renew.simulatorontology.simulation.SimulationRunningException;
import de.renew.util.ObjectInputStreamUsingBottomLoader;
import de.renew.util.ParameteredCommandLine;


/**
 * This command line command sets up a simulation based on the given shadow net
 * system file and primary net name. It can also print a usage help text.
 * <p>
 * This command always tries to reestablish a database backed state if the
 * required properties are set. If such a state is found, the primary net does
 * not get instantiated (as it would be its second instantiation).
 * </p>
 * StartSimulationCommand.java Created: Wed Jul 16 2003
 *
 * @author Michael Duvigneau
 * @since Renew 2.0
 */
class StartSimulationCommand implements CLCommand {
    /**
     * The logger for this class.
     */
    private static final Logger LOGGER = Logger.getLogger(StartSimulationCommand.class);
    private static final String HELP_PARAM = "-h";
    private static final String ONLY_INIT_PARAM = "-i";
    private final ISimulationManager _simulationManager;
    private final SimulatorPlugin _plugin;
    private final ISimulationLockExecutor _lockExecutor;
    private final ISimulationStateRestorer _simulationStateRestorer;

    /**
     * Creates a new <code>StartSimulationCommand</code> instance.
     *
     * @param simulationManager the {@code ISimulationManager} that is used to access the current simulation
     * @param plugin necessary reference to the simulator plugin instance where this command belongs to
     * @param lockExecutor the {@code ISimulationLockExecutor} that should be used to acquire the simulation lock
     *
     * @throws NullPointerException if any of the parameters is {@code null}
     */
    StartSimulationCommand(
        ISimulationManager simulationManager, SimulatorPlugin plugin,
        ISimulationLockExecutor lockExecutor, ISimulationStateRestorer simulationStateRestorer)
    {
        _simulationManager =
            Objects.requireNonNull(simulationManager, "Need ISimulationManager reference");
        _plugin = Objects.requireNonNull(plugin, "Need SimulatorPlugin reference");
        _lockExecutor =
            Objects.requireNonNull(lockExecutor, "Need an ISimulationLockExecutor reference");
        _simulationStateRestorer = Objects
            .requireNonNull(simulationStateRestorer, "Need an ISimulationStateRestorer reference");

    }

    /**
     * Sets up a simulation or prints a help text. See this class documentation
     * for further details.
     *
     * @param args
     *            arguments to this command, specifying the sns file, the name
     *            of the main net and other optional arguments (see output of
     *            the <code>showSyntax()</code> method).
     * @param response
     *            the <code>PrintStream</code> for user feedback.
     */
    @Override
    public void execute(final String[] args, final PrintStream response) {
        final ParameteredCommandLine line = new ParameteredCommandLine(
            args, new String[] { HELP_PARAM, ONLY_INIT_PARAM }, new int[] { 0, 0 });
        final String[] basicArgs = line.getRemainingArgs();

        if (basicArgs.length != 2 || line.hasParameter(HELP_PARAM)) {
            showSyntax(response);
            return;
        }
        Runnable action = () -> {
            try {
                Future<Object> object = SimulationThreadPool.getNew().submitAndWait(() -> {
                    String netSystemFileName = basicArgs[0];
                    String primaryNetName = basicArgs[1];

                    // Load the shadow net system
                    ShadowNetSystem netSystem = getShadowNetSystem(netSystemFileName);

                    // Set up the simulation.
                    _simulationManager.setupSimulation(null);
                    _simulationManager.addShadowNetSystem(netSystem);

                    // Create or restore net instances (depending on
                    // database).
                    SimulationState state = _simulationStateRestorer.restoreStateFromDatabase();
                    if (!state.wasSimulationInited()) {
                        NetInstance primaryInstance = _plugin.createNetInstance(primaryNetName);
                        response.println(
                            "Simulation set up, created net instance " + primaryInstance + ".");
                    } else {
                        response.println("Simulation set up, restored state from database.");
                    }

                    // Run the simulation, if requested.
                    if (!line.hasParameter(ONLY_INIT_PARAM)) {
                        _simulationManager.getCurrentSimulator().startRun();
                        response.println("Simulation running.");
                    } else {
                        try {
                            TransactionSource.simulationStateChanged(true, false);
                        } catch (Exception e) {
                            LOGGER.error(e.getMessage(), e);
                        }
                    }
                    return null;
                });
                // Retrieve the null result just to check for exceptions.
                object.get();
            } catch (ExecutionException e) {
                Throwable t = e.getCause();
                if (t instanceof SimulationRunningException) {
                    response.println("Simulation already running");
                } else {
                    LOGGER.debug(t.toString(), e);
                    response.println(t);
                    // Clean up simulation might be running
                    response.println("Cleaning up.");
                    _simulationManager.terminateSimulation();
                }
            } catch (SimulationRunningException e) {
                response.println("Simulation already running");
            } catch (Exception e) {
                LOGGER.debug(e.toString(), e);
                response.println(e);
                // Clean up simulation might be running
                response.println("Cleaning up.");
                _simulationManager.terminateSimulation();
            }
        };

        _lockExecutor.runWithLock(action);
    }

    /* Loads the shadow net system from the given file and returns it if there is no error */
    private ShadowNetSystem getShadowNetSystem(String netSystemFileName) throws IOException {
        ShadowNetSystem netSystem;
        try (FileInputStream stream = new FileInputStream(netSystemFileName);
            ObjectInput input = new ObjectInputStreamUsingBottomLoader(stream)) {
            netSystem = (ShadowNetSystem) input.readObject();
        } catch (StreamCorruptedException | ClassCastException | ClassNotFoundException e) {
            throw new IllegalArgumentException(
                "Invalid shadow net system (in file " + netSystemFileName + "): " + e);
        }
        return netSystem;
    }

    /* Non-JavaDoc: Specified by the CLCommand interface. */
    @Override
    public String getDescription() {
        return "set up a simulation with given nets (" + HELP_PARAM + " for help).";
    }

    /**
     * @see de.renew.plugin.command.CLCommand#getArguments()
     */
    @Override
    public String getArguments() {
        return "fileNames";
    }

    /**
     * Prints command-line help for this command to <code>System.out</code>.
     *
     * @param response
     *            the <code>PrintStream</code> for user feedback.
     */
    public void showSyntax(PrintStream response) {
        response.println("Parameters: <net system> <primary net> [-i]");
        response
            .println("  <net system>  : The file name of the exported shadow net system to load.");
        response.println("  <primary net> : The name of the primary net to create an instance of.");
        response.println(
            "  " + ONLY_INIT_PARAM + "            : Initialize the simulation only, don't run it.");
        response.println("To configure the simulation engine and installed extensions, use the");
        response.println("available plugin properties before starting the simulation (e.g. ");
        response.println(
            SimulatorPropertyConstants.MODE_PROP_NAME + " or "
                + SimulatorPropertyConstants.EAGER_PROP_NAME + ").");
    }
}