/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2010, Red Hat, Inc. and/or its affiliates or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat, Inc.
 *
 * 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, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY 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
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */

package org.hibernate.search.engine.impl;

import java.io.Serializable;
import java.util.Arrays;
import java.util.zip.DataFormatException;

import org.apache.lucene.document.CompressionTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Fieldable;

import org.apache.lucene.document.NumericField;
import org.hibernate.search.SearchException;
import org.hibernate.search.annotations.Store;
import org.hibernate.search.bridge.FieldBridge;
import org.hibernate.search.bridge.TwoWayFieldBridge;
import org.hibernate.search.bridge.spi.ConversionContext;
import org.hibernate.search.engine.spi.AbstractDocumentBuilder;
import org.hibernate.search.engine.spi.DocumentBuilderIndexedEntity;
import org.hibernate.search.engine.spi.EntityIndexBinder;
import org.hibernate.search.engine.spi.SearchFactoryImplementor;
import org.hibernate.search.util.impl.ClassLoaderHelper;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * @author Hardy Ferentschik
 * @author Sanne Grinovero
 * @author Ales Justin
 */
public final class DocumentBuilderHelper {
	private static final Log log = LoggerFactory.make();
	private static final Object NOT_SET = new Object();

	private DocumentBuilderHelper() {
	}

	public static Class getDocumentClass(String className) {
		try {
			// Use the same class loader used to load this class ...
			return ClassLoaderHelper.classForName( className, DocumentBuilderHelper.class.getClassLoader() );
		}
		catch ( ClassNotFoundException e ) {
			throw new SearchException( "Unable to load indexed class: " + className, e );
		}
	}

	public static Serializable getDocumentId(SearchFactoryImplementor searchFactoryImplementor, Class<?> clazz, Document document, ConversionContext conversionContext) {
		final DocumentBuilderIndexedEntity<?> builderIndexedEntity = getDocumentBuilder(
				searchFactoryImplementor,
				clazz
		);
		final TwoWayFieldBridge fieldBridge = builderIndexedEntity.getIdBridge();
		final String fieldName = builderIndexedEntity.getIdKeywordName();
		try {
			return (Serializable) conversionContext
					.setClass( clazz )
					.pushIdentifierProperty()
					.twoWayConversionContext( fieldBridge )
					.get( fieldName, document );
		}
		finally {
			conversionContext.popProperty();
		}
	}

	public static String getDocumentIdName(SearchFactoryImplementor searchFactoryImplementor, Class<?> clazz) {
		DocumentBuilderIndexedEntity<?> documentBuilder = getDocumentBuilder( searchFactoryImplementor, clazz );
		return documentBuilder.getIdentifierName();
	}

	public static Object[] getDocumentFields(SearchFactoryImplementor searchFactoryImplementor, Class<?> clazz, Document document, String[] fields, ConversionContext conversionContext) {
		DocumentBuilderIndexedEntity<?> builderIndexedEntity = getDocumentBuilder( searchFactoryImplementor, clazz );
		final int fieldNbr = fields.length;
		Object[] result = new Object[fieldNbr];
		Arrays.fill( result, NOT_SET );
		conversionContext.setClass( clazz );
		if ( builderIndexedEntity.getIdKeywordName() != null ) {
			final String fieldName = builderIndexedEntity.getIdKeywordName();
			int matchingPosition = getFieldPosition( fields, fieldName );
			if ( matchingPosition != -1 ) {
				conversionContext.pushProperty( fieldName );
				try {
					populateResult(
							fieldName,
							builderIndexedEntity.getIdBridge(),
							Store.YES,
							result,
							document,
							conversionContext,
							matchingPosition
					);
				}
				finally {
					conversionContext.popProperty();
				}
			}
		}

		final AbstractDocumentBuilder.PropertiesMetadata metadata = builderIndexedEntity.getMetadata();
		processFieldsForProjection( metadata, fields, result, document, conversionContext );
		return result;
	}

