
package in.slanglabs;

import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.widget.Toast;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Promise;
import com.facebook.react.modules.core.DeviceEventManagerModule;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.Map;
import java.util.HashSet;

import in.slanglabs.platform.SlangBuddy;
import in.slanglabs.platform.SlangBuddyOptions;
import in.slanglabs.platform.SlangEntity;
import in.slanglabs.platform.SlangIntent;
import in.slanglabs.platform.SlangLocale;
import in.slanglabs.platform.SlangSession;
import in.slanglabs.platform.action.SlangAction;
import in.slanglabs.platform.action.SlangIntentAction;
import in.slanglabs.platform.action.SlangMultiStepIntentAction;
import in.slanglabs.platform.action.SlangUtteranceAction;
import in.slanglabs.platform.prompt.SlangMessage;
import in.slanglabs.platform.prompt.SlangStatement;
import in.slanglabs.platform.ui.SlangBuiltinUI;

public class RNSlangBuddy extends ReactContextBaseJavaModule {

    private static final String TAG = "RNSlangBuddy";

    // Config constants
    private static final String CONFIG_LOCALE = "locale";
    private static final String CONFIG_REQUESTED_LOCALES = "requestedLocales";
    private static final String CONFIG_ACTION = "action";
    private static final String CONFIG_VISIBILITY = "visibility";
    private static final String CONFIG_ENV = "environment";

    private ReadableMap mConfigOptions;
    private SlangSession mCurrentSession;
    private SlangIntent mCurrentIntent;
    private Boolean isOnUtteranceUnresolvedSet = false;
    private ReactApplicationContext mReactContext;

    public RNSlangBuddy(ReactApplicationContext reactContext) {
        super(reactContext);
        mReactContext = reactContext;
    }

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

    /**
     * Initialize Slang
     * 
     * @param buddyId
     * @param apiKey
     * @param configOptions
     * @param buddyListener
     */
    @ReactMethod
    public void initialize(String buddyId, String apiKey, ReadableMap configOptions, final Callback buddyListener) {
        Log.e(TAG, "initialize");
        mConfigOptions = configOptions;
        SlangUtteranceAction mUtteranceAction = new SlangUtteranceAction() {
            private final ReactApplicationContext mReactContext = getReactApplicationContext();

            @Override
            public void onUtteranceDetected(String userUtterance, SlangSession slangSession) {
                WritableMap params = Arguments.createMap();
                params.putString("userUtterance", userUtterance);
                this.sendEvent(params, "utterance_detected");
            }

            @Override
            public Status onUtteranceUnresolved(String userUtterance, SlangSession slangSession) {
                WritableMap params = Arguments.createMap();
                params.putString("userUtterance", userUtterance);
                this.sendEvent(params, "utterance_unresolved");
                if (isOnUtteranceUnresolvedSet) {
                    return Status.SUCCESS;
                }
                return Status.FAILURE;
            }

            private void sendEvent(WritableMap params, String eventName) {
                mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
            }
        };

        // Initialize SlangBuddy
        try {
            String action = getStringConfig(CONFIG_ACTION);
            String env = getStringConfig(CONFIG_ENV);
            String visibility = getStringConfig(CONFIG_VISIBILITY);
            SlangBuddyOptions.Builder optionBuilder = new SlangBuddyOptions.Builder()
                    .setApplication(getCurrentActivity().getApplication())
                    .setBuddyId(buddyId)
                    .setAPIKey(apiKey)
                    .setListener(new RNSlangBuddyListener(buddyListener))
                    .setUtteranceAction(mUtteranceAction)
                    .setDefaultLocale(RNSlangLocaleMap.getDefaultLocale(getStringConfig(CONFIG_LOCALE)))
                    .setRequestedLocales(RNSlangLocaleMap.getRequestedLocales(getStringListConfig(CONFIG_REQUESTED_LOCALES)))
                    .enableEnhancedSpeechRecognition(true);

            if ( action != null && action.equalsIgnoreCase("multi")) {
                optionBuilder.setIntentAction(new RNSlangMultiStepAction(getReactApplicationContext()));
            } else {
                optionBuilder.setIntentAction(new RNSlangAction(getReactApplicationContext()));
            }
            if (visibility != null && visibility.equalsIgnoreCase("hidden")) {
            } else {
                optionBuilder.setStartActivity(getCurrentActivity());
            }
            if (env != null && env.equalsIgnoreCase("production")) {
                optionBuilder.setEnvironment(SlangBuddy.Environment.PRODUCTION);
            } else {
                optionBuilder.setEnvironment(SlangBuddy.Environment.STAGING);
            }

            SlangBuddyOptions options = optionBuilder.build();
            SlangBuddy.initialize(options);
            Log.d(TAG, "initialize: called: buddyId:" + buddyId + " apiKey:" + apiKey);
        } catch (SlangBuddyOptions.InvalidOptionException e) {
            Log.d(TAG, "initialize: InvalidOptionException");
            e.printStackTrace();
        } catch (SlangBuddy.InsufficientPrivilegeException e) {
            Log.d(TAG, "initialize: InsufficientPrivilegeException");
            e.printStackTrace();
        } catch (NullPointerException e) {
                Log.d(TAG, "mConfigOptions encountered an error");
        }
    }

