package org.jgroups.tests;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import junit.framework.Test;
import junit.framework.TestSuite;

import org.jgroups.Channel;
import org.jgroups.JChannelFactory;
import org.jgroups.Message;
import org.jgroups.View;
import org.jgroups.util.Util;

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

/**
 * Tests concurrent startup with state transfer and concurrent state tranfer.
 * @author bela
 * @version $Id: ConcurrentStartupTest.java,v 1.16.2.1 2006/12/04 22:45:49 vlada Exp $
 */
public class ConcurrentStartupTest extends ChannelTestBase
{

   private int mod = 1;   
   
   public void setUp() throws Exception
   {
      super.setUp();   
      mod = 1;
      CHANNEL_CONFIG = System.getProperty("channel.conf.flush", "flush-udp.xml");      
   }  
   
   public boolean useBlocking()
   {
      return true;
   }
   
   public void testConcurrentStartupLargeState()
   {            
      concurrentStartupHelper(true,false);
   }
   
   public void testConcurrentStartupSmallState()
   { 
      concurrentStartupHelper(false,true);
   }    

   /**
    * Tests concurrent startup and message sending directly after joining
    * See doc/design/ConcurrentStartupTest.txt for details. This will only work 100% correctly once we have
    * FLUSH support (JGroups 2.4)
    * 
    * NOTE: This test is not guaranteed to pass at 100% rate until combined join 
    * and state transfer using one FLUSH phase is introduced (Jgroups 2.5)[1].
    * 
    * [1] http://jira.jboss.com/jira/browse/JGRP-236
    * 
    * 
    */
   protected void concurrentStartupHelper(boolean largeState,boolean useDispatcher)
   {
      String[] names = null;
      
      //mux applications on top of same channel have to have unique name
      if(isMuxChannelUsed())
      {
         names = createMuxApplicationNames(1);
      }
      else
      {
         names = new String[]{"A", "B", "C", "D"};
      }
      
      int count = names.length;
      
      ConcurrentStartupChannel[] channels = new ConcurrentStartupChannel[count];           
      try
      {
         // Create a semaphore and take all its permits
         Semaphore semaphore = new Semaphore(count);
         takeAllPermits(semaphore, count);

         // Create activation threads that will block on the semaphore        
         for (int i = 0; i < count; i++)
         {
            if(largeState)
            {
               if(isMuxChannelUsed())
               {
                  channels[i] = new ConcurrentStartupChannelWithLargeState(names[i],muxFactory[i%getMuxFactoryCount()],semaphore); 
               }
               else
               {
                  channels[i] = new ConcurrentStartupChannelWithLargeState(semaphore, names[i],useDispatcher);
               }
            }            
            else
            {
             
               if(isMuxChannelUsed())
               {
                  channels[i] = new ConcurrentStartupChannel(names[i],muxFactory[i%getMuxFactoryCount()],semaphore); 
               }
               else
               {
                  channels[i] = new ConcurrentStartupChannel(names[i],semaphore,useDispatcher);
               }
            }

            // Release one ticket at a time to allow the thread to start working                           
            channels[i].start();
            semaphore.release(1);
            sleepRandom(1500);
         }

         // Make sure everyone is in sync
         if(isMuxChannelUsed())
         {
            blockUntilViewsReceived(channels,getMuxFactoryCount(), 60000);
         }
         else
         {
            blockUntilViewsReceived(channels, 60000);
         }

         // Sleep to ensure the threads get all the semaphore tickets
         sleepThread(1000);

         // Reacquire the semaphore tickets; when we have them all
         // we know the threads are done         
         acquireSemaphore(semaphore, 60000, count);
         
         //Sleep to ensure async message arrive
         sleepThread(3000);

         //do test verification
         List[] lists = new List[count];
         for (int i = 0; i < count; i++)
         {
            lists[i] = channels[i].getList();
         }

         Map[] mods = new Map[count];
         for (int i = 0; i < count; i++)
         {
            mods[i] = channels[i].getModifications();
         }         
         
         printLists(lists);
         printModifications(mods);

         int len = lists.length;
         for (int i = 0; i < lists.length; i++)
         {
            List l = lists[i];
            assertEquals("list #" + i + " should have " + len + " elements", len, l.size());
         }                
      }
      catch (Exception ex)
      {
         log.warn("Exception encountered during test",ex);
      }
      finally
      {    
         for (int i = 0; i < count; i++)
         {
            sleepThread(500);
            channels[i].cleanup();
         }        
      }
   }
   
   public void testConcurrentLargeStateTranfer()
   {
      concurrentStateTranferHelper(true,false);
   }
   
   public void testConcurrentSmallStateTranfer()
   {
      concurrentStateTranferHelper(false,true);
   }
     
     

