/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2010, Red Hat, Inc. and/or its affiliates or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat, Inc.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.hibernate.search.util.impl;

import java.io.IOException;
import java.io.Serializable;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;

/**
 * Cache following a "Most Recently Used" (MRU) algorithm for maintaining a
 * bounded in-memory size; the "Least Recently Used" (LRU) entry is the first
 * available for removal from the cache.
 * <p/>
 * This implementation uses a "soft limit" to the in-memory size of the cache,
 * meaning that all cache entries are kept within a completely
 * {@link java.lang.ref.SoftReference}-based map with the most recently utilized
 * entries additionally kept in a hard-reference manner to prevent those cache
 * entries soft references from becoming enqueued by the garbage collector. Thus
 * the actual size of this cache impl can actually grow beyond the stated max
 * size bound as long as GC is not actively seeking soft references for
 * enqueuement.
 * <p/>
 * The soft-size is bounded and configurable. This allows controlling memory
 * usage which can grow out of control under some circumstances, especially when
 * very large heaps are in use. Although memory usage per se should not be a
 * problem with soft references, which are cleared when necessary, this can
 * trigger extremely slow stop-the-world GC pauses when nearing full heap usage,
 * even with CMS concurrent GC (i.e. concurrent mode failure). This is most
 * evident when ad-hoc HQL queries are produced by the application, leading to
 * poor soft-cache hit ratios. This can also occur with heavy use of SQL IN
 * clauses, which will generate multiples SQL queries (even if parameterized),
 * one for each collection/array size passed to the IN clause. Many slightly
 * different queries will eventually fill the heap and trigger a full GC to
 * reclaim space, leading to unacceptable pauses in some cases.
 * <p/>
 * <strong>Note:</strong> This class is serializable, however all entries are
 * discarded on serialization.
 *
 * @see org.hibernate.cfg.Environment#QUERY_PLAN_CACHE_MAX_STRONG_REFERENCES
 * @see org.hibernate.cfg.Environment#QUERY_PLAN_CACHE_MAX_SOFT_REFERENCES
 *
 * @author Steve Ebersole
 * @author Manuel Dominguez Sarmiento
 *
 * Copied from Hibernate Core
 */
public class SoftLimitMRUCache implements Serializable {
	/**
	 * The default strong reference count.
	 */
	public static final int DEFAULT_STRONG_REF_COUNT = 128;

	/**
	 * The default soft reference count.
	 */
	public static final int DEFAULT_SOFT_REF_COUNT = 2048;

	private final int strongRefCount;
	private final int softRefCount;

	private transient LRUMap strongRefCache;
	private transient LRUMap softRefCache;
	private transient ReferenceQueue referenceQueue;

	/**
	 * Constructs a cache with the default settings.
	 *
	 * @see #DEFAULT_STRONG_REF_COUNT
	 * @see #DEFAULT_SOFT_REF_COUNT
	 */
	public SoftLimitMRUCache() {
		this( DEFAULT_STRONG_REF_COUNT, DEFAULT_SOFT_REF_COUNT );
	}

	/**
	 * Constructs a cache with the specified settings.
	 *
	 * @param strongRefCount the strong reference count.
	 * @param softRefCount the soft reference count.
	 *
	 * @throws IllegalArgumentException if either of the arguments is less than one, or if the strong
	 * reference count is higher than the soft reference count.
	 */
	public SoftLimitMRUCache(int strongRefCount, int softRefCount) {
		if ( strongRefCount < 1 || softRefCount < 1 ) {
			throw new IllegalArgumentException( "Reference counts must be greater than zero" );
		}
		if ( strongRefCount > softRefCount ) {
			throw new IllegalArgumentException( "Strong reference count cannot exceed soft reference count" );
		}

		this.strongRefCount = strongRefCount;
		this.softRefCount = softRefCount;
		init();
	}

	/**
	 * Gets an object from the cache.
	 *
	 * @param key the cache key.
	 * @return the stored value, or <code>null</code> if no entry exists.
	 */
	public synchronized Object get(Object key) {
		if ( key == null ) {
			throw new NullPointerException( "Key to get cannot be null" );
		}

		clearObsoleteReferences();

		SoftReference ref = (SoftReference) softRefCache.get( key );
		if ( ref != null ) {
			Object refValue = ref.get();
			if ( refValue != null ) {
				// This ensures recently used entries are strongly-reachable
				strongRefCache.put( key, refValue );
				return refValue;
			}
		}

		return null;
	}

	/**
	 * Puts a value in the cache.
	 *
	 * @param key the key.
	 * @param value the value.
	 * @return the previous value stored in the cache, if any.
	 */
	public synchronized Object put(Object key, Object value) {
		if ( key == null || value == null ) {
			throw new NullPointerException(
					getClass().getName() + "does not support null key [" + key + "] or value [" + value + "]"
			);
		}

		clearObsoleteReferences();

		strongRefCache.put( key, value );
		SoftReference ref = (SoftReference) softRefCache.put(
				key,
				new KeyedSoftReference( key, value, referenceQueue )
		);

		return ( ref != null ) ? ref.get() : null;
	}

	/**
	 * Gets the strong reference cache size.
	 *
	 * @return the strong reference cache size.
	 */
	public synchronized int size() {
		clearObsoleteReferences();
		return strongRefCache.size();
	}

	/**
	 * Gets the soft reference cache size.
	 *
	 * @return the soft reference cache size.
	 */
	public synchronized int softSize() {
		clearObsoleteReferences();
		return softRefCache.size();
	}

	/**
	 * Clears the cache.
	 */
	public synchronized void clear() {
		strongRefCache.clear();
		softRefCache.clear();
	}

	private void init() {
		this.strongRefCache = new LRUMap( strongRefCount );
		this.softRefCache = new LRUMap( softRefCount );
		this.referenceQueue = new ReferenceQueue();
	}

	private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
		in.defaultReadObject();
		init();
	}

	private void clearObsoleteReferences() {
		// Clear entries for soft references removed by garbage collector
		KeyedSoftReference obsoleteRef;
		while ( ( obsoleteRef = (KeyedSoftReference) referenceQueue.poll() ) != null ) {
			Object key = obsoleteRef.getKey();
			softRefCache.remove( key );
		}
	}

	private static class KeyedSoftReference extends SoftReference {
		private final Object key;

		@SuppressWarnings({ "unchecked" })
		private KeyedSoftReference(Object key, Object value, ReferenceQueue q) {
			super( value, q );
			this.key = key;
		}

		private Object getKey() {
			return key;
		}
	}
}
