package de.renew.engine.simulator;

import java.lang.reflect.InvocationTargetException;
import java.util.Properties;

import org.apache.log4j.Logger;

import de.renew.engine.thread.SimulationThreadPool;
import de.renew.plugin.PropertyHelper;
import de.renew.simulator.api.SimulatorPropertyConstants;
import de.renew.simulatorontology.simulation.Simulator;

/**
 * This class provides a {@link Simulator} instance that is created according to the given properties.
 */
public class SimulatorFactory implements ISimulatorFactory {

    /**
     * Creates a new {@link SimulatorFactory}.
     * This constructor can only be called in a simulation thread.
     *
     * @throws AssertionError if this constructor is called not in a simulation thread
     */
    public SimulatorFactory() {}

    private static final Logger LOGGER = Logger.getLogger(SimulatorFactory.class);

    @Override
    public Simulator createSimulator(Properties properties) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        final int simulatorMode =
            PropertyHelper.getIntProperty(properties, SimulatorPropertyConstants.MODE_PROP_NAME, 1);
        int simulatorMultiplicity = PropertyHelper
            .getIntProperty(properties, SimulatorPropertyConstants.MULTIPLICITY_PROP_NAME, 1);

        Class<?> simulatorClass = PropertyHelper.getClassProperty(
            properties, SimulatorPropertyConstants.CLASS_PROP_NAME, Simulator.class);

        // Is the simulator class already set?
        if (simulatorClass != null) {
            LOGGER.info(
                "Using simulator class " + simulatorClass.getName() + " with "
                    + simulatorMultiplicity + " simulators ...");
        } else {
            // The simulator class must be set from the simulator mode.

            // What is the simulator mode?
            if (simulatorMode == 1) {
                simulatorClass = ConcurrentSimulator.class;
                LOGGER.info("Using default concurrent simulator ...");
            } else if (simulatorMode == 0 || simulatorMode == -1) {
                simulatorClass = NonConcurrentSimulator.class;
                LOGGER.info("Using sequential simulator ...");
            } else {
                // Use multiple simulators.
                simulatorClass = ParallelSimulator.class;
                // Set the number of simulators to use.
                simulatorMultiplicity = simulatorMode;

                if (simulatorMultiplicity < 0) {
                    LOGGER.warn("Using " + (-simulatorMultiplicity) + " sequential simulators ...");
                } else {
                    LOGGER.warn("Using " + simulatorMultiplicity + " concurrent simulators ...");
                }
                LOGGER.warn("Caution! This is an experimental feature!");
            }
        }

        final boolean eagerSimulation =
            PropertyHelper.getBoolProperty(properties, SimulatorPropertyConstants.EAGER_PROP_NAME);
        if (eagerSimulation) {
            LOGGER.info("Using eager simulation mode.");
        }

        // Create instance of given class with java.lang.reflect.
        // This code was inspired by de.renew.remote.RemoteExtension.createServer(Class<?>).
        Simulator simulator;
        try {
            try {
                // Get constructor taking one integer and one boolean as parameters
                // and apply it with simulatorMultiplicity and !eagerSimulation.
                // See {@link Simulator} documentation.
                simulator = (Simulator) simulatorClass.getConstructor(int.class, boolean.class)
                    .newInstance(simulatorMultiplicity, !eagerSimulation);
            } catch (NoSuchMethodException e) {
                // If the needed constructor does not exist try the following instead:
                // Get constructor taking one boolean as parameter and apply it with !eagerSimulation.
                // See {@link Simulator} documentation.
                simulator = (Simulator) simulatorClass.getConstructor(boolean.class)
                    .newInstance(!eagerSimulation);
                // Set multiplicity to 0 if its sequential and 1 otherwise.
                simulatorMultiplicity = (simulator.isSequential() ? 0 : 1);
            }

            // Catch all declared exceptions.
        } catch (SecurityException | NoSuchMethodException | IllegalArgumentException
            | InstantiationException | IllegalAccessException | InvocationTargetException e) {
            LOGGER.error("Encountered exception when trying to instantiate simulator class:", e);
            simulator = new ConcurrentSimulator(!eagerSimulation);
            simulatorClass = ConcurrentSimulator.class;
            simulatorMultiplicity = 1;
        }

        properties
            .setProperty(SimulatorPropertyConstants.CLASS_PROP_NAME, simulatorClass.getName());
        properties.setProperty(
            SimulatorPropertyConstants.MULTIPLICITY_PROP_NAME,
            Integer.toString(simulatorMultiplicity));

        return simulator;
    }
}
