package com.mobify.astro.messaging;

import android.support.annotation.NonNull;
import android.util.Log;

import com.mobify.astro.AstroException;

import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;

public class RpcMethodDispatcher {

    private static final String TAG = RpcMethodDispatcher.class.getName();

    private final MessageSender messageSender;
    RpcMethodDispatcher(@NonNull MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    /**
     * Dispatches an RPC call and sends the response message based on the return
     * value of the invoked method.
     *
     * @param rpcMethod the RpcMethod to dispatch
     * @param receiver the Object to invoke the method on
     * @param response The rpc response
     * @param parameters the parameters to pass to the method
     */
    void dispatch(RpcMethodWrapper rpcMethod, Object receiver, RpcResponse response, Object[] parameters) {
        if (rpcMethod.methodType == RpcMethodWrapper.RpcMethodType.SYNC) {
            dispatchSync(rpcMethod, receiver, response, parameters);
        } else if (rpcMethod.methodType == RpcMethodWrapper.RpcMethodType.ASYNC) {
            dispatchAsync(rpcMethod, receiver, response, parameters);
        }
    }

    /**
     * Synchronously dispatches an RPC call and sends the response message based on the return
     * value of the invoked method.
     *
     * @param rpcMethod the synchronous RpcMethod to dispatch
     * @param receiver the Object to invoke the method on
     * @param response The rpc response
     * @param parameters the parameters to pass to the method
     */
    protected void dispatchSync(RpcMethodWrapper rpcMethod, Object receiver, RpcResponse response, Object[] parameters) {
        Object methodReturnValue;

        try {
            methodReturnValue = rpcMethod.invoke(receiver, parameters);
        } catch (InvocationTargetException ite) {
            Throwable invocationException = ite.getCause();
            if (invocationException instanceof AstroException){
                response.setError((AstroException)invocationException);
                messageSender.sendRpcResponse(response);
                return;
            }

            throw new RuntimeException(ite.getCause());
        } catch (java.lang.IllegalAccessException iae) {
            throw new RuntimeException(iae);
        }

        // Handle special case of a null return value:
        // It's basically impossible to distinguish an actual returned null from a void return value
        // Consumers beware!
        if (methodReturnValue == null) {
            methodReturnValue = JSONObject.NULL;
        }

        // Send reply message with packed return type
        try {
            response.setResult(methodReturnValue);
        } catch (JSONException e) {
            response.setError(new Exceptions.ResultPackingException(methodReturnValue,
                    rpcMethod.method, parameters));
        }

        try {
            messageSender.sendRpcResponse(response);
        } catch (Exceptions.DuplicateRpcResponseSend f) {
            // give up
            Log.e(TAG, "This response was already sent. Cannot send a response more than once.", f);
        }
    }

    /**
     * Asynchronously dispatches an rpc method, passing a response object as the first parameter to
     * the method invoked
     *
     * @param rpcMethod the asynchronous RpcMethod to dispatch
     * @param receiver the Object to invoke the method on
     * @param response The rpc response object we will pass to the async rpc method
     * @param parameters The parameters to pass to the method
     */
    protected void dispatchAsync(RpcMethodWrapper rpcMethod, Object receiver, RpcResponse response, Object[] parameters) {
        // Prepend the RpcResponse to the list of rpc method parameters
        ArrayList<Object> asyncParameters = new ArrayList<>();
        asyncParameters.add(response);
        Collections.addAll(asyncParameters, parameters);

        // Dispatch, and send back error if there is an immediate exception thrown
        try {
            rpcMethod.invoke(receiver, asyncParameters.toArray());
        } catch (InvocationTargetException ite) {
            Throwable invocationException = ite.getCause();
            if (invocationException instanceof AstroException){
                messageSender.sendRpcResponseWithError(response, (AstroException)invocationException);
                return;
            }

            throw new RuntimeException(ite.getCause());
        } catch (java.lang.IllegalAccessException iae) {
            throw new RuntimeException(iae);
        }
    }
}
