package org.jboss.cache.remoting.jgroups;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.CacheException;
import org.jboss.cache.Fqn;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.factories.annotations.NonVolatile;
import org.jboss.cache.io.ExposedByteArrayOutputStream;
import org.jboss.cache.statetransfer.StateTransferManager;
import org.jboss.util.stream.MarshalledValueInputStream;
import org.jboss.util.stream.MarshalledValueOutputStream;
import org.jgroups.ExtendedMessageListener;
import org.jgroups.Message;
import org.jgroups.util.Util;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * JGroups MessageListener
 *
 * @author Manik Surtani (<a href="mailto:manik@jboss.org">manik@jboss.org</a>)
 * @since 2.1.0
 */
@NonVolatile
public class ChannelMessageListener implements ExtendedMessageListener
{
   /**
    * Reference to an exception that was raised during
    * state installation on this node.
    */
   protected volatile Exception setStateException;
   private final Object stateLock = new Object();
   private Log log = LogFactory.getLog(ChannelMessageListener.class);
   private StateTransferManager stateTransferManager;
   private Configuration configuration;
   /**
    * True if state was initialized during start-up.
    */
   private volatile boolean isStateSet = false;

   @Inject
   private void injectDependencies(StateTransferManager stateTransferManager, Configuration configuration)
   {
      this.stateTransferManager = stateTransferManager;
      this.configuration = configuration;
   }

   public boolean isStateSet()
   {
      return isStateSet;
   }

   public void setStateSet(boolean stateSet)
   {
      isStateSet = stateSet;
   }

   public void waitForState() throws Exception
   {
      synchronized (stateLock)
      {
         while (!isStateSet)
         {
            if (setStateException != null)
            {
               throw setStateException;
            }

            try
            {
               stateLock.wait();
            }
            catch (InterruptedException iex)
            {
            }
         }
      }
   }

   protected void stateReceivedSuccess()
   {
      isStateSet = true;
      setStateException = null;
   }

   protected void stateReceivingFailed(Throwable t)
   {
      if (t instanceof CacheException)
      {
         log.debug(t);
      }
      else
      {
         log.error("failed setting state", t);
      }
      if (t instanceof Exception)
      {
         setStateException = (Exception) t;
      }
      else
      {
         setStateException = new Exception(t);
      }
   }

   protected void stateProducingFailed(Throwable t)
   {
      if (t instanceof CacheException)
      {
         log.debug(t);
      }
      else
      {
         log.error("Caught " + t.getClass().getName()
               + " while responding to state transfer request", t);
      }
   }

   /**
    * Callback, does nothing.
    */
   public void receive(Message msg)
   {
   }

   public byte[] getState()
   {
      MarshalledValueOutputStream out = null;
      byte[] result;
      ExposedByteArrayOutputStream baos = new ExposedByteArrayOutputStream(16 * 1024);
      try
      {
         out = new MarshalledValueOutputStream(baos);

         stateTransferManager.getState(out, Fqn.ROOT, configuration.getStateRetrievalTimeout(), true, true);
      }
      catch (Throwable t)
      {
         stateProducingFailed(t);
      }
      finally
      {
         result = baos.getRawBuffer();
         Util.close(out);
      }
      return result;
   }

   public void setState(byte[] new_state)
   {
      if (new_state == null)
      {
         log.debug("transferred state is null (may be first member in cluster)");
         return;
      }
      ByteArrayInputStream bais = new ByteArrayInputStream(new_state);
      MarshalledValueInputStream in = null;
      try
      {
         in = new MarshalledValueInputStream(bais);
         stateTransferManager.setState(in, Fqn.ROOT);
         stateReceivedSuccess();
      }
      catch (Throwable t)
      {
         stateReceivingFailed(t);
      }
      finally
      {
         Util.close(in);
         synchronized (stateLock)
         {
            // Notify wait that state has been set.
            stateLock.notifyAll();
         }
      }
   }

   public byte[] getState(String state_id)
   {
      MarshalledValueOutputStream out = null;
      String sourceRoot = state_id;
      byte[] result;

      boolean hasDifferentSourceAndIntegrationRoots = state_id.indexOf(StateTransferManager.PARTIAL_STATE_DELIMITER) > 0;
      if (hasDifferentSourceAndIntegrationRoots)
      {
         sourceRoot = state_id.split(StateTransferManager.PARTIAL_STATE_DELIMITER)[0];
      }

      ExposedByteArrayOutputStream baos = new ExposedByteArrayOutputStream(16 * 1024);
      try
      {
         out = new MarshalledValueOutputStream(baos);

         stateTransferManager.getState(out, Fqn.fromString(sourceRoot),
               configuration.getStateRetrievalTimeout(), true, true);
      }
      catch (Throwable t)
      {
         stateProducingFailed(t);
      }
      finally
      {
         result = baos.getRawBuffer();
         Util.close(out);
      }
      return result;
   }

