package org.jgroups.tests;


import org.jgroups.Global;
import org.jgroups.Message;
import org.jgroups.util.Util;
import org.jgroups.util.Tuple;
import org.jgroups.stack.AckReceiverWindow;
import org.testng.Assert;
import org.testng.annotations.Test;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.AfterMethod;

import java.util.concurrent.CountDownLatch;
import java.util.List;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * @author Bela Ban
 */
@Test(groups=Global.FUNCTIONAL,sequential=true)
public class AckReceiverWindowTest {
    AckReceiverWindow win;

    @BeforeMethod
    public void setUp() throws Exception {
        win=new AckReceiverWindow(1);
    }

    @AfterMethod
    public void tearDown() throws Exception {
        win.reset();
    }


    public void testAdd() {
        Message m;
        win.reset(); // use a different window for this test method
        win=new AckReceiverWindow(10);
        Assert.assertEquals(0, win.size());
        win.add(9, msg());
        Assert.assertEquals(0, win.size());

        win.add(10, msg());
        Assert.assertEquals(1, win.size());

        win.add(13, msg());
        Assert.assertEquals(2, win.size());

        m=win.remove();
        assert m != null;
        Assert.assertEquals(1, win.size());

        m=win.remove();
        assert m == null;
        Assert.assertEquals(1, win.size());

        win.add(11, msg());
        win.add(12, msg());
        Assert.assertEquals(3, win.size());

        m=win.remove();
        assert m != null;
        m=win.remove();
        assert m != null;
        m=win.remove();
        assert m != null;
        Assert.assertEquals(0, win.size());
        m=win.remove();
        assert m == null;
    }

    public void testAddExisting() {
        win.add(1, msg());
        assert win.size() == 1;
        win.add(1, msg());
        assert win.size() == 1;
        win.add(2, msg());
        assert win.size() == 2;
    }

    public void testAddLowerThanNextToRemove() {
        win.add(1, msg());
        win.add(2, msg());
        win.remove(); // next_to_remove is 2
        win.add(1, msg());
        assert win.size() == 1;
    }

    public void testRemove() {
        win.add(2, msg());
        Message msg=win.remove();
        assert msg == null;
        assert win.size() == 1;
        win.add(1, msg());
        assert win.size() == 2;
        assert win.remove() != null;
        assert win.size() == 1;
        assert win.remove() != null;
        assert win.size() == 0;
        assert win.remove() == null;
    }



    public void testDuplicates() {
        Assert.assertEquals(0, win.size());
        assert win.add(9, msg());
        assert win.add(1, msg());
        assert win.add(2, msg());
        assert !win.add(2, msg());
        assert !win.add(0, msg());
        assert win.add(3, msg());
        assert win.add(4, msg());
        assert !win.add(4, msg());
    }



    public static void testRemoveRegularMessages() {
        AckReceiverWindow win=new AckReceiverWindow(1);
        win.add(1, msg());
        System.out.println("win = " + win);
        win.remove();
        System.out.println("win = " + win);
        assert win.size() == 0;

        win.add(3, msg());
        win.remove();
        System.out.println("win = " + win);
        assert win.size() == 1;

        win.add(2, msg(true));
        win.remove();
        System.out.println("win = " + win);

        assert win.size() == 1;
    }


    public static void testRemoveMany() {
        AckReceiverWindow win=new AckReceiverWindow(1);
        Tuple<List<Message>, Long> tuple=win.removeMany(100);
        assert tuple == null;

        win.add(2, msg());
        win.add(4, msg());
        tuple=win.removeMany(100);
        assert tuple == null;

        win.add(3, msg());
        win.add(1, msg());

        tuple=win.removeMany(10);
        List<Message> list=tuple.getVal1();
        assert list.size() == 4;
        assert tuple.getVal2() == 4;
    }


    public static void testRemoveMany2() {
        AckReceiverWindow win=new AckReceiverWindow(1);
        for(int i=1; i <=50; i++)
            win.add(i, msg());
        System.out.println("win = " + win);
        List<Message> list=win.removeManyAsList(25);

        System.out.println("win = " + win);
        assert list != null && list.size() == 25;
        assert win.size() == 25;

        list=win.removeManyAsList(30);
        System.out.println("win = " + win);
        assert list != null && list.size() == 25;
        assert win.size() == 0;
    }


