
package org.jgroups.blocks;

import org.jgroups.*;
import org.jgroups.logging.Log;
import org.jgroups.logging.LogFactory;
import org.jgroups.protocols.TP;
import org.jgroups.protocols.relay.SiteAddress;
import org.jgroups.stack.Protocol;
import org.jgroups.stack.StateTransferInfo;
import org.jgroups.util.*;

import java.io.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
import java.util.stream.Stream;


/**
 * Provides synchronous and asynchronous message sending with request-response
 * correlation; i.e., matching responses with the original request.
 * It also offers push-style message reception (by internally using the PullPushAdapter).
 * <p>
 * Channels are simple patterns to asynchronously send a receive messages.
 * However, a significant number of communication patterns in group communication
 * require synchronous communication. For example, a sender would like to send a
 * message to the group and wait for all responses. Or another application would
 * like to send a message to the group and wait only until the majority of the
 * receivers have sent a response, or until a timeout occurred.  MessageDispatcher
 * offers a combination of the above pattern with other patterns.
 * <p>
 * Used on top of channel to implement group requests. Client's {@code handle()}
 * method is called when request is received. Is the equivalent of RpcProtocol on
 * the application instead of protocol level.
 *
 * @author Bela Ban
 */
public class MessageDispatcher implements RequestHandler, Closeable, ChannelListener {
    protected JChannel                              channel;
    protected RequestCorrelator                     corr;
    protected MembershipListener                    membership_listener;
    protected StateListener                         state_listener;
    protected RequestHandler                        req_handler;
    protected boolean                               async_dispatching;
    protected boolean                               wrap_exceptions;
    protected ProtocolAdapter                       prot_adapter;
    protected volatile Collection<Address>          members=new HashSet<>();
    protected Address                               local_addr;
    protected final Log                             log=LogFactory.getLog(MessageDispatcher.class);
    protected final RpcStats                        rpc_stats=new RpcStats(false);
    protected static final RspList                  empty_rsplist=new RspList();
    protected static final GroupRequest             empty_group_request;

    static {
        empty_group_request=new GroupRequest<>(null, Collections.emptyList(), RequestOptions.SYNC());
        empty_group_request.complete(empty_rsplist);
    }


    public MessageDispatcher() {
    }

    public MessageDispatcher(JChannel channel) {
        this.channel=channel;
        prot_adapter=new ProtocolAdapter();
        if(channel != null) {
            channel.addChannelListener(this);
            local_addr=channel.getAddress();
            installUpHandler(prot_adapter, true);
        }
        start();
    }


    public MessageDispatcher(JChannel channel, RequestHandler req_handler) {
        this(channel);
        setRequestHandler(req_handler);
    }


    public JChannel          getChannel()                 {return channel;}
    public RequestCorrelator getCorrelator()              {return corr;}
    public RequestCorrelator correlator()                 {return corr;}
    public boolean           getAsyncDispatching()        {return async_dispatching;}
    public boolean           asyncDispatching()           {return async_dispatching;}
    public boolean           getWrapExceptions()          {return wrap_exceptions;}
    public boolean           wrapExceptions()             {return wrap_exceptions;}
    public UpHandler         getProtocolAdapter()         {return prot_adapter;}
    public UpHandler         protocolAdapter()            {return prot_adapter;}
    public RpcStats          getRpcStats()                {return rpc_stats;}
    public RpcStats          rpcStats()                   {return rpc_stats;}
    public boolean           getExtendedStats()           {return rpc_stats.extendedStats();}
    public boolean           extendedStats()              {return rpc_stats.extendedStats();}
    public <X extends MessageDispatcher> X setExtendedStats(boolean fl) {return extendedStats(fl);}
    public <X extends MessageDispatcher> X extendedStats(boolean fl)    {rpc_stats.extendedStats(fl); return (X)this;}

    public <X extends MessageDispatcher> X setChannel(JChannel ch) {
        if(ch == null)
            return (X)this;
        this.channel=ch;
        if(ch != null) {
            local_addr=channel.getAddress();
            ch.addChannelListener(this);
        }
        if(prot_adapter == null)
            prot_adapter=new ProtocolAdapter();
        // Don't force installing the UpHandler so subclasses can use this method
        return installUpHandler(prot_adapter, false);
    }

