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

import java.io.Reader;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.lucene.analysis.tokenattributes.CharTermAttributeImpl;
import org.apache.lucene.analysis.tokenattributes.FlagsAttributeImpl;
import org.apache.lucene.analysis.tokenattributes.KeywordAttributeImpl;
import org.apache.lucene.analysis.tokenattributes.OffsetAttributeImpl;
import org.apache.lucene.analysis.tokenattributes.PayloadAttributeImpl;
import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttributeImpl;
import org.apache.lucene.analysis.tokenattributes.TypeAttributeImpl;
import org.apache.lucene.document.BinaryDocValuesField;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.DoubleField;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.FloatField;
import org.apache.lucene.document.IntField;
import org.apache.lucene.document.LongField;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.document.SortedSetDocValuesField;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.util.AttributeImpl;
import org.apache.lucene.util.BytesRef;
import org.hibernate.search.backend.FlushLuceneWork;
import org.hibernate.search.exception.SearchException;
import org.hibernate.search.backend.AddLuceneWork;
import org.hibernate.search.backend.DeleteLuceneWork;
import org.hibernate.search.backend.LuceneWork;
import org.hibernate.search.backend.OptimizeLuceneWork;
import org.hibernate.search.backend.PurgeAllLuceneWork;
import org.hibernate.search.backend.UpdateLuceneWork;
import org.hibernate.search.backend.spi.DeleteByQueryLuceneWork;
import org.hibernate.search.backend.spi.DeletionQuery;
import org.hibernate.search.bridge.spi.ConversionContext;
import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator;
import org.hibernate.search.engine.spi.DocumentBuilderIndexedEntity;
import org.hibernate.search.engine.spi.EntityIndexBinding;
import org.hibernate.search.indexes.serialization.spi.LuceneWorksBuilder;
import org.hibernate.search.indexes.serialization.spi.SerializableIndex;
import org.hibernate.search.indexes.serialization.spi.SerializableStore;
import org.hibernate.search.indexes.serialization.spi.SerializableTermVector;
import org.hibernate.search.spi.IndexedTypeIdentifier;
import org.hibernate.search.spi.IndexedTypeMap;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

import static org.hibernate.search.indexes.serialization.impl.SerializationHelper.toSerializable;

/**
 * Default implementation of the {@code LuceneWorksBuilder}. An instance is passed to the
 * {@link org.hibernate.search.indexes.serialization.spi.Deserializer#deserialize(byte[] , LuceneWorksBuilder )} method
 * of the de-serializer of a given {@link org.hibernate.search.indexes.serialization.spi.SerializationProvider}.
 *
 * @author Emmanuel Bernard &lt;emmanuel@hibernate.org&gt;
 * @author Sanne Grinovero
 */
public class LuceneWorkHydrator implements LuceneWorksBuilder {

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

	private final IndexedTypeMap<EntityIndexBinding> typesRegistry;
	private final List<LuceneWork> results;
	private ClassLoader loader;
	private Document luceneDocument;
	private List<AttributeImpl> attributes;
	private List<List<AttributeImpl>> tokens;
	private Serializable id;

	public LuceneWorkHydrator(ExtendedSearchIntegrator searchIntegrator) {
		this.typesRegistry = searchIntegrator.getIndexBindings();
		this.results = new ArrayList<>();
		this.loader = Thread.currentThread().getContextClassLoader();
	}

	public List<LuceneWork> getLuceneWorks() {
		return results;
	}

	@Override
	public void addOptimizeAll() {
		results.add( OptimizeLuceneWork.INSTANCE );
	}

	@Override
	public void addFlush() {
		results.add( FlushLuceneWork.INSTANCE );
	}

	@Override
	public void addPurgeAllLuceneWork(String entityClassName) {
		final IndexedTypeIdentifier fromName = typesRegistry.keyFromName( entityClassName );
		results.add( new PurgeAllLuceneWork( fromName ) );
	}

