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

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.MultiReader;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.uninverting.UninvertingReader;
import org.apache.lucene.uninverting.UninvertingReader.Type;
import org.hibernate.search.indexes.spi.IndexManager;
import org.hibernate.search.indexes.spi.ReaderProvider;
import org.hibernate.search.query.engine.impl.SortConfigurations;
import org.hibernate.search.query.engine.impl.SortConfigurations.SortConfiguration;
import org.hibernate.search.spi.IndexedTypeIdentifier;
import org.hibernate.search.util.StringHelper;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * Wraps a MultiReader to keep references to owning managers.
 *
 * @author Emmanuel Bernard
 * @author Sanne Grinovero (C) 2011 Red Hat Inc.
 */
public class ManagedMultiReader extends MultiReader {

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

	/**
	 * The index readers to be closed in {@link #doClose()}. Will be the readers originally given upon instantiation in
	 * case it was required to wrap those with {@link UninvertingReader}s for the purposes of sorting prior to passing
	 * them to super.
	 */
	final IndexReader[] readersForClosing;
	final ReaderProvider[] readerProviders;


	private ManagedMultiReader(IndexReader[] subReaders, IndexReader[] readersForClosing, ReaderProvider[] readerProviders) throws IOException {
		// If this flag isn't set to true, the MultiReader will increase the usage counter!
		super( subReaders, true );
		assert readersForClosing.length == readerProviders.length;

		this.readersForClosing = readersForClosing;
		this.readerProviders = readerProviders;
	}

	static ManagedMultiReader createInstance(IndexManager[] indexManagers, SortConfigurations configuredSorts, Sort sort, boolean indexUninvertingAllowed) throws IOException {
		final int length = indexManagers.length;

		IndexReader[] subReaders = new IndexReader[length];
		ReaderProvider[] readerProviders = new ReaderProvider[length];
		for ( int index = 0; index < length; index++ ) {
			ReaderProvider indexReaderManager = indexManagers[index].getReaderProvider();
			IndexReader openIndexReader = indexReaderManager.openIndexReader();
			subReaders[index] = openIndexReader;
			readerProviders[index] = indexReaderManager;
		}

		IndexReader[] effectiveReaders = getEffectiveReaders( indexManagers, subReaders, configuredSorts, sort, indexUninvertingAllowed );
		return new ManagedMultiReader( effectiveReaders, subReaders, readerProviders );
	}

	/**
	 * Gets the readers to be effectively used. A given reader will be returned itself if:
	 * <ul>
	 * <li>there is no sort involved.</li>
	 * <li>doc value fields for all requested sort fields are contained in the index, for each entity type mapped to the
	 * index</li>
	 * </ul>
	 * Otherwise the directory reader will be wrapped in a {@link UninvertingReader} configured in a way to satisfy the
	 * requested sorts.
	 */
	private static IndexReader[] getEffectiveReaders(IndexManager[] indexManagers, IndexReader[] subReaders, SortConfigurations configuredSorts, Sort sort, boolean indexUninvertingAllowed) {
		if ( sort == null || sort.getSort().length == 0 ) {
			return subReaders;
		}

		Set<String> indexesToBeUninverted = getIndexesToBeUninverted( configuredSorts, sort, indexUninvertingAllowed );
		Map<String, Type> mappings = indexesToBeUninverted.isEmpty() ? Collections.<String, Type>emptyMap() : getMappings( sort );
		IndexReader[] effectiveReaders = new IndexReader[subReaders.length];

		int i = 0;
		for ( IndexReader reader : subReaders ) {
			// take incoming reader as is
			if ( !indexesToBeUninverted.contains( indexManagers[i].getIndexName() ) ) {
				effectiveReaders[i] = reader;
			}
			// wrap with uninverting reader
			else {
				if ( reader instanceof DirectoryReader ) {
					DirectoryReader directoryReader = (DirectoryReader) reader;

					try {
						effectiveReaders[i] = UninvertingReader.wrap( directoryReader, mappings );
					}
					catch (IOException e) {
						throw log.couldNotCreateUninvertingReader( directoryReader, e );
					}
				}
				else {
					log.readerTypeUnsupportedForInverting( reader.getClass() );
					effectiveReaders[i] = reader;
				}
			}

			i++;
		}

		return effectiveReaders;
	}

