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

import org.apache.lucene.document.CompressionTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.DoubleDocValuesField;
import org.apache.lucene.document.DoubleField;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.document.Field.TermVector;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.FloatDocValuesField;
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.index.IndexOptions;
import org.apache.lucene.util.BytesRef;
import org.hibernate.search.annotations.Store;
import org.hibernate.search.bridge.LuceneOptions;
import org.hibernate.search.bridge.spi.NullMarker;
import org.hibernate.search.engine.metadata.impl.DocumentFieldMetadata;
import org.hibernate.search.exception.SearchException;
import org.hibernate.search.util.StringHelper;

/**
 * A wrapper class for Lucene parameters needed for indexing.
 *
 * @author Hardy Ferentschik
 * @author Sanne Grinovero
 * @author Gustavo Fernandes
 */
public class LuceneOptionsImpl implements LuceneOptions {

	private static final FieldType TYPE_COMPRESSED = new FieldType();
	static {
		TYPE_COMPRESSED.setTokenized( false );
		TYPE_COMPRESSED.setOmitNorms( true );
		TYPE_COMPRESSED.setIndexOptions( IndexOptions.NONE );
		TYPE_COMPRESSED.setStored( true );
		TYPE_COMPRESSED.freeze();
	}

	private final boolean storeCompressed;
	private final boolean storeUncompressed;
	private boolean documentBoostApplied = false; //needs to be applied only once
	private final float fieldLevelBoost;

	/**
	 * Boost inherited from the parent structure of the given field: the document-level boost in case of a top-level
	 * field, the product of the document-level boost and the boost(s) of the parent embeddable(s) in case of an
	 * embedded field
	 */
	private final float inheritedBoost;
	private final Index indexMode;
	private final TermVector termVector;
	private final Store storeType;
	private final String indexNullAs;

	public LuceneOptionsImpl(DocumentFieldMetadata fieldMetadata, float fieldLevelBoost, float inheritedBoost) {
		this.inheritedBoost = inheritedBoost;
		this.indexMode = fieldMetadata.getIndex();
		this.termVector = fieldMetadata.getTermVector();
		this.fieldLevelBoost = fieldLevelBoost;
		this.storeType = fieldMetadata.getStore();
		this.storeCompressed = this.storeType.equals( Store.COMPRESS );
		this.storeUncompressed = this.storeType.equals( Store.YES );
		NullMarker nullMarker = fieldMetadata.getNullMarkerCodec().getNullMarker();
		this.indexNullAs = nullMarker == null ? null : nullMarker.nullRepresentedAsString();
	}

	public LuceneOptionsImpl(Index indexMode, TermVector termVector, Store store, String indexNullAs, float fieldLevelBoost, float inheritedBoost) {
		this.inheritedBoost = inheritedBoost;
		this.indexMode = indexMode;
		this.termVector = termVector;
		this.fieldLevelBoost = fieldLevelBoost;
		this.storeType = store;
		this.storeCompressed = this.storeType.equals( Store.COMPRESS );
		this.storeUncompressed = this.storeType.equals( Store.YES );
		this.indexNullAs = indexNullAs;
	}

	@Override
	public void addFieldToDocument(String name, String indexedString, Document document) {
		//Do not add fields on empty strings, seems a sensible default in most situations
		if ( StringHelper.isNotEmpty( indexedString ) ) {
			if ( !( indexMode.equals( Index.NO ) && storeCompressed ) ) {
				standardFieldAdd( name, indexedString, document );
			}
			if ( storeCompressed ) {
				compressedFieldAdd( name, indexedString, document );
			}
		}
	}

	private void standardFieldAdd(String name, String indexedString, Document document) {
		// Non-stored, non-indexed field may be declared for sorting purposes; don't add it to the document
		if ( storeType == Store.NO && indexMode == Index.NO ) {
			return;
		}

		Field field = new Field( name, indexedString, storeUncompressed ? Field.Store.YES : Field.Store.NO, indexMode, termVector );
		setBoost( field );
		document.add( field );
	}

	private void compressedFieldAdd(String name, String indexedString, Document document) {
		// Non-stored, non-indexed field may be declared for sorting purposes; don't add it to the document
		if ( storeType == Store.NO && indexMode == Index.NO ) {
			return;
		}

		byte[] compressedString = CompressionTools.compressString( indexedString );
		// indexed is implicitly set to false when using byte[]
		Field field = new Field( name, compressedString, TYPE_COMPRESSED );
		document.add( field );
	}

	@Override
	@Deprecated
	public float getBoost() {
		return fieldLevelBoost;
	}

	@Override
	public String indexNullAs() {
		return indexNullAs;
	}

	@Override
	public boolean isCompressed() {
		return storeCompressed;
	}

	@Override
	public Index getIndex() {
		return this.indexMode;
	}

