package de.renew.net;

import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.Vector;

import org.apache.log4j.Logger;

import de.renew.database.Transaction;
import de.renew.database.TransactionSource;
import de.renew.engine.searchqueue.SearchQueue;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.net.event.TokenEvent;
import de.renew.unify.Impossible;
import de.renew.unify.TupleIndex;
import de.renew.util.RenewObjectInputStream;
import de.renew.util.RenewObjectOutputStream;


/**
 * I will collect tokens for a single place instance. I also
 * keep count of the number of tokens that are currently being tested.
 * <p>
 * Whenever my marking is changed I will notify my triggerables.
 * </p>
 * Anybody who needs to make sure that my marking does not change should
 * lock on me. Synchronisation does not help!
 */
public class MultisetPlaceInstance extends SimulatablePlaceInstance {
    /** The logger used by this MultisetPlaceInstance. */
    public static final Logger LOGGER = Logger.getLogger(MultisetPlaceInstance.class);
    @Serial
    private static final long serialVersionUID = 0L;

    /** The amount of tokens that reside in this place and are not currently tested by a transition. */
    private int _freeTokenCount;
    /** The amount of tokens that are currently tested by a transition, but that reside in this place. */
    private int _testedTokenCount;

    /**
     * This field is not really transient, but as we want
     * to cut down the serialization recursion depth, it
     * is serialized manually.
     **/
    transient private TokenBag _freeTokens;

    /**
     * This field is not really transient, but as we want
     * to cut down the serialization recursion depth, it
     * is serialized manually.
     **/
    transient private TestTokenBag _testedTokens;

    /**
     * All elements contained in this index must also
     * appear in {@code testIndex}.
     * <p>
     * This field is transient and will be recomputed
     * on deserialization.
     **/
    transient private TupleIndex _freeIndex;

    /**
     * Must contain at least all elements
     * contained in {@code freeIndex}.
     * <p>
     * This field is transient and will be recomputed
     * on deserialization.
     **/
    transient private TupleIndex _testIndex;

    /**
     * Create a new place instance for a given net instance
     * reflecting a certain place. If desired, the constructor
     * will already calculate in the initial marking.
     *
     * @param netInstance the owning net instance
     * @param place the semantic level place
     * @param wantInitialTokens true, if initial marking should be calculated
     */
    MultisetPlaceInstance(NetInstance netInstance, Place place, boolean wantInitialTokens)
        throws Impossible
    {
        super(netInstance, place, wantInitialTokens);
    }

    @Override
    protected void initTokenStorage() {
        _freeTokens = new TokenBag();
        _testedTokens = new TestTokenBag();
        _freeTokenCount = 0;
        _testedTokenCount = 0;

        _freeIndex = new TupleIndex();
        _testIndex = new TupleIndex();
    }

    /**
     * Returns an enumeration without repetitions
     * of currently untested tokens.
     *
     * @return a {@code CollectionEnumeration} value
     */
    @Override
    public Set<Object> getDistinctTokens() {
        return _freeIndex.getAllElements();
    }

    /**
     * Returns an enumeration without repetitions
     * of currently untested tokens.
     * At least those tokens which are unifiable with pattern
     * should be included.
     *
     * @param pattern an {@code Object} value
     * @return a {@code CollectionEnumeration} value
     */
    @Override
    public Set<Object> getDistinctTokens(Object pattern) {
        return _freeIndex.getPossibleMatches(pattern);
    }

    @Override
    public Set<Object> getDistinctTestableTokens() {
        return _testIndex.getAllElements();
    }

    @Override
    public Set<Object> getDistinctTestableTokens(Object pattern) {
        return _testIndex.getPossibleMatches(pattern);
    }

    @Override
    public int getNumberOfTokens() {
        return _freeTokenCount;
    }

    /**
    * Return the number of tokens that are currently tested
    * by a transition, but that reside in this place.
    * We do not count multiple tests on a single token as
    * multiple tokens.
    */
    @Override
    public int getNumberOfTestedTokens() {
        return _testedTokenCount;
    }

    @Override
    boolean containsToken(Object token) {
        return _freeTokens.includesAnytime(token);
    }

    @Override
    public int getTokenCount(Object token) {
        return _freeTokens.getMultiplicity(token);
    }

