package org.jgroups.tests;

import org.jgroups.Address;
import org.jgroups.JChannel;
import org.jgroups.Message;
import org.jgroups.blocks.MethodCall;
import org.jgroups.blocks.RequestOptions;
import org.jgroups.blocks.RpcDispatcher;
import org.jgroups.protocols.*;
import org.jgroups.protocols.pbcast.GMS;
import org.jgroups.protocols.pbcast.NAKACK2;
import org.jgroups.protocols.pbcast.STABLE;
import org.jgroups.stack.Protocol;
import org.jgroups.stack.ProtocolStack;
import org.jgroups.util.Util;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Tests https://issues.jboss.org/browse/JGRP-1675
 * @author Bela Ban
 * @since  3.5
 */
public class RemoteGetStressTest {
    protected JChannel[]                  channels;
    protected List<Address>               target_members; // B, C, D
    protected RpcDispatcher[]             dispatchers;
    protected Random random = new Random();

    protected static final int            NUM_THREADS=40;
    protected static final int            SIZE=100 * 1000; // size of a GET response
    protected static final byte[]         BUF=new byte[SIZE];
    protected static final MethodCall     GET_METHOD;
    protected static final long           TIMEOUT=3 * 60 * 1000; // ms
    protected static final AtomicInteger  count=new AtomicInteger(1);
    protected static final RequestOptions OPTIONS=RequestOptions.SYNC().timeout(TIMEOUT)
      .flags(Message.Flag.OOB).anycasting(true);
    protected static boolean              USE_SLEEPS=true;

    static {
        try {
            Method get_method=RemoteGetStressTest.class.getMethod("get");
            GET_METHOD=new MethodCall(get_method);
        }
        catch(NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }


    protected void start() throws Exception {
        String[] names={"A", "B", "C", "D"};
        channels=new JChannel[4];
        dispatchers=new RpcDispatcher[channels.length];
        for(int i=0; i < channels.length; i++) {
            channels[i]=createChannel(names[i]);
            dispatchers[i]=new RpcDispatcher(channels[i], this);
            channels[i].connect("cluster");
        }
        Util.waitUntilAllChannelsHaveSameView(10000, 500, channels);
        System.out.println("view A: " + channels[0].getView());

        target_members=Arrays.asList(channels[1].getAddress(), channels[2].getAddress(), channels[3].getAddress());
        final AtomicInteger success=new AtomicInteger(0), failure=new AtomicInteger(0);

        if(USE_SLEEPS)
            insertDISCARD(channels[0], 0.2);

        long start=System.currentTimeMillis();
        Invoker[] invokers=new Invoker[NUM_THREADS];
        for(int i=0; i < invokers.length; i++) {
            invokers[i]=new Invoker(dispatchers[0], success, failure);
            invokers[i].start();
        }

        for(Invoker invoker: invokers)
            invoker.join();

        long time=System.currentTimeMillis() - start;

        System.out.println("\n\n**** success: " + success + ", failure=" + failure + ", time=" + time + " ms");
        Util.keyPress("enter to terminate");
        stop();
    }

    protected void stop() {
        for(RpcDispatcher disp: dispatchers)
            disp.stop();
        Util.close(channels);
    }

    protected static JChannel createChannel(String name) throws Exception {
        Protocol[] protocols={
          new SHARED_LOOPBACK().setValue("thread_pool_min_threads", 1)
            .setValue("thread_pool_max_threads", 5),
          new SHARED_LOOPBACK_PING(),
          new NAKACK2(),
          new UNICAST3(),
          new STABLE(),
          new GMS(),
          new UFC(),
          new MFC().setValue("max_credits", 2000000).setValue("min_threshold", 0.4),
          new FRAG2().fragSize(8000),
        };
        return new JChannel(protocols).name(name);
    }

    public static BigObject get() {
        return new BigObject(count.getAndIncrement());
    }


    public static void main(String[] args) throws Exception {
        RemoteGetStressTest test=new RemoteGetStressTest();
        test.start();
    }

    protected Address randomMember() {
        return Util.pickRandomElement(target_members);
    }

    protected static void insertDISCARD(JChannel ch, double discard_rate) throws Exception {
        TP transport=ch.getProtocolStack().getTransport();
        DISCARD discard=new DISCARD();
        discard.setUpDiscardRate(discard_rate);
        ch.getProtocolStack().insertProtocol(discard, ProtocolStack.Position.ABOVE, transport.getClass());
    }

    protected class Invoker extends Thread {
        protected final RpcDispatcher disp;
        protected final AtomicInteger success, failure;

        public Invoker(RpcDispatcher disp, AtomicInteger success, AtomicInteger failure) {
            this.disp=disp;
            this.success=success;
            this.failure=failure;
        }

        public void run() {
            ArrayList<Address> targets = new ArrayList<>(channels[0].getView().getMembers());
            targets.remove(random.nextInt(targets.size()));
            Collections.rotate(targets, random.nextInt(targets.size()));

            Future<BigObject>[] futures = new Future[targets.size()];
            for(int i=0; i < targets.size(); i++) {
                Address target=targets.get(i);
                try {
                    futures[i]=disp.callRemoteMethodWithFuture(target, GET_METHOD, OPTIONS);
                }
                catch(Exception e) {
                    e.printStackTrace();
                }
            }

            for(Future<BigObject> future: futures) {
                if(future == null) continue;
                try {
                    BigObject result=future.get(TIMEOUT, TimeUnit.MILLISECONDS);
                    if(result != null) {
                        System.out.println("received object #" + result.num);
                        success.incrementAndGet();
                        return;
                    }
                }
                catch(Exception e) {
                    e.printStackTrace();
                }
            }
            failure.incrementAndGet();
        }
    }

    public static class BigObject implements Serializable {
        private static final long serialVersionUID=1265292900051224502L;
        int num;

        public BigObject(int num) {
            this.num = num;
        }

        public BigObject() {
        }

        private void writeObject(ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
            byte[] buf = new byte[SIZE];
            out.write(buf);
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            byte[] buf = new byte[SIZE];
            in.read(buf);
            if(USE_SLEEPS)
                Util.sleepRandom(1, 10);
        }

        @Override
        public String toString() {
            return "BigObject#" + num;
        }
    }

}
