package de.renew.net;

import java.io.IOException;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
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.common.SimulatorEventLogger;
import de.renew.engine.events.TraceEvent;
import de.renew.engine.searcher.TriggerableCollection;
import de.renew.engine.searcher.TriggerableCollectionImpl;
import de.renew.engine.searchqueue.SearchQueue;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.expression.LocalVariable;
import de.renew.expression.VariableMapper;
import de.renew.net.event.PlaceEventListenerSet;
import de.renew.simulatorontology.simulation.StepIdentifier;
import de.renew.unify.Impossible;
import de.renew.unify.Unify;
import de.renew.unify.Variable;
import de.renew.util.DelayedFieldOwner;
import de.renew.util.Lock;
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 abstract class SimulatablePlaceInstance extends PlaceInstance
    implements Serializable, DelayedFieldOwner
{
    @Serial
    private static final long serialVersionUID = 0L;

    /** The logger to use with SimulatablePlaceInstances. */
    private static final Logger LOGGER = Logger.getLogger(SimulatablePlaceInstance.class);

    /**
     * Here we store tokens that constitute the initial marking.
     * These tokens are computed in the constructor, but their
     * insertion must be traced later on, and maybe they must even
     * be inserted into the place instance.
     **/
    protected List<Object> _initTokens;

    /**
     * This flag denotes whether the initial tokens have been put
     * into the place instance.
     */
    protected boolean _earlyTokens;

    /**
     * This field contains those objects that might have to be triggered once after
     * a change to the current marking.
     * <p>
     * It is not really transient, but as we want
     * to cut down the serialization recursion depth, it
     * is serialized manually.
     **/
    protected transient TriggerableCollection _triggerables;

    /**
     * The order in which locks are requested is governed by
     * this number, which is unique within a single simulation.
     * <p>
     * lockOrder and lock were originally final, but to allow the
     * creation of new values on deserialization the modifier
     * had to be removed.
     */
    private transient long _lockOrder;

    /**
     * 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
     */
    SimulatablePlaceInstance(NetInstance netInstance, Place place, boolean wantInitialTokens)
        throws Impossible
    {
        super(netInstance, place);

        _lockOrder = de.renew.util.Orderer.getTicket();

        _triggerables = new TriggerableCollectionImpl();

        initTokenStorage();


        // Copy the early token flag. Maybe this flag changes
        // later on. (Well, it shouldn't, but let's be safe.)
        _earlyTokens = netInstance.getNet().getEarlyTokens();

        if (wantInitialTokens) {
            // Let's calculate the initial bag of tokens.
            _initTokens = new ArrayList<Object>();
            for (TokenSource tokenSource : place.getInscriptions()) {
                VariableMapper mapper = new VariableMapper();
                Variable thisVariable = mapper.map(new LocalVariable("this", false));
                Unify.unify(thisVariable, netInstance, null);

                Object token = tokenSource.createToken(mapper);
                _initTokens.add(token);

                if (_earlyTokens) {
                    // I am requested to insert the tokens now.
                    internallyInsertToken(token, SearchQueue.getTime(), false);
                }
            }
        } else {
            _initTokens = Collections.emptyList();
        }
    }

    /**
     * Initializes the token storage of the SimulatablePlaceInstance.
     */
    protected abstract void initTokenStorage();

    @Override
    public String toString() {
        return _netInstance.toString() + "." + _place.toString();
    }

    /**
     * Gets the {@link #_triggerables} (objects that might have to be triggered once after a change to the place's
     * marking). We do not lock the place instance during accesses to the triggerables contrary to earlier
     * implementations. The triggerables have to take care of locking themselves.
     *
     * @return the triggerables
     */
    public TriggerableCollection triggerables() {
        return _triggerables;
    }

    /**
     * Returns the set of currently untested tokens.
     * At least those tokens which are unifiable with pattern
     * should be included.
     *
     * @param pattern the pattern {@code Object}
     * @return the set of currently untested tokens, containing at least those that match {@code pattern}
     */
    public abstract Set<Object> getDistinctTokens(Object pattern);

    /**
     * Returns the distinct tokens in the place that are testable.
     *
     * @param pattern the pattern {@code Object}
     * @return the set of distinct tokens in the place that are testable, containing at least those that match
     *         {@code pattern}
     */
    public abstract Set<Object> getDistinctTestableTokens(Object pattern);

    abstract boolean containsToken(Object token);

    /**
     * Returns the delay until a set of untested tokens matching a given token
     * is available that matches the given delay times.
     *
     * @param token the token to check for
     * @param times the delay times
     * @return the delay until a set of untested tokens is available that matches the given delay times
     */
    public abstract double computeEarliestTime(Object token, TimeSet times);

    /**
     * Checks whether the place instance contains a given testable token.
     *
     * @param token the token to check for
     * @return whether the place has a testable token matching {@code token}
     */
    public abstract boolean containsTestableToken(Object token);

    /**
     * Get the IDRegistry that is responsible for the SimulatablePlaceInstance's net instance.
     *
     * @return the IDRegistry that is responsible for SimulatablePlaceInstance's net instance
     */
    protected IDRegistry registry() {
        return _netInstance.getRegistry();
    }

    /**
     * Reserves a token once. Makes sure a token keeps its ID. Only while the
     * token is reserved or contained in the place it is guaranteed that it keeps its ID.
     *
     * @param token the token to reserve
     */
    public void reserve(Object token) {
        // Increase the number of registrations.
        registry().reserve(token);
    }

    /**
     * Reserves a token a given amount of times. Makes sure a token keeps its ID. Only while the
     * token is reserved or contained in the place it is guaranteed that it keeps its ID.
     *
     * @param token the token to reserve
     * @param n the amount of times the token should be reserved
     */
    public void reserve(Object token, int n) {
        // Increase the number of registrations by n.
        registry().reserve(token, n);
    }

    /**
     * Returns the ID of a given token according to the registry of the SimulatablePlaceInstance's net instance.
     *
     * @param token the token whose ID should be returned
     * @return the ID of the token
     */
    public String getTokenID(Object token) {
        return registry().getID(token);
    }

    /**
     * Undoes one reservation of a given token.
     *
     * @param token the token to reserve
     */
    public void unreserve(Object token) {
        // Decrease the number of registrations.
        registry().unreserve(token);
    }

    /**
     * Tries to remove the given token from the SimulatablePlaceInstance.
     *
     * @param token the token to remove
     * @param delay the delay after which to remove the token
     * @return the time at which the token was removed
     * @throws Impossible if no token is available at the given time
     */
    public abstract double removeToken(Object token, double delay) throws Impossible;

    /**
     * Tests a given token.
     * <p>
     * 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.
     *
     * @param token the token to test
     * @return the time at which the token was removed
     * @throws Impossible if no matching token is testable at the given time
     */
    public abstract double testToken(Object token) throws Impossible;

    /**
     * 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.
     */
    public abstract void extractAllTokens(Vector<Object> tokens, Vector<Double> timeStamps);

    /**
     * This method inserts a token into the place instance
     * and tries to assign the given ID to it. It is an error,
     * if the token is already registered in the place
     * instance with a different ID.
     * <p>
     * This method should only be called when restoring a
     * simulation state from a database. Only in that case
     * there is any reason to assign a particular ID to a token.
     *
     * @param token the token to be inserted
     * @param id the token's ID
     * @param time the time stamp of the token
     * @exception java.lang.RuntimeException if the object was already
     *   registered with a different ID
     */
    public void insertTokenWithID(Object token, String id, double time) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        _lock.lock();
        try {
            registry().setAndReserveID(token, id);
            internallyInsertToken(token, time, true);
        } finally {
            _lock.unlock();
        }
    }

    /**
     * This method inserts tokens into the place instance with notifications
     * and index updates, although it will not invoke the transaction
     * mechanism.
     * <p>
     * This method must be used with care. It should only be called
     * in those cases where an invocation of the transaction
     * mechanism would be absolutely inappropriate, e.g., within
     * the transaction mechanims itself or if the transaction
     * is called explicitly or if a net instance is restored
     * from a database or if a previous token removal that did not reach the
     * database is undone.
     * <p>
     * This method will take care of locking the place instance
     * automatically. It is however, allowed that the current thread
     * has already locked it, if this is required due to
     * deadlock prevention.
     *
     * @param token the token to be inserted
     * @param time the time stamp of the token
     * @param alreadyRegistered true, if the caller has already
     *   registered the new token at the IDRegistry.
     */
    public abstract void internallyInsertToken(
        Object token, double time, boolean alreadyRegistered);

    /**
     * This method records the effect of a token insertion
     * in a transaction object. If automatic insertion is requested,
     * then the token is actually inserted after the transaction has
     * been committed.
     *
     * @param token the token to be inserted
     * @param time the time stamp of the token
     * @param automaticInsertion true, if the transaction should care about
     *   the insertion of the token.
     */
    public void transactionInsertToken(Object token, double time, boolean automaticInsertion) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        Transaction transaction = TransactionSource.get();
        try {
            transaction.addToken(this, token, time, automaticInsertion);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * Insert a token into the place instance at a specified time.
     * The token deposit is written to the currently active transaction.
     * Only after that transaction has been committed, the token
     * will occur in the place instance. If not transaction is active,
     * the deposit takes place immediately.
     * <p>
     * This is the standard method call that applications should
     * use to get tokens into a net. It will not print trace messages,
     * but it will ultimately notify the listeners and triggerables
     * of the change.
     * <p>
     * This method will take care of locking the place instance
     * automatically. It is however, allowed that the current thread
     * has already locked the place instance, if this is required
     * due to deadlock prevention.
     *
     * @param token the token to be inserted
     * @param time the time stamp of the token
     */
    public void insertToken(Object token, double time) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        transactionInsertToken(token, time, true);
    }

    /**
     * Undoes a {@link #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.
     *
     * @param token the token whose test should be undone
     */
    public abstract void untestToken(Object token);

    /**
     * Print trace messages for the insertion of initial tokens.
     *
     */
    private void traceInitialTokens(StepIdentifier stepIdentifier) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";

        // Anything to do?
        for (Object token : _initTokens) {
            if (_place.getTrace()) {
                // log activity on net level
                SimulatorEventLogger.log(
                    stepIdentifier, new TraceEvent("Initializing " + token + " into " + this),
                    this);
            }
        }
    }

    /**
     * Notifies the database about all tokens that this {@code SimulatablePlaceInstance} kept during its
     * creation, if the tokens must be available early.
     * <p>
     * The associated net instance will call this method while
     * its creation is confirmed and trace messages are printed.
     * It must be called at most once.
     * <p>
     * In fact, this method is rather a kludge in the sense
     * that transaction support should not be activated
     * at all if a net requests early confirmation.
     */
    @Override
    public void earlyConfirmation() {
        if (_earlyTokens) {

            // Record each token individually in the database.
            for (Object token : _initTokens) {
                double time = SearchQueue.getTime();

                // Do not keep a note to insert to tokens
                // at commit time. They were added already during the
                // creation of this net instance.
                transactionInsertToken(token, time, false);
            }
        }
    }

    /**
     * My net will call this method while its creation is traced.
     */
    @Override
    public void earlyConfirmationTrace(StepIdentifier stepIdentifier) {
        if (_earlyTokens) {
            traceInitialTokens(stepIdentifier);
        }
    }

    /**
     * Inserts all the initial tokens kept during the initialisation of this {@code SimulatablePlaceInstance}.
     * <p>
     * This method is called by the {@link #_netInstance} late during its confirmation. It must be called at most once.
     */
    @Override
    public void lateConfirmation(StepIdentifier stepIdentifier) {
        if (!_earlyTokens) {
            traceInitialTokens(stepIdentifier);

            // Anything to do?
            for (Object token : _initTokens) {
                insertToken(token, SearchQueue.getTime());
            }
        }


        // Null the field to allow garbage collection.
        _initTokens = null;
    }

    /**
     * Serialization method, behaves like default writeObject
     * method except storing the not-really-transient field
     * triggerables.
     * <p>
     * If the stream used is a RenewObjectOutputStream, this
     * field is 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(java.io.ObjectOutputStream out) throws IOException {
        RenewObjectOutputStream rOut = null;
        if (out instanceof RenewObjectOutputStream) {
            rOut = (RenewObjectOutputStream) out;
        }
        if (rOut != null) {
            rOut.beginDomain(this);
        }
        out.defaultWriteObject();
        if (rOut != null) {
            rOut.delayedWriteObject(_triggerables, this);
            rOut.endDomain(this);
        } else {
            out.writeObject(_triggerables);
        }
    }

    /**
     * Deserialization method, behaves like default readObject
     * method except restoring additional transient fields.
     * Creates new lock object and gets a new lock order ticket.
     * The method also restores the not-really-transient field
     * {@code triggerables}, <b>if</b> the
     * stream used is <b>not</b> a RenewObjectInputStream.
     *
     * @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(java.io.ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        in.defaultReadObject();
        _lockOrder = de.renew.util.Orderer.getTicket();
        _lock = new Lock();
        _listeners = new PlaceEventListenerSet();

        if (in instanceof RenewObjectInputStream) {
            // Do nothing, the fields will be
            // reassigned by the stream soon.
            return;
        }
        _triggerables = (TriggerableCollection) in.readObject();

    }

    /**
     * Method used on deserialization by RenewObjectInputStream.
     * Reassigns values to the not-really-transient fields,
     * one at a time.
     * <p>
     * Subclasses must implement this method, and the first
     * statement should be a call to {@link #tryReassignField}.
     * Example:
     * <pre>
     *   public void reassignField(Object value) throws java.io.IOException {
     *     if (!tryReassignField(value)) {
     *       // Subclass field assignment goes here...
     *     }
     *   }
     * </pre>
     **/
    @Override
    public abstract void reassignField(Object value) throws java.io.IOException;

    /**
     * Reassigns the value to the not-really-transient field
     * {@code triggerables}.
     * <p>
     * This method must be called by subclasses at the beginning
     * of their {@link #reassignField} implementation.
     * If the method returns {@code false}, the value
     * was not consumed and can be used by the subclass.
     * <p>
     * If a subclass wants to override this method, a {@code super}
     * call and correct interpretation of the return value is mandatory.
     *
     * @param value the value to be reassigned to a field
     * @throws IOException never
     * @return {@code true}  if the value was consumed <br>
     *         {@code false} if the value didn't fit to any delayed field of this class
     **/
    protected boolean tryReassignField(Object value) throws java.io.IOException { //NOTICEthrows
        if (value instanceof TriggerableCollection) {
            _triggerables = (TriggerableCollection) value;
            return true;
        }
        return false;

    }

    /**
     * Getter for {@code lockOrder}.
     *
     * @return the {@code lockOrder}
     */
    public long getLockOrder() {
        return _lockOrder;
    }
}
