package org.jgroups.tests;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

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

import org.jgroups.Address;
import org.jgroups.BlockEvent;
import org.jgroups.Channel;
import org.jgroups.ExtendedReceiver;
import org.jgroups.JChannel;
import org.jgroups.Message;
import org.jgroups.StreamingGetStateEvent;
import org.jgroups.StreamingSetStateEvent;
import org.jgroups.TimeoutException;
import org.jgroups.UnblockEvent;
import org.jgroups.View;
import org.jgroups.ViewId;
import org.jgroups.blocks.RpcDispatcher;
import org.jgroups.util.Util;

/**
 * Tests streaming state transfer for both pull and push mode of channel 
 * operations. Size of the transfer is configurable. Test runner should 
 * specify "pull" and "size" parameter as JVM parameters when running this 
 * test. If not specified default values are to use push mode and transfer 
 * size of 100 MB. 
 * 
 *  <p>
 *  
 *  To specify pull mode and size transfer of 500 MB test runner should pass 
 *  JVM parameters:
 *  
 *  <p>
 *  -Dpull=true -Dsize=500
 *
 * 
 * @author Vladimir Blagojevic
 * @version $Id$ 
 *
 */
public class StreamingStateTransferTest extends TestCase{

	private final static String CHANNEL_PROPS="streaming-state-transfer.xml";
	private final static int INITIAL_NUMBER_OF_MEMBERS=5;
	private int runningTime = 1000*50; // 50 secs 
	private Random r = new Random();
	private boolean usePullMode = false;
    private boolean useDisp = false;
	private int size = 100; //100MB
	
	private final static int MEGABYTE = 1048576;
	
	
	public StreamingStateTransferTest(String arg0) {
		super(arg0);
	}
	
	public void testTransfer() throws Exception
	{
		long start = System.currentTimeMillis();
		boolean running=true;
		List members=new ArrayList();			
		
		//first spawn and join
		for(int i =0;i<INITIAL_NUMBER_OF_MEMBERS;i++)
		{
			GroupMember member = new GroupMember(usePullMode,useDisp,size);			
			members.add(member);
			Thread t = new Thread(member);
			t.start();
			Util.sleep(getRandomDelayInSeconds(10,12)*1000);
		}
		
		for (; running;) {						
			
			//and then flip a coin
			if(r.nextBoolean())
			{
				Util.sleep(getRandomDelayInSeconds(10,12)*1000);
				GroupMember member = new GroupMember(usePullMode,useDisp,size);					
				members.add(member);				
				Thread t = new Thread(member);
				t.start();
			}
			else if(members.size()>1)
			{
				Util.sleep(getRandomDelayInSeconds(3,8)*1000);
				GroupMember unluckyBastard = (GroupMember) members.get(r.nextInt(members.size()));
				if(!unluckyBastard.isCoordinator())
				{
					members.remove(unluckyBastard);				
					unluckyBastard.stopRunning();					
				}
				else
				{
					System.out.println("Not killing coordinator ");
				}
			}				
			running =System.currentTimeMillis() - start <= runningTime;
			System.out.println("Running time " + ((System.currentTimeMillis()-start)/1000) + " secs");
		}
		System.out.println("Done");
	}
	
	protected int getRandomDelayInSeconds(int from,int to)
	{
		return from + r.nextInt(to-from);
	}

	protected void setUp() throws Exception {
		
		//NOTE use -Ddisp=true|false -Dpull=true|false -Dsize=int (size of transfer)
       
        String prop = System.getProperty("disp");
        if(prop!=null)
        {
            useDisp = prop.equalsIgnoreCase("true");
            System.out.println("Using parameter disp=" + useDisp);
        }
		prop = System.getProperty("pull");
		if(prop!=null)
		{
			usePullMode = prop.equalsIgnoreCase("true");
			System.out.println("Using parameter usePullMode=" + usePullMode);
		}
		
		prop = System.getProperty("size");
		if(prop!=null)
		{
			size = Integer.parseInt(System.getProperty("size"));
			System.out.println("Using parameter size=" + size);
		}
		super.setUp();
	}

	protected void tearDown() throws Exception {
		super.tearDown();		
	}
	
	public static Test suite() {		  
	      return new TestSuite(StreamingStateTransferTest.class);
	}
	 
	public static void main(String[] args) {		
		 String[] testCaseName={StreamingStateTransferTest.class.getName()};
	     junit.textui.TestRunner.main(testCaseName);
	}
	
	private static class GroupMember implements Runnable,ExtendedReceiver{
		JChannel ch = null;		
		View currentView;		
		volatile boolean running = true;		
		private int stateSize;	
		private int bufferSize = 8*1024;
		private boolean usePullMode;	
		private Random ran = new Random();
        private boolean useDispacher;

