package blog.inquisitive.util;

import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;

import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

import java.lang.reflect.Array;
import java.lang.reflect.Proxy;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationHandler;

public class Decorator {

	// NOTE Possible improvement
	// we could emit warnings on missing annotations from methods with
	// signatures that would otherwise match or match if the interface
	// parameter was added

	// Use to decorate methods in a decorations object to enable them to
	// override methods in the base object
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.METHOD)
	public static @interface Override {}

	// This will create a new object implementing the interface I that will
	// forward all calls to the object T, which extends I.
	// except:
	//	* if the decorations object implements a method annotated by @Decorator.Override
	//	* that has the same parameters as a method in I with an instance of I added as first parameter
	//	* and have a return type that is covariant with the method in I
	// in that case the method in the decorator will be called instead and 
	// unless the decorator calls it the method in T will not be called.
	//
	// If the interface I does not contain the corresponding methods
	// annotated @Decorator.Override a NoSuchMethodException will be thrown 
	// @SuppressWarnings("unchecked")
	public static <I, T extends I,D> I decorate(T proxyBase, Class<I> asInterface, D decorations)
			throws NoSuchMethodException {
		
		// NOTE Possible improvement
		// verify that all @Decorator.Override methods in D exists in I
		for(Method m:getAllMethodsInHierarchy(decorations.getClass())){
			if(m.getAnnotation(Override.class)!=null){
				// return value ignored, only interested in exception
				getBaseMethod(m,asInterface); 
			}
		}
		
		return (I) Proxy.newProxyInstance(asInterface.getClassLoader(),new Class[]{asInterface},
						new DecoratorProxy<T,D>(proxyBase,asInterface,decorations));
	}

	// NOTE Possible improvement
	// consider adding a set of overloads with functional interfaces
	// supporting lambdas of up to N parameters

	@SuppressWarnings("unchecked")
	private static <V> V[] arrayPrepend(V v,V[] vs) {
		V[] vn=(V[])Array.newInstance(vs.getClass().getComponentType(),vs.length+1);
		vn[0]=v;
		for(int i=1;i<=vs.length;i++){
			vn[i]=vs[i-1];
		}
		return vn;
	}
	
	@SuppressWarnings("unchecked")
	private static Method getBaseMethod(Method m,Class inInterface) throws NoSuchMethodException {
		String name=m.getName();
		Class[] parameters=m.getParameterTypes();
		if( parameters.length<1 || parameters[0]!=inInterface ) {
			throw new NoSuchMethodException();
		}
		Class[] finalParameters=Arrays.copyOfRange(parameters,1,parameters.length);
		return inInterface.getMethod(name, finalParameters);
	}

	@SuppressWarnings("unchecked")
	private static class DecoratorProxy<T,D> implements InvocationHandler {
		T proxied;
		D decorator;
		Class proxiedInterface;
		public DecoratorProxy(T proxied,Class proxiedInterface,D decorator) {
			this.proxied=proxied;
			this.proxiedInterface=proxiedInterface;
			this.decorator=decorator;
		}
		private Method getDecoratedMethod(Method m) {
			try{
				String name=m.getName();
				Class[] parameters=m.getParameterTypes();
				Class[] finalParameters=arrayPrepend(proxiedInterface,parameters);
				Method dm=getMethodInHierarchy(decorator.getClass(),name,finalParameters);
				if(dm.getAnnotation(Override.class)!=null){
					return dm;
				}else{
					return null;
				}
			}catch(NoSuchMethodException e){
				return null;
			}
		}
		public Object invoke (Object proxy, Method method, Object[] args)
				throws Throwable {
			if(args==null)
				args=new Object[0];
			Method decorated=getDecoratedMethod(method);
			if(decorated!=null){
				decorated.setAccessible(true);
				return decorated.invoke(decorator,arrayPrepend(proxied,args));
			}else{
				method.setAccessible(true);
				return method.invoke(proxied,args);
			}
		}
	}

	private static Method getMethodInHierarchy(Class<?> objectClass,String name,Class<?>... parameterTypes)
			throws NoSuchMethodException, SecurityException {
		Method m=objectClass.getDeclaredMethod(name,parameterTypes);
		Class<?> superClass = objectClass.getSuperclass();
		if( m==null && superClass!=null ){
			return getMethodInHierarchy(superClass,name,parameterTypes);
		}else{
			return m;
		}
	}

	/**
	 * Function borrowed from https://coderwall.com/p/wrqcsg/java-reflection-get-all-methods-in-hierarchy
	 * 
	 * Gets an array of all methods in a class hierarchy walking up to parent classes
	 * @param objectClass the class
	 * @return the methods array
	 */
	private static Method[] getAllMethodsInHierarchy(Class<?> objectClass) {
		Set<Method> allMethods = new HashSet<Method>();
		Method[] declaredMethods = objectClass.getDeclaredMethods();
		Method[] methods = objectClass.getMethods();
		if (objectClass.getSuperclass() != null) {
			Class<?> superClass = objectClass.getSuperclass();
			Method[] superClassMethods = getAllMethodsInHierarchy(superClass);
			allMethods.addAll(Arrays.asList(superClassMethods));
		}
		allMethods.addAll(Arrays.asList(declaredMethods));
		allMethods.addAll(Arrays.asList(methods));
		return allMethods.toArray(new Method[allMethods.size()]);
	}
}