    public <X extends MessageDispatcher> X setCorrelator(RequestCorrelator c) {return correlator(c);}
    public <X extends MessageDispatcher> X correlator(RequestCorrelator c) {
        if(c == null)
            return (X)this;
        stop();
        this.corr=c;
        corr.asyncDispatching(this.async_dispatching).wrapExceptions(this.wrap_exceptions);
        start();
        return (X)this;
    }

    public <X extends MessageDispatcher> X setMembershipListener(MembershipListener l) {
        membership_listener=l;
        return (X)this;
    }

    public <X extends MessageDispatcher> X setStateListener(StateListener sl) {
        this.state_listener=sl;
        return (X)this;
    }

    public <X extends MessageDispatcher> X setRequestHandler(RequestHandler rh) {
        req_handler=rh;
        return (X)this;
    }

    public <X extends MessageDispatcher> X setAsynDispatching(boolean flag) {return asyncDispatching(flag);}
    public <X extends MessageDispatcher> X asyncDispatching(boolean flag) {
        async_dispatching=flag;
        if(corr != null)
            corr.asyncDispatching(flag);
        return (X)this;
    }

    public <X extends MessageDispatcher> X setWrapExceptions(boolean flag) {return wrapExceptions(flag);}
    public <X extends MessageDispatcher> X wrapExceptions(boolean flag) {
        wrap_exceptions=flag;
        if(corr != null)
            corr.wrapExceptions(flag);
        return (X)this;
    }

    protected <X extends MessageDispatcher> X setMembers(List<Address> new_mbrs) {
        if(new_mbrs != null)
            members=new HashSet<>(new_mbrs); // volatile write - seen by a subsequent read
        return (X)this;
    }


    public <X extends MessageDispatcher> X start() {
        if(corr == null)
            corr=createRequestCorrelator(prot_adapter, this, local_addr)
              .asyncDispatching(async_dispatching).wrapExceptions(this.wrap_exceptions);
        correlatorStarted();
        corr.start();

        if(channel != null) {
            List<Address> tmp_mbrs=channel.getView() != null ? channel.getView().getMembers() : null;
            setMembers(tmp_mbrs);
            if(channel instanceof JChannel) {
                TP transport=channel.getProtocolStack().getTransport();
                corr.registerProbeHandler(transport);
            }
        }
        return (X)this;
    }

    protected static RequestCorrelator createRequestCorrelator(Protocol transport, RequestHandler handler, Address local_addr) {
        return new RequestCorrelator(transport, handler, local_addr);
    }

    protected void correlatorStarted() {
        ;
    }

    @Override public void close() throws IOException {stop();}

    public <X extends MessageDispatcher> X stop() {
        if(corr != null) {
            corr.stop();
            if(channel instanceof JChannel) {
                TP transport=channel.getProtocolStack().getTransport();
                corr.unregisterProbeHandler(transport);
            }
        }
        return (X)this;
    }


    /**
     * Sets the given UpHandler as the UpHandler for the channel. If the relevant handler is already installed,
     * the {@code canReplace} controls whether this method replaces it (after logging a WARN) or simply
     * leaves {@code handler} uninstalled.<p>
     * Passing {@code false} as the {@code canReplace} value allows callers to use this method to install defaults
     * without concern about inadvertently overriding
     *
     * @param handler the UpHandler to install
     * @param canReplace {@code true} if an existing Channel upHandler can be replaced; {@code false}
     *              if this method shouldn't install
     */
    protected <X extends MessageDispatcher> X installUpHandler(UpHandler handler, boolean canReplace) {
        UpHandler existing = channel.getUpHandler();
        if (existing == null)
            channel.setUpHandler(handler);
        else if(canReplace) {
            log.warn("Channel already has an up handler installed (%s) but now it is being overridden", existing);
            channel.setUpHandler(handler);
        }
        return (X)this;
    }


    /**
     * Sends a message to all members and expects responses from members in dests (if non-null).
     * @param dests A list of group members from which to expect responses (if the call is blocking).
     * @param data The buffer
     * @param offset the offset into data
     * @param length the number of bytes to send
     * @param opts A set of options that govern the call. See {@link org.jgroups.blocks.RequestOptions} for details
     * @return RspList A list of Rsp elements, or null if the RPC is asynchronous
     * @throws Exception If the request cannot be sent
     * @since 4.0
     */
    public <T> RspList<T> castMessage(Collection<Address> dests, byte[] data, int offset, int length,
                                      RequestOptions opts) throws Exception {
        return castMessage(dests, new Buffer(data, offset, length), opts);
    }


