package io.invertase.firebase.database;

import android.annotation.SuppressLint;
import android.os.AsyncTask;
import android.util.Log;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.Query;
import com.google.firebase.database.ValueEventListener;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import io.invertase.firebase.Utils;

class RNFirebaseDatabaseReference {
  private static final String TAG = "RNFirebaseDBReference";
  private String key;
  private Query query;
  private String appName;
  private String dbURL;
  private HashMap<String, ChildEventListener> childEventListeners = new HashMap<>();
  private HashMap<String, ValueEventListener> valueEventListeners = new HashMap<>();

  /**
   * RNFirebase wrapper around FirebaseDatabaseReference,
   * handles Query generation and event listeners.
   *
   * @param app
   * @param refKey
   * @param refPath
   * @param modifiersArray
   */
  RNFirebaseDatabaseReference(
    String app,
    String url,
    String refKey,
    String refPath,
    ReadableArray modifiersArray
  ) {
    key = refKey;
    query = null;
    appName = app;
    dbURL = url;
    buildDatabaseQueryAtPathAndModifiers(refPath, modifiersArray);
  }

  void removeAllEventListeners() {
    if (hasListeners()) {
      Iterator valueIterator = valueEventListeners.entrySet().iterator();

      while (valueIterator.hasNext()) {
        Map.Entry pair = (Map.Entry) valueIterator.next();
        ValueEventListener valueEventListener = (ValueEventListener) pair.getValue();
        query.removeEventListener(valueEventListener);
        valueIterator.remove();
      }

      Iterator childIterator = childEventListeners.entrySet().iterator();

      while (childIterator.hasNext()) {
        Map.Entry pair = (Map.Entry) childIterator.next();
        ChildEventListener childEventListener = (ChildEventListener) pair.getValue();
        query.removeEventListener(childEventListener);
        childIterator.remove();
      }
    }
  }


  /**
   * Used outside of class for keepSynced etc.
   *
   * @return Query
   */
  Query getQuery() {
    return query;
  }

  /**
   * Returns true/false whether this internal ref has a specific listener by eventRegistrationKey.
   *
   * @param eventRegistrationKey
   * @return
   */
  private Boolean hasEventListener(String eventRegistrationKey) {
    return valueEventListeners.containsKey(eventRegistrationKey) || childEventListeners.containsKey(
      eventRegistrationKey);
  }

  /**
   * Returns true/false whether this internal ref has any child or value listeners.
   *
   * @return
   */
  Boolean hasListeners() {
    return valueEventListeners.size() > 0 || childEventListeners.size() > 0;
  }

  /**
   * Remove an event listener by key, will remove either a ValueEventListener or
   * a ChildEventListener
   *
   * @param eventRegistrationKey
   */
  void removeEventListener(String eventRegistrationKey) {
    if (valueEventListeners.containsKey(eventRegistrationKey)) {
      query.removeEventListener(valueEventListeners.get(eventRegistrationKey));
      valueEventListeners.remove(eventRegistrationKey);
    }

    if (childEventListeners.containsKey(eventRegistrationKey)) {
      query.removeEventListener(childEventListeners.get(eventRegistrationKey));
      childEventListeners.remove(eventRegistrationKey);
    }
  }

  /**
   * Add a ValueEventListener to the query and internally keep a reference to it.
   *
   * @param eventRegistrationKey
   * @param listener
   */
  private void addEventListener(String eventRegistrationKey, ValueEventListener listener) {
    valueEventListeners.put(eventRegistrationKey, listener);
    query.addValueEventListener(listener);

  }

  /**
   * Add a ChildEventListener to the query and internally keep a reference to it.
   *
   * @param eventRegistrationKey
   * @param listener
   */
  private void addEventListener(String eventRegistrationKey, ChildEventListener listener) {
    childEventListeners.put(eventRegistrationKey, listener);
    query.addChildEventListener(listener);

  }