    @ReactMethod
    public void isInitialized(Promise promise) {
        try {
            Boolean isInitialized = SlangBuddy.isInitialized();
            WritableMap map = Arguments.createMap();
            map.putBoolean("isInitialized", isInitialized);
            promise.resolve(map);
        } catch (Exception e) {
            promise.reject(e);
        }
    }

    @ReactMethod
    public void setOnUtteranceUnresolved() {
        this.isOnUtteranceUnresolvedSet = true;
    }

    @ReactMethod
    public void cancel() {
        Log.e(TAG, "cancel");
        if (SlangBuddy.isInitialized()) {
            SlangBuddy.getBuiltinUI().cancel();
        }

    }

    @ReactMethod
    public void showUI() {
        Log.e(TAG, "showUI");
        SlangBuddy.getBuiltinUI().show(getCurrentActivity());
    }

    @ReactMethod
    public void hideUI() {
        Log.e(TAG, "hideUI");
        SlangBuddy.getBuiltinUI().hide();
    }

    @ReactMethod
    public void unmute() {
        Log.e(TAG, "unmute");
        if (SlangBuddy.isInitialized()) {
            try {
                if(SlangBuddy.isTalkbackMuted()){
                    SlangBuddy.unmuteTalkback();
                }
            } catch (Exception e) {
                Log.d(TAG, "unmuteTalkback encountered an error");
                // TODO: handle exception
            }
        }

    }

    @ReactMethod
    public void mute() {
        Log.e(TAG, "mute");
        if (SlangBuddy.isInitialized()) {
            try {
                if(!SlangBuddy.isTalkbackMuted()){
                    SlangBuddy.muteTalkback();
                }

            } catch (Exception e) {
                Log.d(TAG, "muteTalkback encountered an error");
                // TODO: handle exception
            }
        }
    }

    @ReactMethod
    public void setSpeechRecognitionHints(ReadableMap speechRecognitionHints) {
        Log.e(TAG, "setSpeechRecognitionHints");

        if (null == speechRecognitionHints) return;
        if (SlangBuddy.isInitialized()) {
            try {
                Set<Locale> locales = RNSlangLocaleMap.getRequestedLocales(getStringListConfig(CONFIG_REQUESTED_LOCALES));
                Map<Locale, Set<String>> hints = new HashMap<>();
                for (Locale locale : locales) {
                    String lang = RNSlangLocaleMap.getLang(locale);
                    if (null == lang) continue;

                    ReadableArray hintsArray = speechRecognitionHints.getArray(lang);
                    if (null != hintsArray && hintsArray.size() > 0) {
                        hints.put(locale, new HashSet(hintsArray.toArrayList()));
                    }
                }
                
                SlangBuddy.setSpeechRecognitionHints(hints);
            } catch (Exception e) {
                Log.d(TAG, "setSpeechRecognitionHints encountered an error");
                // TODO: handle exception
            }
        }
    }

    @ReactMethod
    public void startConversation(String msg, boolean isSpoken) {
        Log.e(TAG, "startConversation");

        if (SlangBuddy.isInitialized()) {
            try {
                Set<Locale> locales = RNSlangLocaleMap.getRequestedLocales(getStringListConfig(CONFIG_REQUESTED_LOCALES));
                HashMap<Locale, String> strings = new HashMap<>();
                for (Locale locale : locales) {
                    strings.put(locale, msg);
                }
                SlangMessage message = SlangMessage.create(strings);
                SlangBuddy.startConversation(message, isSpoken);
            } catch (SlangBuddy.UninitializedUsageException e) {
                e.printStackTrace();
            } catch (SlangBuddy.SlangDisabledException e) {
                e.printStackTrace();
            }
        } else {
            Log.e(TAG, "Slang not initialized, cannot start convertsation.");
        }
    }
    
