/*
   Copyright 2007 Attila Szegedi

   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.dynalang.mop.beans;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.MethodDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import org.dynalang.classtoken.ClassToken;
import org.dynalang.mop.BaseMetaobjectProtocol;
import org.dynalang.mop.CallProtocol;
import org.dynalang.mop.MetaobjectProtocol;

/**
 * Exposes a metaobject protocol for a single Java class, treating it as a 
 * JavaBean. You can use it for shorter access path when you can bind it to the
 * object ahead of time. In other circumstances, you would just use 
 * {@link BeansMetaobjectProtocol} as a central MOP to handle the class MOPs. 
 * In addition to being a generic MOP, it also exposes access to class' static
 * methods and constructors using {@link #newInstance(CallProtocol, Object...)}
 * and {@link #callStatic(String, CallProtocol, Object...)} methods.
 * @param <T> the class of objects this MOP can handle
 * @author Attila Szegedi
 * @version $Id: $
 */
public class BeanMetaobjectProtocol<T extends Object> implements MetaobjectProtocol
{
    private final Map<String, PropertyDescriptor> properties = 
        new HashMap<String, PropertyDescriptor>();
    private final Map<String, DynamicMethod<Method>> methods = 
        new HashMap<String, DynamicMethod<Method>>();
    private final Map<String, DynamicMethod<Method>> staticMethods;
    private final DynamicMethod<Constructor<T>> constructor;
    
    private final Collection<String> names;
    
    /**
     * Constructs a new metaobject protocol for objects of a certain class.
     * @param clazz the class to construct a metaobject protocol for
     * @param methodsEnumerable if true, methods are enumerable through the
     * {@link #properties(Object)} and {@link #propertyIds(Object)}.
     * @throws IntrospectionException
     * @throws IllegalArgumentException if the passed class is a primitive class
     */
    public BeanMetaobjectProtocol(Class<T> clazz, boolean methodsEnumerable) throws IntrospectionException {
	if(clazz.isPrimitive()) {
	    throw new IllegalArgumentException("Class " + clazz.getName() + 
		    " is primitive");
	}
        BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
        Map<MethodSignature, Method> accessibleMethods = 
            discoverAccessibleMethods(clazz);
        PropertyDescriptor[] propDescs = beanInfo.getPropertyDescriptors();
        for (int i = 0; i < propDescs.length; i++) {
            PropertyDescriptor descriptor = propDescs[i];
            Method readMethod = descriptor.getReadMethod();
            if(readMethod != null) {
                descriptor.setReadMethod(getAccessibleMethod(readMethod, 
                        accessibleMethods));
            }
            Method writeMethod = descriptor.getWriteMethod();
            if(writeMethod != null) {
                descriptor.setWriteMethod(getAccessibleMethod(writeMethod, 
                        accessibleMethods));
            }
            if(descriptor.getReadMethod() != null || descriptor.getWriteMethod() != null) {
                properties.put(descriptor.getName(), descriptor);
            }
        }
        
        // Add instance methods
        Map<String, DynamicMethod<Method>> statics = new HashMap<String, DynamicMethod<Method>>();
        MethodDescriptor[] methodDescs = beanInfo.getMethodDescriptors();
        for (int i = 0; i < methodDescs.length; i++) {
            MethodDescriptor descriptor = methodDescs[i];
            Method method = getAccessibleMethod(descriptor.getMethod(), 
                    accessibleMethods);
            if(method == null) {
                continue;
            }
            String name = descriptor.getName();
            if(Modifier.isStatic(method.getModifiers())) {
        	addMember(method, name, clazz, statics);
            }
            else {
        	addMember(method, name, clazz, methods);
            }
        }
        staticMethods = Collections.unmodifiableMap(statics);
        
        // Add constructors
        DynamicMethod<Constructor<T>> dynaCtor = null;
        Constructor<T>[] ctrs = clazz.getConstructors();
        for (int i = 0; i < ctrs.length; i++) {
            Constructor<T> ctr = ctrs[i]; 
            dynaCtor = addMember(ctr, "<init>", clazz, dynaCtor);
        }
        constructor = dynaCtor;
        
        if(methodsEnumerable) {
            Collection<String> cnames = new HashSet<String>(properties.size() + 
            	methods.size() * 4 / 3, 0.75f);
            cnames.addAll(properties.keySet());
            cnames.addAll(methods.keySet());
            // Sorting is not really necessary, but it's nice to make it 
            // deterministic and it doesn't cost that much
            ArrayList<String> lnames = new ArrayList<String>(cnames);
            Collections.sort(lnames);
            lnames.trimToSize();
            names = Collections.unmodifiableCollection(lnames);
        }
        else {
            names = Collections.emptySet();
        }
    }

    private static <M extends Member> void addMember(M method, String name, 
	    Class<?> declaringClass,  Map<String, DynamicMethod<M>> map) {
	DynamicMethod<M> existingMethod = map.get(name);
	DynamicMethod<M> newMethod = addMember(method, name, declaringClass, existingMethod);
	if(newMethod != existingMethod) {
	    map.put(name, newMethod);
	}
    }
    
