package de.renew.database;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.NotSerializableException;
import java.io.OptionalDataException;
import java.io.StreamCorruptedException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

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.net.PlaceInstance;
import de.renew.util.Base64Coder;
import de.renew.util.ClassSource;


/**
 * This class restores data from a database for simulation recovery and implements the {@link RestoreSource} interface.
 */
public class DatabaseRestoreSource implements RestoreSource {

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

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

    /**
     * Creates the database restore source based on an existing connection.
     * @param connection The connection to the database to be used.
     * @param dialect The SQL dialect for database operations.
     */
    public DatabaseRestoreSource(Connection connection, SQLDialect dialect) {
        _connection = connection;
        _dialect = dialect;
    }

    /**
     * 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;
        }
    }

    /**
     * Adds all tokens, that lie in a given place instance,
     * and their ids to vectors.
     * @param placeInstance The place instance to
     * look up the tokens of. Also contains the netInstance.
     * @param ids The vector to append all token ids lying
     * in the place instance to.
     * @throws SQLException If any SQL exception occurs.
     */
    @Override
    public void fillInAllTokens(PlaceInstance placeInstance, Vector<String> ids)
        throws SQLException
    {
        TokenPositionEntity tokenPositionEntity = new TokenPositionEntity(_connection, _dialect);
        Vector<Entity> tokenPositionEntities = Entity.getEntities(
            tokenPositionEntity, "NET_INSTANCE_ID='" + placeInstance.getNetInstance().getID() + "'"
                + " and PLACE_INSTANCE_ID='" + placeInstance.getPlace().getID() + "'");
        Enumeration<Entity> tokenPositionEntitiesEnum = tokenPositionEntities.elements();
        while (tokenPositionEntitiesEnum.hasMoreElements()) {
            tokenPositionEntity = (TokenPositionEntity) tokenPositionEntitiesEnum.nextElement();
            for (int i = tokenPositionEntity.getQuantity(); i > 0; i--) {
                ids.addElement(String.valueOf(tokenPositionEntity.getTokenId()));
            }
        }
    }

    /**
     * Returns all net instance ids in the
     * DatabaseRestoreSource's database.
     * @return The net instance ids as an array.
     * @throws SQLException If any SQL exception occurs.
     */
    @Override
    public String[] getAllNetIDs() throws SQLException {
        Vector<Entity> entities = Entity.getEntities(new NetInstanceEntity(_connection, _dialect));
        String[] netIds = new String[entities.size()];
        for (int i = 0; i < entities.size(); i++) {
            netIds[i] = ((NetInstanceEntity) entities.elementAt(i)).getNetInstanceId().toString();
        }
        return netIds;
    }

    /**
     * Returns the last used net instance or token id.
     * @return The last used net instance or token id.
     * @throws SQLException If any SQL exception occurs.
     */
    public int getLastId() throws SQLException {
        int lastNetInstanceId = 0;
        NetInstanceEntity netInstanceEntity = new NetInstanceEntity(_connection, _dialect);
        Vector<Entity> entities = Entity.getEntities(netInstanceEntity, null, "NET_INSTANCE_ID");
        if (!entities.isEmpty()) {
            try {
                lastNetInstanceId = ((NetInstanceEntity) entities.lastElement()).getNetInstanceId();
            } catch (NumberFormatException e) {
                // lastNetInstanceId automatically stays 0,
                // what is intended.
            }
        }

        int lastTokenId = 0;
        TokenEntity tokenEntity = new TokenEntity(_connection, _dialect);
        entities = Entity.getEntities(tokenEntity, null, "TOKEN_ID");
        if (!entities.isEmpty()) {
            try {
                lastTokenId = ((TokenEntity) entities.lastElement()).getTokenId();
            } catch (NumberFormatException e) {
                // lastTokenId automatically stays 0,
                // what is intended.
            }
        }

        return Math.max(lastTokenId, lastNetInstanceId);
    }

    /**
     * Returns the net instance name for a given net instance id.
     * @param netID The net instance id.
     * @return The net instance name.
     * @throws SQLException If any SQL exception occurs.
     */
    @Override
    public String getNetName(String netID) throws SQLException {
        NetInstanceEntity entity = new NetInstanceEntity(_connection, _dialect);
        entity.load(netID);
        return entity.getName();
    }