    @ReactMethod
    public void notifyUser(String msg) {
        Log.e(TAG, "notifyUser");
        if (SlangBuddy.isInitialized()) {
            try {
                Set<Locale> locales = RNSlangLocaleMap.getRequestedLocales(getStringListConfig(CONFIG_REQUESTED_LOCALES));
                HashMap<Locale, String> strings = new HashMap<>();
                for (Locale locale : locales) {
                    strings.put(locale, msg);
                }
                SlangMessage message = SlangMessage.create(strings);
                SlangBuddy.notifyUser(message);
            } catch (SlangBuddy.UninitializedUsageException e) {
                e.printStackTrace();
            }
        } else {
            Log.e(TAG, "Slang not initialized, cannot notify user.");
        }
    }

    @ReactMethod
    public void trackAppEvent(String eventName, ReadableMap eventData) {
        Log.e(TAG, "trackAppEvent");
        if (SlangBuddy.isInitialized()) {
            Map<String, String> appEventData = new HashMap<>();
            if (null != eventData && eventData.toHashMap().size() > 0) {
                for (ReadableMapKeySetIterator it = eventData.keySetIterator();it.hasNextKey();) {
                    String key = it.nextKey();
                    appEventData.put(key, eventData.getString(key));
                }
            }
            SlangBuddy.trackAppEvent(eventName, appEventData);
        } else {
            Log.e(TAG, "Slang not initialized, cannot track typed search yet.");
        }
    }

    @ReactMethod
    public void setTriggerPosition(String definedPosition, int offsetX, int offsetY, boolean force) {
        Log.e(TAG, "setTriggerPosition");
        SlangBuddy.getBuiltinUI().setPosition(
            getSlangUiPosition(definedPosition), 
            offsetX, 
            offsetY, 
            force
        );
    }

    @ReactMethod
    public void setTriggerImageResource(String imageName) {
        Log.e(TAG, "setTriggerImageResource");
        int resourceId = getImageResourceId(imageName);
        if (resourceId > 0) {
            SlangBuddy.getBuiltinUI().setImageResource(resourceId);
        }
    }

    @ReactMethod
    public void setTriggerSize(int width, int height) {
        Log.e(TAG, "setTriggerSize");
        SlangBuddy.getBuiltinUI().setTriggerSize(width, height);
    }

    @ReactMethod
    public void setTriggerDraggable(boolean isDraggable) {
        Log.e(TAG, "setTriggerDraggable");
        SlangBuddy.getBuiltinUI().setIsDraggable(isDraggable);
    }

    @ReactMethod
    public void setSurfacePrimaryTextColor(String color) {
        Log.e(TAG, "setSurfaceTextColor:" + color);
        SlangBuddy.getBuiltinUI().setTextColor(Color.parseColor(color));
    }

    @ReactMethod
    public void setSurfaceSecondaryTextColor(String color) {
        Log.e(TAG, "setSurfaceSecondaryTextColor:" + Color.parseColor(color));
        SlangBuddy.getBuiltinUI().setSecondaryTextColor(Color.parseColor(color));
    }

    @ReactMethod
    public void setSettingsButtonBackgroundColor(String color) {
        Log.e(TAG, "setSettingsButtonBackgroundColor:" + Color.parseColor(color));
        SlangBuddy.getBuiltinUI().setSettingsButtonBackgroundColor(Color.parseColor(color));
    }

    @ReactMethod
    public void setControlButtonBackgroundColor(String color) {
        Log.e(TAG, "setControlButtonBackgroundColor:" + Color.parseColor(color));
        SlangBuddy.getBuiltinUI().setControlButtonBackgroundColor(Color.parseColor(color));
    }

    @ReactMethod
    public void setUIBackgroundColorGradient(ReadableArray colors) {
        Log.e(TAG, "setSurfaceBackgroundColorGradient");
        if (colors.size() > 0) {
            int[] colorsArray = new int[colors.size()];
            for (int i = 0; i < colorsArray.length; i++) colorsArray[i] = Color.parseColor(colors.getString(i));
            Log.e(TAG, "setSurfaceBackgroundColorGradient:" + Arrays.toString(colorsArray));
            SlangBuddy.getBuiltinUI().setBackgroundColorGradient(colorsArray);
        }
    }

    @ReactMethod
    public void setUIForegroundColorGradient(ReadableArray colors) {
        Log.e(TAG, "setSurfaceForegroundColorGradient");
        if (colors.size() > 0) {
            int[] colorsArray = new int[colors.size()];
            for (int i = 0; i < colorsArray.length; i++) colorsArray[i] = Color.parseColor(colors.getString(i));
            Log.e(TAG, "setSurfaceForegroundColorGradient:" + Arrays.toString(colorsArray));
            SlangBuddy.getBuiltinUI().setForegroundColorGradient(colorsArray);
        }
    }

