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

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;

import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.annotations.common.reflection.XMember;
import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.search.annotations.Factory;
import org.hibernate.search.util.StringHelper;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * @author Emmanuel Bernard
 * @author Hardy Ferentschik
 */
public abstract class ReflectionHelper {
	private static final Log LOG = LoggerFactory.make();
	private static final Object[] EMPTY_ARRAY = new Object[0];

	private ReflectionHelper() {
	}

	/**
	 * Get attribute name out of member unless overridden by <code>name</code>.
	 *
	 * @param member <code>XMember</code> from which to extract the name.
	 * @param name Override value which will be returned in case it is not empty.
	 *
	 * @return attribute name out of member unless overridden by <code>name</code>.
	 */
	public static String getAttributeName(XMember member, String name) {
		return StringHelper.isNotEmpty( name ) ?
				name :
				member.getName(); //explicit field name
	}

	/**
	 * Always use this method to set accessibility regardless of the visibility.
	 * @param member the {@link XMember} to check
	 */
	public static void setAccessible(XMember member) {
		try {
			// always set accessible to true as it bypass the security model checks
			// at execution time and is faster.
			member.setAccessible( true );
		}
		catch (SecurityException se) {
			if ( !Modifier.isPublic( member.getModifiers() ) ) {
				throw se;
			}
		}
	}

	/**
	 * Always use this method to set accessibility regardless of the visibility.
	 * @param member the {@link AccessibleObject} to change
	 */
	public static void setAccessible(AccessibleObject member) {
		try {
			// always set accessible to true as it bypass the security model checks
			// at execution time and is faster.
			member.setAccessible( true );
		}
		catch (SecurityException se) {
			if ( !Modifier.isPublic( ( (Member) member ).getModifiers() ) ) {
				throw se;
			}
		}
	}

	public static Object getMemberValue(Object bean, XMember getter) {
		Object value;
		try {
			//EMPTY_ARRAY used to avoid excessive allocations of empty arrays as
			//the invoke method accepts varargs
			value = getter.invoke( bean, EMPTY_ARRAY );
		}
		catch (Exception e) {
			throw new IllegalStateException( "Could not get property value", e );
		}
		return value;
	}

	/**
	 * Creates the class hierarchy for a given {@code XClass}.
	 *
	 * @param clazz the class for which to create the hierarchy
	 *
	 * @return the list of classes in the hierarchy starting at {@code java.lang.Object}
	 */
	public static List<XClass> createXClassHierarchy(XClass clazz) {
		List<XClass> hierarchy = new LinkedList<>();
		XClass next;
		for ( XClass previousClass = clazz; previousClass != null; previousClass = next ) {
			next = previousClass.getSuperclass();
			if ( next != null ) {
				hierarchy.add( 0, previousClass ); // append to head to create a list in top-down iteration order
			}
		}
		return hierarchy;
	}

