/*
   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.IntrospectionException;
import java.lang.ref.SoftReference;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

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

/**
 * A metaobject protocol implementation that allows access and manipulation of
 * POJOS using semantics adhering to the JavaBeans specification, as well as
 * access and manipulation of native Java arrays. In addition to using it as a
 * generic MOP, you can also use it as a factory for individual 
 * {@link BeanMetaobjectProtocol} objects using 
 * {@link #getBeanMetaobjectProtocol(Class)}. These objects can subsequently
 * be used to access constructors and static methods of the Java classes they
 * represent, but be aware that functionality falls outside of the generic MOP 
 * API.
 * @author Attila Szegedi
 * @version $Id: $
 */
public class BeansMetaobjectProtocol implements MetaobjectProtocol {

    private final boolean methodsEnumerable;
    // Using a soft reference is a compromise solution. If we used a hard 
    // reference, we'd prevent the class from unloading, as the 
    // BeanMetaobjectProtocol holds a reference to Method objects of the 
    // class, which in turn hold a reference to Class objects. If we used a 
    // weak reference, the BeanMetaobjectProtocol would be recreated too 
    // often. Using a soft reference allows a decent amount of caching, but 
    // still ensures that the class information will eventually get cleaned up 
    // after the class is no longer used, allowing it to unload if it is 
    // otherwise unreferenced from the rest of the system.
    private final ConcurrentMap<ClassToken, SoftReference<BeanMetaobjectProtocol<? extends Object>>> mops = 
        new ConcurrentHashMap<ClassToken, SoftReference<BeanMetaobjectProtocol<? extends Object>>>();
    /**
     * Creates a new JavaBeans metaobject protocol that doesn't expose objects'
     * methods and JavaBeans properties as enumerable.
     */
    public BeansMetaobjectProtocol() {
	this(false);
	ClassToken.addClassRefListener(new ClassTokenListener() {
	    public void classTokenInvalidated(ClassTokenInvalidatedEvent e) {
		onClassTokensInvalidated(e.getClassTokens());
	    }
	});
    }
    
    /**
     * Creates a new JavaBeans metaobject protocol.
     * @param methodsEnumerable if true, then objects' methods and JavaBeans 
     * properties show up in the results of {@link #properties(Object)} and
     * {@link #propertyIds(Object)}. If false, these can still be accessed by
     * directly addressing them, but don't show up in aforementioned methods'
     * results. 
     */
    public BeansMetaobjectProtocol(boolean methodsEnumerable) {
	this.methodsEnumerable = methodsEnumerable;
    }
    