	@Override
	public org.apache.lucene.document.Field.Store getStore() {
		if ( storeUncompressed || storeCompressed ) {
			return org.apache.lucene.document.Field.Store.YES;
		}
		else {
			return org.apache.lucene.document.Field.Store.NO;
		}
	}

	@Override
	public TermVector getTermVector() {
		return this.termVector;
	}

	private void setBoost(Field field) {
		/**
		 * MEMO from the Apache Lucene documentation:
		 * It is illegal to return a boost other than 1.0f for a field that is not
		 * indexed ({@link IndexableFieldType#indexed()} is false) or omits normalization values
		 * ({@link IndexableFieldType#omitNorms()} returns true).
		 */
		if ( indexMode.isIndexed() && ! indexMode.omitNorms() ) {
			if ( documentBoostApplied == false ) {
				documentBoostApplied = true;
				//FIXME This isn't entirely accurate as in some cases the LuceneOptionsImpl
				//is being reused for multiple fields: this needs to be significantly different,
				//potentially dropping the LuceneOptionsImpl usage.
				field.setBoost( fieldLevelBoost * inheritedBoost );
			}
			else {
				field.setBoost( fieldLevelBoost );
			}
		}
	}

	private void checkNotCompressed(final String fieldName) {
		//TODO this sanity check should be done at bootstrap, not runtime
		if ( storeType == Store.COMPRESS ) {
			throw new SearchException( "Error indexing field " + fieldName + ", @NumericField cannot be compressed" );
		}
	}

	public void addDoubleFieldToDocument(String fieldName, double doubleValue, Document document) {
		checkNotCompressed( fieldName );
		DoubleField field = new DoubleField( fieldName, doubleValue, storeType != Store.NO ? Field.Store.YES : Field.Store.NO );
		setBoost( field );
		document.add( field );
	}

	public void addFloatFieldToDocument(String fieldName, float floatValue, Document document) {
		checkNotCompressed( fieldName );
		FloatField field = new FloatField( fieldName, floatValue, storeType != Store.NO ? Field.Store.YES : Field.Store.NO );
		setBoost( field );
		document.add( field );
	}

	public void addIntFieldToDocument(String fieldName, int intValue, Document document) {
		checkNotCompressed( fieldName );
		IntField field = new IntField( fieldName, intValue, storeType != Store.NO ? Field.Store.YES : Field.Store.NO );
		setBoost( field );
		document.add( field );
	}

	public void addLongFieldToDocument(String fieldName, long longValue, Document document) {
		checkNotCompressed( fieldName );
		LongField field = new LongField( fieldName, longValue, storeType != Store.NO ? Field.Store.YES : Field.Store.NO );
		setBoost( field );
		document.add( field );
	}

	@Override
	public void addNumericFieldToDocument(String fieldName, Object indexedValue, Document document) {
		if ( indexedValue == null ) {
			throw new IllegalArgumentException( "the indexedValue parameter shall not be null" );
		}
		if ( indexedValue instanceof Double ) {
			addDoubleFieldToDocument( fieldName, ( (Double) indexedValue ).doubleValue(), document );
		}
		else if ( indexedValue instanceof Float ) {
			addFloatFieldToDocument( fieldName, ( (Float) indexedValue ).floatValue(), document );
		}
		else if ( indexedValue instanceof Integer ) {
			addIntFieldToDocument( fieldName, ( (Integer) indexedValue ).intValue(), document );
		}
		else if ( indexedValue instanceof Long ) {
			addLongFieldToDocument( fieldName, ( (Long) indexedValue ).longValue(), document );
		}
		else {
			throw new IllegalArgumentException( "unsupported type of Number" );
		}
	}

	@Override
	public void addSortedDocValuesFieldToDocument(String fieldName, String indexedString, Document document) {
		if ( indexedString != null ) {
			document.add( new SortedDocValuesField( fieldName, new BytesRef( indexedString ) ) );
		}
	}

	@Override
	public void addNumericDocValuesFieldToDocument(String fieldName, Number numericValue, Document document) {
		if ( numericValue == null ) {
			throw new IllegalArgumentException( "the numericValue parameter shall not be null" );
		}
		if ( numericValue instanceof Double ) {
			document.add( new DoubleDocValuesField( fieldName, ( (Double) numericValue ).doubleValue() ) );
		}
		else if ( numericValue instanceof Float ) {
			document.add( new FloatDocValuesField( fieldName, ( (Float) numericValue ).floatValue() ) );
		}
		else if ( numericValue instanceof Integer ) {
			document.add( new NumericDocValuesField( fieldName, ( (Integer) numericValue ).longValue() ) );
		}
		else if ( numericValue instanceof Long ) {
			document.add( new NumericDocValuesField( fieldName, ( (Long) numericValue ).longValue() ) );
		}
		else {
			throw new IllegalArgumentException( "unsupported type of Number" );
		}
	}

}