	@Override
	public void addIdAsJavaSerialized(byte[] idAsByte) {
		this.id = toSerializable( idAsByte, loader );
	}

	@Override
	public void addId(Serializable id) {
		this.id = id;
	}

	@Override
	public void addDeleteLuceneWork(String entityClassName, ConversionContext conversionContext) {
		final IndexedTypeIdentifier fromName = typesRegistry.keyFromName( entityClassName );
		LuceneWork result = new DeleteLuceneWork(
				id,
				objectIdInString( fromName, id, conversionContext ),
				fromName
		);
		results.add( result );
		id = null;
	}

	@Override
	public void addDeleteByQueryLuceneWork(String entityClassName, DeletionQuery deletionQuery) {
		final IndexedTypeIdentifier fromName = typesRegistry.keyFromName( entityClassName );
		LuceneWork result = new DeleteByQueryLuceneWork(
				fromName,
				deletionQuery
		);
		this.results.add( result );
	}

	@Override
	public void addAddLuceneWork(String entityClassName, Map<String, String> fieldToAnalyzerMap, ConversionContext conversionContext) {
		final IndexedTypeIdentifier fromName = typesRegistry.keyFromName( entityClassName );
		LuceneWork result = new AddLuceneWork(
				id,
				objectIdInString( fromName, id, conversionContext ),
				fromName,
				getLuceneDocument(),
				fieldToAnalyzerMap
		);
		results.add( result );
		clearDocument();
		id = null;
	}

	@Override
	public void addUpdateLuceneWork(String entityClassName, Map<String, String> fieldToAnalyzerMap, ConversionContext conversionContext) {
		final IndexedTypeIdentifier fromName = typesRegistry.keyFromName( entityClassName );
		LuceneWork result = new UpdateLuceneWork(
				id,
				objectIdInString( fromName, id, conversionContext ),
				fromName,
				getLuceneDocument(),
				fieldToAnalyzerMap
		);
		results.add( result );
		clearDocument();
		id = null;
	}

	private void clearDocument() {
		luceneDocument = null;
	}

	@Override
	public void defineDocument() {
		//Document level boost is not available anymore: is this method still needed?
		getLuceneDocument();
	}

	@Override
	public void addFieldable(byte[] instanceAsByte) {
		//FIXME implementors of IndexableField ARE NOT SERIALIZABLE :-(
		getLuceneDocument().add( (IndexableField) toSerializable( instanceAsByte, loader ) );
	}

	@Override
	public void addIntNumericField(int value, String name, int precisionStep, SerializableStore store, boolean indexed, float boost, boolean omitNorms, boolean omitTermFreqAndPositions) {
		final IntField numField = new IntField( name, value, getStore( store ) );
		numField.setBoost( boost );
		getLuceneDocument().add( numField );
	}

	@Override
	public void addLongNumericField(long value, String name, int precisionStep, SerializableStore store, boolean indexed, float boost, boolean omitNorms, boolean omitTermFreqAndPositions) {
		final LongField numField = new LongField( name, value, getStore( store ) );
		numField.setBoost( boost );
		getLuceneDocument().add( numField );
	}

	@Override
	public void addFloatNumericField(float value, String name, int precisionStep, SerializableStore store, boolean indexed, float boost, boolean omitNorms, boolean omitTermFreqAndPositions) {
		final FloatField numField = new FloatField( name, value, getStore( store ) );
		numField.setBoost( boost );
		getLuceneDocument().add( numField );
	}

	@Override
	public void addDoubleNumericField(double value, String name, int precisionStep, SerializableStore store, boolean indexed, float boost, boolean omitNorms, boolean omitTermFreqAndPositions) {
		final DoubleField numField = new DoubleField( name, value, getStore( store ) );
		numField.setBoost( boost );
		getLuceneDocument().add( numField );
	}

	@Override
	public void addFieldWithBinaryData(String name, byte[] value, int offset, int length) {
		Field luceneField = new StoredField( name, value, offset, length );
		getLuceneDocument().add( luceneField );
	}