    @Override
    public TimeSet getFreeTimeSet(Object token) {
        return _freeTokens.getTimeSet(token);
    }

    // Returns the delay until a set of untested tokens
    // is available that matches the given delay times.
    @Override
    public double computeEarliestTime(Object token, TimeSet times) {
        return _freeTokens.computeEarliestTime(token, times);
    }

    // Here consider only already tested tokens.
    @Override
    public boolean containsTestedToken(Object token) {
        return _testedTokens.includesTested(token);
    }

    // Here we consider both kinds of tokens.
    @Override
    public boolean containsTestableToken(Object token) {
        // nulls will be converted by the called methods.
        return containsToken(token) || containsTestedToken(token);
    }

    // Try to remove the token. If no token is available at
    // the given time, an exception is thrown.
    @Override
    public double removeToken(Object token, double delay) throws Impossible {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        _lock.lock();
        try {
            double removedTime;
            try {
                removedTime = _freeTokens.removeWithDelay(token, delay);
            } catch (Exception e) {
                throw new Impossible(e);
            }

            _freeTokenCount--;

            if (!_freeTokens.includesAnytime(token)) {
                _freeIndex.remove(token);
                if (!_testedTokens.includesTested(token)) {
                    _testIndex.remove(token);
                }
            }

            // All changes to the place instance state have been performed.
            // It is now safe to notify the current transaction,
            // the triggerables and the listeners (if any) of the change.
            //
            // Up to now the token has still kept its ID, because concurrent
            // accesses to the place instances are stopped by the lock.
            Transaction transaction = TransactionSource.get();
            try {
                transaction.removeToken(this, token, removedTime);
            } catch (Exception e) {
                LOGGER.error(e.getMessage(), e);
            }

            _triggerables.proposeSearch();

            // Notify all listeners
            TokenEvent event = new TokenEvent(this, token);
            _listeners.tokenRemoved(event);

            // Allow the master place to distribute the event, too.
            _place.getListenerSet().tokenRemoved(event);

            // Now all actions are performed. The place instance
            // uses one less reference to the token now.
            unreserve(token);

            return removedTime;
        } finally {
            _lock.unlock();
        }
    }

    // Multiple tests on the same token do not remove
    // the token multiple times. Instead, the token is
    // removed once, and the number of removals is recorded
    // by a multiple insertion into the bag of tested tokens.
    @Override
    public double testToken(Object token) throws Impossible {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";

        _lock.lock();
        try {
            final boolean isTokenAlreadyTested = _testedTokens.includesTested(token);
            boolean shouldNotifyListeners = false;
            double removedTime = SearchQueue.getTime();

            // Do we need to remove a token explicitly?
            // It seems so. This does not really matter, because testing is
            // not really visible in the timed (and hence sequential)
            // formalism.
            if (!isTokenAlreadyTested) {
                try {
                    // Remove a token that is already in the place.
                    removedTime = _freeTokens.removeWithDelay(token, 0);
                } catch (Exception e) {
                    throw new Impossible(e);
                }

                // Fix the token counters. Only required if a
                // token was actually converted from free to tested.
                _testedTokenCount++;
                _freeTokenCount--;

                if (!_freeTokens.includesAnytime(token)) {
                    _freeIndex.remove(token);
                }

                // We must notify the listeners only if a free token has
                // become tested. If a token was already tested, the new
                // test simply delays the time span until a possible
                // activation.
                shouldNotifyListeners = true;
            }

            _testedTokens.addTested(token, removedTime);
            // No need to touch IDRegistry, because token is
            // still kept in the place.

            if (shouldNotifyListeners) {
                _triggerables.proposeSearch();
                // Notify all listeners.
                TokenEvent tokenEvent = new TokenEvent(this, token);
                _listeners.tokenTested(tokenEvent);
                // Allow the master place to distribute the event, too.
                _place.getListenerSet().tokenTested(tokenEvent);
            }

            return removedTime;
        } finally {
            _lock.unlock();
        }
    }

