package org.dynalang.mop.collections;

import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import org.dynalang.mop.BaseMetaobjectProtocol;
import org.dynalang.mop.ClassBasedMetaobjectProtocol;
import org.dynalang.mop.CallProtocol;
import org.dynalang.mop.impl.MetaobjectProtocolBase;

/**
 * A metaobject protocol that knows how to manipulate Java maps. Operates on
 * all keys present in the map, and returns {@link BaseMetaobjectProtocol.Result#noAuthority} for 
 * keys not present (thus allowing fallback to other navigators below it).
 * @author Attila Szegedi
 * @version $Id: $
 */
public class MapMetaobjectProtocol extends MetaobjectProtocolBase implements ClassBasedMetaobjectProtocol{
    public boolean isAuthoritativeForClass(Class clazz) {
        return Map.class.isAssignableFrom(clazz);
    }

    /**
     * @return {@link BaseMetaobjectProtocol.Result#noAuthority} as this MOP 
     * has no concept of callables
     */
    public Object call(Object callable, CallProtocol callProtocol, Map args) {
        return Result.noAuthority;
    }
    
    /**
     * @return {@link BaseMetaobjectProtocol.Result#noAuthority} as this MOP 
     * has no concept of callables
     */
    public Object call(Object callable, CallProtocol callProtocol, Object... args) {
        return Result.noAuthority;
    }
    
    /**
     * Removes a key-value mapping from the map.
     * @param target the map to remove element from
     * @param propertyId the map key
     * @return {@link BaseMetaobjectProtocol.Result#ok} if the removal was successful. If the map is
     * read-only (throws a {@link UnsupportedOperationException} on removal
     * attempt), {@link BaseMetaobjectProtocol.Result#notDeleteable} is returned. If the map does not
     * contain the key, or it throws a {@link NullPointerException} for a null
     * key, or the target is not a map, {@link BaseMetaobjectProtocol.Result#noAuthority} is 
     * returned.
     */
    public Result delete(Object target, Object propertyId) {
        if(target instanceof Map) {
            Map m = (Map)target;
            try {
                if(m.containsKey(propertyId)) {
                    try {
            	    	m.remove(propertyId);
            	    	return Result.ok;
            	    }
                    catch(UnsupportedOperationException e) {
                	// Read-only map
            	    	return Result.notDeleteable;
            	    }
                }
            }
            catch(NullPointerException e) {
        	if(propertyId == null) {
            	    // Map implementation doesn't handle null
            	    return Result.noAuthority;
        	}
        	// key was not null -- probably a bug in the map implementation
        	throw e;
            }
        }
        return Result.noAuthority;
    }
    
    /**
     * Retrieves a value from the map.
     * @param target the map to retrieve from
     * @param propertyId the key for retrieval
     * @return If the map contains the key, returns the associated value. If 
     * the map does not contain the key, or it throws a 
     * {@link NullPointerException} for a null key, or the target is not a map,
     * {@link BaseMetaobjectProtocol.Result#noAuthority} is returned. 
     */
    public Object get(Object target, Object propertyId) {
        if(target instanceof Map) {
            Map m = (Map)target;
            try {
                if(m.containsKey(propertyId)) {
            	    return m.get(propertyId);
                }
            }
            catch(NullPointerException e) {
        	if(propertyId == null) {
            	    // Map implementation doesn't handle null
            	    return Result.noAuthority;
        	}
        	// key was not null -- probably a bug in the map implementation
        	throw e;
            }
        }
        return Result.noAuthority;
    }
    
    /**
     * Determines whether a key is present in the map.
     * @param target the map to test
     * @param propertyId the key to test for
     * @return If the map contains the key, returns {@link Boolean#TRUE}. If 
     * the map does not contain the key, or it throws a 
     * {@link NullPointerException} for a null key, or the target is not a map,
     * {@link BaseMetaobjectProtocol.Result#noAuthority} is returned. 
     */
    public Boolean has(Object target, Object propertyId) {
        if(target instanceof Map) {
            try {
                if(((Map)target).containsKey(propertyId)) {
            	    return Boolean.TRUE;
                }
            }
            catch(NullPointerException e) {
        	if(propertyId == null) {
            	    // Map implementation doesn't handle null
            	    return null;
        	}
        	// key was not null -- probably a bug in the map implementation
        	throw e;
            }
        }
        return null;
    }
    
    /**
     * Retrieves the mappings in the map.
     * @param target the map whose mappings are retrieved
     * @return the iterator over the entry set of the map, or empty iterator if
     * the target is not a map. 
     */
    public Iterator<Entry> properties(Object target) {
        if(target instanceof Map) {
            return ((Map)target).entrySet().iterator();
        }
        return Collections.EMPTY_MAP.entrySet().iterator();
    }
    
    /**
     * Binds a key-value pair into the map.
     * @param target the map where to bind the new mapping
     * @param propertyId the mapping key
     * @param value the mapped value
     * @param callProtocol not used
     * @return {@link BaseMetaobjectProtocol.Result#ok} if the binding was successful. If the map is
     * read-only (throws a {@link UnsupportedOperationException} on put
     * attempt), {@link BaseMetaobjectProtocol.Result#notWritable} is returned. If the map does not
     * contain the key, or it throws a {@link NullPointerException} for a null
     * key or null value, or the target is not a map, 
     * {@link BaseMetaobjectProtocol.Result#noAuthority} is returned. If the map throws 
     * {@link ClassCastException} (because it is limited in types of values it 
     * can accept), {@link BaseMetaobjectProtocol.Result#noRepresentation} is returned.
     */
    public Result put(Object target, Object propertyId, Object value, CallProtocol callProtocol) {
        if(target instanceof Map) {
            try {
        	((Map)target).put(propertyId, value);
        	return Result.ok;
            }
            catch(NullPointerException e) {
        	if(propertyId == null) {
        	    // Map implementation doesn't handle null keys
        	    return Result.noAuthority;  
        	}
        	else if(value == null) {
        	    // Can't store null value in the map
        	    return Result.noRepresentation;
        	}
        	else {
        	    // Neither propertyId nor value was null, but we still have 
        	    // a NPE -- probably a bug in the map implementation, so 
        	    // make sure we don't hide it.
        	    throw e;
        	}
            }
            catch(ClassCastException e) {
        	// Map only accepts limited types, but we can't know what ones
        	// so we can't attempt to marshal it
        	return Result.noRepresentation;
            }
            catch(UnsupportedOperationException e) {
        	// Read-only map
        	return Result.notWritable;
            }
        }
        return Result.noAuthority;
    }
    
    /**
     * @return {@link BaseMetaobjectProtocol.Result#noAuthority} as this MOP has no concept of type
     * conversion
     */
    public Object representAs(Object object, Class targetClass) {
        return Result.noAuthority;
    }
}