/**
 *  RNMEvaluator.java
 *  react-native-eval
 *
 *  Created by Andy Prock on 9/28/15.
 */

package com.evaluator.react;

import android.content.Intent;
import android.support.v4.content.LocalBroadcastManager;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.annotation.Nullable;

/**
 * The NativeModule.
 */
public final class RNMEvaluator extends ReactContextBaseJavaModule {
    private static final String TAG = "RNMEvaluator";

    public static final String EXTRA_STRING = "com.evaluator.extra.STRING";
    public static final String EXTRA_ARRAY  = "com.evaluator.extra.ARRAY";
    public static final String EXTRA_MAP    = "com.evaluator.extra.MAP";
    public static final String EXTRA_INT    = "com.evaluator.extra.INT";
    public static final String EXTRA_DOUBLE = "com.evaluator.extra.DOUBLE";
    public static final String EXTRA_BOOL   = "com.evaluator.extra.BOOL";

    private static final String VALUE_KEY   = "_value";

    private static Map callbacks = new HashMap();

    private boolean mShuttingDown = false;

    public RNMEvaluator(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return TAG;
    }

    @Override
    public void initialize() {
        mShuttingDown = false;
    }

    @Override
    public void onCatalystInstanceDestroy() {
        mShuttingDown = true;
        callbacks.clear();
    }

    /**
     * This broadcasts the given event via the LocalBroadcastManager
     *
     * @param event the name of the event to be broadcast.
     * @param value a {@link ReadableMap} containing the actual data to be
     * broadcast. The data is wrapped in the VALUE_KEY key of this map, and
     * is converted to a standard Java object or primitive, and put into the
     * Intent.
     */
    @ReactMethod
    public void emit(final String event, final ReadableMap value) {
        Intent intent = new Intent(event);

        if (value != null && value.hasKey(VALUE_KEY)); {
            Object data = ConversionUtil.toObject(value, VALUE_KEY);
            if (data instanceof HashMap) {
                intent.putExtra(EXTRA_MAP, (HashMap) data);
            } else if (data instanceof ArrayList) {
                intent.putExtra(EXTRA_ARRAY, (ArrayList) data);
            } else if (data instanceof String) {
                intent.putExtra(EXTRA_STRING, (String) data);
            } else if (data instanceof Integer) {
                intent.putExtra(EXTRA_INT, (Integer) data);
            } else if (data instanceof Double) {
                intent.putExtra(EXTRA_DOUBLE, (Double) data);
            } else if (data instanceof Boolean) {
                intent.putExtra(EXTRA_BOOL, (Boolean) data);
            } else {
                throw new IllegalArgumentException("Could not convert object with type: " + data.getClass().getCanonicalName() + ".");
            }
        }

        LocalBroadcastManager.getInstance(getReactApplicationContext()).sendBroadcast(intent);
    }

    /**
     * This calls the completion callback for the function that was invoked.
     *
     * @param callId the String identifying the call.
     * @param error a String representing the possible error.
     * @param value a {@link ReadableMap} containing the actual data to be
     * returned.  The data is wrapped in the VALUE_KEY key of this map, and
     * is converted to a standard Java object or primitive, and put into the
     * Intent.
     */
    @ReactMethod
    public void functionCallCompleted(final String callId, final String error, final ReadableMap returnValue) {
        if (!callbacks.containsKey(callId)) {
            return;
        }

        EvaluatorCallback cb = (EvaluatorCallback) callbacks.get(callId);
        cb.invoke(error, returnValue.hasKey(VALUE_KEY) ? ConversionUtil.toObject(returnValue, VALUE_KEY) : null);
        callbacks.remove(callId);
    }

    /**
     * Invokes a javascript function via RNMEvaluator.callSyncFunction (blocking)
     */
    public static void callSyncFunction(ReactContext context, String name, Object[] args, EvaluatorCallback cb) {
        RNMEvaluator.callFunction(context, name, args, cb, "RNMEvaluator.callSyncFunction");
    }

    /**
     * Invokes a javascript function via RNMEvaluator.callAsyncFunction (non-blocking)
     */
    public static void callAsyncFunction(ReactContext context, String name, Object[] args, EvaluatorCallback cb) {
        RNMEvaluator.callFunction(context, name, args, cb, "RNMEvaluator.callAsyncFunction");
    }

    /**
     * Marshalls a function call to the javascript layer, via our NativeModule.
     *
     * @param context The context needed to execute this in.
     * @param name The function to execute. e.g. "Math.Pow"
     * @param args The arguments to pass to the function, or null.
     * @param cb The completion callback for the result, or null.
     * @param event The name of the event that our NativeModule is listening for.
     */
    private static void callFunction(ReactContext context, String name, @Nullable Object[] args, @Nullable  EvaluatorCallback cb, String event) {
        String callId = UUID.randomUUID().toString();

        if (null != cb) {
            callbacks.put(callId, cb);
        }

        WritableArray arguments = args != null ? Arguments.fromJavaArgs(args) : Arguments.createArray();
        if (arguments.size() == 0) {
            arguments.pushNull();
        }

        WritableMap eventParams = Arguments.createMap();
        eventParams.putString("name", name);
        eventParams.putArray("args", arguments);
        eventParams.putString("callId", callId);

        // TODO: move to AppEventEmitter once App events are supported on android.
        context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit(event, eventParams);
    }

    /**
     * Callback interface.
     */
    public interface EvaluatorCallback {
        void invoke(String error, Object returnValue);
    }
}