/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/

package org.jboss.serial.classmetamodel;

import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectStreamClass;
import java.io.ObjectStreamField;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.log4j.Logger;
import org.jboss.serial.util.ClassMetaConsts;
import org.jboss.serial.util.FastHashMap;
import org.jboss.serial.util.PartitionedWeakHashMap;

/**
 * $Id: ClassMetamodelFactory.java,v 1.48 2006/07/28 04:57:13 csuconic Exp $
 * @author clebert suconic
 */
public class ClassMetamodelFactory implements ClassMetaConsts
{
	private static final Logger log = Logger.getLogger(ClassMetamodelFactory.class);
   	private static final boolean isDebug = log.isDebugEnabled();

    /** table mapping primitive type names to corresponding class objects.
     *  This code was created by Carlos de Wolf on a fix of EJBTHREE-440.
     *  By coincidence I needed to parse primitive values, and instead of reinventing the wheel I got the same code. */
    private static final HashMap primClasses = new HashMap(8, 1.0F);
    static {
       primClasses.put("boolean", boolean.class);
       primClasses.put("byte", byte.class);
       primClasses.put("char", char.class);
       primClasses.put("short", short.class);
       primClasses.put("int", int.class);
       primClasses.put("long", long.class);
       primClasses.put("float", float.class);
       primClasses.put("double", double.class);
       primClasses.put("void", void.class);
       primClasses.put("[Z", boolean[].class);
       primClasses.put("[B", byte[].class);
       primClasses.put("[C", char[].class);
       primClasses.put("[S", short[].class);
       primClasses.put("[I", int[].class);
       primClasses.put("[J", long[].class);
       primClasses.put("[F", float[].class);
       primClasses.put("[D", double[].class);
    }
   	
    /** We are caching the getClassLoader operation, to avoid locks on the WeakHashMap */
    private static class CacheLoaderReference
    {
    	WeakReference currentClassLoader;
    	Map currentHashMap;
    	
    	public ClassLoader getCurrentClassLoader()
    	{
    		if (currentClassLoader==null)
    			return null;
    		else
    			return (ClassLoader)currentClassLoader.get();
    	}
    	
    	public void setCurrentClassLoader(ClassLoader loader)
    	{
    		currentClassLoader = new WeakReference(loader);
    	}
    	
    	public Map getCurrentMap()
    	{
    		return currentHashMap;
    	}
    	
    	public void setCurrentMap(Map currentMap)
    	{
    		this.currentHashMap = currentMap;
    	}
    }
    

    /**
     * Method for ObjectStreamClass.lookup
     * */
    static Method methodLookup;

    /**
     * Method for ObjectStreamClass.getField
     */
    static Method methodGetField;


    /** The HashMap for SystemclassLoader */
    private static Map systemClassLoaderMap = new FastHashMap();

    private static ObjectStreamClass lookup(Class clazz) throws IllegalAccessException, InvocationTargetException
    {
        return (ObjectStreamClass) methodLookup.invoke(null,new Object[]{clazz,Boolean.TRUE});
    }

    private static Field getField(Object source) throws IllegalAccessException, InvocationTargetException
    {
        return (Field) methodGetField.invoke(source,EMPTY_OBJECT_ARRAY);
    }

    static ThreadLocal cacheLoader = new ThreadLocal();

    static
    {
        try
        {
            methodLookup = ObjectStreamClass.class.getDeclaredMethod("lookup",new Class[]{Class.class,Boolean.TYPE});
            methodLookup.setAccessible(true);
        }
        catch (Throwable e)
        {
            e.printStackTrace();
        }

        try
        {
            methodGetField = ObjectStreamField.class.getDeclaredMethod("getField",new Class[]{});
            methodGetField.setAccessible(true);
        }
        catch (Throwable e)
        {
            e.printStackTrace();
        }
    }


    public static void clear(boolean elimiteClassLoadersCached)
    {
    	Iterator iter = cache.values().iterator();
    	while (iter.hasNext())
    	{
    		Map map = (Map)iter.next();
    		map.clear();
    	}
    	if (elimiteClassLoadersCached) cache.clear();
    	systemClassLoaderMap.clear();
    }
    
    public static void clear()
    {
    	clear(true);
    }
    
    public static Map getCache()
    {
    	return cache;
    }
    
    public static void printCacheDebug()
    {
		java.util.Iterator iter = getCache().keySet().iterator();
		
		while (iter.hasNext())
		{
			Object loader = iter.next();
			log.debug("ClassLoader = " + loader);
			Map classesMap = (Map)getCache().get(loader);
			java.util.Iterator classesIterator = classesMap.keySet().iterator();
			while (classesIterator.hasNext())
			{
				log.debug("Class = " + classesIterator.next());
			}
			
		}
    }
    
    
    /** PartitionedWeakHashMap<ClassLoader,ConcurrentHashMap<String,ClassMetaData>> 
     *  (The current implementation is using FastHashMap instead of ConcurrentHashMap)
     * **/
    static PartitionedWeakHashMap cache = new PartitionedWeakHashMap();
    
    static ClassMetaData proxyMetaData = null;

    static
    {
        try
        {
            proxyMetaData=getClassMetaData("java.lang.reflect.Proxy",Thread.currentThread().getContextClassLoader(),true);
            proxyMetaData.setProxy(true);
        }
        catch (Exception e)
        {
            e.printStackTrace();;
        }
    }
    
