/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * JBoss, Home of Professional Open Source
 * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the @authors tag. All rights reserved.
 * See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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, v. 2.1.
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * 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,
 * v.2.1 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 */
package org.hibernate.search.backend.impl.lucene;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.LogByteSizeMergePolicy;
import org.apache.lucene.index.MergeScheduler;
import org.apache.lucene.search.Similarity;

import org.hibernate.search.Environment;
import org.hibernate.search.backend.impl.lucene.overrides.ConcurrentMergeScheduler;
import org.hibernate.search.backend.spi.LuceneIndexingParameters;
import org.hibernate.search.backend.spi.LuceneIndexingParameters.ParameterSet;
import org.hibernate.search.exception.ErrorContext;
import org.hibernate.search.exception.ErrorHandler;
import org.hibernate.search.exception.impl.ErrorContextBuilder;
import org.hibernate.search.indexes.impl.DirectoryBasedIndexManager;
import org.hibernate.search.store.DirectoryProvider;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * @author Sanne Grinovero <sanne@hibernate.org> (C) 2011 Red Hat Inc.
 */
class IndexWriterHolder {
	private static final Log log = LoggerFactory.make();

	/**
	 * This Analyzer is never used in practice: during Add operation it's overridden.
	 * So we don't care for the Version, using whatever Lucene thinks is safer.
	 */
	private static final Analyzer SIMPLE_ANALYZER = new SimpleAnalyzer( Environment.DEFAULT_LUCENE_MATCH_VERSION );

	private final IndexWriterConfig writerConfig = new IndexWriterConfig(
			Environment.DEFAULT_LUCENE_MATCH_VERSION,
			SIMPLE_ANALYZER
	);
	private final ErrorHandler errorHandler;
	private final ParameterSet indexParameters;
	private final DirectoryProvider directoryProvider;
	private final String indexName;

	// variable state:

	/**
	 * Current open IndexWriter, or null when closed.
	 */
	private final AtomicReference<IndexWriter> writer = new AtomicReference<IndexWriter>();

	/**
	 * Protects from multiple initialization attempts of IndexWriter
	 */
	private final ReentrantLock writerInitializationLock = new ReentrantLock();


	IndexWriterHolder(ErrorHandler errorHandler, DirectoryBasedIndexManager indexManager) {
		this.errorHandler = errorHandler;
		this.indexName = indexManager.getIndexName();
		LuceneIndexingParameters luceneParameters = indexManager.getIndexingParameters();
		this.indexParameters = luceneParameters.getIndexParameters();
		this.directoryProvider = indexManager.getDirectoryProvider();
		luceneParameters.applyToWriter( writerConfig );
		Similarity similarity = indexManager.getSimilarity();
		if ( similarity != null ) {
			writerConfig.setSimilarity( similarity );
		}
		writerConfig.setOpenMode( OpenMode.APPEND ); //More efficient to open
		//TODO remove this awful need to set a reference back again to the indexManager:
		indexManager.setIndexWriterConfig( writerConfig );
	}

	/**
	 * Gets the IndexWriter, opening one if needed.
	 *
	 * @param errorContextBuilder might contain some context useful to provide when handling IOExceptions.
	 * Is an optional parameter.
	 * @return a new IndexWriter or one already open.
	 */
	public IndexWriter getIndexWriter(ErrorContextBuilder errorContextBuilder) {
		IndexWriter indexWriter = writer.get();
		if ( indexWriter == null ) {
			writerInitializationLock.lock();
			try {
				indexWriter = writer.get();
				if ( indexWriter == null ) {
					try {
						indexWriter = createNewIndexWriter();
						log.trace( "IndexWriter opened" );
						writer.set( indexWriter );
					}
					catch ( IOException ioe ) {
						indexWriter = null;
						writer.set( null );
						handleIOException( ioe, errorContextBuilder );
					}
				}
			}
			finally {
				writerInitializationLock.unlock();
			}
		}
		return indexWriter;
	}

	public IndexWriter getIndexWriter() {
		return getIndexWriter( null );
	}

