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

import java.lang.reflect.Method;

import org.hibernate.search.SearchException;
import org.hibernate.search.Environment;
import org.hibernate.search.cfg.SearchMapping;
import org.hibernate.search.cfg.spi.SearchConfiguration;
import org.hibernate.search.annotations.Factory;
import org.hibernate.search.util.impl.ClassLoaderHelper;
import org.hibernate.search.util.impl.ReflectionHelper;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * package class extracting the SearchMappingFactory if needed
 */
public class SearchMappingBuilder {
	private static final Log LOG = LoggerFactory.make();

	private SearchMappingBuilder() {
	}

	/**
	 * This factory method takes a SearchConfiguration object
	 * and returns a SearchMapping object which defines
	 * the programmatic model for indexing entities and fields.
	 *
	 * Throws SearchException:
	 * 1) No @Factory found
	 * 2) Multiple @Factory found
	 * 3) hibernate.search.model_mapping defines a class that cannot be found
	 * 4) Cannot invoke the @Factory method to get new instance of SearchMapping
	 *
	 * @param cfg the search configuration
	 * @return SearchMapping
	 */
	public static SearchMapping getSearchMapping(SearchConfiguration cfg) {

		//try SearchConfiguration object first and then properties
		Object modelMappingProperty = cfg.getProgrammaticMapping();
		if ( modelMappingProperty == null ) {
			modelMappingProperty = cfg.getProperties().get( Environment.MODEL_MAPPING );
		}

		if ( modelMappingProperty == null ) {
			return null;
		}
		SearchMapping mapping = null;
		Object programmaticConfig = modelMappingProperty;
		if ( programmaticConfig instanceof SearchMapping ) {
			mapping = (SearchMapping) programmaticConfig;
			return mapping;
		}
		Class<?> clazz = getProgrammaticMappingClass( programmaticConfig );
		Method[] methods = clazz.getDeclaredMethods();
		int count = 0;
		for ( Method method : methods ) {
			if ( method.isAnnotationPresent( Factory.class ) ) {
				count++;
				ReflectionHelper.setAccessible( method );
				mapping = getNewInstanceOfSearchMapping( clazz, method );
			}
		}
		validateMappingFactoryDefinition( count, clazz );
		return mapping;
	}

	private static SearchMapping getNewInstanceOfSearchMapping(Class<?> clazz, Method method) {
		SearchMapping mapping = null;
		try {
			LOG.debugf( "invoking factory method [ %s.%s ] to get search mapping instance", clazz.getName(), method.getName() );
			Object instance = clazz.newInstance();
			mapping = (SearchMapping) method.invoke( instance );
		}
		catch (Exception e) {
			throw new SearchException( "Unable to call the factory method: " + clazz.getName() + "." + method.getName(), e );
		}
		return mapping;
	}

	private static void validateMappingFactoryDefinition(int count, Class<?> factory) {
		if ( count == 0 ) {
			throw new SearchException( "No @Factory method defined for building programmatic api on " + factory );
		}
		if ( count > 1 ) {
			throw new SearchException( "Multiple @Factory methods defined. Only one factory method required. " + factory );
		}
	}

	private static Class<?> getProgrammaticMappingClass(Object programmaticConfig) {
		Class<?> clazz = null;
		if ( programmaticConfig instanceof String ) {
			final String className = (String) programmaticConfig;
			try {
				clazz = ClassLoaderHelper.classForName( className, SearchMappingBuilder.class.getClassLoader() );
			}
			catch (ClassNotFoundException e) {
				throw new SearchException( "Unable to find " + Environment.MODEL_MAPPING + "=" + className, e );
			}
		}
		else if ( programmaticConfig instanceof Class ) {
			clazz = (Class<?>) programmaticConfig;
		}
		else {
			throw new SearchException( Environment.MODEL_MAPPING + " is of an unknown type: " + programmaticConfig.getClass() );
		}
		if ( clazz == null ) {
			throw new SearchException( "No programmatic factory defined" );
		}
		return clazz;
	}
}
