package org.jgroups.protocols.pbcast;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.Vector;

import org.jgroups.Address;
import org.jgroups.Event;
import org.jgroups.Header;
import org.jgroups.Message;
import org.jgroups.TimeoutException;
import org.jgroups.View;
import org.jgroups.ViewId;
import org.jgroups.stack.Protocol;
import org.jgroups.util.Promise;
import org.jgroups.util.Streamable;
import org.jgroups.util.Util;

import EDU.oswego.cs.dl.util.concurrent.ReentrantLock;

/**
 * Flush, as it name implies, forces group members to flush their pending messages 
 * while blocking them to send any additional messages. The process of flushing 
 * acquiesces the group so that state transfer or a join can be done. It is also 
 * called stop-the-world model as nobody will be able to send messages while a 
 * flush is in process.
 * 
 * <p>
 * Flush is needed for:
 * <p>
 * (1) 	State transfer. When a member requests state transfer, the coordinator 
 * 		tells everyone to stop sending messages and waits for everyone's ack. Then it asks
 *  	the application for its state and ships it back to the requester. After the 
 *  	requester has received and set the state successfully, the coordinator tells 
 *  	everyone to resume sending messages.
 * <p>  
 * (2) 	View changes (e.g.a join). Before installing a new view V2, flushing would 
 * 		ensure that all messages *sent* in the current view V1 are indeed *delivered* 
 * 		in V1, rather than in V2 (in all non-faulty members). This is essentially 
 * 		Virtual Synchrony.
 * 
 * 
 * 
 * @author Vladimir Blagojevic
 * @version $Id$
 * @since 2.4
 */
public class FLUSH extends Protocol
{
   public static final String NAME = "FLUSH";

   // GuardedBy ("sharedLock")
   private View currentView;

   // GuardedBy ("sharedLock")
   private Address localAddress;

   /**
    * Group member that requested FLUSH.
    * For view intallations flush coordinator is the group coordinator
    * For state transfer flush coordinator is the state requesting member
    */
   // GuardedBy ("sharedLock")
   private Address flushCoordinator;

   // GuardedBy ("sharedLock")
   private final Collection flushMembers;

   // GuardedBy ("sharedLock")
   private final Set flushOkSet;

   // GuardedBy ("sharedLock")
   private final Set flushCompletedSet;

   // GuardedBy ("sharedLock")
   private final Set stopFlushOkSet;
   
   // GuardedBy ("sharedLock")
   private final Set suspected;

   private final Object sharedLock = new Object();

   private final Object blockMutex = new Object();

   /**
    * Indicates if FLUSH.down() is currently blocking threads
    * Condition predicate associated with blockMutex
    */
   //GuardedBy ("blockMutex")
   private boolean isBlockingFlushDown = true;

   /**
    * Default timeout for a group member to be in <code>isBlockingFlushDown</code>
    */
   private long timeout = 8000;

   /**
    * Default timeout started when <code>Event.BLOCK</code> is passed to
    * application. Response <code>Event.BLOCK_OK</code> should be received by
    * application within timeout.
    */
   private long block_timeout = 10000;   

   // GuardedBy ("sharedLock")
   private boolean receivedFirstView = false;

   // GuardedBy ("sharedLock")
   private boolean receivedMoreThanOneView = false;

   private long startFlushTime;

   private long totalTimeInFlush;

   private int numberOfFlushes;

   private double averageFlushDuration;

   private final Promise flush_promise = new Promise();

   private final Promise blockok_promise = new Promise();    
   
   private final FlushPhase flushPhase = new FlushPhase();

   /**
    * If true configures timeout in GMS and STATE_TRANFER using FLUSH timeout value
    */
   private boolean auto_flush_conf = true;



   public FLUSH()
   {
      super();      
      currentView = new View(new ViewId(), new Vector());
      flushOkSet = new TreeSet();
      flushCompletedSet = new TreeSet();
      stopFlushOkSet = new TreeSet();
      flushMembers = new ArrayList();
      suspected = new TreeSet();      
   }