  /**
   * Listen for a single .once('value',..) event from firebase.
   *
   * @param promise
   */
  private void addOnceValueEventListener(final Promise promise) {
    @SuppressLint("StaticFieldLeak") final DataSnapshotToMapAsyncTask asyncTask = new DataSnapshotToMapAsyncTask(
      this
    ) {
      @Override
      protected void onPostExecute(WritableMap writableMap) {
        if (this.isAvailable()) promise.resolve(writableMap);
      }
    };

    ValueEventListener onceValueEventListener = new ValueEventListener() {
      @Override
      public void onDataChange(@Nonnull DataSnapshot dataSnapshot) {
        asyncTask.execute(dataSnapshot, null);
      }

      @Override
      public void onCancelled(@Nonnull DatabaseError error) {
        RNFirebaseDatabase.handlePromise(promise, error);
      }
    };

    query.addListenerForSingleValueEvent(onceValueEventListener);
    Log.d(TAG, "Added OnceValueEventListener for key: " + key);
  }

  /**
   * Listen for single '.once(child_X, ...)' event from firebase.
   *
   * @param eventName
   * @param promise
   */
  private void addChildOnceEventListener(final String eventName, final Promise promise) {
    ChildEventListener childEventListener = new ChildEventListener() {
      @Override
      public void onChildAdded(@Nonnull DataSnapshot dataSnapshot, String previousChildName) {
        if ("child_added".equals(eventName)) {
          query.removeEventListener(this);
          WritableMap data = RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, previousChildName);
          promise.resolve(data);
        }
      }

      @Override
      public void onChildChanged(@Nonnull DataSnapshot dataSnapshot, String previousChildName) {
        if ("child_changed".equals(eventName)) {
          query.removeEventListener(this);
          WritableMap data = RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, previousChildName);
          promise.resolve(data);
        }
      }

