package org.eclipse.hawk.sqlite;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;

import org.eclipse.hawk.core.graph.IGraphIterable;
import org.eclipse.hawk.core.graph.IGraphNode;
import org.eclipse.hawk.core.graph.IGraphNodeIndex;
import org.eclipse.hawk.sqlite.SQLiteDatabase.SQLiteConnection;
import org.eclipse.hawk.sqlite.iteration.StatementGraphNodeIterable;
import org.eclipse.hawk.sqlite.iteration.StatementSupplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SQLiteNodeIndex implements IGraphNodeIndex {

	static final Logger LOGGER = LoggerFactory.getLogger(SQLiteNodeIndex.class);
	private final SQLiteDatabase db;
	private final String name;

	public SQLiteNodeIndex(SQLiteDatabase db, String name) {
		this.db = db;
		this.name = name;
	}

	@Override
	public String getName() {
		return name;
	}

	@Override
	public IGraphIterable<? extends IGraphNode> query(String key, Object valueOrPattern) {
		if (valueOrPattern instanceof String && ((String)valueOrPattern).contains("*")) {
			final String pattern = (String)valueOrPattern;

			if("*".equals(key) && "*".equals(pattern)) {
				return new StatementGraphNodeIterable(db,
					() -> db.getConnection().getQueryIndexValueAllPairsStatement(name),
					() -> db.getConnection().getQueryIndexValueAllPairsCountStatement(name),
					() -> db.getConnection().getQueryIndexValueAllPairsSingleStatement(name)
				);
			} else if ("*".equals(pattern)) {
				return new StatementGraphNodeIterable(db,
					() -> db.getConnection().getQueryIndexValueAllValuesStatement(name, key),
					() -> db.getConnection().getQueryIndexValueAllValuesCountStatement(name, key),
					() -> db.getConnection().getQueryIndexValueAllValuesSingleStatement(name, key)
				);
			} else {
				return new StatementGraphNodeIterable(db,
					() -> db.getConnection().getQueryIndexValuePatternStatement(name, key, pattern),
					() -> db.getConnection().getQueryIndexValuePatternCountStatement(name, key, pattern),
					() -> db.getConnection().getQueryIndexValuePatternSingleStatement(name, key, pattern)
				);
			}
		}

		// Exact value
		return get(key, valueOrPattern);
	}

	@Override
	public IGraphIterable<? extends IGraphNode> query(String key, Number from, Number to, boolean fromInclusive, boolean toInclusive) {
		return new StatementGraphNodeIterable(db,
			() -> db.getConnection().getQueryIndexNumberRangeStatement(name, key, from, to, fromInclusive, toInclusive),
			() -> db.getConnection().getQueryIndexNumberRangeCountStatement(name, key, from, to, fromInclusive, toInclusive),
			() -> db.getConnection().getQueryIndexNumberRangeSingleStatement(name, key, from, to, fromInclusive, toInclusive)
		);
	}

	@Override
	public IGraphIterable<? extends IGraphNode> get(String key, Object exactValue) {
		return new StatementGraphNodeIterable(db,
			() -> db.getConnection().getQueryIndexValueExactStatement(name, key, exactValue),
			() -> db.getConnection().getQueryIndexValueExactCountStatement(name, key, exactValue),
			() -> db.getConnection().getQueryIndexValueExactSingleStatement(name, key, exactValue)
		);
	}

	@Override
	public void add(IGraphNode n, String key, Object value) {
		int rowCount = executeUpdate(() -> db.getConnection().getAddNodeIndexEntryStatement(name, (int) n.getId(), key, value));
		assert rowCount == 1 : "One row should be inserted when adding an index entry";
	}

	@Override
	public void remove(IGraphNode n) {
		executeUpdate(() -> db.getConnection().getRemoveNodeFromIndexStatement(name, (int) n.getId()));
	}

	@Override
	public void remove(IGraphNode n, String key, Object value) {
		if (key == null) {
			if (value == null) {
				remove(n);
			} else {
				executeUpdate(() -> db.getConnection().getRemoveNodeValueFromIndexStatement(name, (int) n.getId(), value));
			}
		} else if (value == null) {
			executeUpdate(() ->db.getConnection().getRemoveNodeFieldFromIndexStatement(name, (int) n.getId(), key));
		}

		executeUpdate(() -> db.getConnection().getRemoveIndexEntryStatement(name, (int) n.getId(), key, value));
	}

	@Override
	public void flush() {
		// nothing to do
	}

	@Override
	public void delete() {
		try {
			final SQLiteConnection conn = db.getConnection();

			PreparedStatement stmt = conn.getDeleteNodeIndexStatement(name);
			int rowCount = stmt.executeUpdate();
			assert rowCount == 1 : "A row should have been deleted when deleting a node index";

			try (Statement dropStmt = conn.createStatement()) {
				dropStmt.execute(conn.getClearNodeIndexTableSQL(name));
				conn.dropIndexTableOnCommit(name);
			}
		} catch (SQLException e) {
			LOGGER.error(e.getMessage(), e);
		}
	}

	private int executeUpdate(StatementSupplier stmt) {
		try {
			return stmt.get().executeUpdate();
		} catch (SQLException e) {
			LOGGER.error(e.getMessage(), e);
		}
		return 0;
	}
}
