package experimentdatabase;

import java.io.*;
import java.sql.*;

import org.sqlite.*;
import org.sqlite.SQLiteConfig.JournalMode;
import org.sqlite.SQLiteConfig.SynchronousMode;
import org.sqlite.SQLiteConfig.TempStore;
import org.sqlite.SQLiteConfig.TransactionMode;

public class DatabaseEngine {

	private static final String DRIVER_NAME = "jdbc:sqlite:";

	/**
	 * Database connection object (JDBC).
	 */
	protected final Connection connection;

	/**
	 * Level of current transaction (in other words a size of transaction stack).
	 */
	private int currentTransactionLevel = 0;

	/**
	 * Closed flag.
	 */
	private boolean closed = false;

	public DatabaseEngine(File databaseFile) {
		assert databaseFile != null;
		assert databaseFile.isFile() || !databaseFile.exists();

		try {
			// make sure that driver is loaded
			Class.forName("org.sqlite.JDBC");

			SQLiteConfig config = new SQLiteConfig();
			// disable foreign key support when filling database due to:
			// 1) performance, 
			// 2) ON DELETE clause in foreign key definition, which causes deletions in iterations table when user requires replacement of a tuple in experiment table
			config.enforceForeignKeys(false);
			config.setSynchronous(SynchronousMode.OFF); // optimization: do not force the data to be stored on disk
			config.setTempStore(TempStore.MEMORY);
			config.setTransactionMode(TransactionMode.DEFFERED);
			config.setBusyTimeout(Integer.toString(Integer.MAX_VALUE));
			config.setJournalMode(JournalMode.TRUNCATE);
			config.setPageSize(1 << 15); //32KB

			this.connection = DriverManager.getConnection(
					DRIVER_NAME + databaseFile.getAbsolutePath(),
					config.toProperties());

			this.connection.setAutoCommit(false);

		} catch (Exception ex) {
			throw new DatabaseException("Cannot initialize database.", ex);
		}
	}

	/**
	 * Begins database transaction. The subsequent calls to this method increase the level of transactions stack, i.e.
	 * nested transactions are supported. However the data is guaranteed to be committed to disk, only if the outer most
	 * transaction is committed. The roll backs of outer transactions also roll back the nested transactions.
	 * 
	 * @throws SQLException
	 */
	public void beginTransaction() throws SQLException {
		if (currentTransactionLevel == 0) {
			++currentTransactionLevel;
			return; // do nothing, JDBC will create transaction when needed
		}

		PreparedStatement savepoint = this.connection.prepareStatement("SAVEPOINT sp_" + this.currentTransactionLevel);
		savepoint.execute();
		savepoint.close();

		++currentTransactionLevel; // in case SAVEPOINT failed
	}

	/**
	 * Commits database transaction. If transactions stack contains more than one transaction, then only the top-level
	 * transaction is committed. Call this method again to commit remaining transactions.
	 * 
	 * @throws SQLException
	 */
	public void commit() throws SQLException {
		assert currentTransactionLevel > 0;

		if (currentTransactionLevel == 1) {
			currentTransactionLevel = 0;
			this.connection.commit();
			return;
		}

		PreparedStatement release = this.connection
				.prepareStatement("RELEASE sp_" + (this.currentTransactionLevel - 1));
		release.execute();
		release.close();

		--this.currentTransactionLevel; // in case commit failed
	}

	/**
	 * Roll backs the transaction. If transaction stack contains more than one transaction, then the only the top-level
	 * transactions is rolled back. Call this method again to roll back remaining transactions. Note that rolling back a
	 * transaction also rolls back its nested transactions, that have been already committed.
	 * 
	 * @throws SQLException
	 */
	public void rollback() throws SQLException {
		assert currentTransactionLevel > 0;

		if (currentTransactionLevel == 1) {
			currentTransactionLevel = 0;
			this.connection.rollback();
			return;
		}

		PreparedStatement rollback = this.connection.prepareStatement(
				"ROLLBACK TO SAVEPOINT sp_" + (this.currentTransactionLevel - 1));
		rollback.execute();
		rollback.close();

		--this.currentTransactionLevel; // in case rollback failed
	}

	public PreparedStatement prepareStatement(String statement) throws SQLException {
		return this.connection.prepareStatement(statement);
	}

	public PreparedStatement prepareStatement(String statement, int autoGeneratedKeys) throws SQLException {
		return this.connection.prepareStatement(statement, autoGeneratedKeys);
	}

	public PreparedStatement prepareStatement(String statement, int resultSetType, int resultSetConcurrency)
			throws SQLException {
		return this.connection.prepareStatement(statement, resultSetType, resultSetConcurrency);
	}

	/**
	 * Closes database object. It will become useless after this call. The current transaction is being rolled back.
	 */
	public void close() {
		this.checkClosed();

		try {
			// DO NOT uncomment: do not commit uncommitted data, user has to explicitly call commit
			// if we close the connection during the transaction, the data is probably corrupt.
			//this.connection.commit(); 

			this.connection.close();
			this.closed = true;
		} catch (Exception ex) {
			throw new DatabaseException("An error occurred during closing database", ex);
		}
	}

	@Override
	protected void finalize() {
		if (!this.closed) {
			this.close();
		}
	}

	/**
	 * Checks if the database engine object is closed. If so, then an exception is thrown.
	 * 
	 * @exception DatabaseException
	 */
	private void checkClosed() {
		if (this.closed)
			throw new DatabaseException("Database engine is already closed");
	}
}
