package org.jgroups.tests;

import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.StringTokenizer;

import junit.framework.TestCase;
import junit.framework.TestSuite;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jgroups.Address;
import org.jgroups.Channel;
import org.jgroups.ChannelException;
import org.jgroups.ExtendedReceiver;
import org.jgroups.JChannel;
import org.jgroups.JChannelFactory;
import org.jgroups.Message;
import org.jgroups.View;
import org.jgroups.blocks.RpcDispatcher;
import org.jgroups.mux.MuxChannel;
import org.jgroups.util.Util;

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

/**
 * 
 * @author Bela Ban
 * @author Vladimir Blagojevic
 * @author <a href="mailto://brian.stansberry@jboss.com">Brian Stansberry</a>
 * @version $Revision$
 */
public class ChannelTestBase extends TestCase
{
   
   private static final String TEST_CASES = "tests";
   private static final String ANT_PROPERTY = "${tests}";
   private static final String DELIMITER = ",";
   
   protected final static Random RANDOM = new Random();
   
   private static final int LETTER_A = 64;
   
   protected static String DEFAULT_MUX_FACTORY_COUNT = "4";

   protected static String CHANNEL_CONFIG = "udp.xml";  
   
   protected static String MUX_CHANNEL_CONFIG = "stacks.xml"; 
   
   protected static String MUX_CHANNEL_CONFIG_STACK_NAME ="udp";

   protected int active_threads = 0;
   
   protected JChannelFactory muxFactory[] = null;  

   protected String thread_dump = null;    
   
   protected int currentChannelGeneratedName = LETTER_A;

   protected final Log log = LogFactory.getLog(this.getClass());

   public ChannelTestBase()
   {
      super();
   }
   
   public ChannelTestBase(String name)
   {
      super(name);
   }

   protected void setUp() throws Exception
   {
      super.setUp();     
      MUX_CHANNEL_CONFIG = System.getProperty("mux.conf", MUX_CHANNEL_CONFIG);
      MUX_CHANNEL_CONFIG_STACK_NAME = System.getProperty("mux.conf.stack", MUX_CHANNEL_CONFIG_STACK_NAME);
      CHANNEL_CONFIG = System.getProperty("channel.conf", CHANNEL_CONFIG);
      
      currentChannelGeneratedName = LETTER_A;
      
      if (isMuxChannelUsed())
      {                
         muxFactory = new JChannelFactory[getMuxFactoryCount()];
         
         for (int i = 0; i < muxFactory.length; i++)
         {
            muxFactory[i] = new JChannelFactory();
            muxFactory[i].setMultiplexerConfig(MUX_CHANNEL_CONFIG);
         }                
      }
      
      if (shouldCompareThreadCount())
      {
         active_threads = Thread.activeCount();
         thread_dump = "active threads before (" + active_threads + "):\n" + Util.activeThreads();
      }             
   }

   protected void tearDown() throws Exception
   {
      super.tearDown();

      if (isMuxChannelUsed())
      {
         for (int i = 0; i < muxFactory.length; i++)
         {
            muxFactory[i].destroy();
         }
      }          
      
      Util.sleep(500); // remove this in 2.5 !

      if (shouldCompareThreadCount())
      {
         int current_active_threads = Thread.activeCount();

         String msg = "";
         if (active_threads != current_active_threads)
         {
            System.out.println(thread_dump);
            System.out.println("active threads after (" + current_active_threads + "):\n" + Util.activeThreads());
            msg = "active threads:\n" + Util.activeThreads();
         }
         assertEquals(msg, active_threads, current_active_threads);
      }
   }   
   
   /**
    * Returns an array of mux application/service names with a guarantee that: 
    * <p>
    * - there are no application/service name collissions on top of one channel 
    * (i.e cannot have two application/service(s) with the same name on top of one channel)
    * <p>
    * - each generated application/service name is guaranteed to have a corresponding 
    * pair application/service with the same name on another channel     
    * 
    * @param muxApplicationstPerChannelCount
    * @return
    */
   protected String [] createMuxApplicationNames(int muxApplicationstPerChannelCount)
   {      
      return createMuxApplicationNames(muxApplicationstPerChannelCount,getMuxFactoryCount());      
   }
   
