package de.renew.database;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Vector;

import org.apache.log4j.Logger;

import de.renew.database.entitylayer.Entity;
import de.renew.database.entitylayer.NetInstanceEntity;
import de.renew.database.entitylayer.NoSuchEntityException;
import de.renew.database.entitylayer.SQLDialect;
import de.renew.database.entitylayer.StateEntity;
import de.renew.database.entitylayer.TokenEntity;
import de.renew.database.entitylayer.TokenPositionEntity;
import de.renew.util.Base64Coder;


/**
 * Implementation of the TransactionStrategy interface
 * that encapsulates the code that an application
 * has to execute when the results of a firing transition
 * have to be written to a destination, for example, a database
 * or a log file. This implementation writes the results
 * into an SQL database, so that the database holds the
 * current net instance, place instance, token and token
 * position state at any time.
 */
public class DatabaseTransactionStrategy implements TransactionStrategy {
    /**
     * The logger for database transaction operations.
     */
    public static final Logger LOGGER = Logger.getLogger(DatabaseTransactionStrategy.class);

    /**
     * The connection for database operations.
     */
    private Connection _connection;

    /**
     * The SQL dialect for database operations.
     */
    private final SQLDialect _dialect;

    /**
     * Creates the database transaction strategy.
     * @param dbUrl The url to the database to be used.
     * @param dialect The SQL dialect for database operations.
     */
    public DatabaseTransactionStrategy(String dbUrl, SQLDialect dialect) throws SQLException {
        _connection = DriverManager.getConnection(dbUrl);
        _connection.setAutoCommit(false);
        _dialect = dialect;
    }

    /**
     * Creates the database transaction strategy.
     * @param dbUrl The url to the database to be used.
     * @param dbUser The user for the database to be used.
     * @param dbPassword The password for the database to be used.
     * @param dialect The SQL dialect for database operations.
     */
    public DatabaseTransactionStrategy(
        String dbUrl, String dbUser, String dbPassword, SQLDialect dialect) throws SQLException
    {
        _connection = DriverManager.getConnection(dbUrl, dbUser, dbPassword);
        _connection.setAutoCommit(false);
        _dialect = dialect;
    }

    /**
     * Performs the token add actions. Positions are inserted
     * into the database or their quantities are changed, and
     * tokens are inserted, if they don't exist.
     * Adds an array of TokenActions. These tokens
     * are placed into their respective place instances.
     * @throws SQLException The transaction couldn't
     * be performed because of a database problem.
     * @throws IllegalTokenException A token couldn't be
     * added, because it doesn't support a database write.
     * This is most likely if the token is not serializable.
     */
    private void addTokens(TokenAction[] adds) throws SQLException, IllegalTokenException {
        for (TokenAction add : adds) {
            TokenEntity tokenEntity = new TokenEntity(_connection, _dialect);
            try {
                tokenEntity.load(add.getTokenID());
            } catch (NoSuchEntityException nsee) {
                Object token = add.getToken();

                try {
                    tokenEntity.setTokenId(Integer.valueOf(add.getTokenID()));
                } catch (NumberFormatException e) {
                    throw new IllegalTokenException(
                        "The token with id " + add.getTokenID() + ", class "
                            + token.getClass().getName() + " and representation " + token
                            + " cannot be saved, because the ID is not parsable to an int."
                            + " Currently, only int IDs are supported for tokens.");
                }

                tokenEntity.setClassName(token.getClass().getName());
                byte[] serialisation;
                try {
                    ByteArrayOutputStream outStream = new ByteArrayOutputStream();
                    new ObjectOutputStream(outStream).writeObject(token);
                    serialisation = outStream.toByteArray();
                } catch (Exception e) {
                    ByteArrayOutputStream stream = new ByteArrayOutputStream();
                    e.printStackTrace(new PrintWriter(stream));
                    throw new IllegalTokenException(
                        "The token with id " + add.getTokenID() + ", class "
                            + token.getClass().getName() + " and representation " + token
                            + " couldn't be serialized, because there was an exception during"
                            + " serialisation: " + e + "\n"
                            + "Only serializable tokens can be handled"
                            + " by the DatabaseTransactionStrategy.");
                }
                tokenEntity.setSerialisation(Base64Coder.encode(serialisation));
                tokenEntity.save();
            }

            TokenPositionEntity tokenPositionEntity =
                new TokenPositionEntity(_connection, _dialect);
            try {
                tokenPositionEntity.load(
                    new Object[] { add.getTokenID(), add.getNetID(), add.getPlaceID().toString() });
                tokenPositionEntity.setQuantity(tokenPositionEntity.getQuantity() + 1);
            } catch (NoSuchEntityException nsee) {
                tokenPositionEntity.setTokenId(Integer.valueOf(add.getTokenID()));

                try {
                    tokenPositionEntity.setNetInstanceId(Integer.valueOf(add.getNetID()));
                } catch (NumberFormatException e) {
                    throw new IllegalTokenException(
                        "The net instance ID " + add.getNetID()
                            + " cannot be saved, because it is not parsable to an int."
                            + " Currently, only int IDs are supported for net instances.");
                }

                tokenPositionEntity.setPlaceInstanceId(add.getPlaceID().toString());
                tokenPositionEntity.setQuantity(1);
            }
            tokenPositionEntity.save();
        }
    }

