/*
 * Hibernate Search, full-text search for your domain model
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.search.indexes.impl;

import java.io.IOException;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.store.Directory;
import org.hibernate.search.exception.AssertionFailure;
import org.hibernate.search.exception.SearchException;
import org.hibernate.search.indexes.spi.DirectoryBasedIndexManager;
import org.hibernate.search.indexes.spi.DirectoryBasedReaderProvider;
import org.hibernate.search.store.DirectoryProvider;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * This <code>ReaderProvider</code> shares IndexReaders as long as they are "current";
 * It uses IndexReader.reopen() which should improve performance on larger indexes
 * as it shares buffers with previous IndexReader generation for the segments which didn't change.
 *
 * @author Sanne Grinovero (C) 2011 Red Hat Inc.
 */
public class SharingBufferReaderProvider implements DirectoryBasedReaderProvider {

	private static final Log log = LoggerFactory.make();

	/**
	 * contains all Readers (most current per Directory and all unclosed old readers)
	 */
	protected final Map<IndexReader, ReaderUsagePair> allReaders = new ConcurrentHashMap<IndexReader, ReaderUsagePair>();

	/**
	 * contains last updated Reader; protected by lockOnOpenLatest (in the values)
	 */
	protected final Map<Directory, PerDirectoryLatestReader> currentReaders = new ConcurrentHashMap<Directory, PerDirectoryLatestReader>();

	/**
	 * Each actual IndexReader refresh will change this value. To what value exactly doesn't matter,
	 * as long as it's not a value recently used, so we don't care for overflow conditions.
	 * When a client needs to check for index freshness a lock is acquired to protect from
	 * too many checks (which result in IO operations); when it is actually able to acquire
	 * this lock it should check if the refreshOperationId changed: if so, a refresh can
	 * be skipped and we release the lock quickly as the IndexReader was then for
	 * sure updated in the time interval between this client arriving to request a reader
	 * and actually being able to get one.
	 */
	private volatile int refreshOperationId = 0;

	private DirectoryProvider directoryProvider;
	private String indexName;

	@Override
	public DirectoryReader openIndexReader() {
		log.tracef( "Opening IndexReader for directoryProvider %s", indexName );
		Directory directory = directoryProvider.getDirectory();
		PerDirectoryLatestReader directoryLatestReader = currentReaders.get( directory );
		// might eg happen for FSSlaveDirectoryProvider or for mutable SearchFactory
		if ( directoryLatestReader == null ) {
			directoryLatestReader = createReader( directory );
		}
		return directoryLatestReader.refreshAndGet();
	}

	@Override
	public void closeIndexReader(IndexReader reader) {
		if ( reader == null ) {
			return;
		}
		log.tracef( "Closing IndexReader: %s", reader );
		ReaderUsagePair container = allReaders.get( reader );
		container.close(); //virtual
	}

	@Override
	public void initialize(DirectoryBasedIndexManager indexManager, Properties props) {
		this.directoryProvider = indexManager.getDirectoryProvider();
		this.indexName = indexManager.getIndexName();
		// Initialize at least one, don't forget directoryProvider might return different Directory later
		createReader( directoryProvider.getDirectory() );
	}

	/**
	 * Thread safe creation of <code>PerDirectoryLatestReader</code>.
	 *
	 * @param directory The Lucene directory for which to create the reader.
	 * @return either the cached instance for the specified <code>Directory</code> or a newly created one.
	 * @see <a href="http://opensource.atlassian.com/projects/hibernate/browse/HSEARCH-250">HSEARCH-250</a>
	 */
	private synchronized PerDirectoryLatestReader createReader(Directory directory) {
		PerDirectoryLatestReader reader = currentReaders.get( directory );
		if ( reader != null ) {
			return reader;
		}

		try {
			reader = new PerDirectoryLatestReader( directory );
			currentReaders.put( directory, reader );
			return reader;
		}
		catch (IOException e) {
			throw new SearchException( "Unable to open Lucene IndexReader for IndexManager " + this.indexName, e );
		}
	}

	@Override
	public void stop() {
		for ( IndexReader reader : allReaders.keySet() ) {
			ReaderUsagePair usage = allReaders.get( reader );
			usage.close();
		}

		if ( allReaders.size() != 0 ) {
			log.readersNotProperlyClosedInReaderProvider();
		}
	}

	//overridable method for testability:
	protected DirectoryReader readerFactory(final Directory directory) throws IOException {
		return DirectoryReader.open( directory );
	}

	/**
	 * Container for the couple IndexReader,UsageCounter.
	 */
	protected final class ReaderUsagePair {