   public String getName()
   {
      return NAME;
   }

   public boolean setProperties(Properties props)
   {
      super.setProperties(props);
      timeout = Util.parseLong(props, "timeout", timeout);
      block_timeout = Util.parseLong(props, "block_timeout", block_timeout);
      auto_flush_conf = Util.parseBoolean(props, "auto_flush_conf", auto_flush_conf);

      if (props.size() > 0)
      {
         log.error("the following properties are not recognized: " + props);
         return false;
      }
      return true;
   }

   public void init() throws Exception
   {
      if(auto_flush_conf)
      {
         Map map = new HashMap();
         map.put("flush_timeout", new Long(timeout));
         passUp(new Event(Event.CONFIG, map));
         passDown(new Event(Event.CONFIG, map));
      }
   }

   public void start() throws Exception
   {
      Map map = new HashMap();
      map.put("flush_supported", Boolean.TRUE);
      passUp(new Event(Event.CONFIG, map));
      passDown(new Event(Event.CONFIG, map));

      synchronized(sharedLock)
      {
         receivedFirstView = false;
         receivedMoreThanOneView = false;
      }
      synchronized(blockMutex)
      {
         isBlockingFlushDown = true;
      }
   }

   public void stop()
   {
      synchronized (sharedLock)
      {
         currentView = new View(new ViewId(), new Vector());
         flushCompletedSet.clear();
         flushOkSet.clear();
         stopFlushOkSet.clear();
         flushMembers.clear();
         suspected.clear();
         flushCoordinator = null;
      }
   }

   /* -------------------JMX attributes and operations --------------------- */

   public double getAverageFlushDuration()
   {
      return averageFlushDuration;
   }

   public long getTotalTimeInFlush()
   {
      return totalTimeInFlush;
   }

   public int getNumberOfFlushes()
   {
      return numberOfFlushes;
   }

   public boolean startFlush(long timeout)
   {
      boolean successfulFlush = false;
      down(new Event(Event.SUSPEND));
      flush_promise.reset();
      try
      {
         flush_promise.getResultWithTimeout(timeout);
         successfulFlush = true;
      }
      catch (TimeoutException e)
      {
      }
      return successfulFlush;
   }

   public void stopFlush()
   {
      down(new Event(Event.RESUME));
   }

   /* ------------------- end JMX attributes and operations --------------------- */

   public void down(Event evt)
   {
      switch (evt.getType())
      {         
         case Event.MSG :
            Message msg = (Message) evt.getArg();
            FlushHeader fh = (FlushHeader) msg.removeHeader(getName());
            if (fh != null && fh.type == FlushHeader.FLUSH_BYPASS)
            {
               break;
            }
            else
            {
               blockMessageDuringFlush();
               break;
            }
         case Event.GET_STATE:   
            blockMessageDuringFlush();
            break;

         case Event.CONNECT:
            boolean successfulBlock = sendBlockUpToChannel(block_timeout);
            if (successfulBlock && log.isDebugEnabled())
            {
               log.debug("Blocking of channel " + localAddress + " completed successfully");
            }
            
            break;

         case Event.SUSPEND :            
            attemptSuspend(evt);
            return;

         case Event.RESUME :
            onResume();
            return;

         case Event.BLOCK_OK:
            blockok_promise.setResult(Boolean.TRUE);
            return;
      }
      passDown(evt);
   }

   private void blockMessageDuringFlush()
   {
      boolean shouldSuspendByItself = false;
      long start=0, stop=0;            
      synchronized (blockMutex)
      {
         while (isBlockingFlushDown)
         {
            if (log.isDebugEnabled())
               log.debug("FLUSH block at " + localAddress + " for " + (timeout <= 0? "ever" : timeout + "ms"));
            try
            {
               start=System.currentTimeMillis();
               if(timeout <= 0)
                  blockMutex.wait();
               else
                  blockMutex.wait(timeout);
               stop=System.currentTimeMillis();               
            }
            catch (InterruptedException e)
            {
            }
            if (isBlockingFlushDown)
            {
               isBlockingFlushDown = false;               
               shouldSuspendByItself = true;
               blockMutex.notifyAll();
            }
         }        
      }
      if(shouldSuspendByItself)
      {
         log.warn("unblocking FLUSH.down() at " + localAddress + " after timeout of " + (stop-start) + "ms");
         passUp(new Event(Event.SUSPEND_OK));
         passDown(new Event(Event.SUSPEND_OK));
      }
   }

