/*
* 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.Externalizable;
import java.io.IOException;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;

import org.apache.log4j.Logger;
import org.jboss.serial.exception.SerializationException;
import org.jboss.serial.references.MethodPersistentReference;
import org.jboss.serial.references.PersistentReference;
import org.jboss.serial.util.ClassMetaConsts;
import org.jboss.serial.util.HashStringUtil;

/**
 * @author clebert suconic
 */
public class ClassMetaData implements ClassMetaConsts
{
	
	private static final Logger log = Logger.getLogger(ClassMetaData.class);
   	private static final boolean isDebug = log.isDebugEnabled();
	
    static ConstructorManager[] constructorManagers = {new SunConstructorManager(), new DefaultConstructorManager()};

	
    /** Used to reconstruct the Ghost Constructor used by Serialization's specification */
    static class GhostConstructorPersistentReference extends PersistentReference
    {
    	GhostConstructorPersistentReference (Class clazz, Constructor constructor)
    	{
    		super(clazz,constructor,REFERENCE_TYPE_IN_USE);
    	}

    	public synchronized Object rebuildReference() throws Exception
    	{
    		Object returnValue=null;
    		if ((returnValue=internalGet())!=null) return returnValue;;

    		if (isDebug)
    		{
    			log.debug("Constructor being rebuilt for " + getMappedClass());
    		}
    		Constructor constructorUsed = findConstructor(getMappedClass());
    		buildReference(constructorUsed);
    		return constructorUsed;
    	}
    	
    	public Constructor getConstructor()
    	{
    		return (Constructor)get();
    	}
    }
    
    private Method lookupMethodOnHierarchy(Class clazz, String methodName, Class reflectionArguments[])
    {
    	if (isDebug)
    	{
    		if (clazz.isPrimitive())
    		{
    			log.debug("lookupMethodOnHierarchy::Loookup made on a primitive class");
    		}
    		log.debug("lookupMethodOnHierarchy::class=" + clazz.getName() + " looking for " + methodName);
    	}
    	Class currentClass = clazz;
    	while (currentClass!=Object.class && currentClass!=null)
    	{
    		if (isDebug)
    		{
    			log.debug("lookupMethodOnHierarchy::currentClass=" + currentClass);
    		}
    		try {
				Method method = currentClass.getDeclaredMethod(methodName,reflectionArguments);
				if (method.getReturnType() == Object.class)
				{
					return method;
				}
			} 
    		catch (Exception ignored) {
			} 
    		currentClass=currentClass.getSuperclass();
    	}
    	
    	return null;
    }
    
	public ClassMetaData(Class clazz)
	{
        setClassName(clazz.getName());
        setClazz(clazz);
        setShaHash(HashStringUtil.hashName(clazz.getName()));
        setProxy(Proxy.isProxyClass(clazz));
        lookupInternalMethods(clazz);

        

        try
        {
            setConstructor(findConstructor(clazz));
        }
        catch (NoSuchMethodException e)
        {
            setConstructor(null);
        } 

        setExternalizable(Externalizable.class.isAssignableFrom(clazz));
        setSerializable(Serializable.class.isAssignableFrom(clazz));

        exploreSlots(clazz);
	}
    /**
     * @todo - Is there a way in javassist to create a constructor?
     */
    PersistentReference constructor = emptyReference;

    WeakReference clazz;
	WeakReference arrayRep;

    String className;

    boolean isArray;

    int arrayDepth;

    boolean isProxy;

    boolean isExternalizable;
    
    boolean isSerializable;
    
    long shaHash;
    
    PersistentReference readResolveMethod = emptyReference;
    PersistentReference writeReplaceMethod = emptyReference;
    
    ClassMetaDataSlot[] slots;
    
    public ClassMetaDataSlot[] getSlots()
    {
    	return slots;
    }

    /**
     * @return Returns the className.
     */
    public String getClassName()
    {
        return className;
    }

    /**
     * @param className
     *            The className to set.
     */
    public void setClassName(String className)
    {
        this.className = className;
    }

    private void calculateDepthAndName(Class clazz)
    {
        arrayDepth = 0;
        while (clazz.isArray())
        {
            arrayDepth++;
            clazz = clazz.getComponentType();
        }
        this.clazz=new WeakReference(clazz);
    }

    /**
     * @return Returns the clazz.
     */
    public Class getClazz()
    {
    	if (clazz==null) return null;
    	else
        return (Class)clazz.get();
    }
    
    public Class getArrayRepresentation()
    {
    	if (arrayRep==null) return null;
    	else
    	return (Class)arrayRep.get();
    }
    
    private void constructArrayRepresentationClass(Class clazz)
    {
		arrayRep = new WeakReference(clazz);
    }

    /**
     * @param clazz
     *            The clazz to set.
     */
    public void setClazz(Class clazz)
    {
    	if (clazz==null) 
    	{
    		this.clazz=null;
    	}
    	else
    	{
	        this.clazz = new WeakReference(clazz);
	        if (clazz.isArray())
	        {
	            this.setArray(true);
	            calculateDepthAndName(clazz);
	            constructArrayRepresentationClass(clazz);
	        }
    	}

    }

