package de.renew.engine.simulator;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.apache.log4j.Logger;

import de.renew.database.TransactionSource;
import de.renew.engine.searcher.Searchable;
import de.renew.engine.searcher.Searcher;
import de.renew.engine.searchqueue.SearchQueue;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.simulatorontology.simulation.Simulator;
import de.renew.simulatorontology.simulation.StepIdentifier;

/**
 * A simulator that executes one binding at a time
 * in a single thread.
 * <p>
 * This simulator supports flow control by waiting for
 * the event queue to be empty before every firing.
 * </p>
 */
public class SequentialSimulator implements Simulator, Runnable {
    private static final Logger LOGGER = Logger.getLogger(SequentialSimulator.class);
    private static long _runCounter = 0L;
    private final long _simulationRunId;
    private ExecuteFinder _finder;
    private final Searcher _searcher = new Searcher();
    private boolean _stepFired = false;
    private boolean _isAlive = false;
    private boolean _wantBreak = false;
    private SequentialSimulator _runThread = null;
    private final StepIdentifierFactory _stepIdentifierFactory;

    /**
     * If true, the simulator will wait for the event queue
     * before every firing.
     */
    private final boolean _wantEventQueueDelay;

    /**
     * Create a new sequential simulator.
     * The event queue delay mechanism is enabled.
     *
     * @throws AssertionError if this constructor is called not in a simulation thread
     */
    public SequentialSimulator() {
        this(true);
    }

    /**
     * Create a new sequential simulator.
     *
     * @param wantEventQueueDelay if <code>true</code>, the event queue
     *        delay mechanism is enabled
     * @throws AssertionError if this constructor is called not in a simulation thread
     */
    public SequentialSimulator(boolean wantEventQueueDelay) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        _wantEventQueueDelay = wantEventQueueDelay;

        // 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);
        }

        // If possible, prepare a new binding.
        findNewBinding();
    }

    @Override
    public boolean isActive() {
        return _isAlive;
    }

    @Override
    public synchronized void startRun() {
        if (_runThread != null) {
            return;
        }
        // Only create a new thread if none is currently running.
        // Create a new thread that will run the simulation.
        _runThread = this;

        SimulationThreadPool.getCurrent().execute(this);

        try {
            TransactionSource.simulationStateChanged(true, true);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

    // Gently stop the simulation.
    // This may take a while, because the current search for
    // an activated binding must be completed first.
    // After the completion of this method, no thread is running
    // and runThread==null.
    @Override
    public synchronized void stopRun() {
        SimulationThreadPool.getCurrent().executeAndWait(() -> {
            while (_runThread != null) {
                _wantBreak = true;
                try {
                    wait();
                } catch (InterruptedException e) {
                    // This is expected. But I cannot be sure that
                    // I woke up from a notification.
                }
            }

            try {
                TransactionSource.simulationStateChanged(true, false);
            } catch (Exception e) {
                LOGGER.error(e.getMessage(), e);
            }
        });
    }

    // Terminate the simulation once and for all.
    // Do some final clean-up and exit the thread.
    @Override
    public synchronized void terminateRun() {
        SimulationThreadPool.getCurrent().executeAndWait(() -> {
            // In this case we can simply call stop run, because that
            // is sufficient to terminate all running threads.
            stopRun();

            try {
                TransactionSource.simulationStateChanged(false, false);
            } catch (Exception e) {
                LOGGER.error(e.getMessage(), e);
            }
        });
    }

    private void fire() {
        _stepFired = false;
        while (_isAlive && !_stepFired) {
            // Enforce flow control.
            if (_wantEventQueueDelay) {
                SimulatorEventQueue.awaitEmptyQueue();
            }

            // Fire the binding.
            _stepFired = _finder.execute(nextStepIdentifier(), false);
            findNewBinding();
        }
    }

    // To exclude concurrent access to the searcher, this method may only
    // be called from the running thread or from a
    // synchronized method that has ensured that no thread is running.
    // This method is not synchronized, because it could
    // lock up termination requests.
    // If there are no more enabled transitions, this method simply
    // returns without effect.
    private void findNewBinding() {
        // An ExecuteFinder should not be recycled.
        _finder = new ExecuteFinder();

        // Try all transitions that might be enabled.
        // If a successful binding was found (though not
        // necessarily accepted), requeue this transition
        // at the end of the search.
        // If no bindings are possible, forget about this 
        // transition. A change to a relevant place will
        // trigger its reinsertion.
        while (!_finder.isCompleted() && !SearchQueue.isTotallyEmpty()) {
            // Get a searchable.
            Searchable searchable = SearchQueue.extract();

            // If the searchable is not currently enabled, keep
            // track of the earliest time when it might be
            // enabled.
            OverallEarliestTimeFinder timeFinder = new OverallEarliestTimeFinder(_finder);

            // Perform the search. The searchable itself is used
            // as the triggerable, because it was taken from the search queue.
            _searcher.searchAndRecover(timeFinder, searchable, searchable);

            // Reinsert the searchable back into the search queue,
            // if necessary.
            timeFinder.insertIntoSearchQueue(searchable);
        }

        // Did we find a binding?
        _isAlive = _finder.isCompleted();
    }

    // This method must be synchronized to avoid concurrent
    // accesses to restart another thread after the stop.
    @Override
    public synchronized int step() {
        Future<Integer> future = SimulationThreadPool.getCurrent().submitAndWait(() -> {
            if (_runThread != null) {
                // Just stop the current thread.
                stopRun();
                if (_isAlive && !_stepFired) {
                    // The current thread did not make a step.
                    fire();
                }
            } else {
                // Try to make another step.
                fire();
            }
            if (_stepFired) {
                return _isAlive ? STATUS_STEP_COMPLETE : STATUS_LAST_COMPLETE;
            }
            return STATUS_DISABLED;
        });
        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;
    }

    // This method is called in threads that are generated on each
    // run request. The simulator class cannot inherit from thread,
    // because this method might be run multiple times.
    @Override
    public void run() {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        // Do have a prepared binding?
        if (!_isAlive) {
            // No, let's try to find one, although there is
            // little hope that we can find it.
            findNewBinding();
        }

        // Make steps until deadlock or until a break is requested.
        while (_isAlive && !_wantBreak) {
            // Grant other threads a chance to run.
            // Only needed on systems without preemptive multitasking.
            Thread.yield();

            // Fire and search for a new binding.
            fire();
        }

        // Make sure that runThread and wantBreak are updated
        // atomically.
        synchronized (this) {
            // Signal the end of the run.
            _runThread = null;

            // Clear the break request.
            _wantBreak = false;

            // Notify waiting threads of break.
            notifyAll();
        }
    }

    @Override
    public synchronized void refresh() {
        SimulationThreadPool.getCurrent().executeAndWait(() -> {
            if (_runThread == null) {
                // No thread is currently running. Make sure to update
                // the enabled bindings and the time manually.
                findNewBinding();
            }
        });
    }

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

    @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 new long[] { _simulationRunId };
    }
}