   /**
    * Returns an array of mux application/service names with a guarantee that: 
    * <p>
    * - there are no application/service name collissions on top of one channel 
    * (i.e cannot have two application/service(s) with the same name on top of one channel)
    * <p>
    * - each generated application/service name is guaranteed to have a corresponding 
    * pair application/service with the same name on another channel     
    * 
    * @param muxApplicationstPerChannelCount
    * @param muxFactoryCount how many mux factories should be used (has to be less than getMuxFactoryCount())
    * @return array of mux application id's represented as String objects
    */
   protected String [] createMuxApplicationNames(int muxApplicationstPerChannelCount, int muxFactoryCount)
   {           
      if(muxFactoryCount>getMuxFactoryCount())
      {
         throw new IllegalArgumentException("Parameter muxFactoryCount hs to be less than or equal to getMuxFactoryCount()");
      }
      
      int startLetter = LETTER_A; 
      String names [] = null;      
      int totalMuxAppCount = muxFactoryCount * muxApplicationstPerChannelCount;
      names = new String[totalMuxAppCount];
      
      boolean pickNextLetter = false;
      for (int i = 0; i < totalMuxAppCount; i++)
      {  
         pickNextLetter = (i%muxFactoryCount == 0)?true:false;         
         if(pickNextLetter)
         {
            startLetter++;
         }
         names[i] = Character.toString((char)startLetter);
      }      
      return names;
   }
   
   /**
    * Returns channel name as String next in alphabetic sequence since getNextChannelName()
    * has been called last. Sequence is restarted to letter "A" after each setUp call.
    * 
    * @return
    */
   protected String getNextChannelName()
   {
      return Character.toString((char)++currentChannelGeneratedName);
   }
   
   protected String [] createApplicationNames(int applicationCount)
   {
      String names [] = new String[applicationCount];
      for(int i = 0;i<applicationCount;i++)
      {
         names [i] = getNextChannelName();
      }
      return names;
   }
   
   protected Channel createChannel(Object id) throws Exception
   {
      Channel c = null;
      if (isMuxChannelUsed())
      {
         for (int i = 0; i < muxFactory.length; i++)
         {
            if (!muxFactory[i].hasMuxChannel(MUX_CHANNEL_CONFIG_STACK_NAME, id.toString()))
            {
               c = new DefaultMuxChannelTestFactory(muxFactory[i]).createChannel(id);
               return c;
            }
         }

         throw new Exception("Cannot create mux channel with id " + id
               + " since all currently used channels have already registered service with that id");
      }
      else
      {
         c = new DefaultChannelTestFactory().createChannel(id);
      }
      return c;
   }
   
   protected Channel createChannel() throws Exception
   {
      return createChannel("A");
   }

   /**
    * Default channel factory used in junit tests
    *
    */
   protected class DefaultChannelTestFactory implements ChannelTestFactory
   {      
      public Channel createChannel(Object id) throws Exception
      {
         return createChannel(CHANNEL_CONFIG, useBlocking());
      }

      protected Channel createChannel(String configFile, boolean useBlocking) throws Exception
      {
         HashMap channelOptions = new HashMap();
         channelOptions.put(new Integer(Channel.BLOCK), Boolean.TRUE);
         return createChannel(configFile, channelOptions);
      }

      protected Channel createChannel(String configFile, Map channelOptions) throws Exception
      {
         Channel ch = null;
         log.info("Using configuration file " + configFile);
         ch = new JChannel(configFile);
         for (Iterator iter = channelOptions.keySet().iterator(); iter.hasNext();)
         {
            Integer key = (Integer) iter.next();
            Object value = channelOptions.get(key);
            ch.setOpt(key.intValue(), value);
         }
         return ch;
      }
   }    
   
   /**
    * Default channel factory used in junit tests
    *
    */
   public class DefaultMuxChannelTestFactory implements ChannelTestFactory
   {
      JChannelFactory f = null;
      
      public DefaultMuxChannelTestFactory(JChannelFactory f)
      {
         this.f = f;         
      }

      public Channel createChannel(Object id) throws Exception
      {
         Channel c = f.createMultiplexerChannel(MUX_CHANNEL_CONFIG_STACK_NAME, id.toString());
         if(useBlocking())
         {
            c.setOpt(Channel.BLOCK, Boolean.TRUE);
         }
         Address address = c.getLocalAddress(); 
         String append = "[" + id + "]" + " using " + MUX_CHANNEL_CONFIG + ",stack " + MUX_CHANNEL_CONFIG_STACK_NAME;
         if (address == null)
         {
            log.info("Created unconnected mux channel " + append);
         }
         else
         {
            log.info("Created mux channel "+ address + append); 
         }
         return c;
      }         
   }
   
