package org.jgroups.tests;

import org.jgroups.Global;
import org.jgroups.JChannel;
import org.jgroups.Message;
import org.jgroups.ReceiverAdapter;
import org.jgroups.protocols.TP;
import org.jgroups.protocols.pbcast.GMS;
import org.jgroups.stack.ProtocolStack;
import org.jgroups.util.Promise;
import org.jgroups.util.Util;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 * Measure the latency between messages with message bundling enabled at the transport level
 * @author Bela Ban
 */
@Test(groups=Global.STACK_DEPENDENT,singleThreaded=true)
public class MessageBundlingTest extends ChannelTestBase {
    private JChannel           a, b;
    private MyReceiver         r2;
    private final static long  LATENCY=1500L;
    private final static long  SLEEP=5000L;
    private static final int   MAX_BYTES=64000;


    @BeforeMethod
    protected void createChannels() throws Exception {
        a=createChannel(true, 2, "A");
        setBundling(a,MAX_BYTES,LATENCY);
        a.connect("MessageBundlingTest");
        b=createChannel(a, "B");
        r2=new MyReceiver();
        b.setReceiver(r2);
        b.connect("MessageBundlingTest");
        Util.waitUntilAllChannelsHaveSameView(10000, 1000, a, b);
    }

    @AfterMethod void tearDown() throws Exception {Util.close(b,a);}

    protected boolean useBlocking() {return false;}
    

    public void testSimple() throws Exception {
        final Promise<Boolean> promise=new Promise<>();
        SimpleReceiver receiver=new SimpleReceiver(promise);
        b.setReceiver(receiver);
        long start=System.currentTimeMillis();
        a.send(new Message());
        promise.getResult(5000);
        long diff=System.currentTimeMillis() - start;
        System.out.println("toook " + diff + " ms to send and receive a multicast message");
        assert diff < SLEEP /2;
    }

    public void testLatencyWithoutMessageBundling() throws Exception {
        Message tmp=new Message().setFlag(Message.Flag.DONT_BUNDLE);
        setBundling(a, MAX_BYTES, 30);
        r2.setNumExpectedMesssages(1);
        Promise<Integer> promise=new Promise<>();
        r2.setPromise(promise);
        long time=System.currentTimeMillis();
        a.send(tmp);
        System.out.println(">>> sent message at " + new Date());
        promise.getResult(SLEEP);
        List<Long> list=r2.getTimes();
        Assert.assertEquals(1, list.size());
        Long time2=list.get(0);
        long diff=time2 - time;
        System.out.println("latency: " + diff + " ms");
        assertTrue("latency (" + diff + "ms) should be less than " + LATENCY + " ms", diff <= LATENCY);
    }


    public void testLatencyWithMessageBundling() throws Exception {
        Message tmp=new Message();
        r2.setNumExpectedMesssages(1);
        Promise<Integer> promise=new Promise<>();
        r2.setPromise(promise);
        long time=System.currentTimeMillis();
        a.send(tmp);
        System.out.println(">>> sent message at " + new Date());
        promise.getResult(SLEEP);
        List<Long> list=r2.getTimes();
        Assert.assertEquals(1, list.size());
        Long time2=list.get(0);
        long diff=time2 - time;
        System.out.println("latency: " + diff + " ms");
        assertTrue("latency (" + diff + "ms) should be less than 2 times the LATENCY (" + LATENCY *2 + ")",
                   diff <= LATENCY * 2);
    }



    public void testLatencyWithMessageBundlingAndLoopback() throws Exception {
        Message tmp=new Message();
        r2.setNumExpectedMesssages(1);
        Promise<Integer> promise=new Promise<>();
        r2.setPromise(promise);
        long time=System.currentTimeMillis();
        System.out.println(">>> sending message at " + new Date());
        a.send(tmp);
        promise.getResult(SLEEP);
        List<Long> list=r2.getTimes();
        Assert.assertEquals(1, list.size());
        Long time2=list.get(0);
        long diff=time2 - time;
        System.out.println("latency: " + diff + " ms");
        assertTrue("latency (" + diff + "ms) should be less than 2 times the LATENCY (" + LATENCY *2 + ")",
                   diff <= LATENCY * 2);
    }


    public void testLatencyWithMessageBundlingAndMaxBytes() throws Exception {
        r2.setNumExpectedMesssages(10);
        Promise<Integer> promise=new Promise<>();
        r2.setPromise(promise);
        Util.sleep(LATENCY * 2);
        System.out.println(">>> sending 10 messages at " + new Date());
        for(int i=0; i < 10; i++)
            a.send(new Message(null, null, new byte[2000]));

        promise.getResult(SLEEP); // we should get the messages immediately because max_bundle_size has been exceeded by the 20 messages
        List<Long> list=r2.getTimes();
        Assert.assertEquals(10, list.size());

        for(Iterator<Long> it=list.iterator(); it.hasNext();) {
            Long val=it.next();
            System.out.println(val);
        }
    }



    private static void setBundling(JChannel ch, int max_bytes, long timeout) {
        ProtocolStack stack=ch.getProtocolStack();
        TP transport=stack.getTransport();
        transport.setMaxBundleSize(max_bytes);
        transport.setMaxBundleTimeout(timeout);
        GMS gms=(GMS)stack.findProtocol(GMS.class);
        gms.setViewAckCollectionTimeout(LATENCY * 2);
        gms.setJoinTimeout(LATENCY * 2);
    }



    protected static class SimpleReceiver extends ReceiverAdapter {
        protected final Promise<Boolean> promise;

        public SimpleReceiver(Promise<Boolean> promise) {
            this.promise=promise;
        }

        public void receive(Message msg) {
            promise.setResult(true);
        }
    }

    private static class MyReceiver extends ReceiverAdapter {
        private final List<Long> times=new LinkedList<>();
        private int num_expected_msgs;
        private Promise<Integer> promise;

        public List<Long> getTimes() {
            return times;
        }

        public void setNumExpectedMesssages(int num_expected_msgs) {
            this.num_expected_msgs=num_expected_msgs;
        }

        public void setPromise(Promise<Integer> promise) {
            this.promise=promise;
        }

        public int size() {
            return times.size();
        }

        public void receive(Message msg) {
            times.add(System.currentTimeMillis());
            System.out.println("<<< received message from " + msg.getSrc() + " at " + new Date());
            if(times.size() >= num_expected_msgs && promise != null) {
                promise.setResult(times.size());
            }
        }
    }


}
