package org.jgroups.tests;

import static java.util.concurrent.TimeUnit.SECONDS;

import java.net.InetAddress;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.jgroups.Channel;
import org.jgroups.JChannel;
import org.jgroups.Message;
import org.jgroups.blocks.GroupRequest;
import org.jgroups.blocks.MessageDispatcher;
import org.jgroups.blocks.RequestHandler;
import org.jgroups.protocols.MERGE2;
import org.jgroups.protocols.MPING;
import org.jgroups.protocols.pbcast.GMS;
import org.jgroups.stack.Protocol;
import org.jgroups.stack.ProtocolStack;
import org.jgroups.util.RspList;
import org.jgroups.util.Util;
import org.testng.annotations.Test;

/**
 * Tests concurrent startup
 * @author Brian Goose
 */
@Test(groups="broken",sequential=true)
public class ChannelConcurrencyTest  extends ChannelTestBase{

    public void testPlainChannel () throws Throwable{
        testhelper(false);
    }
    
    public void testwithDispatcher () throws Throwable{
        testhelper(true);
    }
    
    protected  void testhelper(boolean useDispatcher) throws Throwable {
        final int count=8;

        final ExecutorService executor=Executors.newFixedThreadPool(count);
        final CountDownLatch latch=new CountDownLatch(count);
        final JChannel[] channels=new JChannel[count];
        final Task[] tasks=new Task[count];

        final long start=System.currentTimeMillis();
        for(int i=0;i < count;i++) {
            if(i == 0)
                channels[i]=createChannel(true, count);
            else
                channels[i]=createChannel(channels[0]);
            
            tasks[i]=new Task(latch, channels[i],useDispatcher);
            changeMergeInterval(channels[i]);
            changeViewBundling(channels[i]);
            replaceDiscoveryProtocol(channels[i]);
        }

        for(final Task t:tasks) {
            executor.execute(t);
        }

        int timeoutToConverge=120;
        boolean successConnecting  = false;
        try {
            // Wait for all channels to finish connecting
            successConnecting=latch.await(timeoutToConverge, TimeUnit.SECONDS);            
            if(successConnecting) {
                log.info("All connected. Converging...");
                for(Task t:tasks) {
                    Throwable ex=t.getException();
                    if(ex != null)
                        throw ex;
                }

                // Wait for all channels to have the correct number of members in their
                // current view
                boolean converged=false;
                for(int counter=0;counter < timeoutToConverge && !converged;SECONDS.sleep(1),counter++) {
                    for(final JChannel channel:channels) {
                        converged=channel.getView() != null && channel.getView().size() == count;
                        if(!converged)
                            break;
                    }
                }

                final long duration=System.currentTimeMillis() - start;
                log.info("Converged to a single group after " + duration
                                   + " ms; group is:\n");
                for(int i=0;i < channels.length;i++) {
                    log.info("#" + (i + 1)
                                       + ": "
                                       + channels[i].getAddress()
                                       + ": "
                                       + channels[i].getView());
                }                
            }         

            for(final JChannel channel:channels) {
                assertEquals("View ok for channel " + channel.getAddress(), count, channel.getView().size());
            }
            assertTrue("All channels were succefully connected",successConnecting);     
        }
        finally {
            Util.sleep(2500);
            executor.shutdownNow();
            log.info("closing channels: ");            
            for(JChannel ch:channels) {
                ch.close();                               
                //there are sometimes big delays until entire cluster shuts down
                //use sleep to make a smoother shutdown so we avoid false positives
                Util.sleep(500);                
            }                                    
        }
    }


    private static void changeViewBundling(JChannel channel) {
        ProtocolStack stack=channel.getProtocolStack();
        GMS gms=(GMS)stack.findProtocol(GMS.class);
        if(gms != null) {
            gms.setViewBundling(true);
            gms.setMaxBundlingTime(500);
        }
    }

    private static void changeMergeInterval(JChannel channel) {
        ProtocolStack stack=channel.getProtocolStack();
        MERGE2 merge=(MERGE2)stack.findProtocol(MERGE2.class);
        if(merge != null) {
            merge.setMinInterval(5000);
            merge.setMaxInterval(10000);
        }
    }
    
    private static void replaceDiscoveryProtocol(JChannel ch) throws Exception {
        ProtocolStack stack=ch.getProtocolStack();
        Protocol discovery=stack.removeProtocol("TCPPING");
        if(discovery != null){
            Protocol transport = stack.getTransport();
            MPING mping=new MPING();
            InetAddress bindAddress=Util.getBindAddress(new Properties());
            mping.setBindAddr(bindAddress);
            mping.setMulticastAddress("230.1.2.3");
            mping.setMcastPort(8888);            
            stack.insertProtocol(mping, ProtocolStack.ABOVE, transport.getName());
            mping.setProtocolStack(ch.getProtocolStack());
            mping.init();
            mping.start();            
            System.out.println("Replaced TCPPING with MPING. See http://wiki.jboss.org/wiki/Wiki.jsp?page=JGroupsMERGE2");            
        }        
    }

    private static class Task implements Runnable {
        private final Channel c;
        private final CountDownLatch latch;
        private Throwable exception=null;
        private boolean useDispatcher = false;


        public Task(CountDownLatch latch, Channel c,boolean useDispatcher) {
            this.latch=latch;
            this.c=c;
            this.useDispatcher = useDispatcher;
        }

        public Throwable getException() {
            return exception;
        }

        public void run() {
            try {
                c.connect("ChannelConcurrencyTest");
                if(useDispatcher && c.isConnected()) {
                    final MessageDispatcher md=new MessageDispatcher(c, null, null, new MyHandler());
                    for(int i=0;i < 10;i++) {
                        final RspList rsp=md.castMessage(null,
                                                         new Message(null, null, i),
                                                         GroupRequest.GET_ALL,
                                                         2500);
                        for(Object o:rsp.getResults()) {
                            assertEquals("Wrong result received at " + c.getAddress(), i, o);
                        }
                    }
                }
            }
            catch(final Exception e) {
                exception=e;
                e.printStackTrace();
            }
            finally {
                latch.countDown();
            }
        }
    }
    private static class MyHandler implements RequestHandler {

        public Object handle(Message msg) {
            return msg.getObject();
        }

    }
}