package org.jgroups.blocks;


import org.jgroups.Address;
import org.jgroups.SuspectedException;
import org.jgroups.UnreachableException;
import org.jgroups.View;
import org.jgroups.annotations.GuardedBy;
import org.jgroups.protocols.relay.SiteAddress;
import org.jgroups.util.Buffer;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;


/**
 * Sends a request to a single target destination
 *
 * @author Bela Ban
 */
public class UnicastRequest<T> extends Request<T> {
    protected final Address    target;


    public UnicastRequest(RequestCorrelator corr, Address target, RequestOptions options) {
        super(corr, options);
        this.target=target; // target is guaranteed to be non-null
    }


    public void sendRequest(Buffer data) throws Exception {
        try {
            corr.sendUnicastRequest(target, data, options.mode() == ResponseMode.GET_NONE? null : this, this.options);
        }
        catch(Exception ex) {
            corrDone();
            throw ex;
        }
    }
    

    /* ---------------------- Interface RspCollector -------------------------- */
    /**
     * <b>Callback</b> (called by RequestCorrelator or Transport).
     * Adds a response to the response table. When all responses have been received, {@code execute()} returns.
     */
    public void receiveResponse(Object response_value, Address sender, boolean is_exception) {
        if(isDone())
            return;
        if(is_exception && response_value instanceof Throwable)
            completeExceptionally((Throwable)response_value);
        else
            complete((T)response_value);
        corrDone();
    }


    public void siteUnreachable(String site) {
        if(!(target instanceof SiteAddress) || !((SiteAddress)target).getSite().equals(site) || isDone())
            return;
        completeExceptionally(new UnreachableException(target));
        corrDone();
    }

    /**
     * If the target address is not a member of the new view, we'll mark the response as suspected and unblock
     * the caller of execute()
     */
    public void viewChange(View view) {
        if(view == null)
            return;

        // SiteAddresses are not checked as they might be in a different cluster
        if(!(target instanceof SiteAddress) && !view.containsMember(target) && !isDone()) {
            completeExceptionally(new SuspectedException(target));
            corrDone();
        }
    }

    public void transportClosed() {
        if(isDone())
            return;
        completeExceptionally(new IllegalStateException("transport was closed"));
        corrDone();
    }

    /* -------------------- End of Interface RspCollector ----------------------------------- */
    public T get() throws InterruptedException, ExecutionException {
        try {
            return super.get();
        }
        finally {
            corrDone();
        }
    }

    public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        try {
            return super.get(timeout, unit);
        }
        finally {
            corrDone();
        }
    }

    public T join() {
        return around(super::join);
    }

    public T getNow(T valueIfAbsent) {
        return around(() -> super.getNow(valueIfAbsent));
    }

    public T waitForCompletion(long timeout, TimeUnit unit) throws Exception {
        return getResult(() -> get(timeout, unit));
    }

    public T waitForCompletion() throws Exception {
        return getResult(this::get);
    }


    public String toString() {
        return String.format("%s, target=%s", super.toString(), target);
    }


    @GuardedBy("lock")
    public boolean responsesComplete() {
        return options.mode() == ResponseMode.GET_NONE || isDone();
    }

    protected T around(Supplier<T> supplier) {
        try {return supplier.get();}
        finally {corrDone();}
    }


    protected T getResult(Callable<T> supplier) throws Exception {
        try {
            T result=supplier.call();
            if(result == null && !isDone())
                throw new TimeoutException("timeout waiting for response from " + target + ", request: " + toString());
            return result;
        }
        catch(ExecutionException ex) {
            Throwable exception=ex.getCause();
            if(exception instanceof Error) throw (Error)exception;
            else if(exception instanceof RuntimeException) throw (RuntimeException)exception;
            else if(exception instanceof Exception) throw (Exception)exception;
            else throw new RuntimeException(exception);
        }
        finally {
            corrDone();
        }
    }
}