   public class NextAvailableMuxChannelTestFactory implements ChannelTestFactory
   {
      public Channel createChannel(Object id) throws Exception
      {
         return ChannelTestBase.this.createChannel(id);
      }      
   }
   /**
    * Decouples channel creation for junit tests
    *
    */
   protected interface ChannelTestFactory
   {
      public Channel createChannel(Object id) throws Exception;
   }

   /**
    * Base class for all aplications using channel
    *   
    *
    */
   protected abstract class ChannelApplication implements Runnable, MemberRetrievable
   {
      protected Channel channel;

      protected Thread thread;

      protected Throwable exception;

      protected String name;          

      public ChannelApplication(String name,JChannelFactory f) throws Exception
      {
         if(f==null)
         {
            createChannel(name, new DefaultChannelTestFactory());
         }
         else
         {
            createChannel(name, new DefaultMuxChannelTestFactory(f));
         }
      }

      /**
       * Creates a unconnected channel and assigns a name to it.
       * 
       * @param name name of this channel
       * @param factory factory to create Channel
       * @throws ChannelException
       */
      public ChannelApplication(String name, ChannelTestFactory factory) throws Exception
      {
         createChannel(name, factory);
      }
      
      private void createChannel(String name, ChannelTestFactory factory) throws Exception
      {
         this.name = name;
         channel = factory.createChannel(name);
      }

      /**
       * Method allowing implementation of specific test application level logic      
       * @throws Exception
       */
      protected abstract void useChannel() throws Exception;

      public void run()
      {
         try
         {
            useChannel();
         }
         catch (Exception e)
         {
            log.error(name + ": " + e.getLocalizedMessage(), e);

            // Save it for the test to check
            exception = e;
         }
      }

      public List getMembers()
      {
         List result = null;
         View v = channel.getView();
         if (v != null)
         {
            result = v.getMembers();
         }
         return result;
      }
      
      public boolean isUsingMuxChannel()
      {
         return channel instanceof MuxChannel;        
      }

      public Address getLocalAddress()
      {
         return channel.getLocalAddress();
      }

      public void start()
      {
         thread = new Thread(this, getName());
         thread.start();
         Address a = getLocalAddress();
         boolean connected = a != null ? true : false;
         if (connected)
         {
            log.info("Thread for channel " + a + "[" + getName() + "] started");
         }
         else
         {
            log.info("Thread for channel [" + getName() + "] started");
         }
      }

      public void setChannel(Channel ch)
      {
         this.channel = ch;
      }

      public Channel getChannel()
      {
         return channel;
      }

      public String getName()
      {
         return name;
      }

      public void cleanup()
      {
         if (thread != null && thread.isAlive())
         {
            thread.interrupt();
         }
         Address a = getLocalAddress();
         boolean connected = a != null ? true : false;
         if (connected)
         {
            log.info("Closing channel " + a + "[" + getName() + "]");
         }
         else
         {
            log.info("Closing channel [" + getName() + "]");
         }
         channel.close();
      }
   }
   
   protected abstract class PushChannelApplication extends ChannelApplication implements ExtendedReceiver
   {
      RpcDispatcher dispatcher;

      public PushChannelApplication(String name) throws Exception
      {
         this(name, new DefaultChannelTestFactory(), false);
      }
      
      public PushChannelApplication(String name, JChannelFactory f) throws Exception
      {
         this(name, new DefaultMuxChannelTestFactory(f), false);
      }

      public PushChannelApplication(String name, boolean useDispatcher) throws Exception
      {
         this(name, new DefaultChannelTestFactory(), useDispatcher);
      }

      public PushChannelApplication(String name, ChannelTestFactory factory, boolean useDispatcher)
            throws Exception
      {
         super(name, factory);
         if (useDispatcher)
         {
            dispatcher = new RpcDispatcher(channel, this, this, this);
         }
         else
         {
            channel.setReceiver(this);
         }
      }

      public RpcDispatcher getDispatcher()
      {
         return dispatcher;
      }

      public boolean hasDispatcher()
      {
         return dispatcher != null;
      }

      public void block()
      {
         log.debug("Channel " + getLocalAddress() + "[" + getName() + "] in blocking");
      }

      public byte[] getState()
      {
         log.debug("Channel " + getLocalAddress() + "[" + getName() + "] ");
         return null;
      }

      public void getState(OutputStream ostream)
      {
         log.debug("Channel " + getLocalAddress() + "[" + getName() + "]");
      }

