package eu.dnetlib.enabling.aas.saml.adapter;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

import org.opensaml.xml.XMLObject;

import eu.dnetlib.enabling.aas.saml.adapter.conv.IConverterModule;

/**
 * Generic SAMLObject adapter class.
 * @author mhorst
 *
 */
public class SAMLObjectGenericAdapter<Source extends XMLObject> 
	implements InvocationHandler, IAdapter<Source> {


	/**
	 * Adapted object.
	 */
	protected final Source adaptedObject;

	/**
	 * Custom method names map. 
	 * Used before directly mapping names. 
	 */
	protected final Map<String, String> methodNamesMap;
	
	/**
	 * Converters map used to convert result objects.
	 */
	@SuppressWarnings("unchecked")
	protected final Map<Class<?>, 
		IConverterModule> convertersMap;

	/**
	 * Default constructor.
	 * @param source source object
	 */
	public SAMLObjectGenericAdapter(Source source) {
		this(source, null, null);
	}
	
	/**
	 * Derived constructor.
	 * @param source source object
	 * @param methodNamesMap
	 * @param convertersMap
	 */
	@SuppressWarnings("unchecked")
	public SAMLObjectGenericAdapter(Source source, 
			Map<String, String> methodNamesMap,
			Map<Class<?>, IConverterModule> convertersMap) {
		if (source==null) {
			throw new IllegalArgumentException("adaptedObject cannot be null!");
		}
		this.adaptedObject = source;
		this.methodNamesMap = methodNamesMap;
		this.convertersMap = convertersMap;
	}
	
	/* (non-Javadoc)
	 * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
	 */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable, AdapterException {

		if (method.getDeclaringClass()==IAdapter.class) {
			if (this instanceof IAdapter) {
				return method.invoke(this, args);
			} else {
				throw new AdapterException("got " + IAdapter.class.getCanonicalName() +
						'#' + method.getName() + " call, but current adapter doesn't " +
								"implement " + IAdapter.class.getCanonicalName() + " interface!");
			}
		} else {
			Method methodCandidate = findMethodEquivalent(method, args);
			if (methodCandidate!=null) {
				Object resultCandidate = methodCandidate.invoke(adaptedObject, args);
				return adoptObject(resultCandidate);
			} else {
				throw new AdapterException("couldn't find method equivalent for " +
						method.getDeclaringClass().getCanonicalName() + '#' + method.getName());
			}
		}
	}
	
	/**
	 * Adopts source object by creating proxy if necessary.
	 * Resolves arrays and collections.
	 * @param resultCandidate
	 * @return adopted object
	 * @throws AdapterException 
	 */
	@SuppressWarnings("unchecked")
	protected Object adoptObject(Object resultCandidate) throws AdapterException {
		if (resultCandidate!=null) {
			if (convertersMap!=null && 
					convertersMap.containsKey(resultCandidate.getClass())) {
//				if converter is registered
				return convertersMap.get(
						resultCandidate.getClass()).convert(resultCandidate);
			} else if (resultCandidate instanceof XMLObject) {
				IAdapterFactory factory = AdapterFactoryRegistry.lookup(
						(Class<? extends XMLObject>) resultCandidate.getClass());
				if (factory!=null) {
					return factory.newInstance((XMLObject) resultCandidate);
				} else {
					throw new AdapterException("no adapter registered for class: " +
							resultCandidate.getClass().getCanonicalName());
				}
			} else if (resultCandidate instanceof Collection) {
//				handling collections of SAMLObjects
				Collection<?> candidateCollection = (Collection<?>) resultCandidate;
				if (shouldCollectionBeInspected(candidateCollection, XMLObject.class)) {
					Collection resultCollection = null;
//					FIXME using arraylist as collection implementation for testing purposes
//					in fact the collection type should match expected type!!!
					resultCollection = new ArrayList();
					/*
					try {
//						FIXME this is tricky, what if impl doesn't have no arg constr?
//						resultCollection = candidateCollection.getClass().newInstance();
					} catch (InstantiationException e) {
						throw new AdapterException(
								"Unable to instantiate proxy collection", e);
					} catch (IllegalAccessException e) {
						throw new AdapterException(
								"Unable to instantiate proxy collection", e);
					}
					*/
					for (Object currentCandidate : candidateCollection) {
						resultCollection.add(adoptObject(currentCandidate));
					}
					return resultCollection;
				} else {
					return resultCandidate;
				}
				
			} else if (resultCandidate.getClass().isArray()) {
//				handling arrays of SAMLObjects
				Object[] candidateArray = (Object[]) resultCandidate;
				if (shouldArrayBeInspected(candidateArray, XMLObject.class)) {
					Object[] result = (Object[]) Array.newInstance(
							candidateArray.getClass().getComponentType(), 
							candidateArray.length);
					for (int i=0; i< candidateArray.length; i++) {
						result[i] = adoptObject(candidateArray[i]);
					}
					return result;

				} else {
					return resultCandidate;
				}
			} else {
				return resultCandidate;
			}
		} else {
			return resultCandidate;
		}
	}
	
	/**
	 * Returns true if collection contain at least one element of given type.
	 * @param coll
	 * @param classType
	 * @return true if collection contain at least one element of given type
	 */
	protected boolean shouldCollectionBeInspected(Collection<?> coll, Class<?> classType) {
		if (coll!=null) {
			for (Object el : coll) {
				if (el!=null) {
					if (classType.isInstance(el)) {
						return true;
					}
//					checking if collection contains convertable objects
					if (convertersMap!=null) {
						for (Class<?> currentClass : convertersMap.keySet()) {
							if (currentClass.isInstance(el)) {
								return true;
							}
						}
					}
				}
			}
		} 
		return false;
	}
	
	/**
	 * Returns true if collection contain at least one element of given type.
	 * @param coll
	 * @param classType
	 * @return true if collection contain at least one element of given type
	 */
	protected boolean shouldArrayBeInspected(Object[] array, Class<?> classType) {
		if (array!=null) {
			for (Object el : array) {
				if (el!=null) {
					if (classType.isInstance(el)) {
						return true;
					}
//					checking if collection contains convertable objects
					if (convertersMap!=null) {
						for (Class<?> currentClass : convertersMap.keySet()) {
							if (currentClass.isInstance(el)) {
								return true;
							}
						}
					}
				}
			}
		} 
		return false;
	}
	
	/**
	 * Finds method equivalent for given Method.
	 * This method may be overriden by extending classes.
	 * @param sourceMethod
	 * @param sourceArgs
	 * @return source method equivalent in openSAML model
	 * @throws SecurityException
	 * @throws NoSuchMethodException
	 */
	protected Method findMethodEquivalent(Method sourceMethod, Object[] sourceArgs) 
		throws SecurityException, NoSuchMethodException {
//		FIXME currently parameterTypes are not being checked (not required for getters)!
		String foundMethodName = (methodNamesMap!=null)?
				methodNamesMap.get(sourceMethod.getName()):null;
		return this.adaptedObject.getClass().getMethod(
				(foundMethodName!=null)?foundMethodName:sourceMethod.getName(),
				(Class<?>[]) null);
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.saml.adapter.IAdapter#getAdaptedObject()
	 */
	public Source getAdaptedObject() {
		return this.adaptedObject;
	}
}