   public void up(Event evt)
   {

      Message msg = null;
      switch (evt.getType())
      {
         case Event.MSG :
            msg = (Message) evt.getArg();
            FlushHeader fh = (FlushHeader) msg.removeHeader(getName());
            if (fh != null)
            {
               flushPhase.lock();
               if (fh.type == FlushHeader.START_FLUSH)
               {                             
                  if (!flushPhase.isFlushInProgress())
                  {
                     flushPhase.setFirstPhase(true);
                     flushPhase.release();
                     boolean successfulBlock = sendBlockUpToChannel(block_timeout);
                     if (successfulBlock && log.isDebugEnabled())
                     {
                        log.debug("Blocking of channel " + localAddress + " completed successfully");
                     }
                     onStartFlush(msg.getSrc(), fh);
                  }
                  else if (flushPhase.isInFirstPhase())
                  {
                     flushPhase.release();
                     Address flushRequester = msg.getSrc();
                     Address coordinator = null;
                     synchronized(sharedLock)
                     {
                        coordinator = flushCoordinator;
                     }
                     
                     if(flushRequester.compareTo(coordinator)<0)
                     {                        
                        rejectFlush(fh.viewID, coordinator);
                        if(log.isDebugEnabled())
                        {
                           log.debug("Rejecting flush at " + localAddress + " to current flush coordinator " + coordinator + " and switching flush coordinator to " + flushRequester);
                        }
                        synchronized(sharedLock)
                        {
                           flushCoordinator = flushRequester;
                        }                        
                     }
                     else
                     {                        
                        rejectFlush(fh.viewID, flushRequester); 
                        if(log.isDebugEnabled())
                        {
                           log.debug("Rejecting flush at " + localAddress + " to flush requester " + flushRequester);
                        }
                     }                      
                  }
                  else if (flushPhase.isInSecondPhase())
                  {
                     flushPhase.release();
                     Address flushRequester = msg.getSrc();
                     rejectFlush(fh.viewID, flushRequester);  
                     if(log.isDebugEnabled())
                     {
                        log.debug("Rejecting flush in second phase at " + localAddress + " to flush requester " + flushRequester);
                     }
                  }
               }
               else if (fh.type == FlushHeader.STOP_FLUSH)
               {
                  flushPhase.setPhases(false, true); 
                  flushPhase.release();
                  onStopFlush();
               }
               else if (fh.type == FlushHeader.ABORT_FLUSH)
               {
                  //abort current flush  
                  flushPhase.release();
                  passUp(new Event(Event.SUSPEND_FAILED));
                  passDown(new Event(Event.SUSPEND_FAILED));

               }
               else if (isCurrentFlushMessage(fh))
               {
                  flushPhase.release();
                  if (fh.type == FlushHeader.FLUSH_OK)
                  {
                     onFlushOk(msg.getSrc(), fh.viewID);
                  }
                  else if (fh.type == FlushHeader.STOP_FLUSH_OK)
                  {
                     onStopFlushOk(msg.getSrc(),fh.viewID);
                  }
                  else if (fh.type == FlushHeader.FLUSH_COMPLETED)
                  {
                     onFlushCompleted(msg.getSrc());
                  }
               }
               else
               {
                  flushPhase.release();
                  if (log.isDebugEnabled())
                     log.debug(localAddress + " received outdated FLUSH message " + fh + ",ignoring it.");
               }
               return; //do not pass FLUSH msg up
            }
            break;

         case Event.VIEW_CHANGE :
            //if this is channel's first view and its the only member of the group then the
            //goal is to pass BLOCK,VIEW,UNBLOCK to application space on the same thread as VIEW.
            View newView = (View) evt.getArg();
            boolean firstView = onViewChange(newView);
            boolean singletonMember = newView.size()==1 && newView.containsMember(localAddress);
            if(firstView && singletonMember){
               passUp(evt);
               synchronized (blockMutex)
               {
                  isBlockingFlushDown = false;
                  blockMutex.notifyAll();
               }
               if (log.isDebugEnabled())
                  log.debug("At " + localAddress + " unblocking FLUSH.down() and sending UNBLOCK up");
               
               passUp(new Event(Event.UNBLOCK));
               return;
            }
            break;

         case Event.SET_LOCAL_ADDRESS :
            synchronized (sharedLock)
            {
               localAddress = (Address) evt.getArg();
            }            
            break;

         case Event.SUSPECT :
            onSuspect((Address) evt.getArg());
            break;

         case Event.SUSPEND :           
            attemptSuspend(evt);
            return;

         case Event.RESUME :
            onResume();
            return;

      }

      passUp(evt);
   }

