package io.fullstack.firestack;

import android.util.Log;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.ReadableMap;

import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;
import com.google.firebase.database.DataSnapshot;

public class FirestackUtils {
  private static final String TAG = "FirestackUtils";

      // TODO NOTE
  public static void todoNote(final String tag, final String name, final Callback callback) {
    Log.e(tag, "The method " + name + " has not yet been implemented.");
    Log.e(tag, "Feel free to contribute to finish the method in the source.");

    WritableMap errorMap = Arguments.createMap();
    errorMap.putString("error", "unimplemented");
    callback.invoke(null, errorMap);
  }

  /**
  * send a JS event
  **/
  public static void sendEvent(final ReactContext context,
    final String eventName,
    final WritableMap params) {
    if (context.hasActiveCatalystInstance()) {
      context
          .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
          .emit(eventName, params);
    } else {
      Log.d(TAG, "Waiting for CatalystInstance before sending event");
    }
  }

  // snapshot
  public static WritableMap dataSnapshotToMap(String name, 
    String path, 
    DataSnapshot dataSnapshot) {
      WritableMap data = Arguments.createMap();

      data.putString("key", dataSnapshot.getKey());
      data.putBoolean("exists", dataSnapshot.exists());
      data.putBoolean("hasChildren", dataSnapshot.hasChildren());

      data.putDouble("childrenCount", dataSnapshot.getChildrenCount());
      if (!dataSnapshot.hasChildren()) {
        Object value = dataSnapshot.getValue();
        String type = value!=null ? value.getClass().getName() : "";
        switch (type) {
          case "java.lang.Boolean":
            data.putBoolean("value", (Boolean)value);
            break;
          case "java.lang.Long":
            Long longVal = (Long) value;
            data.putDouble("value", (double)longVal);
            break;
          case "java.lang.Double":
            data.putDouble("value", (Double) value);
            break;
          case "java.lang.String":
            data.putString("value",(String) value);
            break;
          default:
            data.putString("value", null);
        }
      } else{
        WritableMap valueMap = FirestackUtils.castSnapshotValue(dataSnapshot);
        data.putMap("value", valueMap);
      }

      // Child keys
      WritableArray childKeys = FirestackUtils.getChildKeys(dataSnapshot);
      data.putArray("childKeys", childKeys);

      Object priority = dataSnapshot.getPriority();
      if (priority == null) {
        data.putString("priority", null);
      } else {
        data.putString("priority", priority.toString());
      }

      WritableMap eventMap = Arguments.createMap();
      eventMap.putString("eventName", name);
      eventMap.putMap("snapshot", data);
      eventMap.putString("path", path);
      return eventMap;
  }

  public static <Any> Any castSnapshotValue(DataSnapshot snapshot) {
    if (snapshot.hasChildren()) {
      WritableMap data = Arguments.createMap();
      for (DataSnapshot child : snapshot.getChildren()) {
        Any castedChild = castSnapshotValue(child);
        switch (castedChild.getClass().getName()) {
          case "java.lang.Boolean":
            data.putBoolean(child.getKey(), (Boolean) castedChild);
            break;
          case "java.lang.Long":
            Long longVal = (Long) castedChild;
            data.putDouble(child.getKey(), (double)longVal);
            break;
          case "java.lang.Double":
            data.putDouble(child.getKey(), (Double) castedChild);
            break;
          case "java.lang.String":
            data.putString(child.getKey(), (String) castedChild);
            break;
          case "com.facebook.react.bridge.WritableNativeMap":
            data.putMap(child.getKey(), (WritableMap) castedChild);
            break;
          default:
            Log.w(TAG, "Invalid type: " + castedChild.getClass().getName());
            break;
        }
      }
      return (Any) data;
    } else {
      if (snapshot.getValue() != null) {
        String type = snapshot.getValue().getClass().getName();
        switch (type) {
          case "java.lang.Boolean":
            return (Any)(snapshot.getValue());
          case "java.lang.Long":
            return (Any)(snapshot.getValue());
          case "java.lang.Double":
            return (Any)(snapshot.getValue());
          case "java.lang.String":
            return (Any)(snapshot.getValue());
          default:
            Log.w(TAG, "Invalid type: "+type);
            return (Any) null;
        }
      }
      return (Any) null;
    }
  }

  public static WritableArray getChildKeys(DataSnapshot snapshot) {
    WritableArray childKeys = Arguments.createArray();

    if (snapshot.hasChildren()) {
      for (DataSnapshot child : snapshot.getChildren()) {
        childKeys.pushString(child.getKey());
      }
    }

    return childKeys;
  }

  public static Map<String, Object> recursivelyDeconstructReadableMap(ReadableMap readableMap) {
      ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
      Map<String, Object> deconstructedMap = new HashMap<>();
      while (iterator.hasNextKey()) {
          String key = iterator.nextKey();
          ReadableType type = readableMap.getType(key);
          switch (type) {
              case Null:
                  deconstructedMap.put(key, null);
                  break;
              case Boolean:
                  deconstructedMap.put(key, readableMap.getBoolean(key));
                  break;
              case Number:
                  deconstructedMap.put(key, readableMap.getDouble(key));
                  break;
              case String:
                  deconstructedMap.put(key, readableMap.getString(key));
                  break;
              case Map:
                  deconstructedMap.put(key, FirestackUtils.recursivelyDeconstructReadableMap(readableMap.getMap(key)));
                  break;
              case Array:
                  deconstructedMap.put(key, FirestackUtils.recursivelyDeconstructReadableArray(readableMap.getArray(key)));
                  break;
              default:
                  throw new IllegalArgumentException("Could not convert object with key: " + key + ".");
          }

      }
      return deconstructedMap;
  }

  public static List<Object> recursivelyDeconstructReadableArray(ReadableArray readableArray) {
      List<Object> deconstructedList = new ArrayList<>(readableArray.size());
      for (int i = 0; i < readableArray.size(); i++) {
          ReadableType indexType = readableArray.getType(i);
          switch(indexType) {
              case Null:
                  deconstructedList.add(i, null);
                  break;
              case Boolean:
                  deconstructedList.add(i, readableArray.getBoolean(i));
                  break;
              case Number:
                  deconstructedList.add(i, readableArray.getDouble(i));
                  break;
              case String:
                  deconstructedList.add(i, readableArray.getString(i));
                  break;
              case Map:
                  deconstructedList.add(i, FirestackUtils.recursivelyDeconstructReadableMap(readableArray.getMap(i)));
                  break;
              case Array:
                  deconstructedList.add(i, FirestackUtils.recursivelyDeconstructReadableArray(readableArray.getArray(i)));
                  break;
              default:
                  throw new IllegalArgumentException("Could not convert object at index " + i + ".");
          }
      }
      return deconstructedList;
  }
}