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

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;

import org.apache.log4j.Logger;
import org.jboss.serial.classmetamodel.ClassMetaData;
import org.jboss.serial.classmetamodel.ClassMetamodelFactory;
import org.jboss.serial.classmetamodel.StreamingClass;
import org.jboss.serial.exception.SerializationException;
import org.jboss.serial.finalcontainers.BooleanContainer;
import org.jboss.serial.finalcontainers.ByteContainer;
import org.jboss.serial.finalcontainers.CharacterContainer;
import org.jboss.serial.finalcontainers.DoubleContainer;
import org.jboss.serial.finalcontainers.FloatContainer;
import org.jboss.serial.finalcontainers.IntegerContainer;
import org.jboss.serial.finalcontainers.LongContainer;
import org.jboss.serial.finalcontainers.ShortContainer;
import org.jboss.serial.objectmetamodel.ObjectsCache.JBossSeralizationInputInterface;
import org.jboss.serial.objectmetamodel.ObjectsCache.JBossSeralizationOutputInterface;
import org.jboss.serial.persister.ClassReferencePersister;
import org.jboss.serial.persister.PersistResolver;
import org.jboss.serial.persister.Persister;
import org.jboss.serial.util.ClassMetaConsts;
import org.jboss.serial.util.StringUtil;


/**
 * @author clebert suconic
 */
public class ObjectDescriptorFactory implements ClassMetaConsts
{
	private static final Logger log = Logger.getLogger(ObjectDescriptorFactory.class);
   	private static final boolean isDebug = log.isDebugEnabled();

    static Object objectFromDescription(final ObjectsCache cache,
    									ObjectsCache.JBossSeralizationInputInterface input)
            throws IOException
    {
        Object description = null;

        byte byteIdentify = (byte)cache.getInput().readByte();

        if (byteIdentify==DataContainerConstants.RESET)
        {
        	cache.reset();
        	return objectFromDescription(cache,input);
        }
        if (byteIdentify==DataContainerConstants.NULLREF)
        {
        	return null;
        }
        else
        if (byteIdentify==DataContainerConstants.NEWDEF)
        {
        	if (isDebug)
        	{
        		log.debug("objectFromDescription::reading new definition");
        	}
            return readObjectDescriptionFromStreaming(cache,input.readObjectReference(),input);
        }
        else if (byteIdentify==DataContainerConstants.SMARTCLONE_DEF)
        {
        	int reference=input.readObjectReference();
        	if (isDebug)
        	{
        		log.debug("objectFromDescription::reading reference from safeClone=" + reference);
        	}
    		if (cache.getSafeToReuse()==null)
    		{
    			throw new IOException("SafeClone repository mismatch");
    		}
    		description = cache.getSafeToReuse().findReference(reference);
    		if (description==null)
    		{
    			throw new IOException("SafeClone repository mismatch - didn't find reference " + reference);
    		}
    		return description;
        	
        }
        else if (byteIdentify==DataContainerConstants.OBJECTREF)
        {
        	int reference=input.readObjectReference();
        	if (isDebug)
        	{
        		log.debug("objectFromDescription::reading circular definition reference=" + reference);
        	}

        	if (description==null)
            {
                description = cache.findObjectInCacheRead(reference);
            }

            if (description==null)
            {
                throw new SerializationException("Object reference " + reference + " was not found");
            }

            return description;
        }
        else
        {
        	return input.readImmutable(byteIdentify,cache);
        }

    }

    


