package de.renew.engine.simulator;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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.searchqueue.SearchQueueListener;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.net.event.ListenerSet;
import de.renew.simulatorontology.simulation.Simulator;
import de.renew.simulatorontology.simulation.StepIdentifier;


/**
 * A simulator that is generally capable of handling concurrent
 * simulations and a concurrently updated search queue. A switch
 * decides how to execute each individual firing, so that a sequential
 * simulation is still possible. This simulator cannot and will
 * not detect a deadlock at the end of a simulation.
 * <p>
 * This class observes the following global synchronization order:
 * <ul>
 * <li>0th: locally synchronized;
 * <li>1st: lock SearchQueue;
 * <li>2nd: lock threadLock.
 * </ul>
 */
public class AbstractConcurrentSimulator implements Runnable, Simulator, SearchQueueListener {
    private static final Logger LOGGER = Logger.getLogger(AbstractConcurrentSimulator.class);
    private static long _runCounter = 0;
    private final long _simulationRunId;
    private final Searcher _searcher = new Searcher();
    private final StepIdentifierFactory _stepIdentifierFactory;

    /**
     * Used to synchronise access to some method calls that start threads.
     * Don't use {@code synchronized} for these methods use lock instead.
     */
    private static final Lock LOCK = new ReentrantLock();

    /**
     * If true, requests that binding be executed concurrently
     * with the search process.
     */
    private final boolean _wantConcurrentExecution;

    /**
     * If true, the simulator will wait for the event queue
     * implemented in {@link ListenerSet} before every firing.
     */
    private final boolean _wantEventQueueDelay;

    // These field may only be accessed by a thread that
    // has grabbed the thread lock.
    private ExecuteFinder _executeFinder;
    private AbortFinder _abortFinder;

    // possible modes:
    private enum SimulationMode {
        TERMINATE(-1), STOP(0), STEP(1), RUN(2);

        private final int _value;

        SimulationMode(int value) {
            _value = value;
        }

        public int getValue() {
            return _value;
        }

        public boolean isGreaterThan(SimulationMode other) {
            return this._value > other._value;
        }

        public boolean isGreaterOrEqualTo(SimulationMode other) {
            return this._value >= other._value;
        }
    }

    private SimulationMode _desiredSimulationMode = SimulationMode.STOP;
    private boolean _idle = true;
    private int _stepStatusCode = 0;
    private boolean _searchQueueIsEmpty = false;
    private boolean _alreadyRegistered = false;
    private final Object _threadLock = new Object();

    /**
     * Create a new concurrent simulator.
     *
     * @param wantEventQueueDelay whether the simulator will wait for the event queue before every firing
     * @param wantConcurrentExecution whether the binding will be executed concurrently with the search process
     */
    public AbstractConcurrentSimulator(
        boolean wantEventQueueDelay, boolean wantConcurrentExecution)
    {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        _wantEventQueueDelay = wantEventQueueDelay;
        _wantConcurrentExecution = wantConcurrentExecution;

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

        setupFinders();

        SimulationThreadPool.getCurrent().execute(this);
        //        Thread thread = new Thread(this);


        // Run the simulation thread in the background.
        //      thread.setPriority(Thread.MIN_PRIORITY);


        // Start the thread.
        //    thread.start();
    }

    private void setupFinders() {
        // Protect the finders against concurrent use.
        synchronized (_threadLock) {
            _executeFinder = new ExecuteFinder();
            _abortFinder = new AbortFinder(_executeFinder);
        }
    }

    // In the concurrent simulator, you never know if there might
    // be more enabled transitions. So I am always active.
    @Override
    public boolean isActive() {
        return true;
    }

    @Override
    public void searchQueueNonempty() {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        synchronized (_threadLock) {
            _searchQueueIsEmpty = false;
            _alreadyRegistered = false;

            // Notify threads that may be waiting for a nonempty search queue.
            _threadLock.notifyAll();
        }
    }

    private void registerAtSearchQueue() {
        synchronized (SearchQueue.class) {
            // Now synchronize locally to access fields safely.
            synchronized (_threadLock) {
                _searchQueueIsEmpty = SearchQueue.isTotallyEmpty();
                if (_searchQueueIsEmpty && !_alreadyRegistered) {
                    SearchQueue.insertListener(this);
                    _alreadyRegistered = true;
                }
            }
        }
    }