		public final DirectoryReader reader;
		/**
		 * When reaching 0 (always test on change) the reader should be really
		 * closed and then discarded.
		 * Starts at 2 because:
		 * first usage token is artificial: means "current" is not to be closed (+1)
		 * additionally when creating it will be used (+1)
		 */
		protected final AtomicInteger usageCounter = new AtomicInteger( 2 );

		ReaderUsagePair(DirectoryReader r) {
			reader = r;
		}

		/**
		 * Closes the <code>IndexReader</code> if no other resource is using it
		 * in which case the reference to this container will also be removed.
		 */
		public void close() {
			int refCount = usageCounter.decrementAndGet();
			if ( refCount == 0 ) {
				//TODO I've been experimenting with the idea of an async-close: didn't appear to have an interesting benefit,
				//so discarded the code. should try with bigger indexes to see if the effect gets more impressive.
				ReaderUsagePair removed = allReaders.remove( reader );//remove ourself
				try {
					reader.close();
				}
				catch (IOException e) {
					log.unableToCloseLuceneIndexReader( e );
				}
				assert removed != null;
			}
			else if ( refCount < 0 ) {
				//doesn't happen with current code, could help spotting future bugs?
				throw new AssertionFailure(
						"Closing an IndexReader for which you didn't own a lock-token, or somebody else which didn't own closed already."
				);
			}
		}

		@Override
		public String toString() {
			return "Reader:" + this.hashCode() + " ref.count=" + usageCounter.get();
		}

	}

	/**
	 * An instance for each DirectoryProvider,
	 * establishing the association between "current" ReaderUsagePair
	 * for a DirectoryProvider and it's lock.
	 */
	protected final class PerDirectoryLatestReader {

		/**
		 * Reference to the most current IndexReader for a DirectoryProvider;
		 * guarded by lockOnReplaceCurrent;
		 */
		public ReaderUsagePair current; //guarded by lockOnReplaceCurrent
		private final Lock lockOnReplaceCurrent = new ReentrantLock();

		/**
		 * @param directory The <code>Directory</code> for which we manage the <code>IndexReader</code>.
		 *
		 * @throws IOException when the index initialization fails.
		 */
		public PerDirectoryLatestReader(Directory directory) throws IOException {
			DirectoryReader reader = readerFactory( directory );
			ReaderUsagePair initialPair = new ReaderUsagePair( reader );
			initialPair.usageCounter.set( 1 ); //a token to mark as active (preventing real close).
			lockOnReplaceCurrent.lock(); //no harm, just ensuring safe publishing.
			current = initialPair;
			lockOnReplaceCurrent.unlock();
			allReaders.put( reader, initialPair );
		}

		/**
		 * Gets an updated IndexReader for the current Directory;
		 * the index status will be checked.
		 *
		 * @return the current IndexReader if it's in sync with underlying index, a new one otherwise.
		 */
		public DirectoryReader refreshAndGet() {
			final DirectoryReader updatedReader;
			//it's important that we read this volatile before acquiring the lock:
			final int preAcquireVersionId = refreshOperationId;
			ReaderUsagePair toCloseReaderPair = null;
			lockOnReplaceCurrent.lock();
			final DirectoryReader beforeUpdateReader = current.reader;
			try {
				if ( refreshOperationId != preAcquireVersionId ) {
					// We can take a good shortcut
					current.usageCounter.incrementAndGet();
					return beforeUpdateReader;
				}
				else {
					try {
						//Guarded by the lockOnReplaceCurrent of current IndexReader
						//technically the final value doesn't even matter, as long as we change it
						refreshOperationId++;
						updatedReader = DirectoryReader.openIfChanged( beforeUpdateReader );
					}
					catch (IOException e) {
						throw new SearchException( "Unable to reopen IndexReader", e );
					}
				}
				if ( updatedReader == null ) {
					current.usageCounter.incrementAndGet();
					return beforeUpdateReader;
				}
				else {
					ReaderUsagePair newPair = new ReaderUsagePair( updatedReader );
					//no need to increment usageCounter in newPair, as it is constructed with correct number 2.
					assert newPair.usageCounter.get() == 2;
					toCloseReaderPair = current;
					current = newPair;
					allReaders.put( updatedReader, newPair );//unfortunately still needs lock
				}
			}
			finally {
				lockOnReplaceCurrent.unlock();
			}
			// doesn't need lock:
			if ( toCloseReaderPair != null ) {
				toCloseReaderPair.close();// release a token as it's not the current any more.
			}
			return updatedReader;
		}
	}

}