	@Override
	public void addFieldWithStringData(String name, String value, SerializableStore store, SerializableIndex index, SerializableTermVector termVector, float boost, boolean omitNorms, boolean omitTermFreqAndPositions) {
		FieldType type = identifyFieldType( store == SerializableStore.YES, //if stored
				index != SerializableIndex.NO, //if indexed
				index == SerializableIndex.ANALYZED || index == SerializableIndex.ANALYZED_NO_NORMS, //if analyzed
				termVector, omitNorms, omitTermFreqAndPositions );
		Field luceneField = new Field( name, value, type );
		luceneField.setBoost( boost );
		getLuceneDocument().add( luceneField );
	}

	@Override
	public void addFieldWithTokenStreamData(String name, SerializableTermVector termVector, float boost, boolean omitNorms, boolean omitTermFreqAndPositions) {
		FieldType type = identifyFieldType( false, true, true, termVector, omitNorms, omitTermFreqAndPositions );
		Field luceneField = new Field( name, new CopyTokenStream( tokens ), type );
		luceneField.setBoost( boost );
		getLuceneDocument().add( luceneField );
		clearTokens();
	}

	private void clearTokens() {
		tokens = new ArrayList<>();
	}

	@Override
	public void addFieldWithSerializableReaderData(String name, byte[] valueAsByte, SerializableTermVector termVector, float boost, boolean omitNorms, boolean omitTermFreqAndPositions) {
		FieldType type = identifyFieldType( false, true, true, termVector, omitNorms, omitTermFreqAndPositions );
		Reader value = (Reader) toSerializable( valueAsByte, loader );
		Field luceneField = new Field( name, value, type );
		luceneField.setBoost( boost );
		getLuceneDocument().add( luceneField );
	}

	@Override
	public void addSerializedAttribute(byte[] bytes) {
		getAttributes().add( (AttributeImpl) toSerializable( bytes, loader ) );
	}

	@Override
	public void addAttributeInstance(AttributeImpl attribute) {
		getAttributes().add( attribute );
	}

	@Override
	public void addTokenTrackingAttribute(List<Integer> positions) {
		//TokenTrackingAttribute is no longer available
		throw new SearchException( "Serialization of TokenTrackingAttribute is no longer supported" );
	}

	@Override
	public void addCharTermAttribute(CharSequence sequence) {
		CharTermAttributeImpl attr = new CharTermAttributeImpl();
		attr.append( sequence );
		getAttributes().add( attr );
	}

	@Override
	public void addPayloadAttribute(byte[] payloads) {
		PayloadAttributeImpl attr = new PayloadAttributeImpl();
		attr.setPayload( new BytesRef( payloads ) );
		getAttributes().add( attr );
	}

	@Override
	public void addKeywordAttribute(boolean isKeyword) {
		KeywordAttributeImpl attr = new KeywordAttributeImpl();
		attr.setKeyword( isKeyword );
		getAttributes().add( attr );
	}

	@Override
	public void addPositionIncrementAttribute(int positionIncrement) {
		PositionIncrementAttributeImpl attr = new PositionIncrementAttributeImpl();
		attr.setPositionIncrement( positionIncrement );
		getAttributes().add( attr );
	}

	@Override
	public void addFlagsAttribute(int flags) {
		FlagsAttributeImpl attr = new FlagsAttributeImpl();
		attr.setFlags( flags );
		getAttributes().add( attr );
	}

	@Override
	public void addTypeAttribute(String type) {
		TypeAttributeImpl attr = new TypeAttributeImpl();
		attr.setType( type );
		getAttributes().add( attr );
	}

	@Override
	public void addOffsetAttribute(int startOffset, int endOffset) {
		OffsetAttributeImpl attr = new OffsetAttributeImpl();
		attr.setOffset( startOffset, endOffset );
		getAttributes().add( attr );
	}

	@Override
	public void addToken() {
		getTokens().add( getAttributes() );
		clearAttributes();
	}

