/**
 * == @Spearal ==>
 * 
 * Copyright (C) 2014 Franck WOLFF & William DRAI (http://www.spearal.io)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.spearal.impl.partial;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;

import org.spearal.SpearalContext;
import org.spearal.configuration.PartialObjectFactory;
import org.spearal.configuration.PropertyFactory.Property;
import org.spearal.impl.cache.AnyMap.ValueProvider;
import org.spearal.impl.cache.CopyOnWriteMap;
import org.spearal.impl.instantiator.ProxyInstantiator;

/**
 * @author Franck WOLFF
 */
public class JavassistPartialObjectFactory implements PartialObjectFactory, ValueProvider<Class<?>, Object, Class<?>> {

	private final CopyOnWriteMap<Class<?>, Object, Class<?>> proxyClassesCache;
	
	public JavassistPartialObjectFactory() {
		this.proxyClassesCache = new CopyOnWriteMap<Class<?>, Object, Class<?>>(true, this);
	}

	@Override
	public Class<?> createValue(SpearalContext context, Class<?> key, Object unused) {
		context.getSecurizer().checkDecodable(key);
		
		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.setFilter(new PartialObjectFilter(context, key));
		proxyFactory.setSuperclass(key);
		proxyFactory.setInterfaces(new Class<?>[] { ExtendedPartialObjectProxy.class });
		return proxyFactory.createClass();
	}

	@Override
	public Object instantiatePartial(SpearalContext context, Class<?> cls, Property[] partialProperties)
		throws InstantiationException, IllegalAccessException {
		
		if (Proxy.isProxyClass(cls))
			return ProxyInstantiator.instantiatePartial(context, cls, partialProperties);
		
		Class<?> proxyClass = proxyClassesCache.getOrPutIfAbsent(context, cls);
		ProxyObject proxyObject = (ProxyObject)proxyClass.newInstance();
		proxyObject.setHandler(new PartialObjectProxyHandler(context, cls, partialProperties));
		return proxyObject;
	}
	
	private static class PartialObjectFilter implements MethodFilter {
		
		private static final Method[] partialObjectProxyMethods = PartialObjectProxy.class.getMethods();

		private final Set<Method> accessors;

		public PartialObjectFilter(SpearalContext ctx, Class<?> cls) {
			this.accessors = new HashSet<Method>();
			
			for (Property property : ctx.getProperties(cls)) {
				if (property.hasGetter())
					accessors.add(property.getGetter());
				if (property.hasSetter())
					accessors.add(property.getSetter());
			}
		}

		@Override
		public boolean isHandled(Method method) {
			return accessors.contains(method) || isPartialObjectProxyMethod(method);
		}
		
		private static boolean isPartialObjectProxyMethod(Method method) {
			for (Method partialObjectProxyMethod : partialObjectProxyMethods) {
				if (partialObjectProxyMethod.equals(method))
					return true;
			}
			return false;
		}
	}
	
	private static class PartialObjectProxyHandler implements MethodHandler {

		private final Property[] allProperties;
		private final Map<String, Property> definedProperties;

		public PartialObjectProxyHandler(SpearalContext context, Class<?> cls, Property[] partialProperties) {
			this.allProperties = context.getProperties(cls);
			
			this.definedProperties = new HashMap<String, Property>(partialProperties.length);
			for (Property property : partialProperties) {
				if (property != null)
					this.definedProperties.put(property.getName(), property);
			}
		}

		public Object invoke(Object obj, Method method, Method proceed, Object[] args) throws Exception {

			// Proxy methods.
			if (method.getDeclaringClass() == PartialObjectProxy.class) {
				String name = method.getName();
				if ("$hasUndefinedProperties".equals(name))
					return Boolean.valueOf(definedProperties.size() < allProperties.length);
				if ("$isDefined".equals(name) && args.length == 1)
					return Boolean.valueOf(definedProperties.containsKey(args[0]));
				if ("$undefine".equals(name) && args.length == 1)
					return definedProperties.remove(args[0]);
				if ("$getDefinedProperties".equals(name))
					return definedProperties.values().toArray(new Property[definedProperties.size()]);
				if ("$getActualClass".equals(name))
					return obj.getClass().getSuperclass();
				throw new UnsupportedOperationException("Internal error: " + method.toString());
			}
			
			// Setters.
			if (method.getReturnType() == void.class) {
				for (Property property : allProperties) {
					if (method.equals(property.getSetter())) {
						proceed.invoke(obj, args);
						definedProperties.put(property.getName(), property);
						return null;
					}
				}
				throw new UnsupportedOperationException("Internal error: " + method.toString());
			}
			
			// Getters.
			for (Property property : definedProperties.values()) {
				if (method.equals(property.getGetter()))
					return proceed.invoke(obj, args);
			}
			
			throw new UndefinedPropertyException(method.toString());
		}
	}
}