      public byte[] getState(String state_id)
      {
         log.debug("Channel " + getLocalAddress() + "[" + getName() + " state id =" + state_id);
         return null;
      }

      public void getState(String state_id, OutputStream ostream)
      {
         log.debug("Channel " + getLocalAddress() + "[" + getName() + "] state id =" + state_id);
      }

      public void receive(Message msg)
      {
      }

      public void setState(byte[] state)
      {
         log.debug("Channel " + getLocalAddress() + "[" + getName() + "] ");
      }

      public void setState(InputStream istream)
      {
         log.debug("Channel " + getLocalAddress() + "[" + getName() + "]");
      }

      public void setState(String state_id, byte[] state)
      {
         log.debug("Channel " + getLocalAddress() + "[" + getName() + "] state id =" + state_id + ", state size is "
               + state.length);
      }

      public void setState(String state_id, InputStream istream)
      {
         log.debug("Channel " + getLocalAddress() + "[" + getName() + "] state id " + state_id);
      }

      public void suspect(Address suspected_mbr)
      {
         log.debug("Channel " + getLocalAddress() + "[" + getName() + "] suspecting " + suspected_mbr);
      }

      public void unblock()
      {
         log.debug("Channel " + getLocalAddress() + "[" + getName() + "] unblocking");
      }

      public void viewAccepted(View new_view)
      {
         log.debug("Channel " + getLocalAddress() + "[" + getName() + "] accepted view " + new_view);
      }
   }

   /**
    * Channel with semaphore allows application to go through fine-grained synchronous step control.
    * <p> 
    * PushChannelApplicationWithSemaphore application will not proceed to useChannel() 
    * until it acquires permit from semphore. After useChannel() completes the acquired 
    * permit will be released.  Test driver should control how semaphore tickets are given 
    * and acquired.
    *
    */
   protected abstract class PushChannelApplicationWithSemaphore extends PushChannelApplication
   {
      protected Semaphore semaphore;

      public PushChannelApplicationWithSemaphore(String name, ChannelTestFactory factory, Semaphore semaphore,
            boolean useDispatcher) throws Exception
      {
         super(name, factory, useDispatcher);
         this.semaphore = semaphore;
      }

      protected PushChannelApplicationWithSemaphore(String name, Semaphore semaphore) throws Exception
      {
         this(name, new DefaultChannelTestFactory(), semaphore, false);
      }
      
      protected PushChannelApplicationWithSemaphore(String name, JChannelFactory f,Semaphore semaphore) throws Exception
      {
         this(name, new DefaultMuxChannelTestFactory(f), semaphore, false);
      }

      protected PushChannelApplicationWithSemaphore(String name, Semaphore semaphore, boolean useDispatcher)
            throws Exception
      {
         this(name, new DefaultChannelTestFactory(), semaphore, useDispatcher);
      }

      public void run()
      {
         boolean acquired = false;
         try
         {
            acquired = semaphore.attempt(60000);
            if (!acquired)
            {
               throw new Exception(name + " cannot acquire semaphore");
            }

            useChannel();
         }
         catch (Exception e)
         {
            log.error(name + ": " + e.getLocalizedMessage(), e);
            // Save it for the test to check
            exception = e;
         }
         finally
         {
            if (acquired)
            {
               semaphore.release();
            }
         }
      }
   }     

   protected interface MemberRetrievable
   {
      public List getMembers();

      public Address getLocalAddress();
   }
   
   /**
    * Returns true if JVM has been started with mux.on system property 
    * set to true, false otherwise. 
    * 
    * @return
    */
   protected boolean isMuxChannelUsed()
   {
      return Boolean.valueOf(System.getProperty("mux.on", "false")).booleanValue();
   }
   
   /**
    * Returns true if JVM has been started with threadcount system property 
    * set to true, false otherwise. 
    * 
    * @return
    */
   protected boolean shouldCompareThreadCount()
   {
      return Boolean.valueOf(System.getProperty("threadcount", "false")).booleanValue();
   }
   
   /**
    * Returns value of mux.factorycount system property has been set, otherwise returns 
    * DEFAULT_MUX_FACTORY_COUNT. 
    * 
    * @return
    */
   protected int getMuxFactoryCount()
   {
      return Integer.parseInt(System.getProperty("mux.factorycount", DEFAULT_MUX_FACTORY_COUNT));   
   }
   
