package eu.dnetlib.enabling.resultset.push;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.Resource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Required;

import eu.dnetlib.enabling.resultset.push.ResultSetDescriptor.Range;
import eu.dnetlib.miscutils.cache.Cache;
import eu.dnetlib.miscutils.factory.Factory;

/**
 * implement a resultset dao.
 *
 * @author marko
 *
 */
public class CacheTransientResultSetDaoImpl implements TransientPushResultSetDao {

	/**
	 * logger.
	 */
	private static final Log log = LogFactory.getLog(CacheTransientResultSetDaoImpl.class); // NOPMD by marko on 11/24/08 5:02 PM

	/**
	 * range cache. Keys are "rsid-rangenumber".
	 */
	private Cache<String, List<String>> cache;

	/**
	 * resultsets
	 */
	private Cache<String, ResultSetDescriptor> resultSetCache;

	/**
	 * resultset descriptor factory.
	 */
	@Resource
	private Factory<ResultSetDescriptor> resultSetDescriptorFactory;

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.enabling.resultset.push.TransientPushResultSetDao#getSize(java.lang.String)
	 */
	public int getSize(final String key) {
		final ResultSetDescriptor desc = resultSetCache.get(key);
		if (desc == null)
			return 0;
		return (desc.getRanges() - 1) * desc.getRangeLength() + cache.get(key + "-" + desc.getLastRange()).size();
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.enabling.resultset.push.TransientPushResultSetDao#addElements(java.lang.String, java.util.List)
	 */
	public void addElements(final String key, final List<String> elements) {
		// TODO: avoid this, obtain the descriptor range from the RS itself.
		ResultSetDescriptor defaultDescriptor = resultSetDescriptorFactory.newInstance();

		// fast path
		if(elements.size() <= defaultDescriptor.getRangeLength())
			addElementsChunk(key, elements);
		else
			addElementsSplitted(key, elements);
	}

	/**
	 * @param key
	 * @param elements
	 */
	private void addElementsSplitted(String key, List<String> elements) {
		final ResultSetDescriptor defaultDescriptor = resultSetDescriptorFactory.newInstance();

		final int rangeLength = defaultDescriptor.getRangeLength();
		final int lastFrom = (elements.size() / rangeLength) * rangeLength;
		for(int from=0; from <= lastFrom; from += rangeLength) {
			int to = from + rangeLength;
			if (to > elements.size())
				to = elements.size();

			addElementsChunk(key, elements.subList(from, to));
		}
	}

	/**
	 * Fills one chunk of maximum page size elements, see #904.
	 *
	 * @param key rs id
	 * @param elements element
	 */
	protected void addElementsChunk(final String key, final List<String> elements) {
		// TODO: huuuuu, chaos, fix this please!
		synchronized (cache) {

			ResultSetDescriptor desc = resultSetCache.get(key);

			log.debug("got desc: " + desc);
			if (desc == null) {
				desc = resultSetDescriptorFactory.newInstance();
				resultSetCache.put(key, desc);
			}
			log.debug("desc now is desc: " + desc);

			if (elements.size() > desc.getRangeLength())
				throw new IllegalArgumentException("The current implementation of the push resultset doesn't accept pages longer than "
						+ desc.getRangeLength() + ", got: " + elements.size());


			int lastRangeIndex = desc.getLastRange();

			if (lastRangeIndex < 0)
				lastRangeIndex = 0;

			log.debug("last range: " + lastRangeIndex);

			List<String> lastRange = cache.get(key + "-" + lastRangeIndex);
			log.debug("stored last range: " + lastRange);

			if (lastRange == null) {
				lastRange = new ArrayList<String>();
				desc.setRanges(desc.getRanges() + 1);
			}

			log.debug("last range is: " + lastRange);

			final int free = desc.getRangeLength() - lastRange.size();
			log.debug("free: " + free);
			log.debug("desc range length: " + desc.getRangeLength());
			log.debug("last range size: " + lastRange.size());

			int toElements = free;
			if (toElements > elements.size()) {
				toElements = elements.size();
			} else {

				final List<String> nextRange = new ArrayList<String>(elements.subList(free, elements.size()));
				log.debug("next range: " + nextRange);

				cache.put(key + "-" + (lastRangeIndex + 1), nextRange);
				log.debug("next range stored at: " + key + "-" + (lastRangeIndex + 1));
				desc.setRanges(desc.getRanges() + 1);
			}
			lastRange.addAll(elements.subList(0, toElements));

			log.debug("LAST RANGE SIZE: " + lastRange.size() + " range index " + lastRangeIndex);

			log.debug("after add: " + lastRange);

			cache.put(key + "-" + lastRangeIndex, lastRange);
			log.debug("range stored at: " + key + "-" + lastRangeIndex);

			resultSetCache.put(key, desc);
		}
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.enabling.resultset.push.TransientPushResultSetDao#getElements(java.lang.String, int, int)
	 */
	public List<String> getElements(final String key, final int fromPosition, final int toPosition) {
		ResultSetDescriptor desc = resultSetCache.get(key);
		log.debug("got desc: " + desc);

		if (desc == null)
			desc = resultSetDescriptorFactory.newInstance();

		final List<String> res = new ArrayList<String>();

		log.debug("ranges containing " + fromPosition + " to " + toPosition);
		for (final Range range : desc.getRangesContaining(fromPosition, toPosition)) {
			log.debug("reading range " + key + "-" + range.getRange() + " begin: " + range.getBegin() + " end: " + range.getEnd());
			res.addAll(cache.get(key + "-" + range.getRange()).subList(range.getBegin(), range.getEnd()));
		}

		return res;
	}

	public Cache<String, List<String>> getCache() {
		return cache;
	}

	@Required
	public void setCache(final Cache<String, List<String>> cache) {
		this.cache = cache;
	}

	public Cache<String, ResultSetDescriptor> getResultSetCache() {
		return resultSetCache;
	}

	@Required
	public void setResultSetCache(final Cache<String, ResultSetDescriptor> resultSetCache) {
		this.resultSetCache = resultSetCache;
	}

	public Factory<ResultSetDescriptor> getResultSetDescriptorFactory() {
		return resultSetDescriptorFactory;
	}

	public void setResultSetDescriptorFactory(final Factory<ResultSetDescriptor> resultSetDescriptorFactory) {
		this.resultSetDescriptorFactory = resultSetDescriptorFactory;
	}

}
