package org.dynalang.mop.impl;

import java.io.Serializable;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

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

/**
 * A MOP that is composed of {@link ClassBasedMetaobjectProtocol} instances. 
 * Every operation is dispatched first to the the first member that claims
 * authority over the class of the target object. If it returns a 
 * non-authoritative answer the operation is also dispatched (where reasonable)
 * to the first member that claims authority over the class of the property id
 * object.
 * @author Attila Szegedi
 * @version $Id: $
 */
public class CompositeClassBasedMetaobjectProtocol implements ClassBasedMetaobjectProtocol, Serializable {
    private static final long serialVersionUID = 1L;

    private final ClassBasedMetaobjectProtocol[] members;
    
    private final Map<ClassToken, ClassBasedMetaobjectProtocol> mops = new
    	ConcurrentHashMap<ClassToken, ClassBasedMetaobjectProtocol>();

    /**
     * Optimizes a list of MOPs. If a group of adjacent MOPs in the list all
     * implement {@link ClassBasedMetaobjectProtocol}, they will be replaced 
     * with a single instance of {@link CompositeClassBasedMetaobjectProtocol}
     * that contains them.
     * @param mops the list of MOPs to optimize
     * @return the optimized list
     */
    public static MetaobjectProtocol[] optimize(MetaobjectProtocol[] mops) {
	List<MetaobjectProtocol> lmops = new LinkedList<MetaobjectProtocol>();
	List<ClassBasedMetaobjectProtocol> cbmops = 
	    new LinkedList<ClassBasedMetaobjectProtocol>();
	for (MetaobjectProtocol protocol : mops) {
	    if(protocol instanceof ClassBasedMetaobjectProtocol) {
		cbmops.add((ClassBasedMetaobjectProtocol)protocol);
	    }
	    else {
		addClassBased(lmops, cbmops);
		lmops.add(protocol);
	    }
	}
	addClassBased(lmops, cbmops);
	return lmops.toArray(new MetaobjectProtocol[lmops.size()]);
    }
    
    private static void addClassBased(List<MetaobjectProtocol> lmops, 
	    List<ClassBasedMetaobjectProtocol> cbmops) {
	switch(cbmops.size()) {
	    case 0: {
		break;
	    }
	    case 1: {
		lmops.addAll(cbmops);
		lmops.clear();
		break;
	    }
	    default: {
		lmops.add(new CompositeClassBasedMetaobjectProtocol(
			cbmops.toArray(
				new ClassBasedMetaobjectProtocol[cbmops.size()])));
		cbmops.clear();
		break;
	    }
	}
    }

    /**
     * Creates a new composite class-based metaobject protocol from the 
     * specified members.
     * @param members the member metaobject protocols.
     */
    public CompositeClassBasedMetaobjectProtocol(ClassBasedMetaobjectProtocol[] members) {
	this.members = members.clone();
	ClassToken.addClassRefListener(new ClassTokenListener() {
	    public void classTokenInvalidated(ClassTokenInvalidatedEvent e) {
		mops.remove(e.getClassTokens());
	    }
	});
    }
    
    private ClassBasedMetaobjectProtocol getMop(Object obj) {
	return obj == null ? null : getMop(obj.getClass());
    }

    private ClassBasedMetaobjectProtocol getMop(Class<?> clazz) {
	ClassToken token = ClassToken.forClass(clazz);
	ClassBasedMetaobjectProtocol mop = mops.get(token);
	if(mop == null) {
	    // Interrogate MOPs in order to see which has authority
    	    for (ClassBasedMetaobjectProtocol protocol : members) {
    		if(protocol.isAuthoritativeForClass(clazz)) {
    		    mops.put(token, protocol);
    		    return protocol;
    		}
    	    }
    	    mops.put(token, this);
    	    return this;
	}
	return mop == this ? null : mop;
    }
    
    public boolean isAuthoritativeForClass(Class clazz) {
        return getMop(clazz) != null;
    }
    
    public Object call(Object callable, CallProtocol callProtocol, Map args) {
	MetaobjectProtocol mop = getMop(callable);
	if(mop != null) {
	    return mop.call(callable, callProtocol, args);
	}
        return Result.noAuthority;
    }

    public Object call(Object target, Object callableId, CallProtocol callProtocol, Map args) {
	MetaobjectProtocol mop = getMop(target);
	Object result = Result.noAuthority;
	if(mop != null) {
	    result = mop.call(target, callableId, callProtocol, args);
	}
	if(result == Result.noAuthority) {
	    mop = getMop(callableId);
	    if(mop != null) {
	        return mop.call(target, callableId, callProtocol, args);
	    }
	}
	return result;
    }