	@Override
	public void addDocValuesFieldWithBinaryData(String name, String type, byte[] value, int offset, int length) {
		DocValuesType docValuesType = Enum.valueOf( DocValuesType.class, type );
		Field docValuesField;
		switch ( docValuesType ) {
			// data is ByteRef
			case BINARY: {
				docValuesField = new BinaryDocValuesField( name, new BytesRef( value, offset, length ) );
				break;
			}
			case SORTED: {
				docValuesField = new SortedDocValuesField( name, new BytesRef( value, offset, length ) );
				break;
			}
			case SORTED_SET: {
				docValuesField = new SortedSetDocValuesField( name, new BytesRef( value, offset, length ) );
				break;
			}
			default: {
				// in case Lucene is going to add more in coming releases
				throw log.unexpectedBinaryDocValuesTypeType( type );
			}
		}
		getLuceneDocument().add( docValuesField );
	}

	@Override
	public void addDocValuesFieldWithNumericData(String name, String type, long value) {
		DocValuesType docValuesType = Enum.valueOf( DocValuesType.class, type );
		Field docValuesField;
		switch ( docValuesType ) {
			case NUMERIC: {
				docValuesField = new NumericDocValuesField( name, value );
				break;
			}
			case SORTED_NUMERIC: {
				docValuesField = new SortedNumericDocValuesField( name, value );
				break;
			}
			default: {
				// in case Lucene is going to add more in coming releases
				throw log.unexpectedBinaryDocValuesTypeType( type );
			}
		}
		getLuceneDocument().add( docValuesField );
	}

	private void clearAttributes() {
		attributes = new ArrayList<>();
	}

	private Document getLuceneDocument() {
		if ( luceneDocument == null ) {
			luceneDocument = new Document();
		}
		return luceneDocument;
	}

	private String objectIdInString(IndexedTypeIdentifier typeKey, Serializable id, ConversionContext conversionContext) {
		EntityIndexBinding indexBindingForEntity = typesRegistry.get( typeKey );
		if ( indexBindingForEntity == null ) {
			throw new SearchException( "Unable to find entity type metadata while deserializing: " + typeKey.getName() );
		}
		DocumentBuilderIndexedEntity documentBuilder = indexBindingForEntity.getDocumentBuilder();
		return documentBuilder.objectToString( documentBuilder.getIdFieldName(), id, conversionContext );
	}

	private FieldType identifyFieldType(boolean stored, boolean indexed, boolean analyzed, SerializableTermVector termVector, boolean omitNorms, boolean omitTermFreqAndPositions) {
		final FieldType type = new FieldType();
		type.setStored( stored );
		type.setTokenized( analyzed );
		type.setStoreTermVectors( termVector != SerializableTermVector.NO );
		type.setStoreTermVectorOffsets( termVector == SerializableTermVector.WITH_OFFSETS || termVector == SerializableTermVector.WITH_POSITIONS_OFFSETS );
		type.setStoreTermVectorPositions( termVector == SerializableTermVector.WITH_POSITIONS || termVector == SerializableTermVector.WITH_POSITIONS_OFFSETS );
		type.setOmitNorms( omitNorms );
		type.setIndexOptions( omitTermFreqAndPositions ? IndexOptions.DOCS : IndexOptions.DOCS_AND_FREQS_AND_POSITIONS );
		return type;
	}

	private Field.Store getStore(SerializableStore store) {
		switch ( store ) {
			case NO:
				return Field.Store.NO;
			case YES:
				return Field.Store.YES;
			default:
				throw log.unableToConvertSerializableStoreToLuceneStore( store.toString() );
		}
	}

	public List<AttributeImpl> getAttributes() {
		if ( attributes == null ) {
			attributes = new ArrayList<>();
		}
		return attributes;
	}

	public List<List<AttributeImpl>> getTokens() {
		if ( tokens == null ) {
			tokens = new ArrayList<>();
		}
		return tokens;
	}
}