    private void onClassTokensInvalidated(ClassToken[] tokens) {
	mops.remove(tokens);
	for(Iterator<SoftReference<BeanMetaobjectProtocol<? extends Object>>> i = mops.values().iterator(); i.hasNext();) {
	    SoftReference<BeanMetaobjectProtocol<? extends Object>> ref = i.next();
	    BeanMetaobjectProtocol<? extends Object> mop = ref.get();
	    if(mop == null) {
		i.remove();
	    }
	    mop.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 callWithMap(target, callableId, callProtocol);
    }

    static Object callWithMap(Object target, Object callableId, CallProtocol callProtocol) {
        // Give chance to notFound etc. first
        Object result = callProtocol.get(target, callableId);
        if(result instanceof Result) {
            return result;
        }
        // Java methods aren't callable with named arguments
        return Result.noAuthority;
    }
    
    public Object call(Object target, Object callableId, CallProtocol callProtocol, 
            Object... args) {
        return getBeanMetaobjectProtocol(target).call(target, callableId, 
                callProtocol, args);
    }

    public Object call(Object callable, CallProtocol callProtocol, 
            Object... args)
    {
	return callMethod(callable, callProtocol, args);
    }

    static Object callMethod(Object callable, CallProtocol callProtocol, Object[] args) {
        if(callable instanceof DynamicInstanceMethod) {
            return ((DynamicInstanceMethod)callable).call(args, 
                    callProtocol);
        }
        return Result.noAuthority;
    }
    
    public Result delete(Object target, long propertyId) {
        return getBeanMetaobjectProtocol(target).delete(target, propertyId); 
    }

    public Result delete(Object target, Object propertyId) {
        return getBeanMetaobjectProtocol(target).delete(target, propertyId); 
    }

    public Object get(Object target, long propertyId) {
        return getBeanMetaobjectProtocol(target).get(target, propertyId);
    }

    public Object get(Object target, Object propertyId) {
        return getBeanMetaobjectProtocol(target).get(target, propertyId);
    }

    public Boolean has(Object target, long propertyId) {
        return getBeanMetaobjectProtocol(target).has(target, propertyId);
    }

    public Boolean has(Object target, Object propertyId) {
        return getBeanMetaobjectProtocol(target).has(target, propertyId);
    }

    public Iterator<Map.Entry> properties(final Object target) {
        return getBeanMetaobjectProtocol(target).properties(target);
    }

    public Iterator<? extends Object> propertyIds(Object target) {
        return getBeanMetaobjectProtocol(target).propertyIds(target);
    }

    public Result put(Object target, long propertyId, Object value, CallProtocol callProtocol) {
        return getBeanMetaobjectProtocol(target).put(target, propertyId, value, callProtocol);
    }

    public Result put(Object target, Object propertyId, Object value, CallProtocol callProtocol) {
        return getBeanMetaobjectProtocol(target).put(target, propertyId, 
                value, callProtocol);
    }

    private static final Map<Class<?>, PrimitiveConverter> primitiveConverters = 
        new IdentityHashMap<Class<?>, PrimitiveConverter>();
    
    private static abstract class PrimitiveConverter {
        abstract Object representAs(Object object);
    }
    
    static {
        primitiveConverters.put(Boolean.TYPE, new PrimitiveConverter() {
            @Override Object representAs(Object object) {
                return object instanceof Boolean ? ((Boolean)object) : Result.noRepresentation;
            }
        });
        primitiveConverters.put(Byte.TYPE, new PrimitiveConverter() {
            @Override Object representAs(Object object) {
                return object instanceof Byte ? ((Byte)object) : Result.noRepresentation;
            }
        });
        primitiveConverters.put(Character.TYPE, new PrimitiveConverter() {
            @Override Object representAs(Object object) {
                return object instanceof Character ? ((Character)object) : Result.noRepresentation;
            }
        });
        primitiveConverters.put(Double.TYPE, new PrimitiveConverter() {
            @Override Object representAs(Object object) {
                if(object instanceof Double) {
                    return object;
                }
                if(object instanceof Integer || object instanceof Long || 
                        object instanceof Float || object instanceof Byte || 
                        object instanceof Short) {
                    return Double.valueOf(((Number)object).doubleValue());
                }
                if(object instanceof Character) {
                    return Double.valueOf(((Character)object).charValue());
                }
                return Result.noRepresentation;
            }
        });
        primitiveConverters.put(Float.TYPE, new PrimitiveConverter() {
            @Override Object representAs(Object object) {
                if(object instanceof Float) {
                    return object;
                }
                if(object instanceof Integer || object instanceof Long || 
                        object instanceof Byte || object instanceof Short) {
                    return Float.valueOf(((Number)object).floatValue());
                }
                if(object instanceof Character) {
                    return Float.valueOf(((Character)object).charValue());
                }
                return Result.noRepresentation;
            }
        });
        primitiveConverters.put(Integer.TYPE, new PrimitiveConverter() {
            @Override Object representAs(Object object) {
                if(object instanceof Integer) {
                    return object;
                }
                if(object instanceof Byte || object instanceof Short) {
                    return Integer.valueOf(((Number)object).intValue());
                }
                if(object instanceof Character) {
                    return Integer.valueOf(((Character)object).charValue());
                }
                return Result.noRepresentation;
            }
        });
        primitiveConverters.put(Long.TYPE, new PrimitiveConverter() {
            @Override Object representAs(Object object) {
                if(object instanceof Long) {
                    return object;
                }
                if(object instanceof Integer || object instanceof Byte || 
                        object instanceof Short) {
                    return Long.valueOf(((Number)object).longValue());
                }
                if(object instanceof Character) {
                    return Long.valueOf(((Character)object).charValue());
                }
                return Result.noRepresentation;
            }
        });
        primitiveConverters.put(Short.TYPE, new PrimitiveConverter() {
            @Override Object representAs(Object object) {
                if(object instanceof Short) {
                    return object;
                }
                if(object instanceof Byte) {
                    return Short.valueOf(((Number)object).shortValue());
                }
                return Result.noRepresentation;
            }
        });
        primitiveConverters.put(Void.TYPE, new PrimitiveConverter() {
            @Override Object representAs(Object object) {
                return Result.noRepresentation;
            }
        });
    }
    
    /**
     * In case object is an instance of the target class, returns it unchanged.
     * In case object is an instance of the boxing class for a primitive target
     * class, returns it unchanged. In case object is null and target class is
     * primitive, returns {@link BaseMetaobjectProtocol.Result#noRepresentation}.
     * In case the target class is primitive, and there exists a 
     * <a href="http://java.sun.com/docs/books/jls/third_edition/html/conversions.html#5.1.2">
     * widening primitive conversion</a> from the object's class unboxed 
     * primitive type to the target class, returns a boxed representation of 
     * the widened value. In all other cases, 
     * {@link BaseMetaobjectProtocol.Result#noRepresentation} is returned.
     */
    public Object representAs(Object object, Class targetClass) {
	return representAsInternal(object, targetClass);
    }
    
    static Object representAsInternal(Object object, Class targetClass) {
        // Object representations for primitive types are allowed
        if(targetClass.isPrimitive()) {
            if(object == null) {
                // null has no primitive representation
                return Result.noRepresentation;
            }
            return primitiveConverters.get(targetClass).representAs(object);
        }
        else if(object == null) {
            // null is a valid representation for all reference types
            return null;
        }
        // If the object is instance of the target class, it itself is the 
        // representation, otherwise there's no representation.
        return targetClass.isInstance(object) ? object : 
            Result.noRepresentation;
    }
    
    /**
     * Returns a MOP instance for a specified Java object that treats the 
     * object as a generic JavaBean. The MOP is class specific.
     * @param object the object in question
     * @return a JavaBeans MOP for the object's class
     * @throws NullPointerException if object is null
     */
    private <T extends Object> BeanMetaobjectProtocol<T> getBeanMetaobjectProtocol(T object) {
        return (BeanMetaobjectProtocol<T>)getBeanMetaobjectProtocol(object.getClass());
    }

    /**
     * Returns a MOP instance for a specified Java class that treats the class
     * instances as a generic JavaBean.
     * @param clazz the class in question
     * @param <T> the type of the class
     * @return a JavaBeans MOP for the class
     */
    public <T extends Object> BeanMetaobjectProtocol<T> getBeanMetaobjectProtocol(Class<T> clazz) {
	ClassToken token = ClassToken.forClass(clazz);
        SoftReference<BeanMetaobjectProtocol<? extends Object>> ref = mops.get(token);
        BeanMetaobjectProtocol<T> mop;
        if(ref != null) {
            mop = (BeanMetaobjectProtocol<T>)ref.get();
            if(mop != null) {
        	return mop;
            }
        }
        try {
    	    if(clazz.isArray()) {
    	        mop = new ArrayMetaobjectProtocol<T>(clazz, methodsEnumerable);
    	    }
    	    else {
    	        mop = new BeanMetaobjectProtocol<T>(clazz, methodsEnumerable);
    	    }
            mops.put(token, new SoftReference<BeanMetaobjectProtocol<? extends Object>>(mop));
        }
        catch(IntrospectionException e) {
            throw new UndeclaredThrowableException(e);
        }
        return mop;
    }
}