    // Terminate the simulation once and for all.
    // Do some final clean-up and exit the thread.
    @Override
    public void terminateRun() {
        SimulationThreadPool.getCurrent().executeAndWait(() -> {
            // Notify the simulation thread about the new mode.
            requestMode(SimulationMode.TERMINATE);

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

    // This method is not synchronized, so that it may
    // interrupt a run or step request.
    @Override
    public void stopRun() {
        SimulationThreadPool.getCurrent().executeAndWait(() -> {
            // Notify the simulation thread about the new mode.
            requestMode(SimulationMode.STOP);

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

    // By synchronizing I can guarantee that no other
    // thread spoils the value in stepStatusCode.
    @Override
    public int step() {
        Future<Integer> future = SimulationThreadPool.getCurrent().submitAndWait(() -> {
            LOCK.lock();
            // Require one more step.
            requestMode(SimulationMode.STEP);
            try {
                TransactionSource.simulationStateChanged(true, false);
            } catch (Exception e) {
                LOGGER.error(e.getMessage(), e);
            } finally {
                LOCK.unlock();
            }
            return _stepStatusCode;
        });
        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;

    }

    @Override
    public void startRun() {
        SimulationThreadPool.getCurrent().execute(() -> {
            // Tell the thread to start running.
            requestMode(SimulationMode.RUN);

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

    // This method used to be synchronized, but there is
    // really no point to it. All accessed fields are
    // controlled by the thread, which will also make
    // sure this method is not executed concurrently.
    private void requestMode(SimulationMode simulationMode) {
        synchronized (_threadLock) {
            // If another thread has already requested a termination,
            // that request will not be taken back.
            if (_desiredSimulationMode.isGreaterThan(SimulationMode.TERMINATE)) {
                // Set the mode.
                _desiredSimulationMode = simulationMode;
                // Set the running state.
                if (simulationMode.isGreaterThan(SimulationMode.STOP)) {
                    _idle = false;
                } else {
                    // Just in case a search is currently running, abort it
                    // by notifying the finder.
                    _abortFinder.abortSearch();
                }

                // Get the thread out of the sleeping state, if it happens to be there.
                _threadLock.notifyAll();
            }

            // Now the calling method must wait for the simulation thread to stop running,
            // if we do not want to let it run indefinitely.
            // Also check desiredMode because it might have changed since we set it.
            // (Note that it is never changed once it was set to TERMINATE.)
            // TODO In the case of mode == SimulationMode.STEP and desiredMode == RUN
            //      it will not even wait until a single step is done. Is that OK?
            if (SimulationMode.RUN.isGreaterThan(simulationMode)
                && SimulationMode.RUN.isGreaterThan(_desiredSimulationMode)) {
                while (!_idle) {
                    try {
                        _threadLock.wait();
                    } catch (InterruptedException e) {
                        //TODO this should never happen see http://www-128.ibm.com/developerworks/java/library/j-jtp05236.html
                        // This is expected. <-- yes but its handles wrong
                    }
                }
            }
        }
    }

    @Override
    public void run() {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        while (_desiredSimulationMode.isGreaterOrEqualTo(SimulationMode.STOP)) {
            // Wait until there is a reasonable chance that
            // a transition instance might be enabled.
            synchronized (_threadLock) {
                while (_searchQueueIsEmpty && _desiredSimulationMode == SimulationMode.RUN) {
                    try {
                        _threadLock.wait();
                    } catch (InterruptedException e) {
                        //TODO this should never happen see http://www-128.ibm.com/developerworks/java/library/j-jtp05236.html
                        // This is expected. <-- yes but its handles wrong
                    }
                }
            }

            // We note that the search was stopped explicitly.
            // Whenever this is not the case, we will find out
            // later and correct the code accordingly.
            _stepStatusCode = STATUS_STOPPED;

            while (_desiredSimulationMode.isGreaterThan(SimulationMode.STOP)) {
                // Try to get a transition instance from the search queue.
                // this might fail, because another thread might have emptied the
                // queue concurrently.
                Searchable searchable = SearchQueue.extract();
                if (searchable == null) {
                    break;
                }

                // Create a new finder. Earlier during the execution another
                // thread might have placed a stop request that has been
                // processed in the meantime.
                //
                // While we are creating the new finder, another thread
                // might access the old finder and place a stop
                // request in it. We solve this problem by rechecking
                // the desired mode afterwards.
                setupFinders();

                // Do we still want to search for a binding?
                if (_desiredSimulationMode.isGreaterThan(SimulationMode.STOP)) {
                    // If the searchable is not currently enabled, keep
                    // track of the earliest time when it might be
                    // enabled. Use the abort finder, so that
                    // the search may be interrupted.
                    OverallEarliestTimeFinder timeFinder =
                        new OverallEarliestTimeFinder(_abortFinder);

                    // Search for a binding.
                    _searcher.searchAndRecover(timeFinder, searchable, searchable);

                    // Grant other threads a chance to run.
                    // Only needed on systems without preemptive multitasking.
                    Thread.yield();

                    if (_abortFinder.isCompleted()) {
                        // Ok, reinsert this searchable. This must be done if either
                        // the search was successful or if the search was interrupted.
                        SearchQueue.includeNow(searchable);
                    } else {
                        // Reinsert the triggerable at the point of time when it 
                        // might be enabled.
                        timeFinder.insertIntoSearchQueue(searchable);
                    }

                    if (_executeFinder.isCompleted()) {
                        // Enforce flow control.
                        if (_wantEventQueueDelay) {
                            SimulatorEventQueue.awaitEmptyQueue();
                        }


                        // Execute the binding asynchronously.
                        _executeFinder.execute(nextStepIdentifier(), _wantConcurrentExecution);


                        // Provide a new finder so that the old finder does not
                        // forbid garbage collection.
                        setupFinders();


                        // Make a note that we found a binding.
                        _stepStatusCode = STATUS_STEP_COMPLETE;
                        // Stop if in single step mode.
                        synchronized (_threadLock) {
                            if (_desiredSimulationMode == SimulationMode.STEP) {
                                _desiredSimulationMode = SimulationMode.STOP;
                            }
                        }
                    }
                }
            }

            // If the queue is empty, let's register there. It's free.
            //
            // This is done even if desiredMode is SimulationMode.STOP, because I would have to
            // lock threadLock to safely access desiredMode. But after locking
            // threadLock, it is forbidden to access SearchQueue.
            //
            // On the other hand, I must not lock SearchQueue all the time,
            // otherwise I might inhibit notifications from SearchQueue
            // that might keep me running.
            registerAtSearchQueue();


            // I must synchronize on the thread lock, otherwise
            // notifications might get lost.
            synchronized (_threadLock) {
                // Did we fail to find any bindings in single step mode?
                if (_desiredSimulationMode == SimulationMode.STEP) {
                    // Request no further firings.
                    _desiredSimulationMode = SimulationMode.STOP;


                    // Leave a note that there are currently no enabled transitions.
                    _stepStatusCode = STATUS_CURRENTLY_DISABLED;
                }

                // Did the user request a break?
                if (SimulationMode.STOP.isGreaterOrEqualTo(_desiredSimulationMode)) {
                    // I am no longer running.
                    _idle = true;


                    // I must send a notification, just in case a stopper waits.
                    // If there are multiple stoppers, I'll notify them all.
                    _threadLock.notifyAll();
                }

                // Now we must stick here until someone allows us to proceed.
                while (_desiredSimulationMode == SimulationMode.STOP) {
                    try {
                        _threadLock.wait();
                    } catch (InterruptedException e) {
                        // This is expected.
                    }
                }
            }
        }

        // Ok, exit the simulator because someone requested its termination.
        synchronized (_threadLock) {
            // Make sure the simulator is marked as idle, otherwise the
            // requestMode(SimulationMode.TERMINATE) call and thus the
            // terminateRun() call might never return.
            if (!_idle) {
                _idle = true;
                _threadLock.notifyAll();
            }
        }
    }

    // No need to refresh. I do not cache variables.
    @Override
    public void refresh() {}

    /* (non-Javadoc)
     * @return depends on the value of <code>wantConcurrentExecution</code>
     * at creation of this simulator.
     * @see #AbstractConcurrentSimulator(boolean, boolean)
     */
    @Override
    public boolean isSequential() {
        return !_wantConcurrentExecution;
    }

    @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 };
    }
}