    /**
    * This method removes all tokens from the place and
    * puts them into a vector that is provided as an argument.
    * The method should be used when the simulator is at rest
    * or at least the place is locked and no firings are
    * testing tokens. Otherwise, some tested token may not
    * be removable.
    * <p>
    * For each token, the original time stamp is recorded
    * in a second vector. The same number of elements
    * is added to each vector, one entry for each token in
    * the place. Multiple identical tokens result in multiple
    * identical elements in the vector.
    *
    * @param tokens vector where removed tokens will be placed
    *   or null, if the tokens should be discarded.
    * @param timeStamps vector where time stamps will be placed
    *   or null, if the tokens should be discarded.
    */
    @Override
    public void extractAllTokens(Vector<Object> tokens, Vector<Double> timeStamps) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        _lock.lock();
        try {
            // In the following, I do not remove tested token.
            List<Object> uniqueValues = new ArrayList<Object>(getDistinctTokens());

            while (!uniqueValues.isEmpty()) {
                Object token = uniqueValues.remove(uniqueValues.size() - 1);


                // The following loop might be optimized, but
                // currently this is the safest solution.
                while (containsToken(token)) {
                    double time = 0;
                    try {
                        time = removeToken(token, Double.NEGATIVE_INFINITY);
                    } catch (Impossible e) {
                        throw new RuntimeException("Token was not removable.", e);
                    }
                    if (tokens != null) {
                        tokens.addElement(token);
                    }
                    if (timeStamps != null) {
                        timeStamps.addElement(time);
                    }
                }
            }
        } finally {
            _lock.unlock();
        }
    }

    @Override
    public void internallyInsertToken(Object token, double time, boolean alreadyRegistered) {
        internallyInsertTokenMultiple(token, time, 1, alreadyRegistered);
    }

    /**
     * Insert one token multiple times. See {@link #internallyInsertToken}.
     *
     * @param token the token to be inserted
     * @param time the time stamp of the token
     * @param n the amount of times it should be inserted
     * @param alreadyRegistered true, if the caller has already
     *   registered the new token at the IDRegistry.
     */
    public void internallyInsertTokenMultiple(
        Object token, double time, int n, boolean alreadyRegistered)
    {

        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        assert n > 0 : "trying to insert non-positive number of tokens";

        _lock.lock();
        try {
            // Insert the tokens.
            boolean isNewToken = !_freeTokens.includesAnytime(token);
            if (isNewToken) {
                _freeIndex.insert(token);

                boolean needsTesting = !_testedTokens.includesTested(token);
                if (needsTesting) {
                    _testIndex.insert(token);
                }
            }

            _freeTokens.add(token, time, n);
            _freeTokenCount += n;

            if (!alreadyRegistered) {
                reserve(token, n);
            }

            // Now inform triggerables and listeners.
            // By now they might find out by chance, so we should better
            // tell them directly.
            _triggerables.proposeSearch();

            // Notify all listeners.
            TokenEvent tokenEvent = new TokenEvent(this, token);
            _listeners.tokenAdded(tokenEvent);

            // Allow the master place to distribute the event, too.
            _place.getListenerSet().tokenAdded(tokenEvent);
        } finally {
            _lock.unlock();
        }
    }

    // Undo a testToken call. Once the last token is returned,
    // a token is reinserted into the bag of free tokens.
    //
    // The time is used as the time stamp of the returned token,
    // if the current test is the last. Otherwise, the argument is
    // ignored.
    @Override
    public void untestToken(Object token) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";

        _lock.lock();
        try {
            double time = _testedTokens.removeTested(token);

            // No need to change the test index, because a tested token
            // will become free or an equivalent token will remain tested.
            // No need to touch IDRegistry, because token is
            // still kept in the place.
            boolean isCompletelyUntested = !_testedTokens.includesTested(token);
            if (!isCompletelyUntested) {
                return; // Token still has tested instances, early return
            }


            boolean needsIndexRegistration = !_freeTokens.includesAnytime(token);
            if (needsIndexRegistration) {
                _freeIndex.insert(token);
            }

            // Reinsert the token as a free token at the given time.
            _freeTokens.add(token, time);
            // Fix the token count.
            _freeTokenCount++;
            _testedTokenCount--;

            // We must notify the listeners only if a tested token has
            // become free. If a token is still tested, the
            // untest simply shortens the time span until a possible
            // activation.
            _triggerables.proposeSearch();

            // Notify all listeners.
            TokenEvent tokenEvent = new TokenEvent(this, token);
            _listeners.tokenUntested(tokenEvent);
            // Allow the master place to distribute the event, too.
            _place.getListenerSet().tokenUntested(tokenEvent);

        } finally {
            _lock.unlock();
        }
    }

    /**
     * Serialization method, behaves like default writeObject
     * method except storing the not-really-transient fields
     * freeTokens, testedTokens and triggerables.
     * If the stream used is a RenewObjectOutputStream, these
     * fields are delayed to cut down recursion depth.
     * The domain trace feature of this special stream is also
     * used.
     * See also: {@link de.renew.util.RenewObjectOutputStream}
     *
     * @param out the stream to serialize to
     * @throws IOException if an error occurs while writing
     **/
    @Serial
    private void writeObject(ObjectOutputStream out) throws IOException {
        assert SimulationThreadPool.isSimulationThread() : "Not in a simulation thread";

        boolean isRenewStream = out instanceof RenewObjectOutputStream;
        RenewObjectOutputStream rOut = isRenewStream ? (RenewObjectOutputStream) out : null;

        if (isRenewStream) {
            rOut.beginDomain(this);
        }

        out.defaultWriteObject();

        if (isRenewStream) {
            rOut.delayedWriteObject(_freeTokens, this);
            rOut.delayedWriteObject(_testedTokens, this);
            rOut.endDomain(this);
        } else {
            out.writeObject(_freeTokens);
            out.writeObject(_testedTokens);
        }
    }

    /**
     * Deserialization method, behaves like default readObject
     * method except restoring additional transient fields.
     * The method also restores the not-really-transient fields
     * freeTokens and testedTokens, <b>if</b> the
     * stream used is <b>not</b> a RenewObjectInputStream.
     * In that case it also recomputes the transient fields
     * freeIndex and testedIndex.
     *
     * @param in the stream to read objects from
     * @throws IOException if an error occurs while reading
     * @throws ClassNotFoundException if an object is read whose class cannot be found
     **/
    @Serial
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        in.defaultReadObject();

        if (in instanceof RenewObjectInputStream) {
            // Do nothing, the fields will be
            // reassigned by the stream soon.
            return;
        }
        // Free tokens must be written first, so that
        // the indexes can be recomputed after reading the
        // tested tokens.
        _freeTokens = (TokenBag) in.readObject();
        _testedTokens = (TestTokenBag) in.readObject();
        recomputeIndexes();

    }

    /**
     * Method used on deserialization by RenewObjectInputStream.
     * Reassigns values to the not-really-transient fields
     * freeTokens and testedTokens, one at a time.
     * Also recomputes freeIndex and testIndex after both
     * TokenBags got reassigned.
     **/
    @Override
    public void reassignField(Object value) throws IOException {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        if (tryReassignField(value)) {
            // do nothing because the superclass used the value
            return;
        }

        if (value instanceof TokenBag) {
            _freeTokens = (TokenBag) value;
        } else if (value instanceof TestTokenBag) {
            _testedTokens = (TestTokenBag) value;
            recomputeIndexes();
        } else {
            throw new NotSerializableException(
                "Value of unexpected type given to MultiSetPlaceInstance.reassignField():"
                    + value.getClass().getName() + ".");
        }
    }

    /**
     * Rebuilds both TupleIndex fields (freeIndex and
     * testIndex) from scratch, using the elements of
     * the TokenBag fields freeTokens and testedTokens.
     * Needed by the deserialization methods readObject()
     * and reassignField() to update the transient
     * TupleIndex fields.
     **/
    private void recomputeIndexes() {
        _freeIndex = new TupleIndex();
        addToIndex(_freeIndex, _freeTokens.uniqueElements());
        _testIndex = new TupleIndex();
        addToIndex(_testIndex, _freeTokens.uniqueElements());
        addToIndex(_testIndex, _testedTokens.uniqueElements());
    }

    /**
     * Inserts all elements of the given TokenBag into
     * the given TupleIndex. Used by recomputeIndexes().
     **/
    private void addToIndex(TupleIndex index, Collection<Object> tokenList) {
        for (Object token : tokenList) {
            index.insert(token);
        }
    }

    @Override
    protected void finalize() throws Throwable {
        // Deregister all tokens at the ID registry.
        for (Object token : _testIndex.getAllElements()) {
            unreserve(token);
        }
        super.finalize();
    }
}