    private static <M extends Member> DynamicMethod<M> addMember(M method, 
	    String name, Class<?> declaringClass, DynamicMethod<M> existing)
    {
	if(existing == null) {
	    return new SimpleDynamicMethod<M>(method);
	}
	else if(existing instanceof SimpleDynamicMethod) {
	    OverloadedDynamicMethod<M> odm = 
	        new OverloadedDynamicMethod<M>(name, declaringClass);
	    odm.addMember(((SimpleDynamicMethod<M>)existing).getMember());
	    odm.addMember(method);
	    return odm;
	}
	else if(existing instanceof OverloadedDynamicMethod) {
	    ((OverloadedDynamicMethod<M>)existing).addMember(method);
	    return existing; 
	}
	throw new AssertionError();
    }
    
    void onClassTokensInvalidated(ClassToken[] tokens) {
	onClassTokensInvalidated(tokens, methods);
	onClassTokensInvalidated(tokens, staticMethods);
	if(constructor instanceof OverloadedDynamicMethod) {
	    ((OverloadedDynamicMethod<Constructor<T>>)constructor).onClassTokensInvalidated(tokens);
	}
    }

    private static <T extends Member> void onClassTokensInvalidated(ClassToken[] tokens, Map<String, DynamicMethod<T>> methods) {
	for (DynamicMethod<T> method : methods.values()) {
	    if(method instanceof OverloadedDynamicMethod) {
		((OverloadedDynamicMethod<T>)method).onClassTokensInvalidated(tokens);
	    }
	}
    }
    
    public Object call(Object callable, CallProtocol callProtocol, Map args) {
        // Java methods aren't callable with named arguments
        return Result.noAuthority;
    }

    public Object call(Object target, Object callableId, CallProtocol callProtocol, 
            Map args) {
	return BeansMetaobjectProtocol.callWithMap(target, callableId, callProtocol);
    }

    public Object call(Object callable, CallProtocol callProtocol, 
            Object... args)
    {
	return BeansMetaobjectProtocol.callMethod(callable, callProtocol, args);
    }
    
    /**
     * Invokes a public static method of the class with the specified name, and 
     * appropriate for the passed arguments. Performs overload resolution and
     * vararg packing if needed.
     * @param name the name of the method to invoke
     * @param callProtocol the call protocol for converting arguments
     * @param args the arguments to the method
     * @return the result of the method invocation. If the method with the 
     * given name does not exist, returns {@link BaseMetaobjectProtocol.Result#doesNotExist}.
     */
    public Object callStatic(String name, CallProtocol callProtocol, Object... args) {
	DynamicMethod<Method> method = staticMethods.get(name);
	if(method == null) {
	    return Result.doesNotExist;
	}
	return method.call(null, callProtocol, args);
    }
    
    /**
     * Creates a new instance of the class, invoking a public constructor 
     * appropriate for the passed arguments. Performs overload resolution and
     * vararg packing if needed.
     * @param callProtocol the call protocol for converting arguments
     * @param args the arguments to the constructor
     * @return a newly constructed object instance. If the class has no public
     * constructors, returns null.
     */
    public T newInstance(CallProtocol callProtocol, Object... args) {
	if(constructor == null) {
	    return null;
	}
	return (T)constructor.call(null, callProtocol, args);
    }

    public Object call(Object target, Object callableId, CallProtocol callProtocol, 
            Object... args) {
        String name = String.valueOf(callableId);
        DynamicMethod<Method> dynaMethod = methods.get(name);
        if(dynaMethod == null) {
            return Result.noAuthority;
        }
        return dynaMethod.call(target, callProtocol, args);
    }

    public Object get(Object target, long propertyId) {
        return Result.noAuthority;
    }
    
    public Object get(Object target, Object propertyId) {
	String name = String.valueOf(propertyId);
        PropertyDescriptor desc = properties.get(name);
        if(desc == null) {
            DynamicMethod<Method> dynaMethod = methods.get(name);
            if(dynaMethod == null) {
                // No property and no method - doesn't exist
                return Result.noAuthority;
            }
            // Return instance-bound method
            return new DynamicInstanceMethod(target, dynaMethod);
        }
        
        Method method = desc.getReadMethod();
        if(method == null) {
            // Write-only property
            return Result.notReadable;
        }
        try {
            return method.invoke(target, (Object[])null);
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new UndeclaredThrowableException(e);
        }
    }
    
    public Boolean has(Object target, long propertyId) {
        return null;
    }
    
    public Boolean has(Object target, Object propertyId) {
        String name = String.valueOf(propertyId);
        if(properties.containsKey(name) || methods.containsKey(name)) {
            return Boolean.TRUE;
        }
        return null;
    }
    
    public Result delete(Object target, long propertyId) {
        return has(target, propertyId) == Boolean.TRUE ? Result.notDeleteable : 
            Result.noAuthority;
    }