   public void getState(OutputStream ostream)
   {
      MarshalledValueOutputStream out = null;
      try
      {
         out = new MarshalledValueOutputStream(ostream);
         stateTransferManager.getState(out, Fqn.ROOT, configuration.getStateRetrievalTimeout(), true, true);
      }
      catch (Throwable t)
      {
         stateProducingFailed(t);
      }
      finally
      {
         Util.close(out);
      }
   }

   public void getState(String state_id, OutputStream ostream)
   {
      String sourceRoot = state_id;
      MarshalledValueOutputStream out = null;
      boolean hasDifferentSourceAndIntegrationRoots = state_id.indexOf(StateTransferManager.PARTIAL_STATE_DELIMITER) > 0;
      if (hasDifferentSourceAndIntegrationRoots)
      {
         sourceRoot = state_id.split(StateTransferManager.PARTIAL_STATE_DELIMITER)[0];
      }
      try
      {
         out = new MarshalledValueOutputStream(ostream);
         stateTransferManager.getState(out, Fqn.fromString(sourceRoot), configuration.getStateRetrievalTimeout(), true, true);
      }
      catch (Throwable t)
      {
         stateProducingFailed(t);
      }
      finally
      {
         Util.close(out);
      }
   }

   public void setState(InputStream istream)
   {
      if (istream == null)
      {
         log.debug("stream is null (may be first member in cluster)");
         return;
      }
      MarshalledValueInputStream in = null;
      try
      {
         in = new MarshalledValueInputStream(istream);
         stateTransferManager.setState(in, Fqn.ROOT);
         stateReceivedSuccess();
      }
      catch (Throwable t)
      {
         stateReceivingFailed(t);
      }
      finally
      {
         Util.close(in);
         synchronized (stateLock)
         {
            // Notify wait that state has been set.
            stateLock.notifyAll();
         }
      }
   }

   public void setState(String state_id, byte[] state)
   {
      if (state == null)
      {
         log.debug("partial transferred state is null");
         return;
      }

      MarshalledValueInputStream in = null;
      String targetRoot = state_id;
      boolean hasDifferentSourceAndIntegrationRoots = state_id.indexOf(StateTransferManager.PARTIAL_STATE_DELIMITER) > 0;
      if (hasDifferentSourceAndIntegrationRoots)
      {
         targetRoot = state_id.split(StateTransferManager.PARTIAL_STATE_DELIMITER)[1];
      }
      try
      {
         log.debug("Setting received partial state for subroot " + state_id);
         Fqn subroot = Fqn.fromString(targetRoot);
//            Region region = regionManager.getRegion(subroot, false);
//            ClassLoader cl = null;
//            if (region != null)
//            {
//               // If a classloader is registered for the node's region, use it
//               cl = region.getClassLoader();
//            }
         ByteArrayInputStream bais = new ByteArrayInputStream(state);
         in = new MarshalledValueInputStream(bais);
         //getStateTransferManager().setState(in, subroot, cl);
         stateTransferManager.setState(in, subroot);
         stateReceivedSuccess();
      }
      catch (Throwable t)
      {
         stateReceivingFailed(t);
      }
      finally
      {
         Util.close(in);
         synchronized (stateLock)
         {
            // Notify wait that state has been set.
            stateLock.notifyAll();
         }
      }
   }

   public void setState(String stateId, InputStream istream)
   {
      if (log.isTraceEnabled()) log.trace("**** Receiving state for " + stateId);
      String targetRoot = stateId;
      MarshalledValueInputStream in = null;
      boolean hasDifferentSourceAndIntegrationRoots = stateId.indexOf(StateTransferManager.PARTIAL_STATE_DELIMITER) > 0;
      if (hasDifferentSourceAndIntegrationRoots)
      {
         targetRoot = stateId.split(StateTransferManager.PARTIAL_STATE_DELIMITER)[1];
      }
      if (istream == null)
      {
         log.debug("stream is null (may be first member in cluster). State is not set");
         return;
      }

      try
      {
         log.debug("Setting received partial state for subroot " + stateId);
         in = new MarshalledValueInputStream(istream);
         Fqn subroot = Fqn.fromString(targetRoot);
//            Region region = regionManager.getRegion(subroot, false);
//            ClassLoader cl = null;
//            if (region != null)
//            {
//               // If a classloader is registered for the node's region, use it
//               cl = region.getClassLoader();
//            }
         //getStateTransferManager().setState(in, subroot, cl);
         stateTransferManager.setState(in, subroot);
         stateReceivedSuccess();
      }
      catch (Throwable t)
      {
         stateReceivingFailed(t);
      }
      finally
      {
         Util.close(in);
         synchronized (stateLock)
         {
            // Notify wait that state has been set.
            stateLock.notifyAll();
         }
      }
   }
}