	/**
	 * Checks for each involved entity type whether it maps all the required sortable fields; If not, it marks the index
	 * for uninverting.
	 */
	private static Set<String> getIndexesToBeUninverted(SortConfigurations configuredSorts, Sort sort, boolean indexUninvertingAllowed) {
		Set<String> indexesToBeUninverted = new HashSet<>();

		for ( SortConfiguration sortConfiguration : configuredSorts ) {
			boolean foundEntityWithAllRequiredSorts = false;
			boolean foundEntityWithMissingSorts = false;

			for ( IndexedTypeIdentifier entityType : sortConfiguration.getEntityTypes() ) {
				List<String> uncoveredSorts = sortConfiguration.getUncoveredSorts( entityType, sort );

				if ( !uncoveredSorts.isEmpty() ) {
					indexesToBeUninverted.add( sortConfiguration.getIndexName() );

					if ( indexUninvertingAllowed ) {
						log.uncoveredSortsRequested( entityType, sortConfiguration.getIndexName(), StringHelper.join( uncoveredSorts, ", " ) );
					}
					else {
						throw log.uncoveredSortsRequestedWithUninvertingNotAllowed(
								entityType,
								sortConfiguration.getIndexName(),
								StringHelper.join( uncoveredSorts, ", " )
						);
					}

					foundEntityWithMissingSorts = true;
				}
				else {
					foundEntityWithAllRequiredSorts = true;
				}
			}

			if ( foundEntityWithAllRequiredSorts && foundEntityWithMissingSorts ) {
				throw log.inconsistentSortableFieldConfigurationForSharedIndex( sortConfiguration.getIndexName(), StringHelper.join( sort.getSort(), ", " ) );
			}
		}

		return indexesToBeUninverted;
	}

	/**
	 * Returns the uninverting reader mappings required for the given non-null sort.
	 */
	private static Map<String, UninvertingReader.Type> getMappings(Sort sort) {
		Map<String,UninvertingReader.Type> mappings = new HashMap<>();

		for ( SortField sortField : sort.getSort() ) {
			if ( sortField.getField() != null ) {
				switch ( sortField.getType() ) {
					case INT: mappings.put( sortField.getField(), Type.INTEGER );
					break;
					case LONG: mappings.put( sortField.getField(), Type.LONG );
					break;
					case FLOAT: mappings.put( sortField.getField(), Type.FLOAT );
					break;
					case DOUBLE: mappings.put( sortField.getField(), Type.DOUBLE );
					break;
					case STRING:
					case STRING_VAL: mappings.put( sortField.getField(), Type.SORTED );
					break;
					case BYTES: mappings.put( sortField.getField(), Type.BINARY );
					break;
					case CUSTOM: // Nothing to do; expecting doc value fields created by the user
					break;
					default: log.sortFieldTypeUnsupported( sortField.getField(), sortField.getType() );
				}
			}
		}

		return mappings;
	}

	@Override
	protected synchronized void doClose() throws IOException {
		/**
		 * Important: we don't really close the sub readers but we delegate to the
		 * close method of the managing ReaderProvider, which might reuse the same
		 * IndexReader.
		 */
		final boolean debugEnabled = log.isDebugEnabled();
		if ( debugEnabled ) {
			log.debugf( "Closing MultiReader: %s", this );
		}
		for ( int i = 0; i < readersForClosing.length; i++ ) {
			ReaderProvider container = readerProviders[i];
			container.closeIndexReader( readersForClosing[i] ); // might be virtual
		}
		if ( debugEnabled ) {
			log.trace( "MultiReader closed." );
		}
	}

	// Exposed only for testing
	public List<? extends IndexReader> getSubReaders() {
		return getSequentialSubReaders();
	}

	@Override
	public String toString() {
		return ManagedMultiReader.class.getSimpleName() + " [readersForClosing=" + Arrays.toString( readersForClosing ) + ", readerProviders=" + Arrays.toString( readerProviders ) + "]";
	}
}