    /**
     * Performs the net create actions. The new net instances
     * are inserted into database.
     * Creates an array of NetActions. They represent
     * the new nets to be added.
     * @throws SQLException The transaction couldn't
     * be performed because of a database problem.
     */
    private void createNets(NetAction[] creates) throws SQLException {
        for (NetAction create : creates) {
            NetInstanceEntity netInstanceEntity = new NetInstanceEntity(_connection, _dialect);
            try {
                netInstanceEntity.load(create.getNetID());
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug(
                        DatabaseTransactionStrategy.class.getSimpleName()
                            + ": loaded net instance entity with id " + create.getNetID());
                }
            } catch (NoSuchEntityException nsee) {
                try {
                    netInstanceEntity.setNetInstanceId(Integer.valueOf(create.getNetID()));
                } catch (NumberFormatException e) {
                    throw new RuntimeException(
                        "The net instance ID " + create.getNetID()
                            + " cannot be saved, because it is not parsable to an int."
                            + " Currently, only int IDs are supported for net instances.");
                }


                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug(
                        DatabaseTransactionStrategy.class.getSimpleName()
                            + ": create net instance entity with name " + create.getName());
                }
                netInstanceEntity.setName(create.getName());
                netInstanceEntity.setDrawingOpen(0);
                netInstanceEntity.save();
            }
        }
    }

    /**
     * Performs the net delete actions. Positions are deleted
     * from the database, and tokens are deleted if they are
     * not used anymore. Finally, the net instance is deleted.
     * Deletes an array of NetActions. They represent
     * the new nets to be removed.
     * @throws SQLException The transaction couldn't
     * be performed because of a database problem.
     */
    private void deleteNets(NetAction[] deletes) throws SQLException {
        for (NetAction delete : deletes) {
            try {
                NetInstanceEntity netInstanceEntity = new NetInstanceEntity(_connection, _dialect);
                netInstanceEntity.load(delete.getNetID());

                TokenPositionEntity tokenPositionEntity =
                    new TokenPositionEntity(_connection, _dialect);
                Vector<Entity> tokenPositionEntities = Entity.getEntities(
                    tokenPositionEntity, "NET_INSTANCE_ID='" + delete.getNetID() + "'");
                Enumeration<Entity> tokenPositionEntitiesEnum = tokenPositionEntities.elements();
                while (tokenPositionEntitiesEnum.hasMoreElements()) {
                    tokenPositionEntity =
                        (TokenPositionEntity) tokenPositionEntitiesEnum.nextElement();

                    String tokenId = String.valueOf(tokenPositionEntity.getTokenId());
                    tokenPositionEntity.delete();

                    if (Entity.getEntities(tokenPositionEntity, "TOKEN_ID='" + tokenId + "'")
                        .isEmpty()) {
                        TokenEntity tokenEntity = new TokenEntity(_connection, _dialect);
                        tokenEntity.load(tokenId);
                        tokenEntity.delete();
                    }
                }

                netInstanceEntity.delete();
            } catch (NoSuchEntityException e) {
                // The garbage collector was too late.
                // The TransactionStrategy has already deleted this entity.
                // So it is safe to ignore this exception.
            }
        }
    }

    /**
     * Closes the connection when the object is collected by the GC.
     * Any open database transaction is rolled back.
     */
    @Override
    protected void finalize() throws SQLException {
        if (_connection != null) {
            _connection.rollback();
            _connection.close();
            _connection = null;
        }
    }

    /**
     * Notifies the database that a net instance
     * is now not being watched with a drawing anymore.
     * @param netInstanceID The net instance id of which the
     * drawing has been closed.
     * @throws SQLException The notification couldn't
     * be passed to the database because of a database problem.
     */
    @Override
    public void netInstanceDrawingClosed(String netInstanceID) throws SQLException {
        try {
            LOGGER.debug("Closed " + netInstanceID);
            NetInstanceEntity netInstanceEntity = new NetInstanceEntity(_connection, _dialect);

            netInstanceEntity.load(netInstanceID);
            netInstanceEntity.setDrawingOpen(0);
            netInstanceEntity.save();
        } catch (NoSuchEntityException e) {
            // The garbage collector was too late.
            // The TransactionStrategy has already deleted this entity.
            // So it is safe to ignore this exception.
        }
    }

    /**
     * Notifies the database that a net instance
     * is now being watched with a drawing.
     * @param netInstanceID The net instance id of which a
     * drawing has been opened.
     * @throws SQLException The notification couldn't
     * be passed to the database because of a database problem.
     */
    @Override
    public void netInstanceDrawingOpened(String netInstanceID) throws SQLException {
        try {
            LOGGER.debug("Opened " + netInstanceID);
            NetInstanceEntity netInstanceEntity = new NetInstanceEntity(_connection, _dialect);

            netInstanceEntity.load(netInstanceID);
            netInstanceEntity.setDrawingOpen(1);
            netInstanceEntity.save();
        } catch (NoSuchEntityException e) {
            // The garbage collector was too late.
            // The TransactionStrategy has already deleted this entity.
            // So it is safe to ignore this exception.
        }
    }

    /**
     * Execute one transaction, e.g. by writing to a database.
     * @param createActions an array of net creation actions
     * @param addActions an array of token insertion actions
     * @param removeActions an array of token removal actions
     * @param deleteActions an array of net deletion actions
     * @throws SQLException The transaction couldn't
     * be performed because of a database problem.
     */
    @Override
    public void perform(
        NetAction[] createActions, TokenAction[] addActions, TokenAction[] removeActions,
        NetAction[] deleteActions) throws SQLException, IllegalTokenException
    {
        createNets(createActions);
        addTokens(addActions);
        removeTokens(removeActions);
        deleteNets(deleteActions);

        _connection.commit();
    }

    /**
     * Performs the token remove actions. Positions are deleted
     * from the database or their quantities are changed, and
     * tokens are deleted, if they are not used anymore.
     * Removes an array of TokenActions. These tokens
     * are removed from their respective place instances.
     * @throws SQLException The transaction couldn't
     * be performed because of a database problem.
     * @throws IllegalTokenException A token couldn't be
     * removed, because it doesn't support a database write.
     * This is most likely if the token is not serializable.
     */
    private void removeTokens(TokenAction[] removes) throws SQLException, IllegalTokenException {
        for (TokenAction remove : removes) {
            Object token = remove.getToken();
            if (!(token instanceof Serializable)) {
                throw new IllegalTokenException(
                    "Token " + remove.getTokenID() + " is not serializable.\n"
                        + "Only serializable tokens can be handled"
                        + " by the DatabaseTransactionStrategy.");
            }

            try {
                TokenEntity tokenEntity = new TokenEntity(_connection, _dialect);
                tokenEntity.load(remove.getTokenID());

                TokenPositionEntity tokenPositionEntity =
                    new TokenPositionEntity(_connection, _dialect);
                try {
                    tokenPositionEntity.load(
                        new Object[] {
                            remove.getTokenID(), remove.getNetID(),
                            remove.getPlaceID().toString() });
                    if (tokenPositionEntity.getQuantity() <= 1) {
                        tokenPositionEntity.delete();
                    } else {
                        tokenPositionEntity.setQuantity(tokenPositionEntity.getQuantity() - 1);
                        tokenPositionEntity.save();
                    }
                } catch (NoSuchEntityException e) {
                    // The garbage collector was too late.
                    // The TransactionStrategy has already deleted this entity.
                    // So it is safe to ignore this exception.
                }

                if (Entity
                    .getEntities(tokenPositionEntity, "TOKEN_ID='" + remove.getTokenID() + "'")
                    .isEmpty()) {
                    tokenEntity.delete();
                }
            } catch (NoSuchEntityException e) {
                // The garbage collector was too late.
                // The TransactionStrategy has already deleted this entity.
                // So it is safe to ignore this exception.
            }
        }
    }

    /**
     * Notifies the database about simulation state changes.
     * @param inited The simulation is inited (not terminated).
     * @param running The simulation is running.
     * @throws SQLException The notification couldn't
     * be passed to the database because of a database problem.
     */
    @Override
    public void simulationStateChanged(boolean inited, boolean running) throws SQLException {
        StateEntity stateEntity = new StateEntity(_connection, _dialect);
        try {
            // Object[0] as primary key loads the only entity.
            stateEntity.load(new Object[0]);
        } catch (NoSuchEntityException e) {
            // Since it is okay that there was no simulation state before,
            // and since the entity is in a correct state afterward,
            // this exception can safely be ignored.
        }

        stateEntity.setInited(inited ? 1 : 0);
        stateEntity.setRunning(running ? 1 : 0);
        stateEntity.save();

        _connection.commit();
    }
}