/*
 * 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.backend.impl.lucene;

import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.IndexWriter;

import org.hibernate.search.cfg.spi.IdUniquenessResolver;
import org.hibernate.search.engine.service.spi.ServiceManager;
import org.hibernate.search.engine.spi.DocumentBuilderIndexedEntity;
import org.hibernate.search.exception.impl.ErrorContextBuilder;
import org.hibernate.search.indexes.impl.PropertiesParseHelper;
import org.hibernate.search.indexes.spi.DirectoryBasedIndexManager;
import org.hibernate.search.spi.IndexedTypeIdentifier;
import org.hibernate.search.spi.IndexedTypeSet;
import org.hibernate.search.spi.WorkerBuildContext;
import org.hibernate.search.store.Workspace;
import org.hibernate.search.store.optimization.OptimizerStrategy;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * Lucene workspace for an IndexManager
 *
 * @author Emmanuel Bernard
 * @author Hardy Ferentschik
 * @author Sanne Grinovero
 */
public abstract class AbstractWorkspaceImpl implements Workspace {

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

	private final OptimizerStrategy optimizerStrategy;
	private final DirectoryBasedIndexManager indexManager;
	private final ServiceManager serviceManager;

	protected final IndexWriterHolder writerHolder;
	private final boolean deleteByTermEnforced;
	private boolean indexMetadataIsComplete;

	/**
	 * Keeps a count of modification operations done on the index.
	 */
	private final AtomicLong operations = new AtomicLong( 0L );

	public AbstractWorkspaceImpl(DirectoryBasedIndexManager indexManager, WorkerBuildContext context, Properties cfg) {
		this.indexManager = indexManager;
		this.optimizerStrategy = indexManager.getOptimizerStrategy();
		this.writerHolder = new IndexWriterHolder( context.getErrorHandler(), indexManager );
		this.indexMetadataIsComplete = PropertiesParseHelper.isIndexMetadataComplete( cfg, context );
		this.deleteByTermEnforced = context.isDeleteByTermEnforced();
		this.serviceManager = context.getServiceManager();
	}

	@Override
	public DocumentBuilderIndexedEntity getDocumentBuilder(IndexedTypeIdentifier type) {
		return indexManager.getIndexBinding( type ).getDocumentBuilder();
	}

	@Override
	public Analyzer getAnalyzer(String name) {
		return indexManager.getAnalyzer( name );
	}

	@Override
	public void optimizerPhase() {
		optimizerStrategy.addOperationWithinTransactionCount( operations.getAndSet( 0L ) );
		optimizerStrategy.optimize( this );
	}

	@Override
	public void performOptimization(IndexWriter writer) {
		optimizerStrategy.performOptimization( writer );
	}

	protected void incrementModificationCounter() {
		operations.addAndGet( 1 );
	}

	@Override
	public IndexedTypeSet getEntitiesInIndexManager() {
		// Do not cache it as an IndexManager receiving a new type should return an updated list
		// and will trigger a LuceneBackendResources rebuild and by side effect
		// a new LuceneWorkVisitor which will need the new list
		return indexManager.getContainedTypes();
	}

	@Override
	public void afterTransactionApplied(boolean someFailureHappened, boolean streaming) {
		getCommitPolicy().onChangeSetApplied( someFailureHappened, streaming );
	}

	public void shutDownNow() {
		getCommitPolicy().onClose();
		log.shuttingDownBackend( indexManager.getIndexName() );
		closeIndexWriter();
	}

	public void closeIndexWriter() {
		log.closingIndexWriter( indexManager.getIndexName() );
		writerHolder.closeIndexWriter();
	}

	@Override
	public IndexWriter getIndexWriter() {
		return writerHolder.getIndexWriter();
	}

	public IndexWriter getIndexWriter(ErrorContextBuilder errorContextBuilder) {
		return writerHolder.getIndexWriter( errorContextBuilder );
	}

	@Override
	public boolean areSingleTermDeletesSafe() {
		return indexMetadataIsComplete && getEntitiesInIndexManager().size() == 1;
	}

	@Override
	public boolean isDeleteByTermEnforced() {
		// if artificially forced: go ahead
		if ( deleteByTermEnforced ) {
			return true;
		}
		// Optimize only if we have all the metadata
		if ( indexMetadataIsComplete ) {
			IndexedTypeSet entitiesInvolved = getEntitiesInIndexManager();
			// a single entity is always safe
			if ( entitiesInvolved.size() == 1 ) {
				return true;
			}
			// ask the source of data for some extra info to be sure it's safe
			IdUniquenessResolver idUniquenessResolver = this.serviceManager.requestService( IdUniquenessResolver.class );
			try {
				// Check all tuple of entities with one another with the following rules:
				//
				// if they use JPA ids, the range of ids is shared for both entity types
				// i.e. two instances of either types are unique if and only if they have the same id value

				// Note that we cannot apply this alternative following rule:
				// "The id field name of one is not used as field name of the second"
				// because we don't know for sure all the fields involved as FiedBridges can do a lot of things
				// behind our back
				// Once we have some new metadata, we can revisit
				for ( IndexedTypeIdentifier firstEntity : entitiesInvolved ) {
					boolean firstEntityIsUsingJPAId =
							indexManager.getIndexBinding( firstEntity )
									.getDocumentBuilder()
									.getTypeMetadata()
									.isJpaIdUsedAsDocumentId();
					boolean followingEntities = false;
					for ( IndexedTypeIdentifier secondEntity : entitiesInvolved ) {
						// Skip all entities already processed and the same entity
						if ( firstEntity.equals( secondEntity ) ) {
							followingEntities = true;
						}
						else if ( followingEntities ) {
							//core of the validation rules
							boolean secondEntityIsUsingJPAId =
									indexManager.getIndexBinding( secondEntity )
											.getDocumentBuilder()
											.getTypeMetadata()
											.isJpaIdUsedAsDocumentId();
							// both use JPA id and they share the same id uniqueness set
							// the boolean evaluation in important: only call areIdsUniqueForClasses if absolutely necessary
							boolean uniqueIdEqualityMeansEntityEquality =
									firstEntityIsUsingJPAId && secondEntityIsUsingJPAId &&
									idUniquenessResolver.areIdsUniqueForClasses( firstEntity, secondEntity );
							if ( !uniqueIdEqualityMeansEntityEquality ) {
								return false;
							}
						}
					}
				}
			}
			finally {
				this.serviceManager.releaseService( IdUniquenessResolver.class );
			}
			return true;
		}
		else {
			return false;
		}
	}

	@Override
	public void flush() {
		getCommitPolicy().onFlush();
	}

	@Override
	public String getIndexName() {
		return this.indexManager.getIndexName();
	}

	public IndexWriterDelegate getIndexWriterDelegate(ErrorContextBuilder errorContextBuilder) {
		IndexWriter indexWriter = getIndexWriter( errorContextBuilder );
		//This to respect the existing semantics of returning null on failure of IW opening
		if ( indexWriter != null ) {
			return new IndexWriterDelegate( indexWriter );
		}
		else {
			return null;
		}
	}

	public IndexWriterDelegate getIndexWriterDelegate() {
		IndexWriter indexWriter = getIndexWriter();
		//This to respect the existing semantics of returning null on failure of IW opening
		if ( indexWriter != null ) {
			return new IndexWriterDelegate( indexWriter );
		}
		else {
			return null;
		}
	}

}