   /**
    * Tests concurrent state transfer. This test should pass at 100% rate when [1] 
    * is solved.
    * 
    * [1]http://jira.jboss.com/jira/browse/JGRP-332
    * 
    * 
    */
   protected void concurrentStateTranferHelper(boolean largeState, boolean useDispatcher)
   {
      String[] names = null;
      
      //mux applications on top of same channel have to have unique name
      if(isMuxChannelUsed())
      {
         names = createMuxApplicationNames(1); 
      }
      else
      {
         names = new String[]{"A", "B", "C", "D"};
      }
      
      int count = names.length;
      ConcurrentStateTransfer[] channels = new ConcurrentStateTransfer[count];
      
      //Create a semaphore and take all its tickets
      Semaphore semaphore = new Semaphore(count);
      takeAllPermits(semaphore, count);
      
      try
      {                  

         // Create activation threads that will block on the semaphore        
         for (int i = 0; i < count; i++)
         {
            if(largeState)
            {
               if(isMuxChannelUsed())
               {
                  channels[i] = new ConcurrentLargeStateTransfer(names[i],muxFactory[i%getMuxFactoryCount()],semaphore);
               }
               else
               {
                  channels[i] = new ConcurrentLargeStateTransfer(names[i],semaphore,useDispatcher);
               }
            }
            else
            {    
               if(isMuxChannelUsed())
               {
                  channels[i] = new ConcurrentStateTransfer(names[i],muxFactory[i%getMuxFactoryCount()],semaphore);
               }
               else
               {
                  channels[i] = new ConcurrentStateTransfer(names[i],semaphore,useDispatcher);
               }
            }

            // Start threads and let them join the channel                           
            channels[i].start();
            sleepThread(2000);
         }

         // Make sure everyone is in sync
         if(isMuxChannelUsed())
         {
            blockUntilViewsReceived(channels,getMuxFactoryCount(), 60000);
         }
         else
         {
            blockUntilViewsReceived(channels, 60000);
         }

         sleepThread(2000);
         //Unleash hell !
         semaphore.release(count);

         // Sleep to ensure the threads get all the semaphore tickets
         sleepThread(2000);

         //Reacquire the semaphore tickets; when we have them all
         //we know the threads are done         
         acquireSemaphore(semaphore, 60000, count);

         //Sleep to ensure async message arrive
         sleepThread(3000);
         //do test verification
         List[] lists = new List[count];
         for (int i = 0; i < count; i++)
         {
            lists[i] = channels[i].getList();
         }

         Map[] mods = new Map[count];
         for (int i = 0; i < count; i++)
         {
            mods[i] = channels[i].getModifications();
         }                

         printLists(lists);
         printModifications(mods);

         int len = lists.length;
         for (int i = 0; i < lists.length; i++)
         {
            List l = lists[i];
            assertEquals("list #" + i + " should have " + len + " elements", len, l.size());
         }
      }
      catch (Exception ex)
      {
         log.warn("Exception encountered during test",ex);
      }
      finally
      {      
         for (int i = 0; i < count; i++)
         {
            sleepThread(500);
            channels[i].cleanup();
         }         
      }
   }
   
   protected int getMod()
   {
      synchronized (this)
      {
         int retval = mod;
         mod++;
         return retval;
      }
   }

   protected void printModifications(Map[] modifications)
   {
      for (int i = 0; i < modifications.length; i++)
      {
         Map modification = modifications[i];
         log.info("modifications for #" + i + ": " + modification);
      }
   }

   protected void printLists(List[] lists)
   {
      for (int i = 0; i < lists.length; i++)
      {
         List l = lists[i];
         log.info(i + ": " + l);
      }
   }

   protected class ConcurrentStateTransfer extends ConcurrentStartupChannel
   {
      public ConcurrentStateTransfer(String name,Semaphore semaphore,boolean useDispatcher) throws Exception
      {
         super(name,semaphore,useDispatcher);
         channel.connect("test");
      }
      
      public ConcurrentStateTransfer(String name,JChannelFactory factory, Semaphore semaphore) throws Exception
      {
         super(name,factory,semaphore);
         channel.connect("test");
      }

      public void useChannel() throws Exception
      {
         boolean success = channel.getState(null, 30000);
         log.info("channel.getState at " + getName() + getLocalAddress() + " returned " + success);
         channel.send(null, null, channel.getLocalAddress());
      }
   }    
   
   protected class ConcurrentLargeStateTransfer extends ConcurrentStateTransfer
   {
      public ConcurrentLargeStateTransfer(String name,Semaphore semaphore,boolean useDispatcher) throws Exception
      {
         super(name,semaphore,useDispatcher);         
      }  
      
      public ConcurrentLargeStateTransfer(String name,JChannelFactory factory,Semaphore semaphore) throws Exception
      {
         super(name,factory,semaphore);         
      }
      
      public void setState(byte[] state)
      {
         super.setState(state);
         sleepThread(5000);
      }

