/*
   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.impl;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.dynalang.mop.BaseMetaobjectProtocol;
import org.dynalang.mop.ClassBasedMetaobjectProtocol;
import org.dynalang.mop.MetaobjectProtocol;
import org.dynalang.mop.beans.BeansMetaobjectProtocol;
import org.dynalang.mop.collections.ListMetaobjectProtocol;
import org.dynalang.mop.collections.MapMetaobjectProtocol;

/**
 * A class with static utility methods for creating a "standard" MOP. A 
 * standard MOP is a MOP that is a compositional of MOPs known and precreated 
 * by the caller as well as any {@link AutoDiscovery automatically discovered} 
 * MOPs.
 * @author Attila Szegedi
 * @version $Id: $
 */
public class StandardMetaobjectProtocolFactory {

    private StandardMetaobjectProtocolFactory() {
    }

    /**
     * This method implements the "usual" way for creation of a composite 
     * metaobject protocol instance that encompasses all available MOPs in the
     * classpath of the current thread's class loader. This makes this method 
     * useful for creating a class loader specific composite MOP for 
     * environments that do not have their own native object model but wish
     * to manipulate all available object models.
     * The returned composite MOP will feature all MOPs found in the class 
     * loader's JAR files, followed by standard fallback MOPs for beans 
     * (POJOs), Maps, and Lists.
     * @param elementsShadowMethods the order of fallback MOPs. If true, List
     * and Map MOP are put before the beans MOP. If false, beans MOP is put 
     * before the List and Map MOP. Note that List and Map MOPs implement 
     * {@link ClassBasedMetaobjectProtocol} while beans MOP does not, so you'll 
     * generally get better performance if you pass true for this parameter.
     * @return a MetaobjectProtocol that optimally composes the automatically 
     * discovered and fallback MOPs. 
     * @param methodsEnumerable if true, then objects' methods and JavaBeans 
     * properties show up in the results of 
     * {@link BaseMetaobjectProtocol#properties(Object)} and 
     * {@link MetaobjectProtocol#propertyIds(Object)}. If false, these can 
     * still be accessed by directly addressing them, but don't show up in 
     * aforementioned methods' results. 
     * @throws IOException
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws ClassNotFoundException
     */
    public static MetaobjectProtocol createStandardMetaobjectProtocol(
	    boolean elementsShadowMethods, boolean methodsEnumerable) 
    throws IOException, InstantiationException, IllegalAccessException, 
    ClassNotFoundException {
	return createStandardMetaobjectProtocol(
		Thread.currentThread().getContextClassLoader(), null,  
		createStandardFallback(elementsShadowMethods, methodsEnumerable));
    }

    /**
     * This method implements the "usual" way for creation of a composite 
     * metaobject protocol instance that encompasses all available MOPs in the
     * classpath of the current thread's class loader, adding a single 
     * metaobject protocol as the first MOP of the composite. This single 
     * metaobject protocol usually implements the default MOP for the 
     * environment's "standard" language. This makes this method useful for 
     * creating a class loader specific composite MOP for environments that 
     * have their own native object model (i.e. dynamic language runtimes).
     * The returned composite MOP will feature the passed MOP as its first 
     * element, followed by any other MOPs found in the class loader's JAR 
     * files, followed by standard fallback MOPs for beans (POJOs), Maps, and 
     * Lists.
     * @param nativeProtocol the MOP for the environment's native objects (i.e.
     * a JavaScript runtime creating a MOP will pass a JavaScript MOP here). If
     * a MOP of the same class is later discovered in JARs on the class path,
     * it will be ignored and the explicitly passed instance used.
     * @param elementsShadowMethods the order of fallback MOPs. If true, List
     * and Map MOP are put before the beans MOP. If false, beans MOP is put 
     * before the List and Map MOP. Note that List and Map MOPs implement 
     * {@link ClassBasedMetaobjectProtocol} while beans MOP does not, so you'll 
     * generally get better performance if you pass true for this parameter.
     * @return a MetaobjectProtocol that optimally composes the preferred MOP, 
     * automatically discovered, and fallback MOPs. Those automatically 
     * discovered MOPs that are of the same class as any of the prioritized or
     * fallback MOPs are ignored.
     * @param methodsEnumerable if true, then objects' methods and JavaBeans 
     * properties show up in the results of 
     * {@link BaseMetaobjectProtocol#properties(Object)} and 
     * {@link MetaobjectProtocol#propertyIds(Object)}. If false, these can 
     * still be accessed by directly addressing them, but don't show up in 
     * aforementioned methods' results. 
     * @throws IOException
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws ClassNotFoundException
     */
    public static MetaobjectProtocol createStandardMetaobjectProtocol(
	    MetaobjectProtocol nativeProtocol, boolean elementsShadowMethods,
	    boolean methodsEnumerable) 
    throws IOException, InstantiationException, IllegalAccessException,
    	ClassNotFoundException {
	return createStandardMetaobjectProtocol(
		Thread.currentThread().getContextClassLoader(), 
		new MetaobjectProtocol[] { nativeProtocol },
		createStandardFallback(elementsShadowMethods, methodsEnumerable));
    }
    
