package de.renew.engine.searchqueue;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.StreamCorruptedException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.log4j.Logger;

import de.renew.engine.common.SimulatorEventLogger;
import de.renew.engine.searcher.Searchable;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.util.RenewObjectInputStream;
import de.renew.util.RenewObjectOutputStream;


/**
 * The class search queue keeps track of the transitions
 * instances that must be searched for activated bindings.
 * This is done statically, so that a net instance
 * can register its transitions as possibly activated even if it
 * does not know of any specific searcher.
 * A simulator can ask the search queue for
 * possibly activated transitions.
 *
 * @author Olaf Kummer
 */
public class SearchQueue {
    private final static Logger LOGGER = Logger.getLogger(SearchQueue.class);

    // This field designates the time instance when
    // the most recently extracted searchable object
    // might be successfully searched.
    private static double _time = 0;

    // An RB map allows a relatively fast extraction
    // of the element with the least index. However,
    // a priority queue would be preferable.
    private static SortedMap<Double, SearchQueueData> _queueByTime = new TreeMap<>();
    private static Hashtable<Searchable, SearchQueueData> _queueBySearchable = new Hashtable<>();

    // This factory creates sub-queues that handle entries
    // for a single instant of time.
    private static SearchQueueFactory _factory = new RandomQueueFactory();

    // These are the listeners that are interested in new entries.
    private static final List<SearchQueueListener> LISTENERS = new ArrayList<>();

    // These are the listeners that are interested in time updates
    private static final List<TimeListener> TIME_LISTENERS = new ArrayList<>();

    // This class is completely static.
    private SearchQueue() {}

    /**
     * Sets the instance of SearchQueueData that manages the
     * queue policy. This enables us to change search queue
     * policies at runtime under the control of the simulator.
     *
     * @param factory the factory to create new search queues
     */
    public synchronized static void setQueueFactory(SearchQueueFactory factory) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        _factory = factory;

        // We have to regenerate the lookup for searchables.
        _queueBySearchable = new Hashtable<>();