	/**
	 * Checks whether the specified class contains any Search-specific annotations.
	 *
	 * <p>This method will return {@code true} if such an annotation is detected
	 * in the class itself or in one of its superclass, either applied directly on the class,
	 * on a field, or on a method.
	 *
	 * @param mappedClass the {@code XClass} to check for Search annotations
	 *
	 * @return Returns {@code true} if the class contains at least one Search annotation, {@code false} otherwise
	 */
	public static boolean containsSearchAnnotations(XClass mappedClass) {
		List<XClass> hierarchy = createXClassHierarchy( mappedClass );

		for ( XClass clazz : hierarchy ) {
			if ( containsLocalSearchAnnotation( clazz ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Checks whether the specified class contains any locally-declared, Search-specific annotations.
	 *
	 * <p>This method will return {@code true} if such an annotation is detected
	 * in the class itself, either applied directly on the class, on a field, or on a method.
	 * <p>Only fields and methods declared by the specified class are taken into account:
	 * inherited fields and methods are ignored.
	 *
	 * @param mappedClass the {@code XClass} to check for Search annotations
	 *
	 * @return Returns {@code true} if the class contains at least one Search annotation, {@code false} otherwise
	 */
	private static boolean containsLocalSearchAnnotation(XClass mappedClass) {
		// check the type annotations
		if ( containsSearchAnnotation( mappedClass.getAnnotations() ) ) {
			return true;
		}

		for ( XProperty method : mappedClass.getDeclaredProperties( XClass.ACCESS_PROPERTY ) ) {
			if ( containsSearchAnnotation( method.getAnnotations() ) ) {
				return true;
			}
		}

		for ( XProperty field : mappedClass.getDeclaredProperties( XClass.ACCESS_FIELD ) ) {
			if ( containsSearchAnnotation( field.getAnnotations() ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Checks if the annotation is a Search annotation by comparing the package of the annotation.
	 *
	 * @param annotation the annotation to check
	 *
	 * @return Returns {@code true} if the annotation is a Search annotation, {@code false} otherwise
	 */
	public static boolean isSearchAnnotation(Annotation annotation) {
		Package p = annotation.annotationType().getPackage();
		return p != null && "org.hibernate.search.annotations".equals( p.getName() );
	}


	/**
	 * Creates an instance of the specified class and returns it. If {@code checkForFactoryAnnotation == true}, the created
	 * instance is checked for a {@code @Factory} annotated method. If one exists the factory method is invoked and the
	 * return value returns as expected instances.
	 *
	 * @param clazz the class to instantiate
	 * @param checkForFactoryAnnotation  whether to check for {@code @Factory} annotated methods
	 * @return the created instance
	 */
	public static Object createInstance(Class<?> clazz, boolean checkForFactoryAnnotation) {
		// create the instance
		Object instance;
		try {
			instance = clazz.newInstance();
		}
		catch (InstantiationException e) {
			throw LOG.noPublicNoArgConstructor( clazz.getName() );
		}
		catch (IllegalAccessException e) {
			throw LOG.unableToAccessClass( clazz.getName() );
		}

		if ( !checkForFactoryAnnotation ) {
			return instance;
		}

		// check for a factory annotation
		int numberOfFactoryMethodsFound = 0;
		for ( Method method : clazz.getMethods() ) {
			if ( method.isAnnotationPresent( Factory.class ) ) {
				if ( numberOfFactoryMethodsFound == 1 ) {
					throw LOG.multipleFactoryMethodsInClass( clazz.getName() );
				}
				if ( method.getReturnType() == void.class ) {
					throw LOG.factoryMethodsMustReturnAnObject( clazz.getName(), method.getName() );
				}
				try {
					instance = method.invoke( instance );
				}
				catch (IllegalAccessException e) {
					throw LOG.unableToAccessMethod( clazz.getName(), method.getName() );
				}
				catch (InvocationTargetException e) {
					throw LOG.exceptionDuringFactoryMethodExecution( e, clazz.getName(), method.getName() );
				}
				numberOfFactoryMethodsFound++;
			}
		}
		return instance;
	}

	/**
	 * Checks whether the specified type is a floating point type.
	 *
	 * @param type the type to check
	 * @return {@code true} if the specified type is an floating point type or a wrapper thereof. {@code false} otherwise.
	 */
	public static boolean isFloatingPointType(Class<?> type) {
		return float.class.equals( type )
				|| Float.class.equals( type )
				|| double.class.equals( type )
				|| Double.class.equals( type );
	}

	/**
	 * Checks whether the specified type is a integer type.
	 *
	 * @param type the type to check
	 * @return {@code true} if the specified type is an primitive integer type or a wrapper thereof. {@code false} otherwise.
	 */
	public static boolean isIntegerType(Class<?> type) {
		return byte.class.equals( type )
				|| Byte.class.equals( type )
				|| short.class.equals( type )
				|| Short.class.equals( type )
				|| int.class.equals( type )
				|| Integer.class.equals( type )
				|| long.class.equals( type )
				|| Long.class.equals( type );
	}

	private static boolean containsSearchAnnotation(Annotation[] annotations) {
		for ( Annotation annotation : annotations ) {
			if ( isSearchAnnotation( annotation ) ) {
				return true;
			}
		}
		return false;
	}
}