      public byte[] getState()
      {
         sleepThread(5000);
         return super.getState();        
      }

      public void getState(OutputStream ostream)
      {
         super.getState(ostream); 
         sleepThread(5000);
      }

      public void setState(InputStream istream)
      {
         super.setState(istream);
         sleepThread(5000);
      }
   }
   
   protected class ConcurrentStartupChannelWithLargeState extends ConcurrentStartupChannel
   {
      public ConcurrentStartupChannelWithLargeState(Semaphore semaphore, String name,boolean useDispatcher) throws Exception
      {
         super(name,semaphore,useDispatcher);         
      }   
      
      public ConcurrentStartupChannelWithLargeState(String name,JChannelFactory f,Semaphore semaphore) throws Exception
      {
         super(name,f,semaphore);
      }
      
      public void setState(byte[] state)
      {
         super.setState(state);
         sleepThread(5000);
      }

      public byte[] getState()
      {
         sleepThread(5000);
         return super.getState();        
      }

      public void getState(OutputStream ostream)
      {
         super.getState(ostream); 
         sleepThread(5000);
      }

      public void setState(InputStream istream)
      {
         super.setState(istream);
         sleepThread(5000);
      }
   }
   

   protected class ConcurrentStartupChannel extends PushChannelApplicationWithSemaphore
   {
      final List l = new LinkedList();

      Channel ch;

      int modCount = 1;

      final Map mods = new TreeMap();     
      
      public ConcurrentStartupChannel(String name,Semaphore semaphore) throws Exception
      {
         super(name,semaphore,true);
      }
      
      public ConcurrentStartupChannel(String name,JChannelFactory f,Semaphore semaphore) throws Exception
      {
         super(name,f,semaphore);
      }
      
      public ConcurrentStartupChannel(String name,Semaphore semaphore,boolean useDispatcher) throws Exception
      {
         super(name,semaphore,useDispatcher);
      }

      public void useChannel() throws Exception
      {
         channel.connect("test");
         channel.getState(null, 25000);
         channel.send(null, null, channel.getLocalAddress());
      }

      List getList()
      {
         return l;
      }

      Map getModifications()
      {
         return mods;
      }

      public void receive(Message msg)
      {
         if (msg.getBuffer() == null)
            return;
         Object obj = msg.getObject();
         synchronized (this)
         {
            l.add(obj);
            Integer key = new Integer(getMod());
            mods.put(key, obj);
         }
      }

      public void viewAccepted(View new_view)
      {
         super.viewAccepted(new_view);
         synchronized (this)
         {
            Integer key = new Integer(getMod());
            mods.put(key, new_view.getVid());
         }
      }

      public void setState(byte[] state)
      {
         super.setState(state);
         try
         {
            List tmp = (List) Util.objectFromByteBuffer(state);
            synchronized (this)
            {
               l.clear();
               l.addAll(tmp);
               log.info("-- [#" + getName() + " (" + channel.getLocalAddress() + ")]: state is " + l);
               Integer key = new Integer(getMod());
               mods.put(key, tmp);
            }
         }
         catch (Exception e)
         {
            e.printStackTrace();
         }
      }

      public byte[] getState()
      {
         super.getState();
         List tmp = null;
         synchronized (this)
         {
            tmp = new LinkedList(l);
            try
            {
               return Util.objectToByteBuffer(tmp);
            }
            catch (Exception e)
            {
               e.printStackTrace();
               return null;
            }
         }
      }

      public void getState(OutputStream ostream)
      {
         super.getState(ostream);
         ObjectOutputStream oos = null;
         try
         {
            oos = new ObjectOutputStream(ostream);
            List tmp = null;
            synchronized (this)
            {
               tmp = new LinkedList(l);
            }
            oos.writeObject(tmp);
            oos.flush();
         }
         catch (IOException e)
         {
            e.printStackTrace();
         }
         finally
         {
            Util.close(oos);
         }
      }

      public void setState(InputStream istream)
      {
         super.setState(istream);
         ObjectInputStream ois = null;
         try
         {
            ois = new ObjectInputStream(istream);
            List tmp = (List) ois.readObject();
            synchronized (this)
            {
               l.clear();
               l.addAll(tmp);
               log.info("-- [#" + getName() + " (" + channel.getLocalAddress() + ")]: state is " + l);
               Integer key = new Integer(getMod());
               mods.put(key, tmp);
            }
         }
         catch (Exception e)
         {
            e.printStackTrace();
         }
         finally
         {
            Util.close(ois);
         }
      }
   }

   public static Test suite()
   {
      return new TestSuite(ConcurrentStartupTest.class);
   }

   public static void main(String[] args)
   {
      String[] testCaseName = {ConcurrentStartupTest.class.getName()};
      junit.textui.TestRunner.main(testCaseName);
   }
}
