package de.renew.engine.simulator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.apache.log4j.Logger;

import de.renew.engine.thread.SimulationThreadPool;
import de.renew.simulatorontology.simulation.Simulator;
import de.renew.simulatorontology.simulation.StepIdentifier;

/**
 * A simulator that aggregates several child simulators
 * and runs them in parallel.
 */
public class ParallelSimulator implements Simulator {
    private static final Logger LOGGER = Logger.getLogger(ParallelSimulator.class);
    private static long _runCounter = 0L;
    private final long _simulationRunId;
    private long[] _collectedSimulationRunIds;
    private final Simulator[] _simulators;
    private final StepIdentifierFactory _stepIdentifierFactory;

    /**
     * Create a new parallel simulator.
     * If <code>multiplicity</code> is positive, concurrent child simulators
     * are created. If it is negative, non-concurrent child simulators are
     * created. If it is zero, a single concurrent child simulator is created.
     *
     * @param multiplicity the number of child simulators to create;
     *        its sign determines whether they are concurrent or not
     * @param wantEventQueueDelay if <code>true</code>, the event queue
     *        delay mechanism is enabled in the child simulators
     * @throws AssertionError if this constructor is called not in a simulation thread
     */
    public ParallelSimulator(int multiplicity, boolean wantEventQueueDelay) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        if (multiplicity == 0) {
            multiplicity = 1;
        }
        boolean wantConcurrent = multiplicity > 0;
        multiplicity = Math.abs(multiplicity);

        // create a new simulationRunId
        _simulationRunId = (((long) getClass().getName().hashCode()) << 32) + _runCounter++;
        _stepIdentifierFactory = new StepIdentifierFactory(_simulationRunId);

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(
                this.getClass().getSimpleName() + ": Starting run with id " + _simulationRunId);
        }

        _simulators = new Simulator[multiplicity];
        for (int i = 0; i < multiplicity; i++) {
            if (wantConcurrent) {
                _simulators[i] = new ConcurrentChildSimulator(wantEventQueueDelay, this);
            } else {
                _simulators[i] = new NonConcurrentChildSimulator(wantEventQueueDelay, this);
            }
        }
        collectAllRunIds();
    }

    /**
     * Update {@link #_collectedSimulationRunIds} based on the nested simulators.
     * Also include the own {@link #_simulationRunId} of this instance.
     */
    private void collectAllRunIds() {
        List<Long> allRunIds = new ArrayList<>();
        allRunIds.add(_simulationRunId);

        for (Simulator simulator : _simulators) {
            for (long runId : simulator.collectSimulationRunIds()) {
                allRunIds.add(runId);
            }
        }

        _collectedSimulationRunIds = allRunIds.stream().mapToLong(Long::longValue).toArray();
    }

    @Override
    public boolean isActive() {
        return Arrays.stream(_simulators).anyMatch(Simulator::isActive);
    }

    // Start the simulation in the background.
    @Override
    public synchronized void startRun() {
        SimulationThreadPool.getCurrent()
            .executeAndWait(() -> Arrays.stream(_simulators).forEach(Simulator::startRun));
    }

    // Gently stop the simulation.
    @Override
    public void stopRun() {
        SimulationThreadPool.getCurrent()
            .executeAndWait(() -> Arrays.stream(_simulators).forEach(Simulator::stopRun));
    }

    // Terminate the simulation once and for all.
    // Do some final clean-up and exit all threads.
    @Override
    public synchronized void terminateRun() {
        SimulationThreadPool.getCurrent()
            .executeAndWait(() -> Arrays.stream(_simulators).forEach(Simulator::terminateRun));
    }

    // Perform just one step or terminate the simulation, if
    // it is running in the background. Return true, if
    // another binding could be found.
    @Override
    public synchronized int step() {
        Future<Integer> future = SimulationThreadPool.getCurrent().submitAndWait(() -> {
            Arrays.stream(_simulators).forEach(Simulator::stopRun);

            // Try to fire any simulator and stop on the first success.
            // This is required when aggregating sequential simulators,
            // because their enabledness status might be different.
            // (Sequential simulators hold the enabled transition instance
            // hostage until the firing is over.)
            int status = 0; // will be overwritten
            for (Simulator simulator : _simulators) {
                status = simulator.step();
                if (status == STATUS_STEP_COMPLETE || status == STATUS_LAST_COMPLETE) {
                    // One simulator performed a step and all simulators
                    // are in stopped state.
                    return status;
                }
            }

            // No simulator was able to perform a step.
            return status;
        });
        try {
            return future.get();
        } catch (InterruptedException e) {
            LOGGER.error("Timeout while waiting for simulation thread to finish", e);
        } catch (ExecutionException e) {
            LOGGER.error("Simulation thread threw an exception", e);
        }

        // We should never return nothing but some error occurred before.
        return -1;
        // Stop all simulators except the first.
    }

    @Override
    public void refresh() {
        SimulationThreadPool.getCurrent()
            .executeAndWait(() -> Arrays.stream(_simulators).forEach(Simulator::refresh));
    }

    /**
     * @return <code>false</code>
     **/
    @Override
    public boolean isSequential() {
        return false;
    }

    @Override
    public StepIdentifier nextStepIdentifier() {
        return _stepIdentifierFactory.nextStepIdentifier();
    }

    @Override
    public StepIdentifier currentStepIdentifier() {
        return _stepIdentifierFactory.currentStepIdentifier();
    }

    /* (non-Javadoc)
     * @see de.renew.simulatorontology.simulation.Simulator#collectSimulationRunIds()
     */
    @Override
    public long[] collectSimulationRunIds() {
        return Arrays.copyOf(_collectedSimulationRunIds, _collectedSimulationRunIds.length);
    }
}