	/**
	 * Create as new IndexWriter using the passed in IndexWriterConfig as a template, but still applies some late changes:
	 * we need to override the MergeScheduler to handle background errors, and a new instance needs to be created for each
	 * new IndexWriter.
	 * Also each new IndexWriter needs a new MergePolicy.
	 */
	private IndexWriter createNewIndexWriter() throws IOException {
		LogByteSizeMergePolicy newMergePolicy = indexParameters.getNewMergePolicy(); //TODO make it possible to configure a different policy?
		writerConfig.setMergePolicy( newMergePolicy );
		MergeScheduler mergeScheduler = new ConcurrentMergeScheduler( this.errorHandler, this.indexName );
		writerConfig.setMergeScheduler( mergeScheduler );
		return new IndexWriter( directoryProvider.getDirectory(), writerConfig );
	}

	/**
	 * Commits changes to a previously opened IndexWriter.
	 *
	 * @param errorContextBuilder use it to handle exceptions, as it might contain a reference to the work performed before the commit
	 */
	public void commitIndexWriter(ErrorContextBuilder errorContextBuilder) {
		IndexWriter indexWriter = writer.get();
		if ( indexWriter != null ) {
			try {
				indexWriter.commit();
				log.trace( "Index changes committed." );
			}
			catch ( IOException ioe ) {
				handleIOException( ioe, errorContextBuilder );
			}
		}
	}

	/**
	 * @see #commitIndexWriter(ErrorContextBuilder)
	 */
	public void commitIndexWriter() {
		commitIndexWriter( null );
	}

	/**
	 * Closes a previously opened IndexWriter.
	 */
	public void closeIndexWriter() {
		final IndexWriter toClose = writer.getAndSet( null );
		if ( toClose != null ) {
			try {
				toClose.close();
				log.trace( "IndexWriter closed" );
			}
			catch ( IOException ioe ) {
				forceLockRelease();
				handleIOException( ioe, null );
			}
		}
	}

	/**
	 * Forces release of Directory lock. Should be used only to cleanup as error recovery.
	 */
	public void forceLockRelease() {
		log.forcingReleaseIndexWriterLock();
		writerInitializationLock.lock();
		try {
			try {
				IndexWriter indexWriter = writer.getAndSet( null );
				if ( indexWriter != null ) {
					indexWriter.close();
					log.trace( "IndexWriter closed" );
				}
			}
			finally {
				IndexWriter.unlock( directoryProvider.getDirectory() );
			}
		}
		catch ( IOException ioe ) {
			handleIOException( ioe, null );
		}
		finally {
			writerInitializationLock.unlock();
		}
	}

	/**
	 * Opens an IndexReader having visibility on uncommitted writes from
	 * the IndexWriter, if any writer is open, or null if no IndexWriter is open.
	 * @param applyDeletes Applying deletes is expensive, say no if you can deal with stale hits during queries
	 * @return a new NRT IndexReader if an IndexWriter is available, or <code>null</code> otherwise
	 */
	public IndexReader openNRTIndexReader(boolean applyDeletes) {
		final IndexWriter indexWriter = writer.get();
		try {
			if ( indexWriter != null ) {
				return IndexReader.open( indexWriter, applyDeletes );
			}
			else {
				return null;
			}
		}
		// following exceptions should be propagated as the IndexReader is needed by
		// the main thread
		catch ( CorruptIndexException cie ) {
			throw log.cantOpenCorruptedIndex( cie, indexName );
		}
		catch ( IOException ioe ) {
			throw log.ioExceptionOnIndex( ioe, indexName );
		}
	}

	/**
	 * Opens an IndexReader from the DirectoryProvider (not using the IndexWriter)
	 */
	public IndexReader openDirectoryIndexReader() {
		try {
			return IndexReader.open( directoryProvider.getDirectory() );
		}
		// following exceptions should be propagated as the IndexReader is needed by
		// the main thread
		catch ( CorruptIndexException cie ) {
			throw log.cantOpenCorruptedIndex( cie, indexName );
		}
		catch ( IOException ioe ) {
			throw log.ioExceptionOnIndex( ioe, indexName );
		}
	}

	/**
	 * @param ioe The exception to handle
	 * @param errorContextBuilder Might be used to enqueue useful information about the lost operations, or be null
	 */
	private void handleIOException(IOException ioe, ErrorContextBuilder errorContextBuilder) {
		if ( log.isTraceEnabled() ) {
			log.trace( "going to handle IOException", ioe );
		}
		final ErrorContext errorContext;
		if ( errorContextBuilder != null ) {
			errorContext = errorContextBuilder.errorThatOccurred( ioe ).createErrorContext();
			this.errorHandler.handle( errorContext );
		}
		else {
			errorHandler.handleException( log.ioExceptionOnIndexWriter(), ioe );
		}
	}
}