    /**
     * @return Returns the constructor.
     */
    public Constructor getConstructor()
    {
        return (Constructor)constructor.get();
    }

    /**
     * @param constructor
     *            The constructor to set.
     */
    public void setConstructor(Constructor constructor)
    {
        if (constructor != null)
        {
            constructor.setAccessible(true);
        }
        this.constructor = new GhostConstructorPersistentReference(this.getClazz(), constructor);
    }

    /**
     * @return Returns the isExternalizable.
     */
    public boolean isExternalizable()
    {
        return isExternalizable;
    }
    /**
     * @param isExternalizable The isExternalizable to set.
     */
    public void setExternalizable(boolean isExternalizable)
    {
        this.isExternalizable = isExternalizable;
    }
    
    public boolean isSerializable() {
		return isSerializable;
	}

	public void setSerializable(boolean isSerializable) {
		this.isSerializable = isSerializable;
	}

	public int hashCode()
    {
        return className.hashCode();
    }

    public boolean equals(Object obj)
    {
        return className.equals(((ClassMetaData)obj).className);
    }


    /**
     * @return Returns the isArray.
     */
    public boolean isArray()
    {
        return isArray;
    }
    /**
     * @param isArray The isArray to set.
     */
    public void setArray(boolean isArray)
    {
        this.isArray = isArray;
    }


    /**
     * @return Returns the arrayDepth.
     */
    public int getArrayDepth()
    {
        return arrayDepth;
    }

    /**
     * @return
     */
    public Object newInstance() throws IOException
    {
    	Constructor localConstructor = getConstructor();
        try
        {
            if (localConstructor==null)
            {
                return this.getClazz().newInstance();
            } else
            {
                return localConstructor.newInstance(EMPTY_OBJECT_ARRAY);
            }
        }
        catch (InstantiationException e)
        {
            throw new SerializationException("Could not create instance of " + this.className + " - " + e.getMessage(),e);
        }
        catch (IllegalAccessException e)
        {
            throw new SerializationException("Could not create instance of " + this.className + " - " + e.getMessage(),e);
        }
        catch (InvocationTargetException e)
        {
            throw new SerializationException("Could not create instance of " + this.className + " - " + e.getMessage(),e);
        }
    }

    public Method getReadResolveMethod()   {
        return (Method)readResolveMethod.get();
    }

    public void setReadResolveMethod(Method readResolveMethod) {
        this.readResolveMethod = new MethodPersistentReference(readResolveMethod,REFERENCE_TYPE_IN_USE);
    }

    public boolean isProxy() {
        return isProxy;
    }

    public void setProxy(boolean proxy) {
        isProxy = proxy;
    }

	public Method getWriteReplaceMethod() {
		return (Method)writeReplaceMethod.get();
	}

	public void setWriteReplaceMethod(Method writeReplaceMethod) {
		this.writeReplaceMethod = new MethodPersistentReference(writeReplaceMethod,REFERENCE_TYPE_IN_USE);
	}

    public long getShaHash() {
		return shaHash;
	}

	public void setShaHash(long shaHash) {
		this.shaHash = shaHash;
	}

	private static Constructor findConstructor(Class clazz) throws NoSuchMethodException
    {
    	if (clazz.isInterface())
    	{
    		return null;
    	}
        for (int i=0;i<constructorManagers.length;i++)
        {
            if (constructorManagers[i].isSupported())
            {
                return constructorManagers[i].getConstructor(clazz);
            }
        }
        // I don't expect this exceptiong being thrown unless the JVM doesn't support reflection at al
        throw new NoSuchMethodException("Constructor not found as having difficulties in reflection");
    }

    private void exploreSlots(Class clazz)
    {
    	ArrayList slots = new ArrayList();
        // if it's externalizable we won't be using any fields
        if (!this.isExternalizable() && !this.isArray && this.isSerializable())
        {
            for (Class classIteration = clazz;classIteration!=null && Serializable.class.isAssignableFrom(classIteration);classIteration=classIteration.getSuperclass())
            {
            	ClassMetaDataSlot slot = new ClassMetaDataSlot(classIteration);
            	slots.add(slot);
            }
        }
        else
        {
        	ClassMetaDataSlot slot = new ClassMetaDataSlot(clazz);
        	slots.add(slot);
        }
        
    	Collections.reverse(slots);
        this.slots = (ClassMetaDataSlot[])slots.toArray(new ClassMetaDataSlot[slots.size()]);
    }

    private void lookupInternalMethods(Class clazz)
    {
    	if (clazz.isInterface())
    	{
    		return;
    	}
    		
        Method method = lookupMethodOnHierarchy(clazz,"readResolve",EMPTY_CLASS_ARRY);
        if (method!=null)
        {
            method.setAccessible(true);
            this.setReadResolveMethod(method);
        }
        
    	method = lookupMethodOnHierarchy(clazz,"writeReplace",EMPTY_CLASS_ARRY);
    	if (method!=null)
    	{
        	method.setAccessible(true);
        	setWriteReplaceMethod(method);
    	}
    }

	
    
}