	public static void populateResult(String fieldName,
									FieldBridge fieldBridge,
									Store store,
									Object[] result,
									Document document,
									ConversionContext conversionContext,
									int matchingPosition) {
		//TODO make use of an isTwoWay() method
		if ( store != Store.NO && TwoWayFieldBridge.class.isAssignableFrom( fieldBridge.getClass() ) ) {
			result[matchingPosition] = conversionContext
					.twoWayConversionContext( (TwoWayFieldBridge) fieldBridge )
					.get( fieldName, document );
			if ( log.isTraceEnabled() ) {
				log.tracef( "Field %s projected as %s", fieldName, result[matchingPosition] );
			}
		}
		else {
			if ( store == Store.NO ) {
				throw new SearchException( "Projecting an unstored field: " + fieldName );
			}
			else {
				throw new SearchException( "FieldBridge is not a TwoWayFieldBridge: " + fieldBridge.getClass() );
			}
		}
	}

	private static void processFieldsForProjection(AbstractDocumentBuilder.PropertiesMetadata metadata, String[] fields, Object[] result, Document document, ConversionContext contextualBridge) {
		//process base fields
		final int nbrFoEntityFields = metadata.fieldNames.size();
		for ( int index = 0; index < nbrFoEntityFields; index++ ) {
			final String fieldName = metadata.fieldNames.get( index );
			int matchingPosition = getFieldPosition( fields, fieldName );
			if ( matchingPosition != -1 ) {
				contextualBridge.pushProperty( fieldName );
				try {
					populateResult(
							fieldName,
							metadata.fieldBridges.get( index ),
							metadata.fieldStore.get( index ),
							result,
							document,
							contextualBridge,
							matchingPosition
					);
				}
				finally {
					contextualBridge.popProperty();
				}
			}
		}

		//process fields of embedded
		final int nbrOfEmbeddedObjects = metadata.embeddedPropertiesMetadata.size();
		for ( int index = 0; index < nbrOfEmbeddedObjects; index++ ) {
			//there is nothing we can do for collections
			if ( metadata.embeddedContainers.get( index ) == AbstractDocumentBuilder.PropertiesMetadata.Container.OBJECT ) {
				contextualBridge.pushProperty( metadata.embeddedFieldNames.get( index ) );
				try {
					processFieldsForProjection(
							metadata.embeddedPropertiesMetadata.get( index ), fields, result, document, contextualBridge
					);
				}
				finally {
					contextualBridge.popProperty();
				}
			}
		}

		//process class bridges
		final int nbrOfClassBridges = metadata.classBridges.size();
		for ( int index = 0; index < nbrOfClassBridges; index++ ) {
			final String fieldName = metadata.classNames.get( index );
			int matchingPosition = getFieldPosition( fields, fieldName );
			if ( matchingPosition != -1 ) {
				populateResult(
						fieldName,
						metadata.classBridges.get( index ),
						metadata.classStores.get( index ),
						result,
						document,
						contextualBridge,
						matchingPosition
				);
			}
		}

		//If we still didn't know the value using any bridge, return the raw value or string:
		for ( int index = 0; index < result.length; index++ ) {
			if ( result[index] == NOT_SET ) {
				result[index] = null; // make sure we never return NOT_SET
				if ( document != null ) {
					Fieldable field = document.getFieldable( fields[index] );
					if ( field != null ) {
						result[index] = extractObjectFromFieldable( field );
					}
				}
			}
		}
	}

	public static Object extractObjectFromFieldable(Fieldable field) {
		if ( field instanceof NumericField ) {
			return NumericField.class.cast( field ).getNumericValue();
		}
		else {
			return extractStringFromFieldable( field );
		}
	}

	public static String extractStringFromFieldable(Fieldable field) {
		if ( field.isBinary() ) {
			try {
				return CompressionTools.decompressString( field.getBinaryValue() );
			}
			catch ( DataFormatException e ) {
				throw log.fieldLooksBinaryButDecompressionFailed( field.name() );
			}
		}
		else {
			return field.stringValue();
		}
	}

	public static int getFieldPosition(String[] fields, String fieldName) {
		int fieldNbr = fields.length;
		for ( int index = 0; index < fieldNbr; index++ ) {
			if ( fieldName.equals( fields[index] ) ) {
				return index;
			}
		}
		return -1;
	}

	private static DocumentBuilderIndexedEntity<?> getDocumentBuilder(SearchFactoryImplementor searchFactoryImplementor, Class<?> clazz) {
		EntityIndexBinder entityIndexBinding = searchFactoryImplementor.getIndexBindingForEntity(
				clazz
		);
		if ( entityIndexBinding == null ) {
			throw new SearchException( "No Lucene configuration set up for: " + clazz );
		}
		return entityIndexBinding.getDocumentBuilder();
	}
}