	/** First level of a describe object, will look if it's a newDef, or if it's already loaded.
     *  If the object was never loaded before, it will call readObjectDescriptionFromStreaming.
     *  If it was already loaded, it will just return from objectCache */
    static void describeObject(final ObjectsCache cache,
                                            Object obj) throws IOException
    {

        ObjectsCache.JBossSeralizationOutputInterface outputParent = cache.getOutput();
        
    	if (obj==null)
    	{
    		outputParent.writeByte(DataContainerConstants.NULLREF);
    		return;
    	}
        if (obj!=null && ClassMetamodelFactory.isImmutable(obj.getClass()))
        {
        	outputParent.saveImmutable(cache,obj);
        	return;
        }

        if (isDebug)
        {
			if (obj==null)
	        {
				log.debug("obj==null",new Exception());
	        }
        }
        if (isDebug)
        {
        	log.debug("describeObject for class=" + obj.getClass().getName());
        }

        ClassMetaData metaData = null;
        if (obj instanceof Class)
        {
        	metaData = ClassMetamodelFactory.getClassMetaData((Class)obj,cache.isCheckSerializableClass());
        }
        else
        {
           	metaData = ClassMetamodelFactory.getClassMetaData(obj.getClass(),cache.isCheckSerializableClass());
            if (metaData.getWriteReplaceMethod()!=null)
            {
            	if (isDebug)
            	{
            		log.debug("describeObject::Calling writeReplace for " + metaData.getClazz().getName());
            	}
            	try {
            		Object orig = obj;
    				obj = metaData.getWriteReplaceMethod().invoke(obj,EMPTY_OBJECT_ARRAY);
    				if (obj!=null && obj!=orig && obj.getClass()!=metaData.getClazz())
    				{
    					if (isDebug)
    					{
    						log.debug("originalObject=" + orig.getClass().getName() + " substituted by " + obj.getClass().getName());
    					}
    					describeObject(cache,obj);
    					return;
    				}
    				metaData = ClassMetamodelFactory.getClassMetaData(obj.getClass(),cache.isCheckSerializableClass());
    			} catch (Exception e) {
    				IOException io = new IOException("Metadata Serialization Error");
    				io.initCause(e);
    				throw io;
    			}
            }
        }

        if (cache.getSubstitution()!=null)
        {
        	if (isDebug)
        	{
        		log.debug("describeObject::checking substitution on interface");
        	}
        	Object orig = obj;
            obj = cache.getSubstitution().replaceObject(obj);
			if (obj!=null && obj!=orig && obj.getClass()!=metaData.getClazz())
			{
	        	if (isDebug)
	        	{
	        		log.debug("describeObject::on check interface, original object[" + orig.getClass().getName() +"] was replaced by [" + obj.getClass().getName() + "]");
	        	}
				describeObject(cache,obj);
				return;
			}
        }
        

        int description = 0;

        if (cache.getSafeToReuse()!=null)
        {
            description = cache.getSafeToReuse().storeSafe(obj);
            if (description!=0)
            {
            	if (isDebug)
            	{
            		log.debug("describeObject::a safeClone reference " + description);
            	}
                outputParent.writeByte(DataContainerConstants.SMARTCLONE_DEF);
            	cache.getOutput().addObjectReference(description);
                return;
            }
            
        }

        description = cache.findIdInCacheWrite(obj);

        if (description != 0)
        {
        	if (isDebug)
        	{
        		log.debug("describeObject::a circular reference " + description);
        	}
            outputParent.writeByte(DataContainerConstants.OBJECTREF);
            cache.getOutput().addObjectReference(description);
            return;
        } else
        {
        	description = cache.putObjectInCacheWrite(obj);
        	if (isDebug)
        	{
        		log.debug("describeObject::a new reference " + description);
        	}
            outputParent.writeByte(DataContainerConstants.NEWDEF);
            cache.getOutput().addObjectReference(description);
            
            int cacheId = cache.findIdInCacheWrite(metaData);
            if (cacheId==0)
            {
            	cacheId = cache.putObjectInCacheWrite(metaData);
            	outputParent.writeByte(DataContainerConstants.NEWDEF);
            	outputParent.addObjectReference(cacheId);
            	StreamingClass.saveStream(metaData,outputParent);
            }
            else
            {
            	outputParent.writeByte(DataContainerConstants.OBJECTREF);
            	outputParent.addObjectReference(cacheId);
            }
            
            Persister persister = PersistResolver.resolvePersister(obj,metaData);

            outputParent.writeByte(persister.getId());
            persister.writeData(metaData, cache.getOutput(), obj, cache.getSubstitution());

            return;
        }
    }

    /* private static Object readImmutable(byte byteDescription,ObjectsCache cache, JBossSeralizationInputInterface input) throws IOException {
		
    	Object retObject=null;
    	switch (byteDescription)
    	{
    	case DataContainerConstants.STRING:
    		retObject = StringUtil.readString(input,cache.getStringBuffer());break;
    	case DataContainerConstants.BYTE:
    		retObject = new Byte(input.readByte());break;
    	case DataContainerConstants.CHARACTER:
    		retObject = new Character(input.readChar());break;
    	case DataContainerConstants.SHORT:
    		retObject = new Short(input.readShort());break;
    	case DataContainerConstants.INTEGER:
    		retObject = new Integer(input.readInt());break;
    	case DataContainerConstants.LONG:
    		retObject = new Long(input.readLong());break;
    	case DataContainerConstants.DOUBLE:
    		retObject = new Double(input.readDouble());break;
    	case DataContainerConstants.FLOAT:
    		retObject = new Float(input.readFloat());break;
    	case DataContainerConstants.BOOLEAN:
    		retObject = new Boolean(input.readBoolean());break;
    	}
    	
    	if (isDebug)
    	{
    		log.debug("byteDescription=" + byteDescription);
    		log.debug("readImmutable return=" + retObject + " class=" + retObject.getClass().getName());
    	}
    	
    	return retObject;
	} */