        for (Double key : _queueByTime.keySet()) {
            SearchQueueData oldQueue = _queueByTime.get(key);
            SearchQueueData newQueue = _factory.makeQueue(oldQueue.getTime());

            // Silently transfer the searchables to the new queue.
            for (Enumeration<Searchable> e = oldQueue.elements(); e.hasMoreElements();) {
                Searchable searchable = e.nextElement();
                newQueue.include(searchable);
                _queueBySearchable.put(searchable, newQueue);
            }

            // Register the new queue.
            _queueByTime.put(key, newQueue);
        }
    }

    private static void setTime(double time) {
        _time = time;
        notifyTimeListeners();
    }

    /**
     * Clear all pending enabled transition instances in the queue.
     * This is required when a new simulation is started. Note that
     * there might be threads running in the background that
     * might reintroduce transition instances into the queue.
     * So be sure to kill everything in sight before calling
     * this method.
     *
     * @param startTime the start time to set the search queue to
     */
    public synchronized static void reset(double startTime) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread "
            + "but instead: " + Thread.currentThread().getThreadGroup();
        _queueByTime = new TreeMap<>();
        _queueBySearchable = new Hashtable<>();
        SimulatorEventLogger.log("Setting time to " + startTime);
        setTime(startTime);
    }

    /**
     * Returns the current time of the search queue.
     *
     * @return the current time
     */
    public synchronized static double getTime() {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        return _time;
    }

    /**
     * Advance the time to the given new time instance.
     * If the new time is not greater than the current time,
     * nothing happens.
     *
     * @param newTime the new time to advance to
     */
    private static void advanceTime(double newTime) {
        if (newTime > _time) {
            LOGGER.info("Advancing time to " + newTime);
            setTime(newTime);
        }
    }

    /**
     * Unless the queue is totally empty, advance the time
     * to the earliest time stamp of a searchable.
     */
    public synchronized static void advanceTime() {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        SearchQueueData queue = getEarliestQueue();
        if (queue != null) {
            _queueByTime.remove(queue.getTime());
            advanceTime(queue.getTime());
        }
    }

    private static SearchQueueData getEarliestQueue() {
        if (_queueByTime.isEmpty()) {
            return null;
        } else {
            Object firstKey = _queueByTime.firstKey();
            return _queueByTime.get(firstKey);
        }
    }

    // A cautious customer can use this method to check whether there
    // are search candidates at this time instance without the
    // danger of moving the clock.

    /**
     * A cautious customer can use this method to check whether there
     * are search candidates at this time instance without the
     * danger of moving the clock.
     *
     * @return true if there are no searchables for the current time
     */
    public synchronized static boolean isCurrentlyEmpty() {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        return !_queueByTime.containsKey(_time);
    }

    /**
     * Check whether the search queue is totally empty.
     *
     * @return true if there are no searchables at all
     */
    public synchronized static boolean isTotallyEmpty() {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        return _queueByTime.isEmpty();
    }

    /**
     * Register a listener for a single shot notification.
     *
     * @param listener the listener to be registered
     */
    public synchronized static void insertListener(SearchQueueListener listener) {
        LISTENERS.add(listener);
    }

    /**
     * Register a time listener permanently.
     * The listener may not perform actions that affect
     * the search queue, except for deregistering the listener.
     * During notification, the notification thread
     * will own the search queue's synchronization lock.
     *
     * @param listener the time listener to be registered
     */
    public synchronized static void insertTimeListener(TimeListener listener) {
        TIME_LISTENERS.add(listener);
    }

    /**
     * Deregister a time listener permanently.
     *
     * @param listener the time listener to be deregistered
     */
    public synchronized static void removeTimeListener(TimeListener listener) {
        TIME_LISTENERS.remove(listener);
    }

    private static void notifyListeners() {
        LISTENERS.forEach(SearchQueueListener::searchQueueNonempty);
        LISTENERS.clear();
    }

    private static void notifyTimeListeners() {
        // Make sure to allow modifications of the listeners.
        List<TimeListener> temp = new ArrayList<>(TIME_LISTENERS);
        temp.forEach(TimeListener::timeAdvanced);
    }

    /**
     * Include a searchable for immediate search.
     *
     * @param searchable the searchable to be included
     */
    public synchronized static void includeNow(Searchable searchable) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        include(searchable, _time);
    }

    // This method should only be called with a future time
    // when the searchable has been searched and the first
    // possible binding was at that time or later.

    /**
     * This method should only be called with a future time
     * when the searchable has been searched and the first
     * possible binding was at that time or later.
     *
     * @param searchable the searchable to be included
     * @param targetTime the time when the searchable should be searched
     */
    public synchronized static void include(Searchable searchable, double targetTime) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        // Make sure the time does not run backwards.
        if (targetTime < _time) {
            targetTime = _time;
        }

        // Notify a searchable that its enabledness will be detected
        // by a future search. This has to be done regardless
        // of the policy of the search queue, but only if the
        // searchable is supposed to be inserted into the queue
        // for an immediate search. If the search is supposed
        // to happen sometimes in the future, there might be new
        // trigger events that shorten the delay.
        if (targetTime == _time) {
            searchable.triggers().clear();
        }

        // Get the appropriate queue.
        SearchQueueData queue = _queueBySearchable.get(searchable);

        if (queue != null && targetTime < queue.getTime()) {
            // Remove from current queue before putting into new queue.
            queue.exclude(searchable);

            // Remove searchable.
            _queueBySearchable.remove(searchable);
            // Discard queue if empty.
            if (queue.size() == 0) {
                _queueByTime.remove(queue.getTime());
            }

            // Take a note that the searchable is not
            // properly registered.
            queue = null;
        }

        // Do we need to insert the searchable?
        if (queue == null) {
            Double key = targetTime;
            if (_queueByTime.containsKey(key)) {
                queue = _queueByTime.get(key);
            } else {
                queue = _factory.makeQueue(targetTime);
                _queueByTime.put(key, queue);
            }

            // Put into new or existing queue.
            queue.include(searchable);
            _queueBySearchable.put(searchable, queue);
        }

        // Does anybody need a searchable?
        notifyListeners();
    }

    /**
     * Extract a searchable for searching.
     * If there is no searchable in the queue,
     * <code>null</code> is returned.
     * If the time of the earliest searchable exceeds
     * the current time, the time is advanced to
     * that time.
     *
     * @return the searchable with the earliest time stamp or null
     */
    public synchronized static Searchable extract() {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        // This call gets the object with the lowest key,
        // i.e. the search queue with the earliest time stamp.
        SearchQueueData queue = getEarliestQueue();

        // Is there a searchable in the queue?
        if (queue == null) {
            return null;
        }

        // Make sure the time gets advanced when the time of
        // the earliest queue exceeds the current time.
        advanceTime(queue.getTime());

        Searchable searchable = queue.extract();
        _queueBySearchable.remove(searchable);

        // If the queue is empty, we must remove it from the map.
        if (queue.size() == 0) {
            _queueByTime.remove(queue.getTime());
        }

        return searchable;
    }

    /**
     * Writes all entries currently in the queue
     * (and all their associated field data)
     * to the given stream. The written information
     * should describe the complete current simulation
     * state.
     * But this information does not necessarily include all
     * nets possibly required by future simulation steps.
     * <p>
     * If the given <code>ObjectOutput</code> is a <code><b>
     * de.renew.util.RenewObjectOutputStream</b></code>, its
     * feature of cutting down the recursion depth by delaying
     * the serialization of some fields will be used.
     * </p><p>
     * <b>Caution:</b> In order to get consistent data written
     * to the stream, you have to ensure that there are no
     * concurrent modifications of the simulation state.
     * This method is not able to lock the simulation.
     * </p>
     * {@link de.renew.util.RenewObjectOutputStream}
     *
     * Added Feb 29 2000  Michael Duvigneau
     *
     * @param output target stream (see note about RenewObjectOutputStream)
     * @throws IOException when writing to the stream fails
     */
    public synchronized static void saveQueue(ObjectOutput output) throws IOException {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        RenewObjectOutputStream rOut = null;
        if (output instanceof RenewObjectOutputStream out) {
            rOut = out;
            rOut.beginDomain(SearchQueue.class);
        }

        // Save all entries currently in the Queue.
        output.writeInt(_queueByTime.size());
        for (Map.Entry<Double, SearchQueueData> entry : _queueByTime.entrySet()) {
            output.writeDouble(entry.getKey());
            SearchQueueData data = entry.getValue();
            output.writeInt(data.size());
            for (Enumeration<Searchable> elements = data.elements(); elements.hasMoreElements();) {
                output.writeObject(elements.nextElement());
            }
        }

        // If a RenewObjectOutputStream is used, write
        // all delayed fields NOW.
        if (rOut != null) {
            rOut.writeDelayedObjects();
        }

        if (rOut != null) {
            rOut.endDomain(SearchQueue.class);
        }
    }

    /**
     * Restores queue elements saved by <code>saveQueue()</code>.
     * Adds all stored elements to the queue.
     * <p>
     * If the given {@link ObjectInput} is a
     * {@link RenewObjectInputStream},
     * the necessary steps to cover delayed serialization
     * will be made.
     * </p><p>
     * The object input stream will be read using
     * <code>de.renew.util.ClassSource</code> to provide
     * its ability of reloading all user defined classes.
     * </p>
     * {@link de.renew.util.ClassSource}
     * {@link de.renew.util.RenewObjectInputStream}
     *
     * Added Apr 11 2000  Michael Duvigneau
     *
     * @param input source stream (see note about RenewObjectInputStream above)
     * @throws IOException when reading from the stream fails
     * @throws ClassNotFoundException when a class of a serialized object cannot be found
     */
    public synchronized static void loadQueue(ObjectInput input)
        throws IOException, ClassNotFoundException
    {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";

        // Read all entries into a list first.
        List<Object> allEntries = new ArrayList<>();
        List<Double> allTimes = new ArrayList<>();

        int count = input.readInt();
        try {
            for (int i = 0; i < count; i++) {
                Double time = input.readDouble();
                int size = input.readInt();
                for (int j = 0; j < size; j++) {
                    allEntries.add(input.readObject());
                    allTimes.add(time);
                }
            }
        } catch (ClassCastException e) {
            LOGGER.debug(e.getMessage(), e);
            throw new StreamCorruptedException(
                "Object other than Searchable found when looking for SearchQueue elements: "
                    + e.getMessage());
        }

        // If a RenewObjectInputStream is used, read
        // all delayed fields NOW.
        if (input instanceof RenewObjectInputStream in) {
            in.readDelayedObjects();
        }

        // Insert all entries into the queue.
        for (int i = 0; i < allEntries.size(); i++) {
            include((Searchable) allEntries.get(i), allTimes.get(i));
        }
    }
}