      @Override
      public void onChildRemoved(@Nonnull DataSnapshot dataSnapshot) {
        if ("child_removed".equals(eventName)) {
          query.removeEventListener(this);
          WritableMap data = RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, null);
          promise.resolve(data);
        }
      }

      @Override
      public void onChildMoved(@Nonnull DataSnapshot dataSnapshot, String previousChildName) {
        if ("child_moved".equals(eventName)) {
          query.removeEventListener(this);
          WritableMap data = RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, previousChildName);
          promise.resolve(data);
        }
      }

      @Override
      public void onCancelled(@Nonnull DatabaseError error) {
        query.removeEventListener(this);
        RNFirebaseDatabase.handlePromise(promise, error);
      }
    };

    query.addChildEventListener(childEventListener);
  }

  /**
   * Handles a React Native JS '.on(..)' request and initializes listeners.
   *
   * @param registration
   */
  void on(String eventType, ReadableMap registration) {
    if (eventType.equals("value")) {
      addValueEventListener(registration);
    } else {
      addChildEventListener(registration, eventType);
    }
  }

  /**
   * Handles a React Native JS 'once' request.
   *
   * @param eventType
   * @param promise
   */
  void once(String eventType, Promise promise) {
    if (eventType.equals("value")) {
      addOnceValueEventListener(promise);
    } else {
      addChildOnceEventListener(eventType, promise);
    }
  }

  /**
   * Add a native .on('child_X',.. ) event listener.
   *
   * @param registration
   * @param eventType
   */
  private void addChildEventListener(final ReadableMap registration, final String eventType) {
    final String eventRegistrationKey = registration.getString("eventRegistrationKey");
    final String registrationCancellationKey = registration.getString("registrationCancellationKey");

    if (!hasEventListener(eventRegistrationKey)) {
      ChildEventListener childEventListener = new ChildEventListener() {
        @Override
        public void onChildAdded(@Nonnull DataSnapshot dataSnapshot, String previousChildName) {
          if ("child_added".equals(eventType)) {
            handleDatabaseEvent("child_added", registration, dataSnapshot, previousChildName);
          }
        }

        @Override
        public void onChildChanged(@Nonnull DataSnapshot dataSnapshot, String previousChildName) {
          if ("child_changed".equals(eventType)) {
            handleDatabaseEvent("child_changed", registration, dataSnapshot, previousChildName);
          }
        }

        @Override
        public void onChildRemoved(@Nonnull DataSnapshot dataSnapshot) {
          if ("child_removed".equals(eventType)) {
            handleDatabaseEvent("child_removed", registration, dataSnapshot, null);
          }
        }

        @Override
        public void onChildMoved(@Nonnull DataSnapshot dataSnapshot, String previousChildName) {
          if ("child_moved".equals(eventType)) {
            handleDatabaseEvent("child_moved", registration, dataSnapshot, previousChildName);
          }
        }

        @Override
        public void onCancelled(@Nonnull DatabaseError error) {
          removeEventListener(eventRegistrationKey);
          handleDatabaseError(registration, error);
        }
      };

      addEventListener(eventRegistrationKey, childEventListener);
    }
  }

  /**
   * Add a native .on('value',.. ) event listener.
   *
   * @param registration
   */
  private void addValueEventListener(final ReadableMap registration) {
    final String eventRegistrationKey = registration.getString("eventRegistrationKey");

    if (!hasEventListener(eventRegistrationKey)) {
      ValueEventListener valueEventListener = new ValueEventListener() {
        @Override
        public void onDataChange(@Nonnull DataSnapshot dataSnapshot) {
          handleDatabaseEvent("value", registration, dataSnapshot, null);
        }

        @Override
        public void onCancelled(@Nonnull DatabaseError error) {
          removeEventListener(eventRegistrationKey);
          handleDatabaseError(registration, error);
        }
      };

      addEventListener(eventRegistrationKey, valueEventListener);
    }
  }

  /**
   * Handles value/child update events.
   *
   * @param eventType
   * @param dataSnapshot
   * @param previousChildName
   */
  private void handleDatabaseEvent(
    final String eventType,
    final ReadableMap registration,
    DataSnapshot dataSnapshot,
    @Nullable String previousChildName
  ) {
    @SuppressLint("StaticFieldLeak")
    DataSnapshotToMapAsyncTask asyncTask = new DataSnapshotToMapAsyncTask(this) {
      @Override
      protected void onPostExecute(WritableMap data) {
        if (this.isAvailable()) {
          WritableMap event = Arguments.createMap();
          event.putMap("data", data);
          event.putString("key", key);
          event.putString("eventType", eventType);
          event.putMap("registration", Utils.readableMapToWritableMap(registration));
          Utils.sendEvent(
            RNFirebaseDatabase.getReactApplicationContextInstance(),
            "database_sync_event",
            event
          );
        }
      }
    };

    asyncTask.execute(dataSnapshot, previousChildName);
  }

  /**
   * Handles a database listener cancellation error.
   *
   * @param error
   */
  private void handleDatabaseError(ReadableMap registration, DatabaseError error) {
    WritableMap event = Arguments.createMap();

    event.putString("key", key);
    event.putMap("error", RNFirebaseDatabase.getJSError(error));
    event.putMap("registration", Utils.readableMapToWritableMap(registration));

    Utils.sendEvent(
      RNFirebaseDatabase.getReactApplicationContextInstance(),
      "database_sync_event",
      event
    );
  }

  /**
   * @param path
   * @param modifiers
   * @return
   */
  private void buildDatabaseQueryAtPathAndModifiers(String path, ReadableArray modifiers) {
    FirebaseDatabase firebaseDatabase = RNFirebaseDatabase.getDatabaseForApp(appName, dbURL);
    query = firebaseDatabase.getReference(path);
    List<Object> modifiersList = Utils.recursivelyDeconstructReadableArray(modifiers);

    for (Object m : modifiersList) {
      Map modifier = (Map) m;
      String type = (String) modifier.get("type");
      String name = (String) modifier.get("name");

      if ("orderBy".equals(type)) {
        applyOrderByModifier(name, type, modifier);
      } else if ("limit".equals(type)) {
        applyLimitModifier(name, type, modifier);
      } else if ("filter".equals(type)) {
        applyFilterModifier(name, modifier);
      }
    }
  }

  /**
   * @param name
   * @param type
   * @param modifier
   */
  private void applyOrderByModifier(String name, String type, Map modifier) {
    switch (name) {
      case "orderByKey":
        query = query.orderByKey();
        break;
      case "orderByPriority":
        query = query.orderByPriority();
        break;
      case "orderByValue":
        query = query.orderByValue();
        break;
      case "orderByChild":
        String key = (String) modifier.get("key");
        query = query.orderByChild(key);
    }
  }

  /* =================
   *  QUERY MODIFIERS
   * =================
   */

  /**
   * @param name
   * @param type
   * @param modifier
   */
  private void applyLimitModifier(String name, String type, Map modifier) {
    int limit = ((Double) modifier.get("limit")).intValue();
    if ("limitToLast".equals(name)) {
      query = query.limitToLast(limit);
    } else if ("limitToFirst".equals(name)) {
      query = query.limitToFirst(limit);
    }
  }

  /**
   * @param name
   * @param modifier
   */
  private void applyFilterModifier(String name, Map modifier) {
    String valueType = (String) modifier.get("valueType");
    String key = (String) modifier.get("key");
    if ("equalTo".equals(name)) {
      applyEqualToFilter(key, valueType, modifier);
    } else if ("endAt".equals(name)) {
      applyEndAtFilter(key, valueType, modifier);
    } else if ("startAt".equals(name)) {
      applyStartAtFilter(key, valueType, modifier);
    }
  }

  /**
   * @param key
   * @param valueType
   * @param modifier
   */
  private void applyEqualToFilter(String key, String valueType, Map modifier) {
    if ("number".equals(valueType)) {
      double value = (Double) modifier.get("value");
      if (key == null) {
        query = query.equalTo(value);
      } else {
        query = query.equalTo(value, key);
      }
    } else if ("boolean".equals(valueType)) {
      boolean value = (Boolean) modifier.get("value");
      if (key == null) {
        query = query.equalTo(value);
      } else {
        query = query.equalTo(value, key);
      }
    } else if ("string".equals(valueType)) {
      String value = (String) modifier.get("value");
      if (key == null) {
        query = query.equalTo(value);
      } else {
        query = query.equalTo(value, key);
      }
    }
  }


  /* ===============
   *  QUERY FILTERS
   * ===============
   */

  /**
   * @param key
   * @param valueType
   * @param modifier
   */
  private void applyEndAtFilter(String key, String valueType, Map modifier) {
    if ("number".equals(valueType)) {
      double value = (Double) modifier.get("value");
      if (key == null) {
        query = query.endAt(value);
      } else {
        query = query.endAt(value, key);
      }
    } else if ("boolean".equals(valueType)) {
      boolean value = (Boolean) modifier.get("value");
      if (key == null) {
        query = query.endAt(value);
      } else {
        query = query.endAt(value, key);
      }
    } else if ("string".equals(valueType)) {
      String value = (String) modifier.get("value");
      if (key == null) {
        query = query.endAt(value);
      } else {
        query = query.endAt(value, key);
      }
    }
  }

  /**
   * @param key
   * @param valueType
   * @param modifier
   */
  private void applyStartAtFilter(String key, String valueType, Map modifier) {
    if ("number".equals(valueType)) {
      double value = (Double) modifier.get("value");
      if (key == null) {
        query = query.startAt(value);
      } else {
        query = query.startAt(value, key);
      }
    } else if ("boolean".equals(valueType)) {
      boolean value = (Boolean) modifier.get("value");
      if (key == null) {
        query = query.startAt(value);
      } else {
        query = query.startAt(value, key);
      }
    } else if ("string".equals(valueType)) {
      String value = (String) modifier.get("value");
      if (key == null) {
        query = query.startAt(value);
      } else {
        query = query.startAt(value, key);
      }
    }
  }

  /**
   * AsyncTask to convert DataSnapshot instances to WritableMap instances.
   * <p>
   * Introduced due to https://github.com/invertase/react-native-firebase/issues/1284
   */
  private static class DataSnapshotToMapAsyncTask extends AsyncTask<Object, Void, WritableMap> {
    private WeakReference<RNFirebaseDatabaseReference> referenceWeakReference;

    DataSnapshotToMapAsyncTask(RNFirebaseDatabaseReference reference) {
      referenceWeakReference = new WeakReference<>(reference);
    }

    @Override
    protected final WritableMap doInBackground(Object... params) {
      DataSnapshot dataSnapshot = (DataSnapshot) params[0];
      @Nullable String previousChildName = (String) params[1];

      try {
        return RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, previousChildName);
      } catch (RuntimeException e) {
        if (isAvailable()) {
          RNFirebaseDatabase.getReactApplicationContextInstance()
            .handleException(e);
        }
        throw e;
      }
    }

    @Override
    protected void onPostExecute(WritableMap writableMap) {
      // do nothing as overridden on usage
    }

    Boolean isAvailable() {
      return RNFirebaseDatabase.getReactApplicationContextInstance() != null && referenceWeakReference.get() != null;
    }
  }
}