   /**
    * Returns true if JVM has been started with useBlocking system property 
    * set to true, false otherwise. 
    * 
    * @return
    */     
   protected boolean useBlocking()
   {
      return Boolean.valueOf(System.getProperty("useBlocking", "false")).booleanValue();
   }

   /**
    * Checks each channel in the parameter array to see if it has the 
    * exact same view as other channels in an array.    
    */
   public static boolean areViewsComplete(MemberRetrievable[] channels,int memberCount)
   {      
      for (int i = 0; i < memberCount; i++)
      {
         if (!isViewComplete(channels[i], memberCount))
         {
            return false;
         }
      }

      return true;
   }

   /**
    * Loops, continually calling {@link #areViewsComplete(MemberRetrievable[])}
    * until it either returns true or <code>timeout</code> ms have elapsed.
    *
    * @param channels  channels which must all have consistent views
    * @param timeout max number of ms to loop
    * @throws RuntimeException if <code>timeout</code> ms have elapse without
    *                          all channels having the same number of members.
    */
   public static void blockUntilViewsReceived(MemberRetrievable[] channels,long timeout)
   {
      blockUntilViewsReceived(channels,channels.length,timeout);
   }
   
   public static void blockUntilViewsReceived(Collection channels,long timeout)
   {
      blockUntilViewsReceived(channels,channels.size(),timeout);
   }
   
   /**
    * Loops, continually calling {@link #areViewsComplete(MemberRetrievable[])}
    * until it either returns true or <code>timeout</code> ms have elapsed.
    *
    * @param channels  channels which must all have consistent views
    * @param timeout max number of ms to loop
    * @throws RuntimeException if <code>timeout</code> ms have elapse without
    *                          all channels having the same number of members.
    */
   public static void blockUntilViewsReceived(MemberRetrievable[] channels, int count, long timeout)
   {
      long failTime = System.currentTimeMillis() + timeout;

      while (System.currentTimeMillis() < failTime)
      {
         sleepThread(100);
         if (areViewsComplete(channels,count))
         {
            return;
         }
      }

      throw new RuntimeException("timed out before caches had complete views");
   }
   
   public static void blockUntilViewsReceived(Collection channels, int count, long timeout)
   {
      long failTime = System.currentTimeMillis() + timeout;

      
      while (System.currentTimeMillis() < failTime)
      {
         sleepThread(100);
         if (areViewsComplete((MemberRetrievable[])channels.toArray(new MemberRetrievable[channels.size()]),count))
         {
            return;
         }
      }

      throw new RuntimeException("timed out before caches had complete views");
   }

   public static boolean isViewComplete(MemberRetrievable channel, int memberCount)
   {

      List members = channel.getMembers();
      if (members == null || memberCount > members.size())
      {
         return false;
      }
      else if (memberCount < members.size())
      {
         // This is an exceptional condition
         StringBuffer sb = new StringBuffer("Channel at address ");
         sb.append(channel.getLocalAddress());
         sb.append(" had ");
         sb.append(members.size());
         sb.append(" members; expecting ");
         sb.append(memberCount);
         sb.append(". Members were (");
         for (int j = 0; j < members.size(); j++)
         {
            if (j > 0)
            {
               sb.append(", ");
            }
            sb.append(members.get(j));
         }
         sb.append(')');

         throw new IllegalStateException(sb.toString());
      }

      return true;
   }

   public static void takeAllPermits(Semaphore semaphore, int count)
   {
      for (int i = 0; i < count; i++)
      {
         try
         {
            semaphore.acquire();
         }
         catch (InterruptedException e)
         {
            //not interested
            e.printStackTrace();
         }
      }
   }

   public static void acquireSemaphore(Semaphore semaphore, long timeout, int count) throws Exception
   {
      for (int i = 0; i < count; i++)
      {
         boolean acquired = false;
         try
         {
            acquired = semaphore.attempt(timeout);
         }
         catch (InterruptedException e)
         {
            //not interested but print it
            e.printStackTrace();
         }
         if (!acquired)
            throw new Exception("Failed to acquire semaphore");
      }
   }

   public static void sleepRandom(int maxTime)
   {
      sleepThread(RANDOM.nextInt(maxTime));
   }

   /**
    * Puts the current thread to sleep for the desired number of ms, suppressing
    * any exceptions.
    *
    * @param sleeptime number of ms to sleep
    */
   public static void sleepThread(long sleeptime)
   {
      try
      {
         Thread.sleep(sleeptime);
      }
      catch (InterruptedException ie)
      {
      }
   }
   

}