    public Result delete(Object target, Object propertyId) {
        return has(target, propertyId) == Boolean.TRUE ? Result.notDeleteable : 
            Result.noAuthority;
    }

    public Iterator<Map.Entry> properties(final Object target) {
        final Iterator<? extends Object> it = propertyIds(target);
        return new Iterator<Map.Entry>() {
            public boolean hasNext() {
                return it.hasNext();
            }
            
            public Entry<Object,Object> next() {
                final Object key = it.next();
                
                return new Map.Entry<Object,Object>() {
                    private boolean hasValue;
                    private Object value;
                    
                    public Object getKey() {
                        return key;
                    }
            
                    public Object getValue() {
                        if(!hasValue) {
                            value = get(target, key);
                            hasValue = true;
                        }
                        return value;
                    }
                    
                    public Object setValue(Object value) {
                        Object previousValue = get(target, key);
                        if(put(target, key, value, BeanMetaobjectProtocol.this) != Result.ok) {
                            throw new UnsupportedOperationException();
                        }
                        return previousValue;
                    }
                    
                    @Override
                    public int hashCode() {
                        Object value = getValue();
                        return 
                            (key == null ? 0 : key.hashCode()) ^ 
                            (value == null ? 0 : value.hashCode());
                    }
                    
                    @Override
                    public boolean equals(Object obj) {
                        if(!(obj instanceof Map.Entry)) {
                            return false;
                        }
                        Map.Entry e = (Map.Entry)obj;
                        Object value = getValue();
                        return (key == null ? e.getKey() == null : 
                            key.equals(e.getKey())) && (value == null ? 
                                    e.getValue() == null : 
                                        value.equals(e.getValue()));
                    }
                };
            }
            
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    public Iterator<? extends Object> propertyIds(Object target) {
        return names.iterator();
    }
    
    public Result put(Object target, long propertyId, Object value, CallProtocol callProtocol) {
        return Result.noAuthority;
    }

    public Result put(Object target, Object propertyId, Object value, CallProtocol callProtocol) {
        PropertyDescriptor desc = properties.get(String.valueOf(propertyId));
        if(desc == null) {
            return Result.noAuthority;
        }
        Method writeMethod = desc.getWriteMethod();
        if(writeMethod == null) {
            return Result.notWritable;
        }
        try {
            Class<?> propType = desc.getPropertyType();
            if(propType == null) {
                return Result.notWritable;
            }
            value = callProtocol.representAs(value, propType);
            if(value == Result.noAuthority || value == Result.noRepresentation) {
                return Result.noRepresentation;
            }
            writeMethod.invoke(target, new Object[] { value });
            return Result.ok;
        }
        catch(RuntimeException e) {
            throw e;
        }
        catch(Exception e) {
            throw new UndeclaredThrowableException(e);
        }
    }

    public Object representAs(Object object, Class targetClass) {
	return BeansMetaobjectProtocol.representAsInternal(object, targetClass);
    }

    private static Map<MethodSignature, Method> discoverAccessibleMethods(Class<?> clazz) {
        Map<MethodSignature, Method> map = new HashMap<MethodSignature, Method>();
        discoverAccessibleMethods(clazz, map);
        return map;
    }
    
    private static void discoverAccessibleMethods(Class<?> clazz, Map<MethodSignature, Method> map) {
        if(Modifier.isPublic(clazz.getModifiers())) {
            try {
                Method[] methods = clazz.getMethods();
                for(int i = 0; i < methods.length; i++) {
                    Method method = methods[i];
                    MethodSignature sig = new MethodSignature(method);
                    map.put(sig, method);
                }
                return;
            }
            catch(SecurityException e) {
                System.err.println(
                        "Could not discover accessible methods of class " + 
                        clazz.getName() + ", attemping superclasses/interfaces.");
                e.printStackTrace();
                // Fall through and attempt to discover superclass/interface 
                // methods
            }
        }

        Class<?>[] interfaces = clazz.getInterfaces();
        for(int i = 0; i < interfaces.length; i++) {
            discoverAccessibleMethods(interfaces[i], map);
        }
        Class<?> superclass = clazz.getSuperclass();
        if(superclass != null) {
            discoverAccessibleMethods(superclass, map);
        }
    }

    private static Method getAccessibleMethod(Method m, Map<MethodSignature, Method> accessibles) {
        return m == null ? null : accessibles.get(new MethodSignature(m));
    }

    private static final class MethodSignature
    {
        private final String name;
        private final Class<?>[] args;
        
        private MethodSignature(String name, Class<?>[] args) {
            this.name = name;
            this.args = args;
        }
        
        MethodSignature(Method method) {
            this(method.getName(), method.getParameterTypes());
        }
        
        public boolean equals(Object o) {
            if(o instanceof MethodSignature) {
                MethodSignature ms = (MethodSignature)o;
                return ms.name.equals(name) && Arrays.equals(args, ms.args);
            }
            return false;
        }
        
        public int hashCode() {
            return name.hashCode() ^ Arrays.hashCode(args);
        }
    }
}