		public GroupMember(boolean pullMode, boolean dispMode, int size) {
			setStateSize(size * MEGABYTE);
			setUsePullMode(pullMode);
			setUseDispatcher(dispMode);
			try {
				ch = new JChannel(CHANNEL_PROPS);
				ch.setOpt(Channel.AUTO_RECONNECT, Boolean.TRUE);
				ch.setOpt(Channel.AUTO_GETSTATE, Boolean.TRUE);
				ch.setOpt(Channel.BLOCK, Boolean.TRUE);
				if (useDispacher) {
					RpcDispatcher disp = new RpcDispatcher(ch, this, this, this);
				} else if (!usePullMode) {
					ch.setReceiver(this);
				}
				ch.connect("transfer");				
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		public final void setUsePullMode(boolean usePullMode) {
			this.usePullMode = usePullMode;
		}
        
        public final void setUseDispatcher(boolean useDispacher) {
            this.useDispacher = useDispacher;
        }

		public String getAddress() {
			if(ch!=null && ch.isConnected())
			{
				return ch.getLocalAddress().toString();
			}
			return null;
		}

		public void stopRunning() {
			running=false;	
			System.out.println("Disconnect " + getAddress());
			if(ch!=null)ch.close();
		}	
		
		protected boolean isCoordinator() {
			if (ch == null)
				return false;
			Object local_addr = ch.getLocalAddress();
			if (local_addr == null)
				return false;
			View view = ch.getView();
			if (view == null)
				return false;
			ViewId vid = view.getVid();
			if (vid == null)
				return false;
			Object coord = vid.getCoordAddress();
			if (coord == null)
				return false;
			return local_addr.equals(coord);
		}

		public final void setStateSize(int stateSize) {
			this.stateSize = stateSize;
		}

		public void run() {
			Runnable r = new Runnable() {
				public void run() {
					try {
						if (ran.nextBoolean()) {
							ch.getState(null, 5000);
						} else {
							String randomStateId = Long.toString(Math.abs(ran.nextLong()), 36);
							ch.getState(null, randomStateId, 5000);
						}
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			};
			if (usePullMode) {	
				
				//when BLOCK events are turned on, pbcast.FLUSH is used and we use pull channel mode 
				//we have to getState on a separate thread. Why? Because joining member has to immediatelly 
				//go into receive and fetch/respond to block event which is received as part of state transfer.
				new Thread(r).start();
			}
			else
			{
				r.run();
			}
			while (running) {
				Object msgReceived = null;
				try {					
					msgReceived = ch.receive(0);
                    if (msgReceived instanceof BlockEvent) {
                       this.block();
                       ch.blockOk();
                    } else if (msgReceived instanceof UnblockEvent) {
                       this.unblock();                           
                    }
					if (!running) {
						// I am not a group member anymore so
						// I will discard any transient message I
						// receive
					} else {
						if (msgReceived instanceof View) {							
						} else if (msgReceived instanceof StreamingGetStateEvent) {
							StreamingGetStateEvent evt = (StreamingGetStateEvent) msgReceived;
							if(evt.getStateId()!=null)
							{
								this.getState(evt.getStateId(),evt.getArg());	
							}
							else
							{
								this.getState(evt.getArg());
							}
						} else if (msgReceived instanceof StreamingSetStateEvent) {
							StreamingSetStateEvent evt = (StreamingSetStateEvent) msgReceived;
							if(evt.getStateId()!=null)
							{
								this.setState(evt.getStateId(),evt.getArg());
							}
							else
							{
								this.setState(evt.getArg());
							}
						} 
					}                    

				} catch (TimeoutException e) {					
				} catch (Exception e) {					
					ch.close();
					running = false;
				}			
			}					
		}
	
		public void getState(OutputStream ostream) {			
			InputStream stream = Thread.currentThread().getContextClassLoader()
					.getResourceAsStream("org/jgroups/JChannel.class");
			System.out.println(Thread.currentThread() + " at " + getAddress()
					+ " is sending state of " + (stateSize / MEGABYTE) + " MB");
			
			int markSize = 1024*100; //100K should be enough
			byte buffer [] = new byte[bufferSize];
			int bytesRead=-1;	
			int size = stateSize;
			try {															
				while(size>0)
				{
					stream.mark(markSize);
					bytesRead=stream.read(buffer);
					ostream.write(buffer);
					stream.reset();									
					size = size-bytesRead;					
				}					
			} catch (IOException e) {				
				e.printStackTrace();
			}	
			finally
			{				
				try {
					ostream.flush();
					ostream.close();
				} catch (IOException e) {					
					e.printStackTrace();
				}				
			}
		}

		public void setState(InputStream istream) {
			int totalRead=0;
			byte buffer [] = new byte[bufferSize];
			int bytesRead=-1;
			long start = System.currentTimeMillis();
			try {
				while((bytesRead=istream.read(buffer))>=0)
				{					
					totalRead+=bytesRead;
				}								
			} catch (IOException e) {				
				e.printStackTrace();
			}
			finally
			{
				try {
					istream.close();
				} catch (IOException e) {					
					e.printStackTrace();
				}
			}
			long readingTime = System.currentTimeMillis()-start;
			System.out.println(Thread.currentThread() + " at " + getAddress()
					+ " read state of " + (totalRead / MEGABYTE) + " MB in "
					+ readingTime + " msec");						
		}

		public void receive(Message msg) {		
		}

		public void setState(byte[] state) {			
		}

		public void viewAccepted(View new_view) {			
		}

		public void suspect(Address suspected_mbr) {		
		}

		public void block() {	
           // System.out.println("Block at " + ch.getLocalAddress());
		}

        public void unblock() {
           // System.out.println("Unblock at " + ch.getLocalAddress());
        }

        public byte[] getState() {
			return null;
		}
		
		public byte[] getState(String state_id) {		
			return null;
		}

		public void setState(String state_id, byte[] state) {						
		}

		public void getState(String state_id, OutputStream ostream) {
			System.out.println("Writing partial streaming state transfer for " + state_id);
			getState(ostream);
		}

		public void setState(String state_id, InputStream istream) {
			System.out.println("Reading partial streaming state transfer for " + state_id);
			setState(istream);			
		}		
	}
}
