package de.renew.database;

import java.util.Vector;

import org.apache.log4j.Logger;

import de.renew.net.NetInstance;
import de.renew.net.SimulatablePlaceInstance;


/**
 * Accept tasks for a transaction and make sure to forward
 * them to the transaction strategy in due time. Additionally,
 * take a note of all token deposits and perform them after the
 * transaction was committed.
 * <p>
 * This enables us to delay the actual token output and hence
 * the firing of further transitions until the firing of
 * the first transition is firmly recorded in the database.
 * </p>
 * For any token that is added or removed, transactions
 * are also responsible for unreserving that token from the
 * place instance once the final fate of the transaction
 * (commit or rollback) has been decided and no more actions are
 * to be performed.
 */
public class Transaction {
    private static final Logger LOGGER = Logger.getLogger(Transaction.class);

    private final boolean _autoCommit;
    private final Vector<NetAction> _createActions;
    private final Vector<NetAction> _deleteActions;

    /**
    * The delayed add actions are those token insertions
    * that have to be performed by the transaction during commit.
    * All delayed add action will also occur as add actions,
    * but not all add actions need to be delayed add actions.
    * Sometimes the simulator takes care of inserting the tokens.
    * <p>
    * This vector contains token actions.
    * {@link de.renew.database.TokenAction}
    */
    private final Vector<TokenAction> _delayedAddActions;

    /**
    * This vector contains token actions for all token insertions.
    * {@link de.renew.database.TokenAction}
    */
    private final Vector<TokenAction> _addActions;

    /**
    * This vector contains token actions for all token removals.
    * {@link de.renew.database.TokenAction}
    */
    private final Vector<TokenAction> _removeActions;

    /**
     * Create a transaction that records all calls and commits them
     * jointly in the end or a transaction that immediately commits each
     * modification.
     *
     * @param autoCommit true, if all actions should be immediately committed
     */
    public Transaction(boolean autoCommit) {
        _autoCommit = autoCommit;

        _createActions = new Vector<NetAction>();
        _deleteActions = new Vector<NetAction>();
        _delayedAddActions = new Vector<TokenAction>();
        _addActions = new Vector<TokenAction>();
        _removeActions = new Vector<TokenAction>();
    }

    /**
     * This method creates a NetAction, logs the creation and commits, if autoCommit is desired.
     *
     * @param instance the instance, which is used for the creation of the NetAction
     * @throws Exception if the net could not be created successfully or if an error
     *                   occurs during the execution of a transaction
     */
    public synchronized void createNet(NetInstance instance) throws Exception {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(Transaction.class.getSimpleName() + ": creating net action.");
        }
        _createActions.addElement(new NetAction(instance));

        if (_autoCommit) {
            commit();
        }
    }

    /**
     * This method deletes a NetAction and commits, if autoCommit is desired.
     *
     * @param instance the instance, which is used for the deletion of the NetAction
     * @throws Exception if the net could not be created successfully or if an error
     *                   occurs during the execution of a transaction
     */
    public synchronized void deleteNet(NetInstance instance) throws Exception {
        _deleteActions.addElement(new NetAction(instance));

        if (_autoCommit) {
            commit();
        }
    }

    /**
     * This method reserves and adds a token in the specified placeInstance and if necessary throws an exception.
     *
     * @param placeInstance the placeInstance, where the token has to be added
     * @param token the token, which has to be added
     * @param time the maximum allowed time for this action to take place
     * @param automaticInsertion true or false, depending on the desired type of insertion
     * @throws Exception if a token couldn't be added, because it doesn't support a database write.
     */
    public synchronized void addToken(
        SimulatablePlaceInstance placeInstance, Object token, double time,
        boolean automaticInsertion) throws Exception
    {
        // We have to reserve the token once, so that it is
        // kept until this transaction is completed.
        placeInstance.reserve(token);

        TokenAction action = new TokenAction(placeInstance, token, time);

        if (automaticInsertion) {
            _delayedAddActions.addElement(action);
        }


        // The ID is now reserved. We can now build the
        // token action which relies on unchanging IDs.
        _addActions.addElement(action);

        if (_autoCommit) {
            commit();
        }
    }

    /**
     * This method reserves and removes a token in the specified placeInstance and if necessary throws an exception.
     *
     * @param placeInstance the placeInstance, where the token has to be removed
     * @param token the token, which has to be removed
     * @param time the maximum allowed time for this action to take place
     * @throws Exception if a token couldn't be removed, because it doesn't support a database write.
     */
    public synchronized void removeToken(
        SimulatablePlaceInstance placeInstance, Object token, double time) throws Exception
    {
        // We have to reserve the token once, so that it is
        // kept until this transaction is completed.
        placeInstance.reserve(token);

        _removeActions.addElement(new TokenAction(placeInstance, token, time));

        if (_autoCommit) {
            commit();
        }
    }


    /**
     * Outputs all delayed token actions by inserting tokens into their respective
     * place instances. This method ensures thread safety by locking the place
     * instance during the insertion process.
     */
    private void outputTokens() {
        for (TokenAction tokenAction : _delayedAddActions) {
            SimulatablePlaceInstance pi = tokenAction.getPlaceInstance();
            Object token = tokenAction.getToken();
            pi._lock.lock();
            try {
                pi.internallyInsertToken(token, tokenAction.getTime(), false);
            } finally {
                pi._lock.unlock();
            }
        }
    }

    private void unreserveTokens(Vector<TokenAction> tokenActions) {
        for (TokenAction tokenAction : tokenActions) {
            SimulatablePlaceInstance pi = tokenAction.getPlaceInstance();
            Object token = tokenAction.getToken();
            pi._lock.lock();
            try {
                pi.unreserve(token);
            } finally {
                pi._lock.unlock();
            }
        }
    }

    private void unreserveOutputTokens() {
        unreserveTokens(_addActions);
    }

    private void unreserveInputTokens() {
        unreserveTokens(_removeActions);
    }

    private void clear() {
        _createActions.removeAllElements();
        _delayedAddActions.removeAllElements();
        _addActions.removeAllElements();
        _removeActions.removeAllElements();
        _deleteActions.removeAllElements();
    }

    /**
     * This method creates, adds, removes and deletes an array of actions, which represent
     * the wanted transaction.
     *
     * @throws Exception if the transaction couldn't be performed
     */
    public synchronized void commit() throws Exception {
        NetAction[] ca = new NetAction[_createActions.size()];
        _createActions.copyInto(ca);
        TokenAction[] aa = new TokenAction[_addActions.size()];
        _addActions.copyInto(aa);
        TokenAction[] ra = new TokenAction[_removeActions.size()];
        _removeActions.copyInto(ra);
        NetAction[] da = new NetAction[_deleteActions.size()];
        _deleteActions.copyInto(da);

        TransactionSource.perform(ca, aa, ra, da);

        outputTokens();
        unreserveOutputTokens();
        unreserveInputTokens();

        clear();
    }

    /**
     * This method rollbacks the current transaction by unreserving the output and input tokens and clearing them.
     */
    public synchronized void rollback() {
        unreserveOutputTokens();
        unreserveInputTokens();
        clear();
    }
}