    public static void testSmallerThanNextToRemove() {
        AckReceiverWindow win=new AckReceiverWindow(1);
        Message msg;
        for(long i=1; i <= 5; i++)
            win.add(i, msg());
        System.out.println("win = " + win);
        boolean added=win.add(3, msg());
        assert !added;
        for(;;) {
            msg=win.remove();
            if(msg == null)
                break;
        }
        added=win.add(3, msg());
        assert !added;
    }


    public static void testConcurrentAdds() throws InterruptedException {
        AckReceiverWindow win=new AckReceiverWindow(1);
        final int NUM=100;
        final int NUM_THREADS=10;
        final CountDownLatch latch=new CountDownLatch(1);

        final Adder[] adders=new Adder[NUM_THREADS];
        for(int i=0; i < adders.length; i++) {
            adders[i]=new Adder(1, NUM, 10, win, latch);
        }
        for(Adder adder: adders)
            adder.start();

        latch.countDown();

        for(Adder adder: adders)
            adder.join();

        System.out.println("win = " + win);
        assert win.size() == NUM;
    }

    @Test(invocationCount=10)
    public static void testConcurrentAddsAndRemoves() throws InterruptedException {
        AckReceiverWindow win=new AckReceiverWindow(1);
        final int NUM=100;
        final int NUM_THREADS=10;
        final CountDownLatch latch=new CountDownLatch(1);

        final Adder[] adders=new Adder[NUM_THREADS];
        for(int i=0; i < adders.length; i++) {
            adders[i]=new Adder(1, NUM, 10, win, latch);
            adders[i].start();
        }

        final AtomicInteger count=new AtomicInteger(NUM);
        final Remover[] removers=new Remover[NUM_THREADS];
        for(int i=0; i < removers.length; i++) {
            removers[i]=new Remover(win, latch, count);
            removers[i].start();
        }

        latch.countDown();

        for(Adder adder: adders)
            adder.join();

        int total=0;
        int index=1;
        for(Remover remover: removers) {
            remover.join();
            List<Message> list=remover.getList();
            System.out.println("remover #" + index++ + ": " + list.size() + " msgs");
            total+=list.size();
        }

        System.out.println("total = " + total + "\n");
        if(total != NUM) {
            for(Remover remover: removers) {
                System.out.println(remover + ": " + print(remover.getList()));
            }
        }
        assert total == NUM;
    }

    private static String print(List<Message> list) {
        StringBuilder sb=new StringBuilder();
        for(Message msg: list) {
            if(msg == AckReceiverWindow.TOMBSTONE)
                sb.append("T ");
            else
                sb.append(msg.getObject() + " ");
        }
        return sb.toString();
    }

    private static Message msg() {
        return msg(false);
    }

    private static Message msg(long seqno) {
        return msg(false, seqno);
    }

    private static Message msg(boolean oob, long seqno) {
        Message retval=new Message(null, null, seqno);
        if(oob)
            retval.setFlag(Message.OOB);
        return retval;
    }

    private static Message msg(boolean oob) {
        Message retval=new Message();
        if(oob)
            retval.setFlag(Message.OOB);
        return retval;
    }


    private static class Adder extends Thread {
        private final long from, to, duplicates;
        private final AckReceiverWindow win;
        private final CountDownLatch latch;

        public Adder(long from, long to, long duplicates, AckReceiverWindow win, CountDownLatch latch) {
            this.from=from;
            this.to=to;
            this.duplicates=duplicates;
            this.win=win;
            this.latch=latch;
            setName("Adder");
        }

        public void run() {
            try {
                latch.await();
            }
            catch(InterruptedException e) {
                e.printStackTrace();
            }
            for(long i=from; i <= to; i++) {
                for(int j=0; j < duplicates; j++) {
                    win.add(i, msg(true, i));
                }
            }
        }
    }

    private static class Remover extends Thread {
        private final AckReceiverWindow win;
        private final CountDownLatch latch;
        private final List<Message> list=new LinkedList<Message>();
        private final AtomicInteger count;

        public Remover(AckReceiverWindow win, CountDownLatch latch, final AtomicInteger count) {
            this.win=win;
            this.latch=latch;
            this.count=count;
            setName("Remover");
        }

        public List<Message> getList() {
            return list;
        }

        public void run() {
            try {
                latch.await();
            }
            catch(InterruptedException e) {
                e.printStackTrace();
                return;
            }

            while(count.get() > 0) {
                Message msg=win.remove();
                if(msg != null) {
                    count.decrementAndGet();
                    list.add(msg);
                }
                else {
                    Util.sleep(100);
                }
            }
        }
    }

}
