package com.johnlpage.mongosyphon;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;

public class MongoConnection implements IDataSource {
	private String connectionString = null;
	private String user;
	private String pass;
	private Logger logger;
	MongoClient mongoClient = null;
	MongoDatabase db = null;
	MongoCollection<Document> collection = null;
	int columnCount = 0;
	MongoCursor<Document> results = null;
	Map<String, Document> cache = null;
	Boolean incache = false;
	Document cachedRow = null;
	String stmttext = null;
	Document prevRow = null;
	
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see com.johnlpage.mongosyphon.IDataSource#close()
	 */
	public void close() {
		try {
			if (results != null)
				results.close();
			results = null;

		} catch (Exception e) {
			logger.error(e.getMessage());
			System.exit(1);
		}

	}

	public MongoConnection(String connectionString, boolean usecache) {
		logger = LoggerFactory.getLogger(MongoConnection.class);
		this.connectionString = connectionString;
		if (usecache) { // Cannot merge AND cache
			cache = new HashMap<String, Document>();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.johnlpage.mongosyphon.IDataSource#hasResults()
	 */
	public boolean hasResults() {
		return (results != null);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.johnlpage.mongosyphon.IDataSource#Connect(java.lang.String,
	 * java.lang.String)
	 */
	public void Connect(String user, String pass) {
		try {
			logger.info("Connecting to " + connectionString);

			// Authaenticate
			// MongoCredential credential =
			// MongoCredential.createCredential(user,
			// "admin",
			// pass); //Only users on admin as that will be mandatory in 3.6

			mongoClient = new MongoClient(new MongoClientURI(connectionString));

			mongoClient.getDatabase("admin")
					.runCommand(new Document("ping", 1));

		} catch (Exception e) {
			logger.error("Unable to connect to MongoDB");
			logger.error(e.getMessage());
			System.exit(1);
		}
		this.user = user;
		this.pass = pass;
	}

	private Object getFieldFromDocument(Document o, String fieldName) {

		final String[] fieldParts = fieldName.split("\\.");

		int i = 1;
		Object val = o.get(fieldParts[0]);

		while (i < fieldParts.length && val instanceof Document) {
			val = ((Document) val).get(fieldParts[i]);
			i++;
		}

		return val;
	}

	// Inject numbered parameters
	@SuppressWarnings("unchecked")
	private Document ParameteriseDocument(Document d, ArrayList<String> params,
			Document parent) {
		Document rval = new Document();
		for (String key : d.keySet()) {
			Object value = d.get(key);
			// A string may be parameterised
			if (value instanceof String) {
				if (((String) value).startsWith("$")) {
					try {
						// Only $1 to $9 allowed
						if (Character.isDigit(((String) value).charAt(1))) {
							String paramNoString = ((String) value).substring(1,
									2);
							int paramNo = Integer.parseInt(paramNoString);
							value = getFieldFromDocument(parent,
									params.get(paramNo - 1)); // Replace with
																// parent obj
						}
					} catch (Exception e) {
						logger.info(e.getMessage());
					}
				}
			} else if (value instanceof Document) {
				// Recurse down
				value = ParameteriseDocument((Document) value, params, parent);
			} else if (value instanceof ArrayList) {
				ArrayList<Object> newList = new ArrayList<Object>();
				for (Object lv : (ArrayList<Object>) value) {

					if (lv instanceof Document) {
						newList.add(ParameteriseDocument((Document) lv, params,
								parent));
					} else {
						// It's a scalar but we still need it
						Document tmpDoc = new Document("value", "lv");
						newList.add(ParameteriseDocument(tmpDoc, params, parent)
								.get("value"));
					}
				}
				value = newList;
			}
			rval.append(key, value);
		}
		return rval;
	}

	@SuppressWarnings("unchecked")
	public void RunQuery(String mongoql, ArrayList<String> params,
			Document parent) {
		Document parameterised = new Document();
		Document mongoQLDoc = new Document();
		Document find = null;
		ArrayList<Document> aggregate = null;
		Document projection = null;
		Document sort = null;
		Integer limit = new Integer(0);
		try {
			mongoQLDoc = Document.parse(mongoql);
			find = mongoQLDoc.get("find", Document.class);
			try {
				aggregate = mongoQLDoc.get("aggregate", ArrayList.class);
			} catch (Exception e) {
				logger.error("Aggregation pipeline not specified correctly as an Array");
				logger.error(e.getMessage());
				System.exit(1);
			}
			if (find == null && aggregate == null) {
				// Get all
				find = new Document();
			}

			projection = mongoQLDoc.get("project", Document.class);
			sort = mongoQLDoc.get("sort", Document.class);
			limit = mongoQLDoc.get("limit", Integer.class);

			if (aggregate != null) {
				if (find != null || sort != null || projection != null || limit != null) {
					logger.error(
							"You cannot specify find, sort or project with aggreagte");
					System.exit(1);
				}
			}

			// The only thing we are letting them parameterise is a Match at the
			// start
			if (aggregate != null) {
				Document firstMatch = aggregate.get(0);
				if (firstMatch != null) {
					Document matchDoc = firstMatch.get("$match",
							Document.class);
					if (matchDoc == null) {
						logger.error(
								"An aggregation must start with a $match");
						System.exit(1);
					}
					parameterised = ParameteriseDocument(matchDoc, params,
							parent);
					firstMatch.put("$match", parameterised);
				} else {
					logger.error(
							"You cannot specify an empty aggregation pipeline");
					System.exit(1);
				}
			} else {
				// Parameterise the find
				parameterised = ParameteriseDocument(find, params, parent);
			}
		} catch (Exception e) {
			logger.error(e.getMessage());
			System.exit(1);
		}

		// If we have this cached simply return it
		// In theory we should just use a representation of the params
		stmttext = parameterised.toString();
		if (cache != null) {
			if (cache.containsKey(stmttext)) {
				results = null;
				incache = true;
				cachedRow = cache.get(stmttext);
				return;// Don't
			}
		}
		incache = false;
		logger.info("executing " + stmttext);

		String databaseName = mongoQLDoc.getString("database");
		String collectionName = mongoQLDoc.getString("collection");
		MongoCollection<Document> collection = mongoClient
				.getDatabase(databaseName).getCollection(collectionName);

		if (find != null) {
			FindIterable<Document> fi = collection.find(parameterised);
			if (projection != null) {
				fi = fi.projection(projection);
			}
			if (sort != null) {
				fi = fi.sort(sort);
			}
			if (limit != null) {
				fi = fi.limit(limit);
			}
			results = fi.iterator();
		} else {
			// Aggregation
			results = collection.aggregate(aggregate).iterator();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.johnlpage.mongosyphon.IDataSource#PushBackRow(org.bson.Document)
	 */
	public void PushBackRow(Document row) {
		prevRow = row;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.johnlpage.mongosyphon.IDataSource#GetNextRow()
	 */
	public Document GetNextRow() throws Exception {

		// Handle a rewind
		if (prevRow != null) {
			Document r = prevRow;
			prevRow = null;
			return r;
		}

		if (incache == true) {
			return cachedRow;
		}
		if (results == null) {
			return null;
		}
		if (results.hasNext() == false) {
			
			return null;
		}

		Document row = results.next();
		
		if (cache != null && stmttext != null) {
			cache.put(stmttext, row);
		}
		return row;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.johnlpage.mongosyphon.IDataSource#getConnectionString()
	 */
	public String getConnectionString() {
		return connectionString;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.johnlpage.mongosyphon.IDataSource#getUser()
	 */
	public String getUser() {
		return user;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.johnlpage.mongosyphon.IDataSource#getPass()
	 */
	public String getPass() {
		return pass;
	}

	public String getType() {
		return "MONGO";
	}

}