   public Vector providedDownServices()
   {
      Vector retval = new Vector(2);
      retval.addElement(new Integer(Event.SUSPEND));
      retval.addElement(new Integer(Event.RESUME));
      return retval;
   }
   
   private void attemptSuspend(Event evt)
   {
      View v = (View) evt.getArg();
      if(log.isDebugEnabled())
         log.debug("Received SUSPEND at " + localAddress + ", view is " + v);
      
      flushPhase.lock();
      if (!flushPhase.isFlushInProgress())
      {
         flushPhase.release();
         onSuspend(v);
      }
      else
      {
         flushPhase.release();
         passUp(new Event(Event.SUSPEND_FAILED));
         passDown(new Event(Event.SUSPEND_FAILED));
      }
   }
   
   private void rejectFlush(long viewId, Address flushRequester)
   {
      Message reject = new Message(flushRequester);
      reject.putHeader(getName(), new FlushHeader(FlushHeader.ABORT_FLUSH, viewId));
      passDown(new Event(Event.MSG, reject));
   }
   
   private boolean sendBlockUpToChannel(long btimeout)
   {
      boolean successfulBlock = false;
      blockok_promise.reset();
      
      new Thread(Util.getGlobalThreadGroup(), new Runnable()
      {
         public void run()
         {
            passUp(new Event(Event.BLOCK));
         }
      }, "FLUSH block").start();
      
      try
      {
         blockok_promise.getResultWithTimeout(btimeout);
         successfulBlock = true;
      }
      catch (TimeoutException e)
      {
         log.warn("Blocking of channel using BLOCK event timed out after " + btimeout + " msec.");
      }
      return successfulBlock;
   }

   private boolean isCurrentFlushMessage(FlushHeader fh)
   {
      return fh.viewID == currentViewId();
   }

   private long currentViewId()
   {
      long viewId = -1;
      synchronized (sharedLock)
      {
         ViewId view = currentView.getVid();
         if (view != null)
         {
            viewId = view.getId();
         }
      }
      return viewId;
   }

   private boolean onViewChange(View view)
   {
      boolean amINewCoordinator = false; 
      boolean isThisOurFirstView = false;
      synchronized (sharedLock)
      {
         if (receivedFirstView)
         {
            receivedMoreThanOneView = true;
         }
         if (!receivedFirstView)
         {
            receivedFirstView = true;
         }   
         isThisOurFirstView = receivedFirstView && !receivedMoreThanOneView;
         suspected.retainAll(view.getMembers());
         currentView = view;         
         amINewCoordinator = flushCoordinator != null && !view.getMembers().contains(flushCoordinator)
               && localAddress.equals(view.getMembers().get(0));
      }
      
      //If coordinator leaves, its STOP FLUSH message will be discarded by
      //other members at NAKACK layer. Remaining members will be hung, waiting
      //for STOP_FLUSH message. If I am new coordinator I will complete the
      //FLUSH and send STOP_FLUSH on flush callers behalf.
      if (amINewCoordinator)
      {
         if (log.isDebugEnabled())
            log.debug("Coordinator left, " + localAddress + " will complete flush");
         onResume();
      }
      
      if (log.isDebugEnabled())
         log.debug("Installing view at  " + localAddress + " view is " + view);
      
      return isThisOurFirstView;
   }