    @ReactMethod
    public void setUITheme(String theme) {
        Log.e(TAG, "setUITheme");
        if (null != theme || theme.toLowerCase().equals("light")) {
            SlangBuddy.getBuiltinUI().setUITheme(SlangBuiltinUI.SlangUITheme.LIGHT);
        } else {
            SlangBuddy.getBuiltinUI().setUITheme(SlangBuiltinUI.SlangUITheme.DARK);
        }
    }

    @ReactMethod
    public void setAssistanceHints(ReadableMap jsHints) {
        Log.e(TAG, "setAssistanceHints");
        if (null == jsHints) return;

        Map<String, Object> jsHintsMap = jsHints.toHashMap();
        if (jsHintsMap.size() == 0) return;

        Set<Locale> locales = RNSlangLocaleMap.getRequestedLocales(getStringListConfig(CONFIG_REQUESTED_LOCALES));
        Map<Locale, List<String>> hints = new HashMap();
        for (Locale locale : locales) {
            String lang = RNSlangLocaleMap.getLang(locale);
            if (null == lang || !jsHintsMap.containsKey(lang)) continue;

            hints.put(locale, (List<String>)jsHintsMap.get(lang));
        }
        SlangBuddy.getBuiltinUI().setAssistanceText(hints);
    }

    @ReactMethod
    public void overrideAffirmative(String s) {
        if (mCurrentIntent != null) {
            List<String> prompts = new ArrayList<String>();
            prompts.add(s);
            mCurrentIntent.setCompletionStatement(new SlangStatement(prompts, null));
        }

    }

    /**
     * Notify Slang of a action's success/failure status for Slang to show
     * completion statement
     *
     * @param isActionResolutionSuccess
     */
    @ReactMethod
    public void notifyActionCompleted(boolean isActionResolutionSuccess) {
        if (mCurrentSession != null) {
            mCurrentSession.notifyActionCompleted(
                    isActionResolutionSuccess ? SlangAction.Status.SUCCESS : SlangAction.Status.FAILURE);
        }
    }

    private int getImageResourceId(String imageName) {
        if (null != mReactContext) {
            return mReactContext.getCurrentActivity().getResources().getIdentifier(imageName , "drawable", mReactContext.getPackageName());
        } else {
            return -1;
        }
    }
    /**
     * Returns string configuration for the key passed in
     * {@link #initialize(String, String, ReadableMap, Callback)} method
     *
     * @param key Configuration key
     * @returns Value corresponding to {@param key}
     */
    private String getStringConfig(String key) {
        if (mConfigOptions == null) {
            return null;
        }
        try {
            String tempStr = mConfigOptions.getString(key);
            if(isNullOrEmpty(tempStr)) {
                return null;
            }
            return tempStr;    
        } catch (Exception e) {
            Log.d(TAG, "mConfigOptions encountered an error");
        }
        return null;
    }

    private List<String> getStringListConfig(String key) {
        if (mConfigOptions == null) {
            return null;
        }
        try {
            ReadableArray stringArray = mConfigOptions.getArray(key);
            if (null == stringArray || stringArray.size() == 0) {
                return null;
            }

            List<String> stringList = new ArrayList<>(stringArray.size());
            for (Object object : stringArray.toArrayList()) {
                stringList.add(object != null ? object.toString() : null);
            }
            return stringList;    
        } catch (Exception e) {
            Log.d(TAG, "mConfigOptions encountered an error");
        }
        return null;
    }

    private boolean isNullOrEmpty(String str) {
        if(str != null && !str.isEmpty())
            return false;
        return true;
    }

    private SlangBuiltinUI.SlangUIPosition getSlangUiPosition(String definedPosition) {
        SlangBuiltinUI.SlangUIPosition position = null;
        if (definedPosition != null) {
            return SlangBuiltinUI.SlangUIPosition.valueOf(definedPosition);
        } else {
            return SlangBuiltinUI.SlangUIPosition.CENTER_BOTTOM;
        }
    }

    private class RNSlangBuddyListener implements SlangBuddy.Listener {

        private Callback mBuddyListener;

        public RNSlangBuddyListener(Callback rnBuddyListener) {
            mBuddyListener = rnBuddyListener;
        }

        @Override
        public void onInitialized() {
            Log.d(TAG, "onInitialized: Success");
            mBuddyListener.invoke(true);
        }

        @Override
        public void onInitializationFailed(SlangBuddy.InitializationError initializationError) {
            Log.d(TAG, "onInitializationFailed: Failed");
            mBuddyListener.invoke(false);
        }