    /* private static void saveImmutable(ObjectsCache cache, Object obj, JBossSeralizationOutputInterface output) throws IOException {
    	if (isDebug)
    	{
    		log.debug("saveImmutable::obj=" +obj + " class=" + obj.getClass().getName());
    	}
        if (obj instanceof String)
        {
        	output.writeByte(DataContainerConstants.STRING);
        	StringUtil.saveString(output,(String)obj,cache.getStringBuffer());
        } else
        if (obj instanceof Byte)
        {
        	output.writeByte(DataContainerConstants.BYTE);
        	output.writeByte(((Byte)obj).byteValue());
        } else
        if (obj instanceof Character)
        {
        	output.writeByte(DataContainerConstants.CHARACTER);
        	output.writeChar(((Character)obj).charValue());
        } else
        if (obj instanceof Short)
        {
        	output.writeByte(DataContainerConstants.SHORT);
        	output.writeShort(((Short)obj).shortValue());
        } else
        if (obj instanceof Integer)
        {
        	output.writeByte(DataContainerConstants.INTEGER);
        	output.writeInt(((Integer)obj).intValue());
        } else
        if (obj instanceof Long)
        {
        	output.writeByte(DataContainerConstants.LONG);
        	output.writeLong(((Long)obj).longValue());
        } else
        if (obj instanceof Double)
        {
        	output.writeByte(DataContainerConstants.DOUBLE);
        	output.writeDouble(((Double)obj).doubleValue());
        } else
        if (obj instanceof Float)
        {
        	output.writeByte(DataContainerConstants.FLOAT);
        	output.writeFloat(((Float)obj).floatValue());
        } else
        if (obj instanceof BooleanContainer || obj instanceof Boolean)
        {
        	output.writeByte(DataContainerConstants.BOOLEAN);
        	output.writeBoolean(((Boolean)obj).booleanValue());
        } else
        {
            throw new SerializationException("I don't know how to write type " + obj.getClass().getName() + " yet");
        }
    	
	} */


	private static Object readObjectDescriptionFromStreaming(final ObjectsCache cache,
                                        final int reference, ObjectsCache.JBossSeralizationInputInterface input) throws IOException
    {
    	byte defClass = input.readByte();
    	StreamingClass streamingClass = null;
    	if (defClass==DataContainerConstants.NEWDEF)
    	{
    		int referenceId = input.readObjectReference();
    		streamingClass = StreamingClass.readStream(input,cache.getClassResolver(),cache.getLoader());
    		cache.putObjectInCacheRead(referenceId,streamingClass);
    	}
    	else
    	{
    		int referenceId = input.readObjectReference();
    		streamingClass = (StreamingClass)cache.findObjectInCacheRead(referenceId);
    		if (streamingClass==null)
    		{
    			throw new IOException("Didn't find StreamingClass circular refernce id=" + referenceId);
    		}
    		
    	}

        ClassMetaData metaData = streamingClass.getMetadata();

        if (isDebug)
        {
        	log.debug("Reading object for id=" + reference + " classLoader=" + cache.getLoader() + " className = " + metaData.getClassName());
        }
        
        byte persisterId = input.readByte();
        Persister persister = PersistResolver.resolvePersister(persisterId);
        //Persister persister = PersistResolver.resolvePersister(description.getMetaData().getClazz(),
        //        description.getMetaData(),description.getMetaData().isArray());

        /*ObjectDescription description = new ObjectDescription();
        description.setMetaData(ClassMetamodelFactory.getClassMetaData(reference.getClassName(),cache.getLoader(),false));
        cache.putObjectInCache(reference,description); */


        Object value = persister.readData(cache.getLoader(), streamingClass, metaData, reference, cache, cache.getInput(), cache.getSubstitution());

        if (!(persister instanceof ClassReferencePersister)) //JBSER-83
        {
	        if (cache.getSubstitution()!=null)
	        {
	            value = cache.getSubstitution().replaceObject(value);
	        }
	
	        try
	        {
	            if (metaData.getReadResolveMethod()!=null)
	            {
	            	if (isDebug)
	            	{
	            		log.debug("readObjectDescriptionFromStreaming::calling readResolve for className = " + metaData.getClassName());
	            	} 
	                value = metaData.getReadResolveMethod().invoke(value,new Object[]{});
	                cache.reassignObjectInCacheRead(reference,value);
	            }
	        }
	        catch (IllegalAccessException e)
	        {
	            throw new SerializationException(e);
	        }
	        catch (InvocationTargetException e)
	        {
	            throw new SerializationException(e);
	        }
        }

        return value;
    }

}