   private void onStopFlush()
   {
      if (stats)
      {
         long stopFlushTime = System.currentTimeMillis();
         totalTimeInFlush += (stopFlushTime - startFlushTime);
         if (numberOfFlushes > 0)
         {
            averageFlushDuration = totalTimeInFlush / (double)numberOfFlushes;
         }
      }            
      //ack this STOP_FLUSH
      Message msg = new Message(null);
      msg.putHeader(getName(), new FlushHeader(FlushHeader.STOP_FLUSH_OK,currentViewId()));      
      passDown(new Event(Event.MSG, msg));
      
      if (log.isDebugEnabled())
         log.debug("Received STOP_FLUSH and sent STOP_FLUSH_OK from " + localAddress); 
   }

   private void onSuspend(View view)
   {
      Message msg = null;
      Collection participantsInFlush = null;
      synchronized (sharedLock)
      {
         //start FLUSH only on group members that we need to flush
         if (view != null)
         {
            participantsInFlush = new ArrayList(view.getMembers());
            participantsInFlush.retainAll(currentView.getMembers());
         }
         else
         {
            participantsInFlush = new ArrayList(currentView.getMembers());
         }
         msg = new Message(null);
         msg.putHeader(getName(), new FlushHeader(FlushHeader.START_FLUSH, currentViewId(), participantsInFlush));
      }
      if (participantsInFlush.isEmpty())
      {
         passUp(new Event(Event.SUSPEND_OK));
         passDown(new Event(Event.SUSPEND_OK));
      }
      else
      {
         passDown(new Event(Event.MSG, msg));
         if (log.isDebugEnabled())
            log.debug("Received SUSPEND at " + localAddress + ", sent START_FLUSH to " + participantsInFlush);
      }
   }

   private void onResume()
   {
	  long viewID = currentViewId();	
      Message msg = new Message(null);
      msg.putHeader(getName(), new FlushHeader(FlushHeader.STOP_FLUSH,viewID));
      passDown(new Event(Event.MSG, msg));
      if (log.isDebugEnabled())
         log.debug("Received RESUME at " + localAddress + ", sent STOP_FLUSH to all");
   }

   private void onStartFlush(Address flushStarter, FlushHeader fh)
   {      
      if (stats)
      {
         startFlushTime = System.currentTimeMillis();
         numberOfFlushes += 1;
      }            
      synchronized (sharedLock)
      {         
         flushCoordinator = flushStarter;
         flushMembers.clear();
         if(fh.flushParticipants!=null)
         {
            flushMembers.addAll(fh.flushParticipants);
         }        
         flushMembers.removeAll(suspected);
      }
      Message msg = new Message(null);
      msg.putHeader(getName(), new FlushHeader(FlushHeader.FLUSH_OK, fh.viewID));
      passDown(new Event(Event.MSG, msg));
      if (log.isDebugEnabled())
         log.debug("Received START_FLUSH at " + localAddress + " responded with FLUSH_OK");
   }

   private void onFlushOk(Address address, long viewID)
   {

      boolean flushOkCompleted = false;
      Message m = null;
      synchronized (sharedLock)
      {
         flushOkSet.add(address);
         flushOkCompleted = flushOkSet.containsAll(flushMembers);
         if (flushOkCompleted)
         {
            m = new Message(flushCoordinator);
         }
         
         if (log.isDebugEnabled())
             log.debug("At " + localAddress + " FLUSH_OK from " + address + ",completed " 
                   + flushOkCompleted + ",  flushOkSet " + flushOkSet.toString());
      }      

      if (flushOkCompleted)
      {
         synchronized(blockMutex)
         {
            isBlockingFlushDown = true;
         }
         m.putHeader(getName(), new FlushHeader(FlushHeader.FLUSH_COMPLETED, viewID));
         passDown(new Event(Event.MSG, m));
         if (log.isDebugEnabled())
            log.debug(localAddress + " is blocking FLUSH.down(). Sent FLUSH_COMPLETED message to " + flushCoordinator);
      }
   }
   
