#ifdef __APPLE__
#include <ExpoModulesJSI/JSIUtils.h>
#else
#include "JSIUtils.h"
#endif

#include "EventEmitter.h"
#include "LazyObject.h"

#include <cxxreact/ErrorUtils.h>

namespace expo::EventEmitter {

#pragma mark - Listeners

void Listeners::add(jsi::Runtime &runtime, const std::string& eventName, const jsi::Function &listener) noexcept {
  listenersMap[eventName].emplace_back(runtime, listener);
}

void Listeners::remove(jsi::Runtime &runtime, const std::string& eventName, const jsi::Function &listener) noexcept {
  if (!listenersMap.contains(eventName)) {
    return;
  }
  jsi::Value listenerValue(runtime, listener);

  listenersMap[eventName].remove_if([&](const jsi::Value &item) {
    return jsi::Value::strictEquals(runtime, listenerValue, item);
  });
}

void Listeners::removeAll(const std::string& eventName) noexcept {
  if (listenersMap.contains(eventName)) {
    listenersMap[eventName].clear();
  }
}

void Listeners::clear() noexcept {
  listenersMap.clear();
}

size_t Listeners::listenersCount(const std::string& eventName) noexcept {
  if (!listenersMap.contains(eventName)) {
    return 0;
  }
  return listenersMap[eventName].size();
}

void Listeners::call(jsi::Runtime &runtime, const std::string& eventName, const jsi::Object &thisObject, const jsi::Value *args, size_t count) noexcept {
  if (!listenersMap.contains(eventName)) {
    return;
  }
  ListenersList &listenersList = listenersMap[eventName];
  size_t listSize = listenersList.size();

  if (listSize == 0) {
    // Nothing to call.
    return;
  }
  if (listSize == 1) {
    // The most common scenario – just call the only listener.
    try {
      listenersList
        .front()
        .asObject(runtime)
        .asFunction(runtime)
        .callWithThis(runtime, thisObject, args, count);
    } catch (jsi::JSError& error) {
      facebook::react::handleJSError(runtime, error, false);
    }
    return;
  }
  // When there are more than one listener, we copy the list to a vector as the list may be modified during the loop.
  std::vector<jsi::Function> listenersVector;
  listenersVector.reserve(listSize);

  // Copy listeners to vector already as jsi::Function so we don't additionally copy jsi::Value
  for (const jsi::Value &listener : listenersList) {
    listenersVector.push_back(listener.asObject(runtime).asFunction(runtime));
  }

  // Call listeners from the vector. The list can be modified by the listeners but it will not affect this loop,
  // i.e. newly added listeners will not be called and removed listeners will be called one last time.
  // This is compliant with the EventEmitter in Node.js
  for (const jsi::Function &listener : listenersVector) {
    // As opposed to Node.js and fbemitter, when the listener throws an error the behavior is the same as on web.
    // That is, it doesn't stop the execution of subsequent listeners and the error is not propagated to the `emit` function.
    // The motivation behind this is that errors thrown from a module or user's code shouldn't affect other modules' behavior.
    try {
      listener.callWithThis(runtime, thisObject, args, count);
    } catch (jsi::JSError& error) {
      facebook::react::handleJSError(runtime, error, false);
    }
  }
}

#pragma mark - NativeState

NativeState::NativeState() : jsi::NativeState() {}

NativeState::~NativeState() {
  listeners.clear();
}

NativeState::Shared NativeState::get(jsi::Runtime &runtime, const jsi::Object &object, bool createIfMissing) {
  if (object.hasNativeState<NativeState>(runtime)) {
    return object.getNativeState<NativeState>(runtime);
  }
  if (createIfMissing) {
    NativeState::Shared state = std::make_shared<NativeState>();
    object.setNativeState(runtime, state);
    return state;
  }
  return nullptr;
}

#pragma mark - Utils

void callObservingFunction(jsi::Runtime &runtime, const jsi::Object &object, const char* functionName, const std::string& eventName) {
  jsi::Value fnValue = object.getProperty(runtime, functionName);

  if (!fnValue.isObject()) {
    // Skip it if there is no observing function.
    return;
  }

  fnValue
    .getObject(runtime)
    .asFunction(runtime)
    .callWithThis(runtime, object, {
      jsi::Value(runtime, jsi::String::createFromUtf8(runtime, eventName))
    });
}

void addListener(jsi::Runtime &runtime, const jsi::Object &emitter, const std::string &eventName, const jsi::Function &listener) {
  if (NativeState::Shared state = NativeState::get(runtime, emitter, true)) {
    state->listeners.add(runtime, eventName, listener);

    if (state->listeners.listenersCount(eventName) == 1) {
      callObservingFunction(runtime, emitter, "__expo_onStartListeningToEvent", eventName);
      callObservingFunction(runtime, emitter, "startObserving", eventName);
    }
  }
}

void removeListener(jsi::Runtime &runtime, const jsi::Object &emitter, const std::string &eventName, const jsi::Function &listener) {
  if (NativeState::Shared state = NativeState::get(runtime, emitter, false)) {
    size_t listenersCountBefore = state->listeners.listenersCount(eventName);

    state->listeners.remove(runtime, eventName, listener);

    if (listenersCountBefore >= 1 && state->listeners.listenersCount(eventName) == 0) {
      callObservingFunction(runtime, emitter, "__expo_onStopListeningToEvent", eventName);
      callObservingFunction(runtime, emitter, "stopObserving", eventName);
    }
  }
}

void removeAllListeners(jsi::Runtime &runtime, const jsi::Object &emitter, const std::string &eventName) {
  if (NativeState::Shared state = NativeState::get(runtime, emitter, false)) {
    size_t listenersCountBefore = state->listeners.listenersCount(eventName);

    state->listeners.removeAll(eventName);

    if (listenersCountBefore >= 1) {
      callObservingFunction(runtime, emitter, "__expo_onStopListeningToEvent", eventName);
      callObservingFunction(runtime, emitter, "stopObserving", eventName);
    }
  }
}

void emitEvent(jsi::Runtime &runtime, const jsi::Object &emitter, const std::string &eventName, const jsi::Value *args, size_t count) {
  if (NativeState::Shared state = NativeState::get(runtime, emitter, false)) {
    state->listeners.call(runtime, eventName, emitter, args, count);
  }
}

size_t getListenerCount(jsi::Runtime &runtime, const jsi::Object &emitter, const std::string &eventName) {
  if (NativeState::Shared state = NativeState::get(runtime, emitter, false)) {
    return state->listeners.listenersCount(eventName);
  }
  return 0;
}

jsi::Value createEventSubscription(jsi::Runtime &runtime, const std::string &eventName, const jsi::Object &emitter, const jsi::Function &listener) {
  jsi::Object subscription(runtime);
  jsi::PropNameID removeProp = jsi::PropNameID::forAscii(runtime, "remove", 6);
  std::shared_ptr<jsi::Value> emitterValue = std::make_shared<jsi::Value>(runtime, emitter);
  std::shared_ptr<jsi::Value> listenerValue = std::make_shared<jsi::Value>(runtime, listener);

  jsi::HostFunctionType removeSubscription = [eventName, emitterValue, listenerValue](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value {
    jsi::Object emitter = emitterValue->getObject(runtime);
    jsi::Function listener = listenerValue->getObject(runtime).getFunction(runtime);

    removeListener(runtime, emitter, eventName, listener);
    return jsi::Value::undefined();
  };

  subscription.setProperty(runtime, removeProp, jsi::Function::createFromHostFunction(runtime, removeProp, 0, removeSubscription));

  return jsi::Value(runtime, subscription);
}

#pragma mark - Public API

void emitEvent(jsi::Runtime &runtime, jsi::Object &emitter, const std::string &eventName, const std::vector<jsi::Value> &arguments) {
  emitEvent(runtime, emitter, eventName, arguments.data(), arguments.size());
}

jsi::Function getClass(jsi::Runtime &runtime) {
  return common::getCoreObject(runtime)
    .getPropertyAsFunction(runtime, "EventEmitter");
}

void installClass(jsi::Runtime &runtime) {
  jsi::Function eventEmitterClass = common::createClass(runtime, "EventEmitter", [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value {
    // To provide backwards compatibility with the old EventEmitter where the native module object was passed as an argument.
    // We're checking if the argument is already an instance of the new emitter and if so, just return it without unnecessarily wrapping it.
    if (count > 0) {
      // We need the tmp object to correctly unwrap the lazy object.
      // For some reason, if we inline the retrieval of the first argument, the instanceOf check fails on Android.
      // This is probably because the object is copied somewhere in the process.
      const jsi::Object &tmp = args[0].asObject(runtime);
      const jsi::Object &firstArg = LazyObject::unwrapObjectIfNecessary(runtime, tmp);

      jsi::Function constructor = thisValue.getObject(runtime).getPropertyAsFunction(runtime, "constructor");

      if (firstArg.instanceOf(runtime, constructor)) {
        return jsi::Value(runtime, args[0]);
      }
    }
    return jsi::Value(runtime, thisValue);
  });
  jsi::Object prototype = eventEmitterClass.getPropertyAsObject(runtime, "prototype");

  jsi::HostFunctionType addListenerHost = [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value {
    std::string eventName = args[0].asString(runtime).utf8(runtime);
    jsi::Function listener = args[1].asObject(runtime).asFunction(runtime);
    jsi::Object thisObject = thisValue.getObject(runtime);

    // `this` might be an object that is representing a host object, in which case it's not possible to get the native state.
    // For native modules we need to unwrap it to get the object used under the hood by `LazyObject` host object.
    const jsi::Object &emitter = LazyObject::unwrapObjectIfNecessary(runtime, thisObject);

    addListener(runtime, emitter, eventName, listener);
    return createEventSubscription(runtime, eventName, emitter, listener);
  };

  jsi::HostFunctionType removeListenerHost = [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value {
    std::string eventName = args[0].asString(runtime).utf8(runtime);
    jsi::Function listener = args[1].asObject(runtime).asFunction(runtime);
    jsi::Object thisObject = thisValue.getObject(runtime);

    // Unwrap `this` object if it's a lazy object (e.g. native module).
    const jsi::Object &emitter = LazyObject::unwrapObjectIfNecessary(runtime, thisObject);

    removeListener(runtime, emitter, eventName, listener);
    return jsi::Value::undefined();
  };

  jsi::HostFunctionType removeAllListenersHost = [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value {
    std::string eventName = args[0].asString(runtime).utf8(runtime);
    jsi::Object thisObject = thisValue.getObject(runtime);

    // Unwrap `this` object if it's a lazy object (e.g. native module).
    const jsi::Object &emitter = LazyObject::unwrapObjectIfNecessary(runtime, thisObject);

    removeAllListeners(runtime, emitter, eventName);
    return jsi::Value::undefined();
  };

  jsi::HostFunctionType emit = [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value {
    std::string eventName = args[0].asString(runtime).utf8(runtime);
    jsi::Object thisObject = thisValue.getObject(runtime);

    // Unwrap `this` object if it's a lazy object (e.g. native module).
    const jsi::Object &emitter = LazyObject::unwrapObjectIfNecessary(runtime, thisObject);

    // Make a new pointer that skips the first argument which is the event name.
    const jsi::Value *eventArgs = count > 1 ? &args[1] : nullptr;

    emitEvent(runtime, emitter, eventName, eventArgs, count - 1);
    return jsi::Value::undefined();
  };

  jsi::HostFunctionType listenerCountHost = [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value {
    std::string eventName = args[0].asString(runtime).utf8(runtime);
    jsi::Object thisObject = thisValue.getObject(runtime);

    // Unwrap `this` object if it's a lazy object (e.g. native module).
    const jsi::Object &emitter = LazyObject::unwrapObjectIfNecessary(runtime, thisObject);

    return jsi::Value((int)getListenerCount(runtime, emitter, eventName));
  };

  // Added for compatibility with the old EventEmitter API.
  jsi::HostFunctionType removeSubscriptionHost = [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value {
    jsi::Object subscription = args[0].asObject(runtime);

    subscription.getProperty(runtime, "remove")
      .asObject(runtime)
      .asFunction(runtime)
      .callWithThis(runtime, subscription, {});

    return jsi::Value::undefined();
  };

  jsi::PropNameID addListenerProp = jsi::PropNameID::forAscii(runtime, "addListener", 11);
  jsi::PropNameID removeListenerProp = jsi::PropNameID::forAscii(runtime, "removeListener", 14);
  jsi::PropNameID removeAllListenersProp = jsi::PropNameID::forAscii(runtime, "removeAllListeners", 18);
  jsi::PropNameID emitProp = jsi::PropNameID::forAscii(runtime, "emit", 4);
  jsi::PropNameID listenerCountProp = jsi::PropNameID::forAscii(runtime, "listenerCount", 13);
  jsi::PropNameID removeSubscriptionProp = jsi::PropNameID::forAscii(runtime, "removeSubscription", 18);

  prototype.setProperty(runtime, addListenerProp, jsi::Function::createFromHostFunction(runtime, addListenerProp, 2, addListenerHost));
  prototype.setProperty(runtime, removeListenerProp, jsi::Function::createFromHostFunction(runtime, removeListenerProp, 2, removeListenerHost));
  prototype.setProperty(runtime, removeAllListenersProp, jsi::Function::createFromHostFunction(runtime, removeAllListenersProp, 1, removeAllListenersHost));
  prototype.setProperty(runtime, emitProp, jsi::Function::createFromHostFunction(runtime, emitProp, 2, emit));
  prototype.setProperty(runtime, listenerCountProp, jsi::Function::createFromHostFunction(runtime, listenerCountProp, 1, listenerCountHost));
  prototype.setProperty(runtime, removeSubscriptionProp, jsi::Function::createFromHostFunction(runtime, removeSubscriptionProp, 1, removeSubscriptionHost));

  common::getCoreObject(runtime)
    .setProperty(runtime, "EventEmitter", eventEmitterClass);
}

} // namespace expo::EventEmitter