    private static Map getLoaderMap(ClassLoader loader)
    {
    	if (loader==null)
    	{
    		return systemClassLoaderMap;
    	}
    	
    	CacheLoaderReference loaderReference = (CacheLoaderReference)cacheLoader.get();
    	if (loaderReference==null)
    	{
    		loaderReference = new CacheLoaderReference();
    		cacheLoader.set(loaderReference);
    	}
    	
    	if (loaderReference.getCurrentClassLoader()==loader)
    	{
    		return loaderReference.getCurrentMap();
    	}
    		
    	
    	Map hashMap = (Map)cache.get(loader);
    	
    	ClassLoader returnLoader = null;
    	if (hashMap==null)
    	{
    		hashMap = new FastHashMap();
    		cache.put(loader,hashMap);
    		hashMap= (Map)cache.get(loader);
    	} 
    	
    	loaderReference.setCurrentClassLoader(loader);
    	loaderReference.setCurrentMap(hashMap);
    	return hashMap;
    }
    private static ClassMetaData getClassMetaData(String clazzName, ClassLoader loader, boolean checkSerializable) throws IOException
    {
    	return getClassMetaData(clazzName,null,loader,checkSerializable);
    }
    

	private static Class resolveClassByName(String clazzName, ClassResolver resolver, ClassLoader loader) throws ClassNotFoundException 
	{
		Class clazz = (Class)primClasses.get(clazzName);
		
		if (clazz==null)
		{
			if (resolver!=null)
			{
				clazz = resolver.resolveClass(clazzName);
				if (clazz==null)
				{
					clazz = Class.forName(clazzName,false,loader); 
				}
			}
			else
			{
				clazz = Class.forName(clazzName,false,loader); 
			}
		}
		return clazz;
	}
	
    public static ClassMetaData getClassMetaData(String clazzName, ClassResolver resolver, ClassLoader loader, boolean checkSerializable) throws IOException
    {
    	try
    	{
	    	Map loaderMap = getLoaderMap(loader);
	    	ClassMetaData classMetadata = (ClassMetaData)loaderMap.get(clazzName);
	    	if (classMetadata==null)
	    	{
	    		
	    		Class clazz = resolveClassByName(clazzName, resolver, loader);

	            if (checkSerializable && !Serializable.class.isAssignableFrom(clazz))
	            {
	                throw new NotSerializableException(clazz.getName());
	            }
	    		classMetadata = new ClassMetaData(clazz);

	    		loaderMap = getLoaderMap(loader);
	    		loaderMap.put(clazzName,classMetadata);
	    		classMetadata = (ClassMetaData)loaderMap.get(clazzName);
	    	}
	    	
	    	if (classMetadata.getClazz()==null)
	    	{
	    		// this never happens, so, this is just in case
	    		// The only possibility for that would be crossed classLoader operations.
	    		// I tried to replicate this scenario on a testcase and it was not possible. 
	    		// So, I decided to have this verification here to be safe
	    		if (isDebug)
	    		{
	    			log.debug("Rebuilding clazz due to cross Loader GC (getClassMetaData(String class, ClassLoader loader)");
	    		}
	    		Class clazz = resolveClassByName(clazzName,resolver,loader); 
	    		loaderMap.remove(clazzName);
	    		classMetadata = new ClassMetaData(clazz);
	    		loaderMap.put(clazz.getName(),classMetadata);
	    		classMetadata = (ClassMetaData)loaderMap.get(clazzName);
	    	}
	    	
	    	
	        // It looks dummy but doing Class.isProxy is a very expensive operation, so we just do it on a cached basis
	        if (classMetadata.isProxy())
	        {
	            return proxyMetaData;
	        } else
	        {
	            return classMetadata;
	        }
    	}
    	catch (ClassNotFoundException e)
    	{
    		
    		IOException ex = new IOException (e.getMessage());
    		ex.initCause(e);
    		throw ex;
    	}

    }
    
    
    public static ClassMetaData getClassMetaData(Class clazz, boolean checkSerializable) throws IOException
    {
        if (checkSerializable && !Serializable.class.isAssignableFrom(clazz) && !clazz.isPrimitive())
        {
            throw new NotSerializableException(clazz.getName());
        }
    	Map loaderMap = getLoaderMap(clazz.getClassLoader());
    	ClassMetaData classMetadata = (ClassMetaData)loaderMap.get(clazz.getName());
    	if (classMetadata==null)
    	{
    		classMetadata = new ClassMetaData(clazz);
    		loaderMap.put(clazz.getName(),classMetadata);
    		classMetadata = (ClassMetaData)loaderMap.get(clazz.getName());
    	} 
    	if (classMetadata.getClazz()==null)
    	{
    		// this never happens, so, this is just in case
    		// The only possibility for that would be crossed classLoader operations.
    		// I tried to replicate this scenario on a testcase and it was not possible. 
    		// So, I decided to have this verification here to be safe
    		if (isDebug)
    		{
    			log.debug("Rebuilding clazz due to cross Loader GC (getClassMetaData(Class clazz, ClassLoader loader)");
    		}
    		loaderMap.remove(clazz.getName());
    		classMetadata = new ClassMetaData(clazz);
    		loaderMap.put(clazz.getName(),classMetadata);
    		classMetadata = (ClassMetaData)loaderMap.get(clazz.getName());
    	}
    	
        // It looks dummy but doing Class.isProxy is a very expensive operation, so we just do it on a cached basis
        if (classMetadata.isProxy())
        {
            return proxyMetaData;
        } else
        {
            return classMetadata;
        }
    }
    

    public static boolean isImmutable(Class clazz)
    {
        return (clazz==Character.class || clazz==String.class ||
                clazz == Long.class ||
                clazz == Byte.class ||
                clazz == Double.class ||
                clazz == Float.class ||
                clazz == Integer.class ||
                clazz == Short.class ||
                clazz == Boolean.class ||
                clazz.isPrimitive());
    }

}