   private void onStopFlushOk(Address address, long viewID)
   {

      boolean stopFlushOkCompleted = false;
      synchronized (sharedLock)
      {
         stopFlushOkSet.add(address);
         TreeSet membersCopy = new TreeSet(currentView.getMembers());
         membersCopy.removeAll(suspected);
         stopFlushOkCompleted = stopFlushOkSet.containsAll(membersCopy);
         
         if (log.isDebugEnabled())
             log.debug("At " + localAddress + " STOP_FLUSH_OK from " + address + ",completed " + stopFlushOkCompleted
                   + ",  stopFlushOkSet " + stopFlushOkSet.toString());
      }      

      if (stopFlushOkCompleted)
      {         
         synchronized (sharedLock)
         {
            flushCompletedSet.clear();
            flushOkSet.clear();   
            stopFlushOkSet.clear();
            flushMembers.clear();
            suspected.clear();
            flushCoordinator = null;
         }
         flushPhase.lock();
         flushPhase.setSecondPhase(false);
         flushPhase.release();
         
         if (log.isDebugEnabled())
            log.debug("At " + localAddress + " unblocking FLUSH.down() and sending UNBLOCK up");
         
         synchronized (blockMutex)
         {
            isBlockingFlushDown = false;
            blockMutex.notifyAll();
         }
         passUp(new Event(Event.UNBLOCK));         
      }     
   }

   private void onFlushCompleted(Address address)
   {
      boolean flushCompleted = false;
      synchronized (sharedLock)
      {
         flushCompletedSet.add(address);
         flushCompleted = flushCompletedSet.containsAll(flushMembers);
         
         if (log.isDebugEnabled())
             log.debug("At " + localAddress + " FLUSH_COMPLETED from " + address 
                   + ",completed " + flushCompleted + ",flushCompleted "
                   + flushCompletedSet.toString());
      }      

      if (flushCompleted)
      {
         //needed for jmx operation startFlush(timeout);
         flush_promise.setResult(Boolean.TRUE);
         passUp(new Event(Event.SUSPEND_OK));
         passDown(new Event(Event.SUSPEND_OK));
         if (log.isDebugEnabled())
            log.debug("All FLUSH_COMPLETED received at " + localAddress + " sent SUSPEND_OK down/up");
      }
   }

   private void onSuspect(Address address)
   {
      boolean flushOkCompleted = false;
      Message m = null;
      long viewID = 0;
      synchronized (sharedLock)
      {
         suspected.add(address);
         flushMembers.removeAll(suspected);
         viewID = currentViewId();
         flushOkCompleted = !flushOkSet.isEmpty() && flushOkSet.containsAll(flushMembers);
         if (flushOkCompleted)
         {
            m = new Message(flushCoordinator);
         }
         
         if (log.isDebugEnabled())
             log.debug("Suspect is " + address + ",completed " + flushOkCompleted + ",  flushOkSet " + flushOkSet
                   + " flushMembers " + flushMembers);
      }
      if (flushOkCompleted)
      {
         m.putHeader(getName(), new FlushHeader(FlushHeader.FLUSH_COMPLETED, viewID));
         passDown(new Event(Event.MSG, m));
         if (log.isDebugEnabled())
            log.debug(localAddress + " sent FLUSH_COMPLETED message to " + flushCoordinator);
      }      
   }  
   
   private static class FlushPhase
   {
      private boolean inFirstFlushPhase = false;      
      private boolean inSecondFlushPhase = false;
      private final ReentrantLock lock = new ReentrantLock();
      
      public FlushPhase(){}          
      
      public void lock()
      {
         try
         {
            lock.acquire();
         }
         catch (InterruptedException e)
         {            
            e.printStackTrace();
         }
      }
      
      public void release()
      {
         lock.release();
      }
      