    /**
     * Returns a hashtable containing all tokens as values
     * with their respective ids as keys.
     * @param map The net instance map to fetch a net instance by its id.
     * @return The token hashtable.
     * @throws SQLException If any SQL exception occurs.
     * @throws IllegalTokenException If any of the
     * tokens in the database is not instantiatable,
     * deserializable or causes any other problem.
     */
    @Override
    public Hashtable<String, Object> getTokens(NetInstanceMap map)
        throws SQLException, IllegalTokenException
    {
        TokenEntity tokenEntity = new TokenEntity(_connection, _dialect);
        Vector<Entity> tokenEntities = Entity.getEntities(tokenEntity);

        Hashtable<String, Object> tokens = new Hashtable<>();
        Enumeration<Entity> tokenEntitiesEnum = tokenEntities.elements();
        while (tokenEntitiesEnum.hasMoreElements()) {
            tokenEntity = (TokenEntity) tokenEntitiesEnum.nextElement();

            Class<?> tokenClass = null;
            ByteArrayInputStream inStream = null;
            NetInstanceResolutionInputStream netStream = null;
            try {
                tokenClass = ClassSource.classForName(tokenEntity.getClassName());

                inStream =
                    new ByteArrayInputStream(Base64Coder.decode(tokenEntity.getSerialisation()));
                netStream = new NetInstanceResolutionInputStream(inStream, map);
                Object token = netStream.readObject();
                tokens.put(String.valueOf(tokenEntity.getTokenId()), token);
            } catch (InvalidClassException e) {
                //NOTICE null
                throw new IllegalTokenException(
                    "The database's token with id " + tokenEntity.getTokenId() + " is invalid.\n"
                        + "Class " + tokenClass.getName() //NOTICE null
                        + " is no valid class for deserialisation, because:\n" + e);
            } catch (StreamCorruptedException e) {
                throw new IllegalTokenException(
                    "The database's token with id " + tokenEntity.getTokenId() + " is invalid.\n"
                        + "It has invalid serialisation data: " + "Control information corrupt.");
            } catch (OptionalDataException e) {
                throw new IllegalTokenException(
                    "The database's token with id " + tokenEntity.getTokenId() + " is invalid.\n"
                        + "It has invalid serialisation data: " + "Serialisation not recognized.");
            } catch (NotSerializableException e) {
                //NOTICE null
                throw new IllegalTokenException(
                    "The database's token with id " + tokenEntity.getTokenId() + " is invalid.\n"
                        + "It has invalid serialisation data or the class " + tokenClass.getName() //NOTICE null
                        + " is not serializable.");
            } catch (IOException e) {
                throw new IllegalTokenException(
                    "The database's token with id " + tokenEntity.getTokenId() + " is invalid.\n"
                        + "I/O exception during deserialisation:\n" + e);
            } catch (ClassNotFoundException e) {
                throw new IllegalTokenException(
                    "The database's token with id " + tokenEntity.getTokenId() + " is invalid.\n"
                        + "Class " + (tokenClass != null ? tokenClass.getName() : null) //NOTICE null
                        + " cannot be found.");
            } catch (IllegalArgumentException e) {
                throw new IllegalTokenException(
                    "The database's token with id " + tokenEntity.getTokenId() + " is invalid. "
                        + "It has not a valid base 64 coded serialisation.");
            } catch (SecurityException e) {
                throw new IllegalTokenException(
                    "The database's token with id " + tokenEntity.getTokenId() + " is invalid.\n"
                        + "The default constructor of class "
                        + (tokenClass != null ? tokenClass.getName() : null) //NOTICE null
                        + " is not accessible.");
            } finally {
                try {
                    if (netStream != null) {
                        netStream.close();
                    }
                    if (inStream != null) {
                        inStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return tokens;
    }

    /**
     * Returns all net instance ids in the
     * DatabaseRestoreSource's database that
     * had a corresponding open drawing.
     * @return The net instance ids as an array.
     * @throws  SQLException If any SQL exception occurs.
     */
    @Override
    public String[] getViewedNetIDs() throws Exception {
        Vector<Entity> entities =
            Entity.getEntities(new NetInstanceEntity(_connection, _dialect), "DRAWING_OPEN=1");
        String[] netIds = new String[entities.size()];
        for (int i = 0; i < entities.size(); i++) {
            netIds[i] = ((NetInstanceEntity) entities.elementAt(i)).getNetInstanceId().toString();
        }
        return netIds;
    }

    /**
     * Returns if the simulation was inited (not terminated).
     * @return If the simulation was inited.
     * @throws SQLException If any SQL exception occurs.
     */
    @Override
    public boolean wasSimulationInited() 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) {
            // A missing entity is interpreted as a fresh database.
            // This means, of course, that there was no simulation inited.
            return false;
        }

        return stateEntity.getInited() == 1;
    }

    /**
     * Returns if the simulation was running.
     * @return If the simulation was running.
     * @throws  SQLException If any SQL exception occurs.
     */
    @Override
    public boolean wasSimulationRunning() 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) {
            // A missing entity is interpreted as a fresh database.
            // This means, of course, that there was no simulation running.
            return false;
        }

        return stateEntity.getRunning() == 1;
    }
}