    /**
     * Sends a message to all members and expects responses from members in dests (if non-null).
     * @param dests A list of group members from which to expect responses (if the call is blocking).
     * @param data The message to be sent
     * @param opts A set of options that govern the call. See {@link org.jgroups.blocks.RequestOptions} for details
     * @return RspList A list of Rsp elements, or null if the RPC is asynchronous
     * @throws Exception If the request cannot be sent
     * @since 2.9
     */
    public <T> RspList<T> castMessage(final Collection<Address> dests, Buffer data, RequestOptions opts) throws Exception {
        GroupRequest<T> req=cast(dests, data, opts, true);
        return req != null? req.getNow(null) : null;
    }


    /**
     * Sends a message to all members and expects responses from members in dests (if non-null).
     * @param dests A list of group members from which to expect responses (if the call is blocking).
     * @param data The message to be sent
     * @param opts A set of options that govern the call. See {@link org.jgroups.blocks.RequestOptions} for details
     * @return CompletableFuture<T> A future from which the results (RspList) can be retrieved, or null if the request
     *                              was sent asynchronously
     * @throws Exception If the request cannot be sent
     */
    public <T> CompletableFuture<RspList<T>> castMessageWithFuture(final Collection<Address> dests, Buffer data,
                                                                   RequestOptions opts) throws Exception {
        return cast(dests,data,opts,false);
    }


    protected <T> GroupRequest<T> cast(final Collection<Address> dests, byte[] data, int offset, int length,
                                       RequestOptions options, boolean block_for_results) throws Exception {
        return cast(dests, new Buffer(data, offset, length), options, block_for_results);
    }

    protected <T> GroupRequest<T> cast(final Collection<Address> dests, Buffer data, RequestOptions options,
                                       boolean block_for_results) throws Exception {
        if(options == null) {
            log.warn("request options were null, using default of sync");
            options=RequestOptions.SYNC();
        }

        List<Address> real_dests;
        // we need to clone because we don't want to modify the original
        if(dests != null)
            real_dests=dests.stream().filter(dest -> dest instanceof SiteAddress || this.members.contains(dest))
              .collect(ArrayList::new, (list,dest) -> {if(!list.contains(dest)) list.add(dest);}, (l,r) -> {});
        else
            real_dests=new ArrayList<>(members);

        // Remove the local member from the target destination set if we should not deliver our own message
        JChannel tmp=channel;
        if((tmp != null && tmp.getDiscardOwnMessages()) || options.transientFlagSet(Message.TransientFlag.DONT_LOOPBACK)) {
            if(local_addr == null)
                local_addr=tmp != null? tmp.getAddress() : null;
            real_dests.remove(local_addr);
        }

        if(options.hasExclusionList())
            Stream.of(options.exclusionList()).forEach(real_dests::remove);

        if(real_dests.isEmpty()) {
            log.trace("destination list is empty, won't send message");
            return empty_group_request;
        }

        boolean sync=options.mode() != ResponseMode.GET_NONE;
        boolean non_blocking=!sync || !block_for_results, anycast=options.anycasting();
        if(non_blocking)
            updateStats(real_dests, anycast, sync, 0);

        if(!sync) {
            corr.sendRequest(real_dests, data, null, options);
            return null;
        }

        GroupRequest<T> req=new GroupRequest<>(corr, real_dests, options);
        long start=non_blocking || !rpc_stats.extendedStats()? 0 : System.nanoTime();
        req.execute(data, block_for_results);
        long time=non_blocking || !rpc_stats.extendedStats()? 0 : System.nanoTime() - start;
        if(!non_blocking)
            updateStats(real_dests, anycast, true, time);
        return req;
    }


    public void done(long req_id) {
        corr.done(req_id);
    }


