package gov.va.med.cds.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;

import org.dom4j.DocumentFactory;
import org.dom4j.Element;

@SuppressWarnings("serial")
public class QueryCache extends LinkedHashMap<QueryCacheKey, QueryCacheValue> {
	private long cacheHitCount = 0;
	private long cacheMissCount = 0;
	private long maxEntries = QueryCacheManager.DEFAULT_MAX_ENTRIES_PER_DATASOURCE;
	private long maxEntryAgeInSeconds = QueryCacheManager.DEFAULT_MAX_ENTRY_AGE_IN_SECONDS;

	public QueryCache() {
	}

	public synchronized void put(String queryIdentifier, String paramName, Object paramValue, List<Object> results) {
		HashMap<String, Object> queryParams = new HashMap<String, Object>();
		queryParams.put(paramName, paramValue);
		put(queryIdentifier, queryParams, results);
	}

	public synchronized void put(String queryIdentifier, HashMap<String, Object> queryParams, List<Object> results) {
		if (results == null || results.isEmpty())
			return;

		QueryCacheKey key = new QueryCacheKey(queryIdentifier, queryParams);
		put(key, new QueryCacheValue(results));
		// / the idea is to remove the least fresh entries - so we put the most
		// fresh guy in and then remove
		// / this algorithm could be improved...
		if (size() > maxEntries) {
			removeElementsExceedingMaxSize();
		}
	}

	public synchronized List<Object> get(String queryIdentifier, String paramName, Object paramValue) {
		QueryCacheKey key = new QueryCacheKey(queryIdentifier, paramName, paramValue);
		return getWithKey(key);
	}

	public synchronized List<Object> get(String queryIdentifier, HashMap<String, Object> queryParams) {
		QueryCacheKey key = new QueryCacheKey(queryIdentifier, queryParams);
		return getWithKey(key);
	}

	private synchronized List<Object> getWithKey(QueryCacheKey key) {
		QueryCacheValue cachedValue = super.get(key);

		if (cachedValue == null) {
			cacheMissCount++;
			return null;
		}
		if (cachedValue.exceedsMaxAge(maxEntryAgeInSeconds)) {
			cachedValue = null;
			remove(key); // / remove the entry from the cache since the data it
							// contains is too old (exceeds max age)
			cacheMissCount++;
			return null;
		}
		cacheHitCount++;

		// / these next two lines implement the LRU functionality of the cache -
		// a returned cached value also becomes the most recently used element
		// in the list
		remove(key);
		put(key, cachedValue); // / put the entry into the cache to up its
								// status in the LRU category

		return cachedValue.getCachedValue();
	}

	public long getCacheHitCount() {
		return cacheHitCount;
	}

	public long getCacheMissCount() {
		return cacheMissCount;
	}

	public String toString() {
		return toElement().asXML();
	}

	public Element toElement() {
		double hitPercentage = Math.round(cacheHitCount * 100.0 / (cacheMissCount + cacheHitCount));
		DocumentFactory df = new DocumentFactory();
		Element e = df.createElement(getClass().getSimpleName());
		e.add(df.createAttribute(e, "cacheSize", "" + size()));
		e.add(df.createAttribute(e, "cacheHitCount", "" + cacheHitCount));
		e.add(df.createAttribute(e, "cacheMissCount", "" + cacheMissCount));
		e.add(df.createAttribute(e, "cacheHitPercentage", "" + hitPercentage));
		Element entries = df.createElement("entries");
		e.add(entries);
		for (QueryCacheKey key : keySet()) {
			entries.add(key.toElement());
			entries.add(get(key).toElement());
		}
		return e;
	}

	@Override
	protected synchronized boolean removeEldestEntry(java.util.Map.Entry<QueryCacheKey, QueryCacheValue> eldest) {
		return size() > maxEntries;
	}

	public synchronized long getMaxEntries() {
		return maxEntries;
	}

	public synchronized void setMaxEntries(long maxEntries) {
		this.maxEntries = maxEntries;
		if (maxEntries < size()) {
			removeElementsExceedingMaxSize();
		}
	}

	public synchronized long getMaxEntryAgeInSeconds() {
		return maxEntryAgeInSeconds;
	}

	public synchronized void setMaxEntryAgeInSeconds(long maxEntryAgeInSeconds) {
		this.maxEntryAgeInSeconds = maxEntryAgeInSeconds;
		removeEntriesExceedingMaxEntryAge();
	}

	private synchronized void removeElementsExceedingMaxSize() {
		int currentSize = size();
		int reductionNeeded = currentSize - (int) maxEntries;
		if (reductionNeeded <= 0)
			return;

		ArrayList<QueryCacheKey> keysToRemove = new ArrayList<QueryCacheKey>();
		int counter = 0;
		for (QueryCacheKey key : keySet()) {
			if (counter >= reductionNeeded)
				break;
			keysToRemove.add(key);
			counter++;
		}
		for (QueryCacheKey key : keysToRemove) {
			remove(key);
		}
	}

	public synchronized int removeEntriesExceedingMaxEntryAge() {
		int count = 0;

		ArrayList<QueryCacheKey> keysToRemove = new ArrayList<QueryCacheKey>();
		for (QueryCacheKey key : keySet()) {
			if (key == null)
				continue;
			QueryCacheValue value = get(key);
			if (value == null) {
				keysToRemove.add(key);
				continue;
			}
			if (value.exceedsMaxAge(maxEntryAgeInSeconds))
				keysToRemove.add(key);
		}

		for (QueryCacheKey key : keysToRemove) {
			remove(key);
			count++;
		}
		return count;
	}

}