    public Object call(Object target, Object callableId, CallProtocol callProtocol, Object... args) {
	MetaobjectProtocol mop = getMop(target);
	Object result = Result.noAuthority;
	if(mop != null) {
	    result = mop.call(target, callableId, callProtocol, args);
	}
	if(result == Result.noAuthority) {
	    mop = getMop(callableId);
	    if(mop != null) {
	        return mop.call(target, callableId, callProtocol, args);
	    }
	}
	return result;
    }

    public Object call(Object callable, CallProtocol callProtocol, Object... args) {
	MetaobjectProtocol mop = getMop(callable);
	if(mop != null) {
	    return mop.call(callable, callProtocol, args);
	}
        return Result.noAuthority;
    }

    public Result delete(Object target, long propertyId) {
	MetaobjectProtocol mop = getMop(target);
	if(mop != null) {
	    return mop.delete(target, propertyId);
	}
        return Result.noAuthority;
    }

    public Result delete(Object target, Object propertyId) {
	MetaobjectProtocol mop = getMop(target);
	Result result = Result.noAuthority;
	if(mop != null) {
	    result = mop.delete(target, propertyId);
	}
	if(result == Result.noAuthority) {
	    mop = getMop(propertyId);
	    if(mop != null) {
		return mop.delete(target, propertyId);
	    }
	}
	return result;
    }

    public Object get(Object target, long propertyId) {
	MetaobjectProtocol mop = getMop(target);
	if(mop != null) {
	    return mop.get(target, propertyId);
	}
        return Result.noAuthority;
    }

    public Object get(Object target, Object propertyId) {
	MetaobjectProtocol mop = getMop(target);
	Object result = Result.noAuthority;
	if(mop != null) {
	    result = mop.get(target, propertyId);
	}
	if(result == Result.noAuthority) {
	    mop = getMop(propertyId);
	    if(mop != null) {
		return mop.get(target, propertyId);
	    }
	}
	return result;
    }

    public Boolean has(Object target, long propertyId) {
	MetaobjectProtocol mop = getMop(target);
	if(mop != null) {
	    return mop.has(target, propertyId);
	}
        return null;
    }

    public Boolean has(Object target, Object propertyId) {
	MetaobjectProtocol mop = getMop(target);
	Boolean result = null;
	if(mop != null) {
	    result = mop.has(target, propertyId);
	}
	if(result == null) {
	    mop = getMop(propertyId);
	    if(mop != null) {
		return mop.has(target, propertyId);
	    }
	}
	return result;
    }

    public Iterator<Entry> properties(Object target) {
	MetaobjectProtocol mop = getMop(target);
	if(mop != null) {
	    return mop.properties(target);
	}
        return Collections.EMPTY_MAP.entrySet().iterator();
    }

    public Iterator<? extends Object> propertyIds(Object target) {
	MetaobjectProtocol mop = getMop(target);
	if(mop != null) {
	    return mop.propertyIds(target);
	}
        return Collections.EMPTY_MAP.entrySet().iterator();
    }

    public Result put(Object target, long propertyId, Object value, CallProtocol callProtocol) {
	MetaobjectProtocol mop = getMop(target);
	if(mop != null) {
	    return mop.put(target, propertyId, value, callProtocol);
	}
	return Result.noAuthority;
    }

    public Result put(Object target, Object propertyId, Object value, CallProtocol callProtocol) {
	MetaobjectProtocol mop = getMop(target);
	Result result = Result.noAuthority;
	if(mop != null) {
	    result = mop.put(target, propertyId, value, callProtocol);
	}
	if(result == Result.noAuthority) {
	    mop = getMop(propertyId);
	    if(mop != null) {
		return mop.put(target, propertyId, value, callProtocol);
	    }
	}
	return result;
    }

    public Object representAs(Object object, Class targetClass) {
	MetaobjectProtocol mop = getMop(object);
	if(mop != null) {
	    Object result = mop.representAs(object, targetClass);
	    if(result != Result.noAuthority && result != Result.noRepresentation) {
		return result;
	    }
	}
	mop = getMop(targetClass);
	if(mop != null) {
	    return mop.representAs(object, targetClass);
	}
	return Result.noAuthority;
    }
}