    /**
     * Sends a unicast message and - depending on the options - returns a result
     * @param dest the target to which to send the unicast message. Must not be null.
     * @param data the payload to send
     * @param offset the offset at which the data starts
     * @param length the number of bytes to send
     * @param opts the options to be used
     * @return T the result. Null if the call is asynchronous (non-blocking) or if the response is null
     * @throws Exception If there was problem sending the request, processing it at the receiver, or processing
     *                   it at the sender.
     * @throws TimeoutException If the call didn't succeed within the timeout defined in options (if set)
     */
    public <T> T sendMessage(Address dest, byte[] data, int offset, int length, RequestOptions opts) throws Exception {
        return sendMessage(dest, new Buffer(data, offset, length), opts);
    }

    /**
     * Sends a unicast message and - depending on the options - returns a result
     * @param dest the target to which to send the unicast message. Must not be null.
     * @param data the payload to send
     * @param opts the options to be used
     * @return T the result. Null if the call is asynchronous (non-blocking) or if the response is null
     * @throws Exception If there was problem sending the request, processing it at the receiver, or processing
     *                   it at the sender.
     * @throws TimeoutException If the call didn't succeed within the timeout defined in options (if set)
     */
    public <T> T sendMessage(Address dest, Buffer data, RequestOptions opts) throws Exception {
        if(dest == null)
            throw new IllegalArgumentException("message destination is null, cannot send message");

        if(opts == null) {
            log.warn("request options were null, using default of sync");
            opts=RequestOptions.SYNC();
        }

        // invoke an async RPC directly and return null, without creating a UnicastRequest instance
        if(opts.mode() == ResponseMode.GET_NONE) {
            rpc_stats.add(RpcStats.Type.UNICAST, dest, false, 0);
            corr.sendUnicastRequest(dest, data, null, opts);
            return null;
        }

        // now it must be a sync RPC
        UnicastRequest<T> req=new UnicastRequest<>(corr, dest, opts);
        long start=!rpc_stats.extendedStats()? 0 : System.nanoTime();
        try {
            return req.execute(data, true);
        }
        finally {
            long time=!rpc_stats.extendedStats()? 0 : System.nanoTime() - start;
            rpc_stats.add(RpcStats.Type.UNICAST, dest, true, time);
        }
    }


     /**
     * Sends a unicast message to the target defined by msg.getDest() and returns a future
     * @param dest the target to which to send the unicast message. Must not be null.
     * @param data the payload to send
     * @param offset the offset at which the data starts
     * @param length the number of bytes to send
     * @param opts the options
     * @return CompletableFuture<T> A future from which the result can be fetched, or null if the call was asynchronous
     * @throws Exception If there was problem sending the request, processing it at the receiver, or processing
     *                   it at the sender. {@link java.util.concurrent.Future#get()} will throw this exception
     */
     public <T> CompletableFuture<T> sendMessageWithFuture(Address dest, byte[] data, int offset, int length,
                                                           RequestOptions opts) throws Exception {
         return sendMessageWithFuture(dest, new Buffer(data, offset, length), opts);
     }

    /**
     * Sends a unicast message to the target defined by msg.getDest() and returns a future
     * @param dest the target to which to send the unicast message. Must not be null.
     * @param data the payload to send
     * @param opts the options
     * @return CompletableFuture<T> A future from which the result can be fetched, or null if the call was asynchronous
     * @throws Exception If there was problem sending the request, processing it at the receiver, or processing
     *                   it at the sender. {@link java.util.concurrent.Future#get()} will throw this exception
     */
    public <T> CompletableFuture<T> sendMessageWithFuture(Address dest, Buffer data, RequestOptions opts) throws Exception {
        if(dest == null)
            throw new IllegalArgumentException("message destination is null, cannot send message");

        if(opts == null) {
            log.warn("request options were null, using default of sync");
            opts=RequestOptions.SYNC();
        }
        rpc_stats.add(RpcStats.Type.UNICAST, dest, opts.mode() != ResponseMode.GET_NONE, 0);

        if(opts.mode() == ResponseMode.GET_NONE) {
            corr.sendUnicastRequest(dest, data, null, opts);
            return null;
        }

        // if we get here, the RPC is synchronous
        UnicastRequest<T> req=new UnicastRequest<>(corr, dest, opts);
        req.execute(data, false);
        return req;
    }