      public void setFirstPhase(boolean inFirstPhase)
      {
         inFirstFlushPhase = inFirstPhase;
      }
      
      public void setSecondPhase(boolean inSecondPhase)
      {
         inSecondFlushPhase = inSecondPhase;
      }
      
      public void setPhases(boolean inFirstPhase,boolean inSecondPhase)
      {
         inFirstFlushPhase = inFirstPhase;
         inSecondFlushPhase = inSecondPhase;
      }
      
      public boolean isInFirstPhase()
      {
         return inFirstFlushPhase;
      }
      
      public boolean isInSecondPhase()
      {
         return inSecondFlushPhase;
      }       
      
      public boolean isFlushInProgress()
      {
         return inFirstFlushPhase || inSecondFlushPhase;
      }
   }

   public static class FlushHeader extends Header implements Streamable
   {
      public static final byte START_FLUSH = 0;

      public static final byte FLUSH_OK = 1;

      public static final byte STOP_FLUSH = 2;

      public static final byte FLUSH_COMPLETED = 3;
      
      public static final byte STOP_FLUSH_OK = 4;
      
      public static final byte ABORT_FLUSH = 5;  
      
      public static final byte FLUSH_BYPASS = 6;

      byte type;

      long viewID;

      Collection flushParticipants;

      public FlushHeader()
      {
         this(START_FLUSH,0);
      } // used for externalization
      
      public FlushHeader(byte type)
      {
         this(type,0);
      } 
     
      public FlushHeader(byte type, long viewID)
      {
         this(type, viewID, null);
      }

      public FlushHeader(byte type, long viewID, Collection flushView)
      {
         this.type = type;
         this.viewID = viewID;
         this.flushParticipants = flushView;
      }

      public String toString()
      {
         switch (type)
         {
            case START_FLUSH :
               return "FLUSH[type=START_FLUSH,viewId=" + viewID + ",members=" + flushParticipants + "]";
            case FLUSH_OK :
               return "FLUSH[type=FLUSH_OK,viewId=" + viewID + "]";
            case STOP_FLUSH :
               return "FLUSH[type=STOP_FLUSH,viewId=" + viewID + "]";
            case STOP_FLUSH_OK :
               return "FLUSH[type=STOP_FLUSH_OK,viewId=" + viewID + "]";   
            case ABORT_FLUSH :
               return "FLUSH[type=ABORT_FLUSH,viewId=" + viewID + "]";                  
            case FLUSH_COMPLETED :
               return "FLUSH[type=FLUSH_COMPLETED,viewId=" + viewID + "]";
            case FLUSH_BYPASS :
               return "FLUSH[type=FLUSH_BYPASS,viewId=" + viewID + "]";   
            default :
               return "[FLUSH: unknown type (" + type + ")]";
         }
      }

      public void writeExternal(ObjectOutput out) throws IOException
      {
         out.writeByte(type);
         out.writeLong(viewID);
         out.writeObject(flushParticipants);
      }

      public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
      {
         type = in.readByte();
         viewID = in.readLong();
         flushParticipants = (Collection) in.readObject();
      }

      public void writeTo(DataOutputStream out) throws IOException
      {
         out.writeByte(type);
         out.writeLong(viewID);
         if (flushParticipants != null && !flushParticipants.isEmpty())
         {
            out.writeShort(flushParticipants.size());
            for (Iterator iter = flushParticipants.iterator(); iter.hasNext();)
            {
               Address address = (Address) iter.next();
               Util.writeAddress(address, out);
            }
         }
         else
         {
            out.writeShort(0);
         }
      }

      public void readFrom(DataInputStream in) throws IOException, IllegalAccessException, InstantiationException
      {
         type = in.readByte();
         viewID = in.readLong();
         int flushParticipantsSize = in.readShort();
         if (flushParticipantsSize > 0)
         {
            flushParticipants = new ArrayList(flushParticipantsSize);
            for (int i = 0; i < flushParticipantsSize; i++)
            {
               flushParticipants.add(Util.readAddress(in));
            }
         }
      }
   }
}
