package org.springframework.data.simpledb.reflection;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.annotation.Reference;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

public final class ReflectionUtils {

	private static final String METHOD_SETTER = "setter";
	private static final String METHOD_GETTER = "getter";
	private static final Logger LOG = LoggerFactory.getLogger(ReflectionUtils.class);

	private ReflectionUtils() {
		// utility class
	}

	public static Object callGetter(Object obj, String fieldName) {
		Object object = null;
		try {
			if (obj != null) {
				Field field = getField(obj.getClass(), fieldName);
				if (isPersistentField(field)) {
					 object = getPersistentFieldValue(field, obj);
				} else {
					Method getterMethod = retrieveGetterFrom(obj.getClass(), fieldName);
					Assert.notNull(getterMethod, "No getter found for: " + fieldName);

					object = getterMethod.invoke(obj);
				}
			}
			return object;

		} catch(IllegalAccessException e) {
			throw toMappingException(e, METHOD_GETTER, fieldName, obj);
		} catch(InvocationTargetException e) {
			throw toMappingException(e, METHOD_GETTER, fieldName, obj);
		} catch(IllegalArgumentException e) {
			throw toMappingException(e, METHOD_GETTER, fieldName, obj);
		}
	}

	private static Object getPersistentFieldValue(Field field, Object obj) throws IllegalAccessException {
		boolean fieldAccessible = field.isAccessible();
		try {
			if (!fieldAccessible) {
				field.setAccessible(true);
			}
			return field.get(obj);
			
		} finally {
			field.setAccessible(fieldAccessible);
		}
	}

	public static void callSetter(Object obj, String fieldName, Object fieldValue) {
		try {
			Field field = getField(obj.getClass(), fieldName);
			if (isPersistentField(field)) {
				setPersistentFieldValue(field, obj, fieldValue);
			} else {
				Method setterMethod = retrieveSetterFrom(obj.getClass(), fieldName);
				Assert.notNull(setterMethod, "No setter found for: " + fieldName);
				setterMethod.invoke(obj, fieldValue);
			}

		} catch(IllegalAccessException e) {
			throw toMappingException(e, METHOD_SETTER, fieldName, obj);
		} catch(InvocationTargetException e) {
			throw toMappingException(e, METHOD_SETTER, fieldName, obj);
		} catch(IllegalArgumentException e) {
			throw toMappingException(e, METHOD_SETTER, fieldName, obj);
		}
	}

	private static void setPersistentFieldValue(Field field, Object obj, Object fieldValue) throws IllegalAccessException {
		boolean fieldAccessible = field.isAccessible();
		try {
			if (!fieldAccessible) {
				field.setAccessible(true);
			}
			field.set(obj, fieldValue);
			
		} finally {
			field.setAccessible(fieldAccessible);
		}
	}

	/**
	 * This method checks if the declared Field is accessible through getters and setters methods Fields which have only
	 * setters OR getters and NOT both are discarded from serialization process
	 */
	public static <T> boolean hasDeclaredGetterAndSetter(final Field field, Class<T> entityClazz) {
		boolean hasDeclaredAccessorsMutators = true;

		Method getter = retrieveGetterFrom(entityClazz, field.getName());
		Method setter = retrieveSetterFrom(entityClazz, field.getName());

		if(getter == null || setter == null) {
			hasDeclaredAccessorsMutators = false;
		}

		return hasDeclaredAccessorsMutators;
	}

	public static Class<?> getFieldClass(final Class<?> entityClazz, final String fieldName) {
		Field field = getField(entityClazz, fieldName);
		return field.getType();
	}

	public static Field getField(final Class<?> entityClazz, final String fieldName) {
		try {
			return getDeclaredFieldInHierarchy(entityClazz, fieldName);
		} catch(NoSuchFieldException e) {
			throw new IllegalArgumentException("Field doesn't exist in entity :" + fieldName, e);
		}
	}

	public static boolean isOfType(Type type, final Class<?> entityClazz, final String fieldName) {
		try {
			Field field = getDeclaredFieldInHierarchy(entityClazz, fieldName); 
			Type fieldType = field.getGenericType();

			return isSameConcreteType(type, fieldType);

		} catch(NoSuchFieldException e) {
			throw new IllegalArgumentException("Field doesn't exist in entity :" + fieldName, e);
		}
	}

	public static boolean isListOfListOfObject(Type type) {
		if(type instanceof ParameterizedType) {
			ParameterizedType secondGenericType = (ParameterizedType) type;
			Class<?> rowType = (Class<?>) secondGenericType.getRawType();
			if(!List.class.isAssignableFrom(rowType)) {
				return false;
			}
			Class<?> genericObject = (Class<?>) secondGenericType.getActualTypeArguments()[0];

			if(genericObject.equals(Object.class)) {
				return true;
			}
		}
		return false;
	}

	public static List<String> getReferencedAttributeNames(Class<?> clazz) {
		List<String> referenceFields = new ArrayList<String>();

		for(Field eachField : clazz.getDeclaredFields()) {

			if(eachField.getAnnotation(Reference.class) != null) {
				referenceFields.add(eachField.getName());
			}
		}
		return referenceFields;
	}