    /* ------------------------ RequestHandler Interface ---------------------- */
    @Override
    public Object handle(Message msg) throws Exception {
        if(req_handler != null)
            return req_handler.handle(msg);
        return null;
    }

    @Override
    public void handle(Message request, Response response) throws Exception {
        if(req_handler != null) {
            if(async_dispatching)
                req_handler.handle(request, response);
            else {
                Object retval=req_handler.handle(request);
                if(response != null)
                    response.send(retval, false);
            }
            return;
        }

        Object retval=handle(request);
        if(response != null)
            response.send(retval, false);
    }
    /* ------------------ End of RequestHandler Interface----------------- */





    protected void updateStats(Collection<Address> dests, boolean anycast, boolean sync, long time) {
        if(anycast)
            rpc_stats.addAnycast(sync, time, dests);
        else
            rpc_stats.add(RpcStats.Type.MULTICAST, null, sync, time);
    }

    protected Object handleUpEvent(Event evt) throws Exception {
        switch(evt.getType()) {
            case Event.GET_APPLSTATE: // reply with GET_APPLSTATE_OK
                byte[] tmp_state=null;
                if(state_listener != null) {
                    ByteArrayOutputStream output=new ByteArrayOutputStream(1024);
                    state_listener.getState(output);
                    tmp_state=output.toByteArray();
                }
                return new StateTransferInfo(null, 0L, tmp_state);

            case Event.GET_STATE_OK:
                if(state_listener != null) {
                    StateTransferResult result=evt.getArg();
                    if(result.hasBuffer()) {
                        ByteArrayInputStream input=new ByteArrayInputStream(result.getBuffer());
                        state_listener.setState(input);
                    }
                }
                break;

            case Event.STATE_TRANSFER_OUTPUTSTREAM:
                OutputStream os=evt.getArg();
                if(state_listener != null && os != null)
                    state_listener.getState(os);
                break;

            case Event.STATE_TRANSFER_INPUTSTREAM:
                InputStream is=evt.getArg();
                if(state_listener != null && is!=null)
                    state_listener.setState(is);
                break;

            case Event.VIEW_CHANGE:
                View v=evt.getArg();
                List<Address> new_mbrs=v.getMembers();
                setMembers(new_mbrs);
                if(membership_listener != null)
                    membership_listener.viewAccepted(v);
                break;

            case Event.SET_LOCAL_ADDRESS:
                log.trace("setting local_addr (%s) to %s", local_addr, evt.getArg());
                local_addr=evt.getArg();
                break;

            case Event.SUSPECT:
                if(membership_listener != null)
                    membership_listener.suspect(evt.getArg());
                break;

            case Event.BLOCK:
                if(membership_listener != null)
                    membership_listener.block();
                break;
            case Event.UNBLOCK:
                if(membership_listener != null)
                    membership_listener.unblock();
                break;
        }

        return null;
    }

    public void channelConnected(JChannel channel) {
        ;
    }

    public void channelDisconnected(JChannel channel) {
        stop();
    }

    public void channelClosed(JChannel channel) {
        stop();
    }


    class ProtocolAdapter extends Protocol implements UpHandler {


        /* ------------------------- Protocol Interface --------------------------- */

        @Override
        public String getName() {
            return "MessageDispatcher";
        }


        /**
         * Called by channel (we registered before) when event is received. This is the UpHandler interface.
         */
        @Override
        public Object up(Event evt) {
            if(corr != null && !corr.receive(evt)) {
                try {
                    return handleUpEvent(evt);
                }
                catch(Throwable t) {
                    throw new RuntimeException(t);
                }
            }
            return null;
        }

        public Object up(Message msg) {
            if(corr != null)
                corr.receiveMessage(msg);
            return null;
        }

        public void up(MessageBatch batch) {
            if(corr == null)
                return;
            corr.receiveMessageBatch(batch);
        }

        @Override
        public Object down(Event evt) {
            return channel != null? channel.down(evt) : null;
        }

        public Object down(Message msg) {
            if(channel != null) {
                if(!(channel.isConnected() || channel.isConnecting())) {
                    // return null;
                    throw new IllegalStateException("channel is not connected");
                }
                return channel.down(msg);
            }
            return null;
        }

        /* ----------------------- End of Protocol Interface ------------------------ */

    }



}