    /**
     * This method implements the "usual" way for creation of a composite 
     * metaobject protocol instance that encompasses all available MOPs. It is
     * more generic than the other two methods of the same name, allowing 
     * maximum flexibility in configuration.
     * @param classLoader the class loader within which the MOPs are 
     * discovered. null can be used to denote the 
     * {@link ClassLoader#getSystemClassLoader() system class loader}, although
     * you'll usually want the thread context class loader instead.
     * @param prioritized an array of precreated, prioritized MOPs. These MOPs
     * will be given a chance to handle objects before any automatically 
     * discovered MOPs. Can be null.
     * @param fallbacks an array of precreated fallback MOPs. These MOPs will
     * be given a chance to handle objects after any automatically discovered 
     * MOPs. Tipically, you'll want to specify an instance of 
     * {@link BeansMetaobjectProtocol}, an instance of 
     * {@link ListMetaobjectProtocol}, an instance of 
     * {@link MapMetaobjectProtocol} (in order of your preference), and an 
     * instance of {@link BottomMetaobjectProtocol} to seal it all. The method
     * {@link #createStandardFallback(boolean, boolean)} can be used to 
     * conveniently create this standard fallback MOP configuration.
     * @return a MetaobjectProtocol that optimally composes all prioritized, 
     * automatically discovered, and fallback MOPs. Those automatically 
     * discovered MOPs that are of the same class as any of the prioritized or
     * fallback MOPs are ignored.
     * @throws IOException
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws ClassNotFoundException
     */
    public static MetaobjectProtocol createStandardMetaobjectProtocol(
            ClassLoader classLoader, MetaobjectProtocol[] prioritized, 
            MetaobjectProtocol[] fallbacks) 
    throws IOException, InstantiationException, IllegalAccessException, 
    ClassNotFoundException {
        // Treat nulls appropriately 
        if(prioritized == null) {
            prioritized = new MetaobjectProtocol[0];
        }
        if(fallbacks == null) {
            fallbacks = new MetaobjectProtocol[0];
        }
        // Perform discovery
        MetaobjectProtocol[] discovered = 
            MetaobjectProtocolAdaptor.toMetaobjectProtocols(
        	    AutoDiscovery.discoverBaseMetaobjectProtocols(classLoader));
    
        // Gather classes of all precreated (prioritized and fallback) MOPs.
        // We'll filter out any discovered MOPs of the same class.
        Set<Class<? extends MetaobjectProtocol>> knownMopClasses = 
            new HashSet<Class<? extends MetaobjectProtocol>>();
        for (MetaobjectProtocol protocol : prioritized) {
            knownMopClasses.add(protocol.getClass());
        }
        for (MetaobjectProtocol protocol : fallbacks) {
            knownMopClasses.add(protocol.getClass());
        }
        
        // Now, concatenate ...
        List<MetaobjectProtocol> mops = new ArrayList<MetaobjectProtocol>(
        	prioritized.length + discovered.length + fallbacks.length);
        // ... prioritized MOPs, ... 
        mops.addAll(Arrays.asList(prioritized));
        // ... filtered discovered MOPs, ... 
        for (int i = 0; i < discovered.length; i++) {
            MetaobjectProtocol protocol = discovered[i];
            if(!knownMopClasses.contains(protocol.getClass())) {
        	mops.add(protocol);
            }
        }
        // ... and finally fallback MOPs. 
        mops.addAll(Arrays.asList(fallbacks));
        
        // Optimize the final sequence for class-based lookup
        MetaobjectProtocol[] optimized = 
            CompositeClassBasedMetaobjectProtocol.optimize(mops.toArray(
        	    new MetaobjectProtocol[mops.size()]));
        
        switch(optimized.length) {
            case 0: {
        	return new BottomMetaobjectProtocol();
            }
            case 1: {
        	return optimized[0];
            }
            default: {
        	return new CompositeMetaobjectProtocol(optimized);
            }
        }
    }

    /**
     * Creates "standard" fallback MOP sequence to use as the fallback 
     * parameter in {@link #createStandardMetaobjectProtocol(ClassLoader, 
     * MetaobjectProtocol[], MetaobjectProtocol[])}. The sequence consists of
     * a {@link BeansMetaobjectProtocol}, {@link MapMetaobjectProtocol},
     * {@link ListMetaobjectProtocol}, and ultimately of a 
     * {@link BottomMetaobjectProtocol}.
     * @param elementsShadowMethods if true, map and list MOPs are put before
     * the beans MOP (thus, map elements will hide bean properties and 
     * methods). If false, beans MOP is put first (thus, bean properties and
     * methods will shadow map elements). Note that List and Map MOPs implement 
     * {@link ClassBasedMetaobjectProtocol} while beans MOP does not, so you'll 
     * generally get better performance if you pass true for this parameter.
     * @return an array containing the four standard fallback MOPs.
     * @param methodsEnumerable if true, then objects' methods and JavaBeans 
     * properties show up in the results of 
     * {@link BaseMetaobjectProtocol#properties(Object)} and 
     * {@link MetaobjectProtocol#propertyIds(Object)}. If false, these can 
     * still be accessed by directly addressing them, but don't show up in 
     * aforementioned methods' results. 
     */
    public static MetaobjectProtocol[] createStandardFallback(
            boolean elementsShadowMethods, boolean methodsEnumerable) {
        MetaobjectProtocol[] fallback = new MetaobjectProtocol[4];
        MetaobjectProtocol listMop = new ListMetaobjectProtocol();
        MetaobjectProtocol mapMop = new MapMetaobjectProtocol();
        MetaobjectProtocol beansMop = new BeansMetaobjectProtocol(methodsEnumerable);
        if(elementsShadowMethods) {
            fallback[0] = mapMop;
            fallback[1] = listMop;
            fallback[2] = beansMop;
        }
        else {
            fallback[0] = beansMop;
            fallback[1] = mapMop;
            fallback[2] = listMop;
        }
        fallback[3] = new BottomMetaobjectProtocol();
        
        return fallback;
    }

}