        @Override
        public void onLocaleChanged(Locale locale) {
            // TODO Emit an event to let the bridge know
        }

        @Override
        public void onLocaleChangeFailed(Locale locale, SlangBuddy.LocaleChangeError localeChangeError) {
            // TODO Emit an event to let the bridge know
        }

        @Override
        public void onSessionStart() {

        }

        @Override
        public void onSessionEnd() {

        }
    }

    private class RNSlangAction implements SlangIntentAction {
        private final ReactApplicationContext mReactContext;

        private RNSlangAction(ReactApplicationContext mReactContext) {
            this.mReactContext = mReactContext;
        }

        @Override
        public Status action(SlangIntent slangIntent, SlangSession slangSession) {
            mCurrentSession = slangSession;
            mCurrentIntent = slangIntent;
            mCurrentSession.waitForActionCompletion();
            WritableMap map = getMapFromIntentAndEntity(slangIntent);
            sendEvent(map);
            return Status.SUCCESS;
        }

         // TODO: Revisit and separate out the data transformation to separate class as
        // this shouldn't change in subsequent versions
        private WritableMap getMapFromIntentAndEntity(SlangIntent intent) {
            WritableMap params = Arguments.createMap();
            if (intent != null) {
                WritableMap intentMap = Arguments.createMap();

                intentMap.putString("name", intent.getName());
                intentMap.putString("userUtterance", intent.getUserUtterance());
                intentMap.putString("status", intent.getStatus().toString()); // #TODO: fix this
                intentMap.putString("completionStatement_affirmative",
                        intent.getCompletionStatement().getAffirmative());
                intentMap.putString("completionStatement_negative", intent.getCompletionStatement().getNegative());

                params.putMap("intent", intentMap);

                // Get all the entities for this intent
                WritableArray entitiesArray = Arguments.createArray();
                for (SlangEntity entity : intent.getEntities()) {
                    WritableMap entityMap = extractEntity(entity);
                    entitiesArray.pushMap(entityMap);
                }
                params.putArray("entities", entitiesArray);
            }

            return params;
        }

        private WritableMap extractListEntity(SlangEntity entity) {
            WritableMap entityMap = Arguments.createMap();
            entityMap.putString("name", entity.getName());
            entityMap.putString("value", entity.getValue());
            entityMap.putString("type", entity.getType().getName());
            entityMap.putBoolean("isResolved", entity.isResolved());
            entityMap.putBoolean("isRequired", entity.isRequired());
            entityMap.putBoolean("isList", false);
            entityMap.putArray("listValues", null);
            if (entity.getPrompt() != null) {
                entityMap.putString("prompt_affirmative", entity.getPrompt().getAffirmative());
                entityMap.putString("prompt_negative", entity.getPrompt().getNegative());
            }

            return entityMap;
        }

        private WritableMap extractEntity(SlangEntity entity) {
            WritableMap entityMap = Arguments.createMap();
            entityMap.putString("name", entity.getName());
            entityMap.putString("value", entity.getValue());
            entityMap.putString("type", entity.getType().getName());
            entityMap.putBoolean("isResolved", entity.isResolved());
            entityMap.putBoolean("isRequired", entity.isRequired());
            entityMap.putBoolean("isList", entity.isList());
            WritableArray listValues = Arguments.createArray();

            if (entity.isList() && entity.isResolved()) {
                for (SlangEntity listEntity : entity.getListValues()) {
                    WritableMap listEntityMap = extractListEntity(listEntity);
                    listValues.pushMap(listEntityMap);
                }
            }
            entityMap.putArray("listValues", listValues);

            if (entity.getPrompt() != null) {
                entityMap.putString("prompt_affirmative", entity.getPrompt().getAffirmative());
                entityMap.putString("prompt_negative", entity.getPrompt().getNegative());
            }

            return entityMap;
        }

        private void sendEvent(WritableMap params) {
            mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("slang_action",
                    params);
        }
    }
    
    private class RNSlangMultiStepAction extends RNSlangAction implements SlangMultiStepIntentAction {

        private RNSlangMultiStepAction(ReactApplicationContext mReactContext) {
            super(mReactContext);
        }

        @Override
        public void onIntentResolutionBegin(SlangIntent intent, SlangSession session) {}

        @Override
        public Status onEntityUnresolved(SlangEntity entity, SlangSession session) { return Status.SUCCESS; }

        @Override
        public Status onEntityResolved(SlangEntity entity, SlangSession session) { return Status.SUCCESS; }

        @Override
        public void onIntentResolutionEnd(SlangIntent intent, SlangSession session) {}
      
    }
}