/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#pragma once

#include <react/bridging/Function.h>
#include <functional>
#include <memory>
#include <mutex>
#include <unordered_map>

#define FRIEND_TEST(test_case_name, test_name) \
  friend class test_case_name##_##test_name##_Test

namespace facebook::react {

class EventSubscription {
 public:
  explicit EventSubscription(std::function<void()> remove)
      : remove_(std::move(remove)) {}
  ~EventSubscription() = default;
  EventSubscription(EventSubscription&&) noexcept = default;
  EventSubscription& operator=(EventSubscription&&) noexcept = default;
  EventSubscription(const EventSubscription&) = delete;
  EventSubscription& operator=(const EventSubscription&) = delete;

 private:
  friend Bridging<EventSubscription>;

  std::function<void()> remove_;
};

template <>
struct Bridging<EventSubscription> {
  static jsi::Object toJs(
      jsi::Runtime& rt,
      const EventSubscription& eventSubscription,
      const std::shared_ptr<CallInvoker>& jsInvoker) {
    auto result = jsi::Object(rt);
    result.setProperty(
        rt, "remove", bridging::toJs(rt, eventSubscription.remove_, jsInvoker));
    return result;
  }
};

class IAsyncEventEmitter {
 public:
  IAsyncEventEmitter() noexcept = default;
  virtual ~IAsyncEventEmitter() noexcept = default;
  IAsyncEventEmitter(IAsyncEventEmitter&&) noexcept = default;
  IAsyncEventEmitter& operator=(IAsyncEventEmitter&&) noexcept = default;
  IAsyncEventEmitter(const IAsyncEventEmitter&) = delete;
  IAsyncEventEmitter& operator=(const IAsyncEventEmitter&) = delete;

  virtual jsi::Object get(
      jsi::Runtime& rt,
      const std::shared_ptr<CallInvoker>& jsInvoker) const = 0;
};

template <typename... Args>
class AsyncEventEmitter : public IAsyncEventEmitter {
  static_assert(
      sizeof...(Args) <= 1,
      "AsyncEventEmitter must have at most one argument");

 public:
  AsyncEventEmitter() : state_(std::make_shared<SharedState>()) {
    listen_ = [state = state_](AsyncCallback<Args...> listener) {
      std::lock_guard<std::mutex> lock(state->mutex);
      auto listenerId = state->listenerId++;
      state->listeners.emplace(listenerId, std::move(listener));
      return EventSubscription([state, listenerId]() {
        std::lock_guard<std::mutex> innerLock(state->mutex);
        state->listeners.erase(listenerId);
      });
    };
  }
  ~AsyncEventEmitter() override = default;
  AsyncEventEmitter(AsyncEventEmitter&&) noexcept = default;
  AsyncEventEmitter& operator=(AsyncEventEmitter&&) noexcept = default;
  AsyncEventEmitter(const AsyncEventEmitter&) = delete;
  AsyncEventEmitter& operator=(const AsyncEventEmitter&) = delete;

  void emit(std::function<jsi::Value(jsi::Runtime&)>&& converter) {
    std::lock_guard<std::mutex> lock(state_->mutex);
    for (auto& [_, listener] : state_->listeners) {
      listener.call([converter](jsi::Runtime& rt, jsi::Function& jsFunction) {
        jsFunction.call(rt, converter(rt));
      });
    }
  }

  void emit(Args... value) {
    std::lock_guard<std::mutex> lock(state_->mutex);
    for (const auto& [_, listener] : state_->listeners) {
      listener.call(static_cast<Args>(value)...);
    }
  }

  jsi::Object get(
      jsi::Runtime& rt,
      const std::shared_ptr<CallInvoker>& jsInvoker) const override {
    return bridging::toJs(rt, listen_, jsInvoker);
  }

 private:
  friend Bridging<AsyncEventEmitter>;
  FRIEND_TEST(BridgingTest, eventEmitterTest);

  struct SharedState {
    std::mutex mutex;
    std::unordered_map<size_t, AsyncCallback<Args...>> listeners;
    size_t listenerId{};
  };

  std::function<EventSubscription(AsyncCallback<Args...>)> listen_;
  std::shared_ptr<SharedState> state_;
};

template <typename... Args>
struct Bridging<AsyncEventEmitter<Args...>> {
  static jsi::Object toJs(
      jsi::Runtime& rt,
      const AsyncEventEmitter<Args...>& eventEmitter,
      const std::shared_ptr<CallInvoker>& jsInvoker) {
    return eventEmitter.get(rt, jsInvoker);
  }
};

} // namespace facebook::react
