package org.eclipse.hawk.sqlite;

import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;

import org.eclipse.hawk.core.graph.IGraphDatabase;
import org.eclipse.hawk.core.graph.IGraphEdge;
import org.eclipse.hawk.core.graph.IGraphNode;
import org.eclipse.hawk.sqlite.SQLiteDatabase.SQLiteConnection;
import org.eclipse.hawk.sqlite.iteration.StatementIterable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SQLiteNode implements IGraphNode {

	private static final Logger LOGGER = LoggerFactory.getLogger(SQLiteNode.class);
	private final SQLiteDatabase db;
	private final int id;

	public SQLiteNode(SQLiteDatabase db, int nodeId) {
		this.db = db;
		this.id = nodeId;
	}

	@Override
	public Integer getId() {
		return id;
	}

	@Override
	public Set<String> getPropertyKeys() {
		try {
			PreparedStatement stmt = db.getConnection().getNodePropKeysStatement(id);
			stmt.execute();
			return db.getConnection().getStrings(stmt);
		} catch (SQLException ex) {
			LOGGER.error(ex.getMessage(), ex);
			return Collections.emptySet();
		}
	}

	@Override
	public Object getProperty(String name) {
		try {
			final SQLiteConnection conn = db.getConnection();
			PreparedStatement stmt = conn.getNodePropValueStatement();
			stmt.setInt(1, id);
			stmt.setString(2, name);
			stmt.execute();

			return conn.getPropertyValue(stmt);
		} catch (SQLException|IOException|ClassNotFoundException e) {
			LOGGER.error(e.getMessage(), e);
		}
		
		return null;
	}

	@Override
	public void setProperty(String name, Object value) {
		if (value == null) {
			removeProperty(name);
		} else {
			try {
				final SQLiteConnection conn = db.getConnection();
				value = conn.preprocessPropertyValue(value);
				PreparedStatement stmt = conn.getUpsertNodePropStatement(id, name, value);
				int rows = stmt.executeUpdate();
				assert rows == 1 : "One row should have been inserted or updated";
			} catch (SQLException|IOException ex) {
				LOGGER.error(ex.getMessage(), ex);
			}
		}
	}

	@Override
	public Iterable<IGraphEdge> getEdges() {
		return new StatementIterable<>(
			() -> db.getConnection().getEdgesStatement(id),
			(rs) -> {
				final int edgeId = rs.getInt(1);
				final int startId = rs.getInt(2);
				final int endId = rs.getInt(3);
				final String type = rs.getString(4);
				return new SQLiteEdge(db, edgeId, startId, endId, type);
			}
		);
	}

	@Override
	public Iterable<IGraphEdge> getEdgesWithType(String type) {
		return new StatementIterable<>(
			() -> db.getConnection().getEdgesByTypeStatement(id, type),
			(rs) -> {
				final int edgeId = rs.getInt(1);
				final int startId = rs.getInt(2);
				final int endId = rs.getInt(3);
				return new SQLiteEdge(db, edgeId, startId, endId, type);
			}
		);
	}

	@Override
	public Iterable<IGraphEdge> getOutgoingWithType(String type) {
		return new StatementIterable<>(
			() -> db.getConnection().getOutgoingEdgesWithTypeStatement(id, type),
			(rs) -> {
				final int edgeId = rs.getInt(1);
				final int endId = rs.getInt(2);
				return new SQLiteEdge(db, edgeId, id, endId, type);
			});
	}

	@Override
	public Iterable<IGraphEdge> getIncomingWithType(String type) {
		return new StatementIterable<>(
			() -> db.getConnection().getIncomingEdgesWithTypeStatement(id, type),
			(rs) -> {
				final int edgeId = rs.getInt(1);
				final int startId = rs.getInt(2);
				return new SQLiteEdge(db, edgeId, startId, id, type);
			});
	}

	@Override
	public Iterable<IGraphEdge> getIncoming() {
		return new StatementIterable<>(
			() -> db.getConnection().getIncomingEdgesStatement(id),
			(rs) -> {
				final int edgeId = rs.getInt(1);
				final int startId = rs.getInt(2);
				final String type = rs.getString(3);
				return new SQLiteEdge(db, edgeId, startId, id, type);
			}
		);
	}

	@Override
	public Iterable<IGraphEdge> getOutgoing() {
		return new StatementIterable<>(
			() -> db.getConnection().getOutgoingEdgesStatement(id),
			(rs) -> {
				final int edgeId = rs.getInt(1);
				final int endId = rs.getInt(2);
				final String type = rs.getString(3);
				return new SQLiteEdge(db, edgeId, id, endId, type);
			}
		);
	}

	@Override
	public void delete() {
		try {
			final SQLiteConnection conn = db.getConnection();
			conn.getDeleteNodePropsStatement(id).executeUpdate();
			conn.getDeleteEdgePropsForNodeStatement(id).executeUpdate();
			conn.getDeleteEdgesForNodeStatement(id).executeUpdate();
			conn.getDeleteNodeStatement(id).executeUpdate();

			for (String idxName : db.getNodeIndexNames()) {
				db.getOrCreateNodeIndex(idxName).remove(this);
			}
		} catch (SQLException ex) {
			LOGGER.error(ex.getMessage(), ex);
		}
	}

	@Override
	public IGraphDatabase getGraph() {
		return db;
	}

	@Override
	public void removeProperty(String name) {
		try {
			PreparedStatement stmt = db.getConnection().getDeleteNodePropStatement(id, name);
			stmt.executeUpdate();
		} catch (SQLException ex) {
			LOGGER.error(ex.getMessage(), ex);
		}
	}

	@Override
	public int hashCode() {
		return Objects.hash(id);
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		SQLiteNode other = (SQLiteNode) obj;
		return id == other.id;
	}

	@Override
	public String toString() {
		return String.format("SQLiteNode [id=%s]", id);
	}
}