	public static List<Field> getReferenceAttributesList(Class<?> clazz) {
		List<Field> references = new ArrayList<Field>();
		List<String> referencedFieldNames = ReflectionUtils.getReferencedAttributeNames(clazz);

		for(String fieldName : referencedFieldNames) {
			Field referenceField = ReflectionUtils.getField(clazz, fieldName);
			references.add(referenceField);
            /* recursive call */
			references.addAll(getReferenceAttributesList(referenceField.getType()));
		}

		return references;
	}

    /**
     * Get only the first Level of Nested Reference Attributes from a given class
     * @param clazz
     * @return List<Field> of referenced fields
     */
    public static List<Field> getFirstLevelOfReferenceAttributes(Class<?> clazz) {
        List<Field> references = new ArrayList<Field>();
        List<String> referencedFields = ReflectionUtils.getReferencedAttributeNames(clazz);

        for(String eachReference : referencedFields) {
            Field referenceField = ReflectionUtils.getField(clazz, eachReference);
            references.add(referenceField);
        }

        return references;
    }

	private static MappingException toMappingException(Exception cause, String accessMethod, String fieldName,
			Object fieldObject) {
		return new MappingException("Could not call " + accessMethod + " for field " + fieldName + " in class:  "
				+ fieldObject.getClass(), cause);
	}

	private static <T> Method retrieveGetterFrom(final Class<T> entityClazz, final String fieldName) {
		Method getterMethod;
		try {
			final PropertyDescriptor descriptor = new PropertyDescriptor(fieldName, entityClazz);
			getterMethod = descriptor.getReadMethod();
		} catch(IntrospectionException e) {
			getterMethod = null;
			LOG.debug("Field {} has not declared getter method", fieldName, e);
		}
		return getterMethod;
	}

	private static <T> Method retrieveSetterFrom(final Class<T> entityClazz, final String fieldName) {
		Method setterMethod;

		try {
			final PropertyDescriptor descriptor = new PropertyDescriptor(fieldName, entityClazz);
			setterMethod = descriptor.getWriteMethod();
		} catch(IntrospectionException e) {
			setterMethod = null;
			LOG.debug("Field {} has not declared setter method", fieldName, e);
		}
		return setterMethod;
	}

	private static boolean isSameConcreteType(Type firstType, Type secondType) {
		if(firstType instanceof ParameterizedType && secondType instanceof ParameterizedType) {

			Type firstRawType = ((ParameterizedType) firstType).getRawType();
			Class<?> firstTypeClass = (Class<?>) firstRawType;
			Type secondRawType = ((ParameterizedType) secondType).getRawType();
			Class<?> secondTypeClass = (Class<?>) secondRawType;

			if(firstTypeClass.isAssignableFrom(secondTypeClass)) {
				Type firstTypeArgument = ((ParameterizedType) firstType).getActualTypeArguments()[0];
				Type secondTypeArgument = ((ParameterizedType) secondType).getActualTypeArguments()[0];
				return isSameConcreteType(firstTypeArgument, secondTypeArgument);
			}
			return false;
		} else {
			return firstType.equals(secondType);
		}
	}

	public static boolean isReference(Field field) {
		return field.getAnnotation(Reference.class) != null;
	}
	
	public static boolean isPersistentField(Field field) {
		return field.isAnnotationPresent(Persistent.class);
	}
	
	/**
	 * Retrieve the {@link Field} corresponding to the propertyPath in the given
	 * class.
	 * 
	 * @param clazz
	 * @param propertyPath
	 * @return
	 */
	public static Field getPropertyField(Class<?> clazz, String propertyPath) {
		
		Field propertyField = null;
		try {
			String[] properties = propertyPath.split("\\.");
			Field carField = getDeclaredFieldInHierarchy(clazz, properties[0]); 
			if (properties.length == 1) {
				propertyField = carField;
			} else {
				String cdr = StringUtils.arrayToDelimitedString(
						Arrays.copyOfRange(properties, 1, properties.length), ".");
				propertyField = getPropertyField(carField.getType(), cdr);
			}
		} catch (Exception e) {
			throw new IllegalArgumentException("Error accessing propertyPath: " + propertyPath + 
					" on class: " + clazz.getName(), e);
		}
		return propertyField;
	}

	/**
	 * Finds a declared field in given <tt>clazz</tt> and continues to search
	 * up the superclass until no more super class is present.
	 * 
	 * @param clazz
	 * @param fieldName
	 * @return
	 * @throws NoSuchFieldException 
	 */
	public static Field getDeclaredFieldInHierarchy(Class<?> clazz, String fieldName) throws NoSuchFieldException {
		
		Field field = null;
		for (Class<?> acls = clazz; acls != null; acls = acls.getSuperclass()) {
			try {
				field = acls.getDeclaredField(fieldName);
				break;
			} catch (NoSuchFieldException e) {
				// ignore
			}
		}
		if (field == null) {
			throw new NoSuchFieldException("Could not find field '" + fieldName + "' in class '" + clazz.getName() + "' or its super classes");
		}
		
		return field;
	}
	
	/**
	 * Finds all declared fields in given <tt>clazz</tt> and its super classes.
	 * 
	 * @param clazz
	 * @return
	 */
	public static Field[] getDeclaredFieldsInHierarchy(Class<?> clazz) {
		
		List<Field> fields = new ArrayList<Field>();
		for (Class<?> acls = clazz; acls != null; acls = acls.getSuperclass()) {
			for (Field field : acls.getDeclaredFields()) {
				fields.add(field);
			}
		}
		
		return fields.toArray(new Field[fields.size()]);
	}
	
}