package org.jboss.cache.marshall;

import org.jboss.cache.CacheException;
import org.jboss.util.stream.MarshalledValueInputStream;

import java.io.*;
import java.util.Arrays;

/**
 * Wrapper that wraps cached data, providing lazy deserialization using the calling thread's context class loader.
 * <p/>
 * The {@link org.jboss.cache.interceptors.MarshalledValueInterceptor} handles transparent
 * wrapping/unwrapping of cached data.
 * <p/>
 *
 * @author Manik Surtani (<a href="mailto:manik@jboss.org">manik@jboss.org</a>)
 * @see org.jboss.cache.interceptors.MarshalledValueInterceptor
 * @since 2.1.0
 */
public class MarshalledValue implements Externalizable
{
   protected Object instance;
   protected byte[] raw;
   private int cachedHashCode = 0;
   // by default equals() will test on the istance rather than the byte array if conversion is required.
   private transient boolean equalityPreferenceForInstance = true;

   public MarshalledValue(Object instance) throws NotSerializableException
   {
      if (instance == null) throw new NullPointerException("Null values cannot be wrapped as MarshalledValues!");

      if (instance instanceof Serializable)
         this.instance = instance;
      else
         throw new NotSerializableException("Marshalled values can only wrap Objects that are serializable!  Instance of " + instance.getClass() + " won't Serialize.");
   }

   public MarshalledValue()
   {
      // empty ctor for serialization
   }

   public void setEqualityPreferenceForInstance(boolean equalityPreferenceForInstance)
   {
      this.equalityPreferenceForInstance = equalityPreferenceForInstance;
   }

   public synchronized void serialize()
   {
      if (raw == null)
      {
         try
         {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(instance);
            oos.close();
            baos.close();
            // Do NOT set instance to null over here, since it may be used elsewhere (e.g., in a cache listener).
            // this will be compacted by the MarshalledValueInterceptor when the call returns.
//            instance = null;
            raw = baos.toByteArray();
         }
         catch (Exception e)
         {
            throw new CacheException("Unable to marshall value " + instance, e);
         }
      }
   }

   public synchronized void deserialize()
   {
      if (instance == null)
      {
         try
         {
            ByteArrayInputStream bais = new ByteArrayInputStream(raw);
            // use a MarshalledValueInputStream since it needs to be aware of any context class loaders on the current thread.
            ObjectInputStream ois = new MarshalledValueInputStream(bais);
            instance = ois.readObject();
            ois.close();
            bais.close();
//            raw = null;
         }
         catch (Exception e)
         {
            throw new CacheException("Unable to unmarshall value", e);
         }
      }
   }

   /**
    * Compacts the references held by this class to a single reference.  If only one representation exists this method
    * is a no-op unless the 'force' parameter is used, in which case the reference held is forcefully switched to the
    * 'preferred representation'.
    * <p/>
    * Either way, a call to compact() will ensure that only one representation is held.
    * <p/>
    *
    * @param preferSerializedRepresentation if true and both representations exist, the serialized representation is favoured.  If false, the deserialized representation is preferred.
    * @param force                          ensures the preferred representation is maintained and the other released, even if this means serializing or deserializing.
    */
   public void compact(boolean preferSerializedRepresentation, boolean force)
   {
      // reset the equalityPreference
      equalityPreferenceForInstance = true;
      if (force)
      {
         if (preferSerializedRepresentation && raw == null) serialize();
         else if (!preferSerializedRepresentation && instance == null) deserialize();
      }

      if (instance != null && raw != null)
      {
         // need to lose one representation!

         if (preferSerializedRepresentation)
         {
            instance = null;
         }
         else
         {
            raw = null;
         }
      }
   }

   public void writeExternal(ObjectOutput out) throws IOException
   {
      if (raw == null) serialize();
      out.writeInt(raw.length);
      out.write(raw);
      out.writeInt(hashCode());
   }

   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
   {
      int size = in.readInt();
      raw = new byte[size];
      cachedHashCode = 0;
      in.readFully(raw);
      cachedHashCode = in.readInt();
   }

   public Object get() throws IOException, ClassNotFoundException
   {
      if (instance == null) deserialize();
      return instance;
   }

   @Override
   public boolean equals(Object o)
   {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      MarshalledValue that = (MarshalledValue) o;

      // if both versions are serialized or deserialized, just compare the relevant representations.
      if (raw != null && that.raw != null) return Arrays.equals(raw, that.raw);
      if (instance != null && that.instance != null) return instance.equals(that.instance);

      // if conversion of one representation to the other is necessary, then see which we prefer converting.
      if (equalityPreferenceForInstance)
      {
         if (instance == null) deserialize();
         if (that.instance == null) that.deserialize();
         return instance.equals(that.instance);
      }
      else
      {
         if (raw == null) serialize();
         if (that.raw == null) that.serialize();
         return Arrays.equals(raw, that.raw);
      }
   }

   @Override
   public int hashCode()
   {
      if (cachedHashCode == 0)
      {
         // always calculate the hashcode based on the instance since this is where we're getting the equals()
         if (instance == null) deserialize();
         cachedHashCode = instance.hashCode();
         if (cachedHashCode == 0) // degenerate case
         {
            cachedHashCode = 0xFEED;
         }
      }
      return cachedHashCode;
   }

   @Override
   public String toString()
   {
      return "MarshalledValue(cachedHashCode=" + cachedHashCode + "; serialized=" + (raw != null) + ")";
   }
}
