#include <jsi/JSIDynamic.h>
#include <jsi/jsi.h>
#include <react/debug/react_native_assert.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/core/RawProps.h>
#include <react/renderer/scheduler/Scheduler.h>
#include <react/renderer/uimanager/UIManagerBinding.h>
#include <react/renderer/uimanager/primitives.h>
#include <reanimated/CSS/configs/CSSTransitionConfig.h>
#include <reanimated/CSS/easing/EasingFunctions.h>
#include <reanimated/Compat/ReactNativeVersionCompat.h>
#include <reanimated/Compat/WorkletsApi.h>
#include <reanimated/Events/UIEventHandler.h>
#include <reanimated/Fabric/updates/PropsLayoutFilter.h>
#include <reanimated/Fabric/updates/SynchronousPropNames.h>
#include <reanimated/LayoutAnimations/LayoutAnimationsProxy.h>
#include <reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h>
#include <reanimated/NativeModules/PropValueProcessor.h>
#include <reanimated/NativeModules/ReanimatedModuleProxy.h>
#include <reanimated/NativeModules/SynchronousPropsBufferSerializer.h>
#include <reanimated/RuntimeDecorators/UIRuntimeDecorator.h>
#include <reanimated/Tools/FeatureFlags.h>
#include <reanimated/Tools/ReaJSIUtils.h>
#include <reanimated/Tools/ReanimatedSystraceSection.h>

#ifdef __ANDROID__
#include <fbjni/fbjni.h>
#endif // __ANDROID__

#include <algorithm>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

namespace reanimated {

using namespace worklets;

static inline std::shared_ptr<const ShadowNode> shadowNodeFromValue(
    jsi::Runtime &rt,
    const jsi::Value &shadowNodeWrapper) {
  return Bridging<std::shared_ptr<const ShadowNode>>::fromJs(rt, shadowNodeWrapper);
}

namespace {

void mergeAnimatedProps(AnimatedProps &target, AnimatedProps &&source) {
  if (source.rawProps) {
    if (target.rawProps) {
      auto mergedRawProps = target.rawProps->toDynamic();
      mergedRawProps.merge_patch(source.rawProps->toDynamic());
      target.rawProps = std::make_unique<RawProps>(std::move(mergedRawProps));
    } else {
      target.rawProps = std::move(source.rawProps);
    }
  }

  for (auto &prop : source.props) {
    target.props.push_back(std::move(prop));
  }
}

#ifdef ANDROID
constexpr bool shouldUseSynchronousUpdatesInPerformOperations() {
  return StaticFeatureFlags::getFlag("ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS");
}
#elif __APPLE__
constexpr bool shouldUseSynchronousUpdatesInPerformOperations() {
  return StaticFeatureFlags::getFlag("IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS");
}
#else
constexpr bool shouldUseSynchronousUpdatesInPerformOperations() {
  return false;
}
#endif

std::pair<UpdatesBatch, UpdatesBatch> partitionUpdates(UpdatesBatch &&updatesBatch, const bool allowPartialUpdates) {
  const auto isSynchronous = [&](const std::string &keyStr, [[maybe_unused]] const folly::dynamic &value) {
    if (!isSynchronousPropName(keyStr)) {
      return false;
    }
#ifdef ANDROID
    // The Android synchronous path serializes color props into an int buffer via `value.asInt()`,
    // so non-numeric color values (e.g. PlatformColor) must fall back to the shadow tree commit path.
    const bool isColorProp = keyStr.find("Color") != std::string::npos;
    if (isColorProp && !value.isNumber()) {
      return false;
    }
#endif // ANDROID
    return true;
  };

  UpdatesBatch synchronousUpdatesBatch;
  UpdatesBatch shadowTreeUpdatesBatch;

  if (allowPartialUpdates) {
    for (const auto &[shadowNodeFamily, props] : updatesBatch) {
      folly::dynamic synchronousProps = folly::dynamic::object();
      folly::dynamic shadowTreeProps = folly::dynamic::object();

      for (const auto &[key, value] : props.items()) {
        const auto keyStr = key.asString();
        if (isSynchronous(keyStr, value)) {
          synchronousProps[keyStr] = value;
        } else {
          shadowTreeProps[keyStr] = value;
        }
      }

      if (!synchronousProps.empty()) {
        synchronousUpdatesBatch.emplace_back(shadowNodeFamily, std::move(synchronousProps));
      }

      if (!shadowTreeProps.empty()) {
        shadowTreeUpdatesBatch.emplace_back(shadowNodeFamily, std::move(shadowTreeProps));
      }
    }

    return {std::move(synchronousUpdatesBatch), std::move(shadowTreeUpdatesBatch)};
  }

  std::unordered_set<const ShadowNodeFamily *> familiesRequiringCommit;
  for (const auto &[shadowNodeFamily, props] : updatesBatch) {
    const bool hasOnlySynchronousProps = std::all_of(props.items().begin(), props.items().end(), [&](const auto &kv) {
      return isSynchronous(kv.first.asString(), kv.second);
    });
    if (!hasOnlySynchronousProps) {
      familiesRequiringCommit.insert(shadowNodeFamily.get());
    }
  }

  for (auto &[shadowNodeFamily, props] : updatesBatch) {
    auto &targetBatch =
        familiesRequiringCommit.contains(shadowNodeFamily.get()) ? shadowTreeUpdatesBatch : synchronousUpdatesBatch;
    targetBatch.emplace_back(shadowNodeFamily, std::move(props));
  }

  return {std::move(synchronousUpdatesBatch), std::move(shadowTreeUpdatesBatch)};
}

} // namespace

ReanimatedModuleProxy::ReanimatedModuleProxy(
    const std::shared_ptr<WorkletRuntime> &uiRuntime,
    const std::shared_ptr<UIScheduler> &uiScheduler,
    jsi::Runtime &rnRuntime,
    const std::shared_ptr<CallInvoker> &jsCallInvoker,
    const PlatformDepMethodsHolder &platformDepMethodsHolder,
    const bool isReducedMotion)
    : isReducedMotion_(isReducedMotion),
      uiRuntime_(uiRuntime),
      uiScheduler_(uiScheduler),
      jsInvoker_(jsCallInvoker),
      eventHandlerRegistry_(std::make_unique<UIEventHandlerRegistry>()),
      requestRender_(platformDepMethodsHolder.requestRender),
      animatedSensorModule_(platformDepMethodsHolder),
      layoutAnimationsManager_(std::make_shared<LayoutAnimationsManager>()),
      getAnimationTimestamp_(platformDepMethodsHolder.getAnimationTimestamp),
#ifdef __APPLE__
      forceScreenSnapshot_(platformDepMethodsHolder.forceScreenSnapshotFunction),
#endif
      staticPropsRegistry_(std::make_shared<StaticPropsRegistry>()),
      updatesRegistryManager_(std::make_shared<UpdatesRegistryManager>(staticPropsRegistry_)),
      operationsLoop_(std::make_shared<OperationsLoop>(
          uiScheduler,
          platformDepMethodsHolder.requestRender,
          platformDepMethodsHolder.getAnimationTimestamp,
          updatesRegistryManager_)),
      animatedPropsRegistry_(std::make_shared<AnimatedPropsRegistry>()),
      viewStylesRepository_(std::make_shared<ViewStylesRepository>(staticPropsRegistry_, animatedPropsRegistry_)),
      cssEventsEmitter_(std::make_shared<CSSEventsEmitter>(jsCallInvoker)),
      cssAnimationKeyframesRegistry_(std::make_shared<CSSKeyframesRegistry>()),
      cssAnimationsRegistry_(std::make_shared<CSSAnimationsRegistry>(
          operationsLoop_,
          cssAnimationKeyframesRegistry_,
          platformDepMethodsHolder.platformAnimationFactory,
          cssEventsEmitter_)),
      cssTransitionsRegistry_(std::make_shared<CSSTransitionsRegistry>(
          viewStylesRepository_,
          operationsLoop_,
          std::make_shared<CSSPlatformTransitionProxy>(platformDepMethodsHolder.platformTransitionBackend),
          cssEventsEmitter_)),
      pseudoStylesRegistry_(std::make_shared<PseudoStylesRegistry>(
          platformDepMethodsHolder.attachPseudoSelector,
          platformDepMethodsHolder.detachPseudoSelector,
          cssTransitionsRegistry_,
          updatesRegistryManager_)),
      synchronouslyUpdateUIPropsFunction_(platformDepMethodsHolder.synchronouslyUpdateUIPropsFunction),
#ifdef ANDROID
      filterUnmountedTagsFunction_(platformDepMethodsHolder.filterUnmountedTagsFunction),
#endif // ANDROID
      subscribeForKeyboardEventsFunction_(platformDepMethodsHolder.subscribeForKeyboardEvents),
      unsubscribeFromKeyboardEventsFunction_(platformDepMethodsHolder.unsubscribeFromKeyboardEvents) {
  // Add registries in order of their priority (from the lowest to the
  // highest). CSS transitions should be overriden by animated style
  // animations; animated style animations should be overriden by CSS
  // animations.
  updatesRegistryManager_->addRegistry(cssTransitionsRegistry_);
  updatesRegistryManager_->addRegistry(animatedPropsRegistry_);
  updatesRegistryManager_->addRegistry(cssAnimationsRegistry_);

#ifdef ANDROID
  // Pre-allocate the synchronous props buffers so the first frame doesn't pay
  // for vector growth allocations.
  if constexpr (StaticFeatureFlags::getFlag("ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS")) {
    synchronousPropsIntBuffer_.reserve(1024);
    synchronousPropsDoubleBuffer_.reserve(1024);
  }
#endif // ANDROID
}

void ReanimatedModuleProxy::init(const PlatformDepMethodsHolder &platformDepMethodsHolder) {
  if constexpr (StaticFeatureFlags::getFlag("USE_ANIMATION_BACKEND")) {
    // Backend path: callbacks are drained later inside runGrandCallback under
    // the manager lock; no wrap needed here.
    requestRender_ = [weakThis = weak_from_this()](std::function<void(const double)> callback) {
      auto strongThis = weakThis.lock();
      if (!strongThis) {
        return;
      }
      strongThis->pendingFrameCallbacks_.push_back(std::move(callback));
      strongThis->startBackendIfNeeded();
    };
  } else {
    // Non-backend path: wrap the platform RAF callback so the subsystem work
    // runs under the manager lock.
    requestRender_ = [weakThis = weak_from_this(), platformRequestRender = platformDepMethodsHolder.requestRender](
                         std::function<void(const double)> callback) {
      platformRequestRender([weakThis, callback = std::move(callback)](const double timestamp) {
        if (auto strongThis = weakThis.lock()) {
          auto lock = strongThis->updatesRegistryManager_->lock();
          callback(timestamp);
        }
      });
    };
  }

  auto updateProps = [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &operations) {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return;
    }

    const auto timestamp = strongThis->getAnimationTimestamp_();
    auto lock = strongThis->updatesRegistryManager_->lock();
    strongThis->animatedPropsRegistry_->update(rt, operations, timestamp);
  };

  auto measure = [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &shadowNodeValue) -> jsi::Value {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return jsi::Value::undefined();
    }
    return strongThis->measure(rt, shadowNodeValue);
  };

  auto dispatchCommand = [weakThis = weak_from_this()](
                             jsi::Runtime &rt,
                             const jsi::Value &shadowNodeValue,
                             const jsi::Value &commandNameValue,
                             const jsi::Value &argsValue) {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return;
    }

    strongThis->dispatchCommand(rt, shadowNodeValue, commandNameValue, argsValue);
  };

  ProgressLayoutAnimationFunction progressLayoutAnimation =
      [weakThis = weak_from_this()](jsi::Runtime &rt, int tag, const jsi::Object &newStyle) {
        // Always on UI thread.
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        auto surfaceId = strongThis->layoutAnimationsProxyRegistry_->progressLayoutAnimation(tag, newStyle);
        if (!surfaceId) {
          return;
        }
        // The flush set is drained by `executeLayoutAnimationsRequests`, driven
        // by `runGrandCallback` (backend) or a JS `requestAnimationFrameFinalizer` (non-backend).
        strongThis->layoutAnimationFlushRequests_.insert(*surfaceId);
      };

  EndLayoutAnimationFunction endLayoutAnimation = [weakThis = weak_from_this()](int tag, bool shouldRemove) {
    // Always on UI thread.
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return;
    }

    auto surfaceId = strongThis->layoutAnimationsProxyRegistry_->endLayoutAnimation(tag, shouldRemove);
    if (!surfaceId) {
      return;
    }
    strongThis->layoutAnimationFlushRequests_.insert(*surfaceId);
  };

  auto obtainProp = [weakThis = weak_from_this()](
                        jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper, const jsi::Value &propName) {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return jsi::String::createFromUtf8(rt, "");
    }

    return strongThis->obtainProp(rt, shadowNodeWrapper, propName);
  };

  std::optional<worklets::RequestAnimationFrameHostFunction> requestAnimationFrame;
  if constexpr (StaticFeatureFlags::getFlag("USE_ANIMATION_BACKEND")) {
    requestAnimationFrame = [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &callback) {
      auto strongThis = weakThis.lock();
      if (!strongThis) {
        return;
      }

      auto callbackFunction = std::make_shared<jsi::Function>(callback.asObject(rt).asFunction(rt));
      strongThis->pendingAnimationFrameCallbackFromWorklets_ =
          [callbackFunction = std::move(callbackFunction),
           weakRuntime = getWeakRuntimeFromJSIRuntime(rt)](double timestamp) {
            auto runtime = weakRuntime.lock();
            if (!runtime) {
              return;
            }

            runSyncOnRuntime(runtime, *callbackFunction, jsi::Value(timestamp));
          };
      strongThis->startBackendIfNeeded();
    };
  }

  jsi::Runtime &uiRuntime = getJSIRuntimeFromWorkletRuntime(uiRuntime_);
  UIRuntimeDecorator::decorate(
      uiRuntime,
      obtainProp,
      updateProps,
      measure,
      dispatchCommand,
      platformDepMethodsHolder.getAnimationTimestamp,
      platformDepMethodsHolder.setGestureStateFunction,
      progressLayoutAnimation,
      endLayoutAnimation,
      platformDepMethodsHolder.maybeFlushUIUpdatesQueueFunction,
      requestAnimationFrame);
}

ReanimatedModuleProxy::~ReanimatedModuleProxy() {
  // event handler registry and frame callbacks store some JSI values from UI
  // runtime, so they have to go away before we tear down the runtime
  eventHandlerRegistry_.reset();
  // The emitter holds a function from the RN runtime and is still reachable
  // from a frame in flight, so stop it before that runtime goes away.
  cssEventsEmitter_->invalidate();
}

jsi::Value ReanimatedModuleProxy::registerEventHandler(
    jsi::Runtime &rt,
    const jsi::Value &worklet,
    const jsi::Value &eventName,
    const jsi::Value &emitterReactTag) {
  static uint64_t NEXT_EVENT_HANDLER_ID = 1;

  uint64_t newRegistrationId = NEXT_EVENT_HANDLER_ID++;
  auto eventNameStr = eventName.asString(rt).utf8(rt);
  auto handlerSerializable = extractSerializable(
      rt, worklet, "[Reanimated] Event handler must be a serializable worklet.", Serializable::ValueType::WorkletType);
  int emitterReactTagInt = emitterReactTag.asNumber();

  scheduleOnUI(uiScheduler_, [=, weakThis = weak_from_this()]() {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return;
    }
    auto handler =
        std::make_shared<UIEventHandler>(newRegistrationId, eventNameStr, emitterReactTagInt, handlerSerializable);
    strongThis->eventHandlerRegistry_->registerEventHandler(handler);
  });

  return jsi::Value(static_cast<double>(newRegistrationId));
}

void ReanimatedModuleProxy::unregisterEventHandler(jsi::Runtime &, const jsi::Value &registrationId) {
  uint64_t id = registrationId.asNumber();
  scheduleOnUI(uiScheduler_, [=, weakThis = weak_from_this()]() {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return;
    }
    strongThis->eventHandlerRegistry_->unregisterEventHandler(id);
  });
}

std::string ReanimatedModuleProxy::obtainPropFromShadowNode(
    jsi::Runtime &rt,
    const std::string &propName,
    const std::shared_ptr<const ShadowNode> &shadowNode) {
  auto newestCloneOfShadowNode = uiManager_->getNewestCloneOfShadowNode(*shadowNode);

  return PropValueProcessor::processPropValue(propName, newestCloneOfShadowNode, rt);
}

jsi::Value ReanimatedModuleProxy::getViewProp(
    jsi::Runtime &rnRuntime,
    const jsi::Value &shadowNodeWrapper,
    const jsi::Value &propName,
    const jsi::Value &callback) {
  const auto propNameStr = propName.asString(rnRuntime).utf8(rnRuntime);
  const auto funPtr = std::make_shared<jsi::Function>(callback.getObject(rnRuntime).asFunction(rnRuntime));
  const auto shadowNode = shadowNodeFromValue(rnRuntime, shadowNodeWrapper);
  scheduleOnUI(uiScheduler_, [=, weakThis = weak_from_this()]() {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return;
    }
    jsi::Runtime &uiRuntime = getJSIRuntimeFromWorkletRuntime(strongThis->uiRuntime_);
    std::string resultStr;
    try {
      resultStr = strongThis->obtainPropFromShadowNode(uiRuntime, propNameStr, shadowNode);
    } catch (const std::exception &error) {
      resultStr = std::string("error: ") + error.what();
    }

    strongThis->jsInvoker_->invokeAsync([=](jsi::Runtime &rnRuntime) {
      const auto resultValue = jsi::String::createFromUtf8(rnRuntime, resultStr);
      funPtr->call(rnRuntime, resultValue);
    });
  });
  return jsi::Value::undefined();
}

jsi::Value ReanimatedModuleProxy::getStaticFeatureFlag(jsi::Runtime &rt, const jsi::Value &name) {
  return reanimated::StaticFeatureFlags::getFlag(name.asString(rt).utf8(rt));
}

jsi::Value
ReanimatedModuleProxy::setDynamicFeatureFlag(jsi::Runtime &rt, const jsi::Value &name, const jsi::Value &value) {
  reanimated::DynamicFeatureFlags::setFlag(name.asString(rt).utf8(rt), value.asBool());
  return jsi::Value::undefined();
}

jsi::Value ReanimatedModuleProxy::configureLayoutAnimationBatch(
    jsi::Runtime &rt,
    const jsi::Value &layoutAnimationsBatch) {
  auto array = layoutAnimationsBatch.asObject(rt).asArray(rt);
  size_t length = array.size(rt);
  std::vector<LayoutAnimationConfig> batch(length);
  for (int i = 0; i < length; i++) {
    auto item = array.getValueAtIndex(rt, i).asObject(rt);
    auto &batchItem = batch[i];
    batchItem.tag = item.getProperty(rt, "viewTag").asNumber();
    batchItem.type = static_cast<LayoutAnimationType>(item.getProperty(rt, "type").asNumber());
    auto config = item.getProperty(rt, "config");
    if (config.isUndefined()) {
      batchItem.config = nullptr;
    } else {
      batchItem.config = extractSerializable(
          rt, config, "[Reanimated] Layout animation config must be an object.", Serializable::ValueType::ObjectType);
    }
    auto sharedTag = item.getProperty(rt, "sharedTransitionTag");
    if (!sharedTag.isUndefined()) {
      batchItem.sharedTransitionTag = sharedTag.asString(rt).utf8(rt);
    }
  }
  layoutAnimationsManager_->configureAnimationBatch(batch);
  return jsi::Value::undefined();
}

void ReanimatedModuleProxy::setShouldAnimateExiting(
    jsi::Runtime &rt,
    const jsi::Value &viewTag,
    const jsi::Value &shouldAnimate) {
  layoutAnimationsManager_->setShouldAnimateExiting(viewTag.asNumber(), shouldAnimate.getBool());
}

bool ReanimatedModuleProxy::isAnyHandlerWaitingForEvent(const std::string &eventName, const int emitterReactTag) {
  return eventHandlerRegistry_->isAnyHandlerWaitingForEvent(eventName, emitterReactTag);
}

jsi::Value ReanimatedModuleProxy::registerSensor(
    jsi::Runtime &rt,
    const jsi::Value &sensorType,
    const jsi::Value &interval,
    const jsi::Value &iosReferenceFrame,
    const jsi::Value &sensorDataHandler) {
  return animatedSensorModule_.registerSensor(
      rt, uiRuntime_, sensorType, interval, iosReferenceFrame, sensorDataHandler);
}

void ReanimatedModuleProxy::unregisterSensor(jsi::Runtime &, const jsi::Value &sensorId) {
  animatedSensorModule_.unregisterSensor(sensorId);
}

void ReanimatedModuleProxy::cleanupSensors() {
  animatedSensorModule_.unregisterAllSensors();
}

void ReanimatedModuleProxy::setViewStyle(jsi::Runtime &rt, const jsi::Value &viewTag, const jsi::Value &viewStyle) {
  auto lock = updatesRegistryManager_->lock();
  staticPropsRegistry_->set(rt, viewTag.asNumber(), viewStyle);
}

void ReanimatedModuleProxy::markNodeAsRemovable(jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper) {
  auto shadowNode = shadowNodeFromValue(rt, shadowNodeWrapper);
  auto lock = updatesRegistryManager_->lock();
  updatesRegistryManager_->markNodeAsRemovable(shadowNode);
}

void ReanimatedModuleProxy::unmarkNodeAsRemovable(jsi::Runtime &rt, const jsi::Value &viewTag) {
  auto lock = updatesRegistryManager_->lock();
  updatesRegistryManager_->unmarkNodeAsRemovable(viewTag.asNumber());
}

void ReanimatedModuleProxy::registerCSSKeyframes(
    jsi::Runtime &rt,
    const jsi::Value &animationName,
    const jsi::Value &compoundComponentName,
    const jsi::Value &keyframesConfig) {
  const auto compoundComponentNameStr = compoundComponentName.asString(rt).utf8(rt);
  auto parsedConfig =
      parseCSSAnimationKeyframesConfig(rt, keyframesConfig, compoundComponentNameStr, viewStylesRepository_);

  auto lock = updatesRegistryManager_->lock();
  cssAnimationKeyframesRegistry_->set(
      animationName.asString(rt).utf8(rt), compoundComponentNameStr, std::move(parsedConfig));
}

void ReanimatedModuleProxy::unregisterCSSKeyframes(
    jsi::Runtime &rt,
    const jsi::Value &animationName,
    const jsi::Value &compoundComponentName) {
  auto lock = updatesRegistryManager_->lock();
  cssAnimationKeyframesRegistry_->remove(
      animationName.asString(rt).utf8(rt), compoundComponentName.asString(rt).utf8(rt));
}

void ReanimatedModuleProxy::applyCSSAnimations(
    jsi::Runtime &rt,
    const jsi::Value &shadowNodeWrapper,
    const jsi::Value &compoundComponentName,
    const jsi::Value &animationUpdates) {
  auto shadowNode = shadowNodeFromValue(rt, shadowNodeWrapper);
  const auto compoundComponentNameStr = compoundComponentName.asString(rt).utf8(rt);
  const auto updates = parseCSSAnimationUpdates(rt, animationUpdates);

  auto lock = updatesRegistryManager_->lock();
  cssAnimationsRegistry_->apply(shadowNode, compoundComponentNameStr, updates);
}

void ReanimatedModuleProxy::unregisterCSSAnimations(const jsi::Value &viewTag) {
  auto lock = updatesRegistryManager_->lock();
  cssAnimationsRegistry_->remove(viewTag.asNumber());
}

void ReanimatedModuleProxy::setCSSEventHandler(jsi::Runtime &rt, const jsi::Value &handler) {
  cssEventsEmitter_->setEmitFunction(std::make_shared<jsi::Function>(handler.asObject(rt).asFunction(rt)));
}

void ReanimatedModuleProxy::runCSSTransition(
    jsi::Runtime &rt,
    const jsi::Value &shadowNodeWrapper,
    const jsi::Value &transitionConfig,
    const jsi::Value &eventMask) {
  auto shadowNode = shadowNodeFromValue(rt, shadowNodeWrapper);
  auto config = parseCSSTransitionConfig(rt, shadowNode->getComponentName(), transitionConfig);

  auto lock = updatesRegistryManager_->lock();
  // Subscribed before the run so the first `transitionrun` already has a listener.
  cssTransitionsRegistry_->setEventMask(shadowNode, static_cast<CSSEventMask>(eventMask.asNumber()));
  cssTransitionsRegistry_->updateConfigOrRun(rt, shadowNode, std::move(config));
}

void ReanimatedModuleProxy::unregisterCSSTransition(jsi::Runtime &rt, const jsi::Value &viewTag) {
  auto lock = updatesRegistryManager_->lock();
  cssTransitionsRegistry_->remove(viewTag.asNumber());
}

void ReanimatedModuleProxy::registerPseudoStyles(
    jsi::Runtime &rt,
    const jsi::Value &shadowNodeWrapper,
    const jsi::Value &config) {
  const auto shadowNode = shadowNodeFromValue(rt, shadowNodeWrapper);
  const auto tag = shadowNode->getTag();
  const auto configObj = config.asObject(rt);

  const auto defaultStyle = jsi::dynamicFromValue(rt, configObj.getProperty(rt, "defaultStyle"));
  const auto selectorsArray = configObj.getProperty(rt, "selectors").asObject(rt).asArray(rt);
  const auto selectorsCount = selectorsArray.size(rt);

  std::vector<PseudoStylesRegistry::SelectorRegistration> selectors;
  selectors.reserve(selectorsCount);

  auto lock = updatesRegistryManager_->lock();

  for (size_t i = 0; i < selectorsCount; ++i) {
    const auto entryObj = selectorsArray.getValueAtIndex(rt, i).asObject(rt);
    const auto selectorEnum = pseudoSelectorFromString(stringFromValue(rt, entryObj.getProperty(rt, "selector")));
    if (!selectorEnum) {
      continue;
    }

    auto transitionConfig =
        css::parseCSSTransitionConfig(rt, shadowNode->getComponentName(), entryObj.getProperty(rt, "transition"));
    // TODO - clean up pseudo selectors registration not to have to clear the changedProperties object
    transitionConfig.changedProperties.clear();
    cssTransitionsRegistry_->updateConfigOrRun(rt, shadowNode, std::move(transitionConfig));

    selectors.push_back({*selectorEnum, jsi::dynamicFromValue(rt, entryObj.getProperty(rt, "selectorStyle"))});
  }

  pseudoStylesRegistry_->registerPseudoStyles(tag, shadowNode, defaultStyle, selectors);
}

void ReanimatedModuleProxy::unregisterPseudoStyles(jsi::Runtime &, const jsi::Value &viewTag) {
  auto lock = updatesRegistryManager_->lock();
  pseudoStylesRegistry_->remove(viewTag.asNumber());
}

jsi::Value ReanimatedModuleProxy::getSettledUpdates(jsi::Runtime &rt) {
  react_native_assert(
      StaticFeatureFlags::getFlag("FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS") &&
      "getSettledUpdates requires FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS static feature flag to be enabled");

  constexpr double SETTLED_ANIMATION_THRESHOLD_MS = 1000;

  // TODO(future): use unified timestamp
  const auto currentTimestamp = getAnimationTimestamp_();

  // TODO(future): flush updates from CSS animations and CSS transitions registries
  auto lock = updatesRegistryManager_->lock();
  return animatedPropsRegistry_->collectSettledUpdates(rt, currentTimestamp - SETTLED_ANIMATION_THRESHOLD_MS);
}

bool ReanimatedModuleProxy::handleEvent(
    const std::string &eventName,
    const int emitterReactTag,
    const jsi::Value &payload,
    double currentTime) {
  ReanimatedSystraceSection s("ReanimatedModuleProxy::handleEvent");

  eventHandlerRegistry_->processEvent(uiRuntime_, currentTime, eventName, emitterReactTag, payload);

  // TODO: return true if Reanimated successfully handled the event
  // to avoid sending it to JavaScript
  return false;
}

bool ReanimatedModuleProxy::handleRawEvent(const RawEvent &rawEvent, double currentTime) {
  ReanimatedSystraceSection s("ReanimatedModuleProxy::handleRawEvent");

  const EventTarget *eventTarget = rawEvent.eventTarget.get();
  if (eventTarget == nullptr) {
    // after app reload scrollView is unmounted and its content offset is set
    // to 0 and view is thrown into recycle pool setting content offset
    // triggers scroll event eventTarget is null though, because it's
    // unmounting we can just ignore this event, because it's an event on
    // unmounted component
    return false;
  }

#if REACT_NATIVE_VERSION_MINOR >= 87
  const auto tag = eventTarget->getTag();
#else
  // A stale event dispatched during unmount may carry an EventTarget with a null
  // InstanceHandle which getTag() would dereference (see #9925).
  // Fixed in React Native 0.87 by https://github.com/facebook/react-native/pull/56763.
  const auto shadowNodeFamily = rawEvent.shadowNodeFamily.lock();
  if (shadowNodeFamily == nullptr) {
    return false;
  }
  const auto tag = shadowNodeFamily->getTag();
#endif

  auto eventType = rawEvent.type;
  if (eventType.rfind("top", 0) == 0) {
    eventType = "on" + eventType.substr(3);
  }

  if constexpr (StaticFeatureFlags::getFlag("ENABLE_SHARED_ELEMENT_TRANSITIONS")) {
    if (eventType == "onTransitionProgress") {
      jsi::Runtime &uiRuntime = getJSIRuntimeFromWorkletRuntime(uiRuntime_);
      const auto &eventPayload = rawEvent.eventPayload;
      jsi::Object payload = eventPayload->asJSIValue(uiRuntime).asObject(uiRuntime);
      auto progress = payload.getProperty(uiRuntime, "progress").asNumber();
      auto closing = static_cast<bool>(payload.getProperty(uiRuntime, "closing").asNumber());
      auto goingForward = static_cast<bool>(payload.getProperty(uiRuntime, "goingForward").asNumber());

      if (!layoutAnimationsProxyRegistry_) {
        return false;
      }
      auto surfaceId = layoutAnimationsProxyRegistry_->onTransitionProgress(tag, progress, closing, goingForward);
      if (!surfaceId) {
        return false;
      }
      uiManager_->getShadowTreeRegistry().visit(
          *surfaceId, [](const ShadowTree &shadowTree) { shadowTree.notifyDelegatesOfUpdates(); });
      return false;
    } else if (eventType == "onGestureCancel") {
      if (!layoutAnimationsProxyRegistry_) {
        return false;
      }
      auto surfaceId = layoutAnimationsProxyRegistry_->onGestureCancel(tag);
      if (!surfaceId) {
        return false;
      }
      uiManager_->getShadowTreeRegistry().visit(
          *surfaceId, [](const ShadowTree &shadowTree) { shadowTree.notifyDelegatesOfUpdates(); });
      return false;
    }
  }

  if (!isAnyHandlerWaitingForEvent(eventType, tag)) {
    return false;
  }

  jsi::Runtime &uiRuntime = getJSIRuntimeFromWorkletRuntime(uiRuntime_);
  const auto &eventPayload = rawEvent.eventPayload;
  jsi::Value payload = eventPayload->asJSIValue(uiRuntime);

  if constexpr (StaticFeatureFlags::getFlag("USE_ANIMATION_BACKEND")) {
    return handleEventAndFlush(eventType, tag, payload, GrandCallbackSource::Event);
  }

  auto res = handleEvent(eventType, tag, payload, currentTime);
  // TODO: we should call performOperations conditionally if event is handled
  // (res == true), but for now handleEvent always returns false. Thankfully,
  // performOperations does not trigger a lot of code if there is nothing to
  // be done so this is fine for now.
  performOperations();
  return res;
}

void ReanimatedModuleProxy::flushLayoutAnimationOperations() {
  if (layoutAnimationsProxyRegistry_) {
    layoutAnimationsProxyRegistry_->flushLayoutAnimationOperations();
  }
}

void ReanimatedModuleProxy::executeLayoutAnimationsRequests() {
  std::set<SurfaceId> flushRequestsCopy = std::move(layoutAnimationFlushRequests_);
  for (const auto surfaceId : flushRequestsCopy) {
    uiManager_->getShadowTreeRegistry().visit(
        surfaceId, [](const ShadowTree &shadowTree) { shadowTree.notifyDelegatesOfUpdates(); });
  }
}

void ReanimatedModuleProxy::performOperations() {
  if constexpr (StaticFeatureFlags::getFlag("USE_ANIMATION_BACKEND")) {
    react_native_assert(
        false && "[Reanimated] performOperations must not be called when USE_ANIMATION_BACKEND is enabled");
    return;
  }

  ReanimatedSystraceSection s("ReanimatedModuleProxy::performOperations");

  flushLayoutAnimationOperations();
  executeLayoutAnimationsRequests();

  UpdatesBatch synchronousUpdatesBatch;
  std::unordered_map<SurfaceId, PropsMap> propsMapBySurface;
  bool flushRegistry = false;
  bool hasCommitUpdates = false;
  {
    ReanimatedSystraceSection s2("ReanimatedModuleProxy::flushUpdates");

    auto lock = updatesRegistryManager_->lock();

    UpdatesBatch commitUpdatesBatch;
    if (cssTransitionsRegistry_->needsFlush()) {
      // Update CSS transitions and flush updates
      cssTransitionsRegistry_->flushUpdates(commitUpdatesBatch);
    }

    // Flush all animated props updates
    animatedPropsRegistry_->flushUpdates(commitUpdatesBatch);

    if (cssAnimationsRegistry_->needsFlush()) {
      // Update CSS animations and flush updates
      cssAnimationsRegistry_->flushUpdates(commitUpdatesBatch);
    }

    if constexpr (shouldUseSynchronousUpdatesInPerformOperations()) {
      auto [synchronousBatch, shadowTreeBatch] = partitionUpdates(std::move(commitUpdatesBatch), false);
      synchronousUpdatesBatch = std::move(synchronousBatch);
      commitUpdatesBatch = std::move(shadowTreeBatch);
    }

#ifdef ANDROID
    updatesRegistryManager_->collectPropsToRevertBySurface(propsMapBySurface);
#endif
    flushRegistry = shouldFlushRegistry_.exchange(false);
    hasCommitUpdates = !commitUpdatesBatch.empty();
    if (flushRegistry) {
      for (auto &[family, props] : updatesRegistryManager_->collectProps()) {
        auto &propsVector = propsMapBySurface[family->getSurfaceId()][family];
        for (auto &prop : props) {
          propsVector.emplace_back(std::move(prop));
        }
      }
    } else if constexpr (shouldUseSynchronousUpdatesInPerformOperations()) {
      // The synchronous path does not update the shadow tree, so a commit also carries the registry values.
      std::unordered_map<ShadowNodeFamily::Shared, folly::dynamic> propsByFamily;
      for (auto &[family, props] : commitUpdatesBatch) {
        auto [it, inserted] = propsByFamily.try_emplace(family, std::move(props));
        if (!inserted) {
          it->second.update(props);
        }
      }
      for (auto &[family, props] : propsByFamily) {
        updatesRegistryManager_->mergeRegistryProps(family->getTag(), props);
        propsMapBySurface[family->getSurfaceId()][family].emplace_back(std::move(props));
      }
#ifdef ANDROID
      for (auto &[_, propsMap] : propsMapBySurface) {
        for (auto &[family, propsVector] : propsMap) {
          if (propsByFamily.contains(family)) {
            continue;
          }
          folly::dynamic registryProps = folly::dynamic::object;
          updatesRegistryManager_->mergeRegistryProps(family->getTag(), registryProps);
          if (!registryProps.empty()) {
            propsVector.emplace_back(std::move(registryProps));
          }
        }
      }
#endif
    } else {
      for (auto &[family, props] : commitUpdatesBatch) {
        propsMapBySurface[family->getSurfaceId()][family].emplace_back(std::move(props));
      }
    }
  }

  if constexpr (shouldUseSynchronousUpdatesInPerformOperations()) {
    applySynchronousUpdates(synchronousUpdatesBatch);
  }

  if (updatesRegistryManager_->shouldReanimatedSkipCommit()) {
    // It may happen that `performOperations` is called on the UI thread
    // while React Native tries to commit a new tree on the JS thread.
    // In this case, we should skip the commit here and let React Native do
    // it. The commit will include the current values from the updates manager
    // which will be applied in ReanimatedCommitHook.
    if (flushRegistry) {
      shouldFlushRegistry_ = true;
    }
    if (hasCommitUpdates) {
      updatesRegistryManager_->pleaseCommitAfterPause();
    }
    return;
  }

  commitUpdates(propsMapBySurface);
}

void ReanimatedModuleProxy::performNonLayoutOperations() {
  ReanimatedSystraceSection s("ReanimatedModuleProxy::performNonLayoutOperations");

  UpdatesBatch updatesBatch;
  {
    auto lock = updatesRegistryManager_->lock();
    updatesBatch = animatedPropsRegistry_->getPendingUpdates();
  }
  applySynchronousUpdates(partitionUpdates(std::move(updatesBatch), true).first);
}

AnimationMutations ReanimatedModuleProxy::collectNonLayoutAnimationUpdates() {
  ReanimatedSystraceSection s("ReanimatedModuleProxy::collectNonLayoutAnimationUpdates");

  AnimationMutations mutations;
  jsi::Runtime &uiRuntime = getJSIRuntimeFromWorkletRuntime(uiRuntime_);
  {
    auto lock = updatesRegistryManager_->lock();
    animatedPropsRegistry_->flushNonLayoutUpdates(uiRuntime, mutations);
  }
  return mutations;
}

std::shared_ptr<UIManagerAnimationBackend> ReanimatedModuleProxy::getAnimationBackend() {
  react_native_assert(
      uiManager_ != nullptr && "[Reanimated] Animation Backend used before the uiManager was registered");
  auto locked = uiManager_->unstable_getAnimationBackend().lock();
  // UIManager owns the backend, so this is just a sanity check.
  react_native_assert(
      locked != nullptr &&
      "[Reanimated] Animation Backend is null (UIManager not wired to a backend yet or already torn down)");
  return locked;
}

void ReanimatedModuleProxy::startBackendIfNeeded() {
  if constexpr (StaticFeatureFlags::getFlag("USE_ANIMATION_BACKEND")) {
    if (isAnimationRunning_) {
      return;
    }
    animationBackendCallbackId_ =
        getAnimationBackend()->start([weakThis = weak_from_this()](AnimationTimestamp timestamp) {
          auto strongThis = weakThis.lock();
          if (!strongThis) {
            return AnimationMutations{};
          }
          return strongThis->runGrandCallback(timestamp, GrandCallbackSource::AnimationLoop);
        });
    isAnimationRunning_ = true;
  }
}

void ReanimatedModuleProxy::stopBackendIfIdle(const bool producedMutations) {
  if constexpr (StaticFeatureFlags::getFlag("USE_ANIMATION_BACKEND")) {
    const bool hasWork = producedMutations || !pendingFrameCallbacks_.empty() ||
        pendingAnimationFrameCallbackFromWorklets_ != nullptr || operationsLoop_->hasOngoingOperations() ||
        animatedPropsRegistry_->hasPendingAnimatedPropsUpdates() || shouldFlushRegistry_ ||
        !layoutAnimationFlushRequests_.empty();
    if (!hasWork) {
      getAnimationBackend()->stop(animationBackendCallbackId_);
      isAnimationRunning_ = false;
    }
  }
}

AnimationMutations ReanimatedModuleProxy::mutationsFromAnimatedPropsBatch(
    UpdatesBatchAnimatedProps &&animatedPropsBatch) {
  // This is a temporary fix, in reanimated we can sometimes produce multiple updates for the same view
  // In that case Animation Backend will only apply the last one.
  // Until the fix is implemented there, we will keep the merging logic here.
  AnimationMutations mutations;
  mutations.batch.reserve(animatedPropsBatch.size());
  std::unordered_map<Tag, size_t> mutationIndexByTag;
  for (auto &[shadowNodeFamily, animatedProps, hasLayoutUpdates] : animatedPropsBatch) {
    const auto tag = shadowNodeFamily->getTag();

    const auto it = mutationIndexByTag.find(tag);
    if (it == mutationIndexByTag.end()) {
      mutationIndexByTag.emplace(tag, mutations.batch.size());
      mutations.batch.push_back(AnimationMutation{tag, shadowNodeFamily, std::move(animatedProps), hasLayoutUpdates});
    } else {
      auto &mutation = mutations.batch[it->second];
      mutation.hasLayoutUpdates |= hasLayoutUpdates;
      mergeAnimatedProps(mutation.props, std::move(animatedProps));
    }
  }
  return mutations;
}

AnimationMutations ReanimatedModuleProxy::runGrandCallback(
    const AnimationTimestamp timestamp,
    const GrandCallbackSource source) {
  ReanimatedSystraceSection s("ReanimatedModuleProxy::runGrandCallback");

  switch (source) {
    case GrandCallbackSource::AnimationLoop: {
      // Worklet callbacks and layout animation flushes run outside the manager
      // lock: they touch only UI-thread state and may re-enter the proxy (via
      // requestAnimationFrame or the commit hook).
      executeWorkletsForFrame(timestamp);
      flushLayoutAnimationOperations();
      executeLayoutAnimationsRequests();

      AnimationMutations mutations;
      {
        auto lock = updatesRegistryManager_->lock();
        executeOperationsLoop(timestamp);
        mutations = executeOperationsAndCollectUpdates(timestamp);
        stopBackendIfIdle(!mutations.batch.empty());
      }
      return mutations;
    }

    case GrandCallbackSource::Event: {
      flushLayoutAnimationOperations();
      executeLayoutAnimationsRequests();
      return collectEventUpdates();
    }

    case GrandCallbackSource::EventInAndroidDraw:
      return collectNonLayoutAnimationUpdates();
  }

  return AnimationMutations{};
}

void ReanimatedModuleProxy::executeOperationsLoop(const AnimationTimestamp timestamp) {
  ReanimatedSystraceSection s("ReanimatedModuleProxy::executeOperationsLoop");

  auto pendingFrameCallbacks = std::exchange(pendingFrameCallbacks_, {});
  for (auto &cb : pendingFrameCallbacks) {
    cb(timestamp.count());
  }
}

void ReanimatedModuleProxy::executeWorkletsForFrame(const AnimationTimestamp timestamp) {
  if (!pendingAnimationFrameCallbackFromWorklets_) {
    return;
  }
  auto cb = std::exchange(pendingAnimationFrameCallbackFromWorklets_, nullptr);
  cb(timestamp.count());
}

AnimationMutations ReanimatedModuleProxy::executeOperationsAndCollectUpdates(const AnimationTimestamp timestamp) {
  ReanimatedSystraceSection s("ReanimatedModuleProxy::executeOperationsAndCollectUpdates");

  UpdatesBatchAnimatedProps batch;

  (void)timestamp;
  cssTransitionsRegistry_->flushUpdates(batch);
  animatedPropsRegistry_->flushUpdates(batch);
  cssAnimationsRegistry_->flushUpdates(batch);

  return mutationsFromAnimatedPropsBatch(std::move(batch));
}

AnimationMutations ReanimatedModuleProxy::collectEventUpdates() {
  ReanimatedSystraceSection s("ReanimatedModuleProxy::collectEventUpdates");

  UpdatesBatchAnimatedProps batch;
  auto lock = updatesRegistryManager_->lock();
  animatedPropsRegistry_->flushUpdates(batch);

  return mutationsFromAnimatedPropsBatch(std::move(batch));
}

bool ReanimatedModuleProxy::handleEventAndFlush(
    const std::string &eventName,
    const int emitterReactTag,
    const jsi::Value &payload,
    const GrandCallbackSource source) {
  bool handled = false;
  getAnimationBackend()->pushAnimationMutations([&, source](AnimationTimestamp timestamp) {
    handled = handleEvent(eventName, emitterReactTag, payload, timestamp.count());
    return runGrandCallback(timestamp, source);
  });
  return handled;
}

void ReanimatedModuleProxy::applySynchronousUpdates(const UpdatesBatch &synchronousUpdatesBatch) {
  if (layoutAnimationsProxyRegistry_ && !synchronousUpdatesBatch.empty()) {
    layoutAnimationsProxyRegistry_->applySynchronousProps(
        synchronousUpdatesBatch, DynamicFeatureFlags::getFlag("TRACK_SYNCHRONOUS_PROPS_IN_LAYOUT_ANIMATIONS"));
  }

#ifdef ANDROID
  if (!synchronousUpdatesBatch.empty()) {
    serializeSynchronousPropsToBuffers(
        synchronousUpdatesBatch, synchronousPropsIntBuffer_, synchronousPropsDoubleBuffer_);
    synchronouslyUpdateUIPropsFunction_(synchronousPropsIntBuffer_, synchronousPropsDoubleBuffer_);
  }
#endif // ANDROID

#if __APPLE__
  for (const auto &[shadowNodeFamily, props] : synchronousUpdatesBatch) {
    synchronouslyUpdateUIPropsFunction_(shadowNodeFamily->getTag(), props);
  }
#endif // __APPLE__
}

void ReanimatedModuleProxy::requestFlushRegistry() {
  if constexpr (StaticFeatureFlags::getFlag("USE_ANIMATION_BACKEND")) {
    shouldFlushRegistry_ = true;
    startBackendIfNeeded();
  } else {
    requestRender_([weakThis = weak_from_this()](const double) {
      if (auto strongThis = weakThis.lock()) {
        strongThis->shouldFlushRegistry_ = true;
      }
    });
  }
}

void ReanimatedModuleProxy::commitUpdates(const std::unordered_map<SurfaceId, PropsMap> &propsMapBySurface) {
  ReanimatedSystraceSection s("ReanimatedModuleProxy::commitUpdates");
  react_native_assert(uiManager_ != nullptr);
  const auto &shadowTreeRegistry = uiManager_->getShadowTreeRegistry();

  // No registry lock is held here - shadowTree.commit re-enters via ReanimatedCommitHook.
  for (auto const &[surfaceId, propsMap] : propsMapBySurface) {
    shadowTreeRegistry.visit(surfaceId, [&](ShadowTree const &shadowTree) {
      const auto status = shadowTree.commit(
          [&](RootShadowNode const &oldRootShadowNode) -> RootShadowNode::Unshared {
            if (updatesRegistryManager_->shouldReanimatedSkipCommit()) {
              return nullptr;
            }

            auto rootNode = cloneShadowTreeWithNewProps(oldRootShadowNode, propsMap);

            // Mark the commit as Reanimated commit so that we can distinguish
            // it in ReanimatedCommitHook.

            auto reaShadowNode = std::reinterpret_pointer_cast<ReanimatedCommitShadowNode>(rootNode);
            reaShadowNode->setReanimatedCommitTrait();

            return rootNode;
          },
          {/* .enableStateReconciliation = */
           false,
           /* .mountSynchronously = */ true});

#ifdef ANDROID
      if (status == ShadowTree::CommitStatus::Succeeded) {
        auto lock = updatesRegistryManager_->lock();
        updatesRegistryManager_->clearPropsToRevert(surfaceId);
      }
#else
      (void)status;
#endif
    });
  }
}

void ReanimatedModuleProxy::dispatchCommand(
    jsi::Runtime &rt,
    const jsi::Value &shadowNodeValue,
    const jsi::Value &commandNameValue,
    const jsi::Value &argsValue) {
  const auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue);
  std::string commandName = stringFromValue(rt, commandNameValue);
  folly::dynamic args = commandArgsFromValue(rt, argsValue);
  const auto &scheduler = static_cast<Scheduler *>(uiManager_->getDelegate());

  if (!scheduler) {
    return;
  }

  const auto &schedulerDelegate = scheduler->getDelegate();

  if (schedulerDelegate) {
    const auto shadowView = ShadowView(*shadowNode);
    schedulerDelegate->schedulerDidDispatchCommand(shadowView, commandName, args);
  }
}

jsi::String
ReanimatedModuleProxy::obtainProp(jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper, const jsi::Value &propName) {
  jsi::Runtime &uiRuntime = getJSIRuntimeFromWorkletRuntime(uiRuntime_);
  const auto propNameStr = propName.asString(rt).utf8(rt);
  const auto shadowNode = shadowNodeFromValue(rt, shadowNodeWrapper);
  const auto resultStr = obtainPropFromShadowNode(uiRuntime, propNameStr, shadowNode);
  return jsi::String::createFromUtf8(rt, resultStr);
}

jsi::Value ReanimatedModuleProxy::measure(jsi::Runtime &rt, const jsi::Value &shadowNodeValue) {
  // based on implementation from UIManagerBinding.cpp

  auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue);
  auto layoutMetrics = uiManager_->getRelativeLayoutMetrics(*shadowNode, nullptr, {/* .includeTransform = */ true});

  if (layoutMetrics == EmptyLayoutMetrics) {
    // Originally, in this case React Native returns `{0, 0, 0, 0, 0, 0}`,
    // most likely due to the type of measure callback function which accepts
    // just an array of numbers (not null). In Reanimated, `measure` returns
    // `MeasuredDimensions | null`.
    return jsi::Value::null();
  }
  auto newestCloneOfShadowNode = uiManager_->getNewestCloneOfShadowNode(*shadowNode);

  auto layoutableShadowNode = dynamic_cast<LayoutableShadowNode const *>(newestCloneOfShadowNode.get());
  facebook::react::Point originRelativeToParent = layoutableShadowNode != nullptr
      ? layoutableShadowNode->getLayoutMetrics().frame.origin
      : facebook::react::Point();

  auto frame = layoutMetrics.frame;

  jsi::Object result(rt);
  result.setProperty(rt, "x", jsi::Value(static_cast<double>(originRelativeToParent.x)));
  result.setProperty(rt, "y", jsi::Value(static_cast<double>(originRelativeToParent.y)));
  result.setProperty(rt, "width", jsi::Value(static_cast<double>(frame.size.width)));
  result.setProperty(rt, "height", jsi::Value(static_cast<double>(frame.size.height)));
  result.setProperty(rt, "pageX", jsi::Value(static_cast<double>(frame.origin.x)));
  result.setProperty(rt, "pageY", jsi::Value(static_cast<double>(frame.origin.y)));
  return result;
}

void ReanimatedModuleProxy::initializeFabric(const std::shared_ptr<UIManager> &uiManager) {
  uiManager_ = uiManager;
  viewStylesRepository_->setUIManager(uiManager_);

  if constexpr (StaticFeatureFlags::getFlag("USE_ANIMATION_BACKEND")) {
    if (!ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
      react_native_assert(
          false &&
          "[Reanimated] USE_ANIMATION_BACKEND flag is enabled, "
          "but ReactNativeFeatureFlags::useSharedAnimatedBackend is disabled. "
          "Enable the Experimental Release level in React Native, "
          "or disable the Reanimated Feature Flag");
    }
  }

  initializeLayoutAnimationsProxyRegistry();

  const std::function<void()> request = [weakThis = weak_from_this()]() {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return;
    }

    strongThis->requestFlushRegistry();
  };

  // TODO: with the animation backend we still need a way to handleNodeRemovals,
  // for now we leave this to leak the memory, a fix will come in a follow-up
  mountHook_ = std::make_shared<ReanimatedMountHook>(
      uiManager_, updatesRegistryManager_, viewStylesRepository_, layoutAnimationsProxyRegistry_, request);

  commitHook_ = std::make_shared<ReanimatedCommitHook>(
      uiManager_, updatesRegistryManager_, viewStylesRepository_, layoutAnimationsProxyRegistry_);
}

void ReanimatedModuleProxy::initializeLayoutAnimationsProxyRegistry() {
  auto scheduler = reinterpret_cast<Scheduler *>(uiManager_->getDelegate());
  auto componentDescriptorRegistry =
      scheduler->getContextContainer()
          ->at<std::weak_ptr<const ComponentDescriptorRegistry>>("ComponentDescriptorRegistry_DO_NOT_USE_PRETTY_PLEASE")
          .lock();
  // The Scheduler owns the registry and outlives this module, so the weak_ptr
  // always locks here. Everything downstream (the commit hook included)
  // relies on the proxy registry being non-null.
  react_native_assert(componentDescriptorRegistry && "ComponentDescriptorRegistry must be alive during initialization");

  const auto requestLayoutAnimationFlush = [weakThis = weak_from_this()](const SurfaceId surfaceId) {
    const auto strongThis = weakThis.lock();
    if (!strongThis) {
      return;
    }
    scheduleOnUI(strongThis->uiScheduler_, [weakThis, surfaceId] {
      if (const auto strongThis = weakThis.lock()) {
        strongThis->requestRender_([weakThis, surfaceId](const double) {
          if (const auto strongThis = weakThis.lock()) {
            strongThis->layoutAnimationFlushRequests_.insert(surfaceId);
          }
        });
      }
    });
  };

  const LayoutAnimationsProxyDependencies dependencies{
      layoutAnimationsManager_,
      componentDescriptorRegistry,
      scheduler->getContextContainer(),
      getJSIRuntimeFromWorkletRuntime(uiRuntime_),
      uiScheduler_,
      uiManager_,
      requestLayoutAnimationFlush,
#ifdef ANDROID
      filterUnmountedTagsFunction_,
      jsInvoker_,
#endif
#ifdef __APPLE__
      forceScreenSnapshot_,
#endif
  };

  if constexpr (!StaticFeatureFlags::getFlag("USE_LEGACY_LAYOUT_ANIMATIONS_PROXY")) {
    layoutAnimationsProxyRegistry_ = createLayoutAnimationsProxyDefaultRegistry(dependencies);
  } else {
    layoutAnimationsProxyRegistry_ = createLayoutAnimationsProxyLegacyRegistry(dependencies);
  }
}

#ifdef IS_REANIMATED_EXAMPLE_APP

std::string format(bool b) {
  return b ? "✅" : "❌";
}

std::function<std::string()> ReanimatedModuleProxy::createRegistriesLeakCheck() {
  return [weakThis = weak_from_this()]() {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return std::string("");
    }

    std::string result = "";

    auto lock = strongThis->updatesRegistryManager_->lock();
    result += "AnimatedPropsRegistry: " + format(strongThis->animatedPropsRegistry_->isEmpty());
    result += "\nCSSAnimationsRegistry: " + format(strongThis->cssAnimationsRegistry_->isEmpty());
    result += "\nCSSTransitionsRegistry: " + format(strongThis->cssTransitionsRegistry_->isEmpty());
    result += "\nStaticPropsRegistry: " + format(strongThis->staticPropsRegistry_->isEmpty()) + "\n";

    return result;
  };
}

#endif // IS_REANIMATED_EXAMPLE_APP

jsi::Value ReanimatedModuleProxy::subscribeForKeyboardEvents(
    jsi::Runtime &rt,
    const jsi::Value &handlerWorklet,
    const jsi::Value &isStatusBarTranslucent,
    const jsi::Value &isNavigationBarTranslucent) {
  auto serializableHandler = extractSerializable(
      rt,
      handlerWorklet,
      "[Reanimated] Keyboard event handler must be a worklet.",
      Serializable::ValueType::WorkletType);
  return subscribeForKeyboardEventsFunction_(
      [=, weakThis = weak_from_this()](int keyboardState, int height) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        runSyncOnRuntime(strongThis->uiRuntime_, serializableHandler, jsi::Value(keyboardState), jsi::Value(height));
      },
      isStatusBarTranslucent.getBool(),
      isNavigationBarTranslucent.getBool());
}

void ReanimatedModuleProxy::unsubscribeFromKeyboardEvents(jsi::Runtime &, const jsi::Value &listenerId) {
  unsubscribeFromKeyboardEventsFunction_(listenerId.asNumber());
}

void ReanimatedModuleProxy::toggleSlowAnimationsOnUIRuntime() const {
  this->jsInvoker_->invokeAsync([](jsi::Runtime &rt) {
    const auto toggleFn = rt.global().getProperty(rt, "__toggleSlowAnimationsOnUIRuntime");
    if (!(toggleFn.isObject() && toggleFn.asObject(rt).isFunction(rt))) [[unlikely]] {
      throw std::runtime_error("[Reanimated] __toggleSlowAnimationsOnUIRuntime function missing on global.");
    }

    toggleFn.asObject(rt).asFunction(rt).call(rt);
  });
}

jsi::Object ReanimatedModuleProxy::toOptimizedObject(jsi::Runtime &rt) {
  using jsi_utils::addMethod;
  using jsi_utils::at;

  auto obj = jsi::Object(rt);

  addMethod<3>(
      rt,
      obj,
      "registerEventHandler",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[3]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return jsi::Value::undefined();
        }
        return strongThis->registerEventHandler(rt, at<0>(args), at<1>(args), at<2>(args));
      });

  addMethod<1>(
      rt,
      obj,
      "unregisterEventHandler",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[1]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->unregisterEventHandler(rt, at<0>(args));
      });

  addMethod<3>(
      rt,
      obj,
      "getViewProp",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[3]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return jsi::Value::undefined();
        }
        return strongThis->getViewProp(rt, at<0>(args), at<1>(args), at<2>(args));
      });

  addMethod<4>(
      rt,
      obj,
      "registerSensor",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[4]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return jsi::Value::undefined();
        }
        return strongThis->registerSensor(rt, at<0>(args), at<1>(args), at<2>(args), at<3>(args));
      });

  addMethod<1>(
      rt,
      obj,
      "unregisterSensor",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[1]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->unregisterSensor(rt, at<0>(args));
      });

  addMethod<1>(
      rt,
      obj,
      "getStaticFeatureFlag",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[1]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return jsi::Value::undefined();
        }
        return strongThis->getStaticFeatureFlag(rt, at<0>(args));
      });

  addMethod<2>(
      rt,
      obj,
      "setDynamicFeatureFlag",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[2]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return jsi::Value::undefined();
        }
        return strongThis->setDynamicFeatureFlag(rt, at<0>(args), at<1>(args));
      });

  addMethod<3>(
      rt,
      obj,
      "subscribeForKeyboardEvents",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[3]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return jsi::Value::undefined();
        }
        return strongThis->subscribeForKeyboardEvents(rt, at<0>(args), at<1>(args), at<2>(args));
      });

  addMethod<1>(
      rt,
      obj,
      "unsubscribeFromKeyboardEvents",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[1]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->unsubscribeFromKeyboardEvents(rt, at<0>(args));
      });

  addMethod<1>(
      rt,
      obj,
      "configureLayoutAnimationBatch",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[1]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return jsi::Value::undefined();
        }
        return strongThis->configureLayoutAnimationBatch(rt, at<0>(args));
      });

  addMethod<2>(
      rt,
      obj,
      "setShouldAnimateExitingForTag",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[2]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->setShouldAnimateExiting(rt, at<0>(args), at<1>(args));
      });

  addMethod<2>(
      rt,
      obj,
      "setViewStyle",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[2]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->setViewStyle(rt, at<0>(args), at<1>(args));
      });

  addMethod<1>(
      rt,
      obj,
      "markNodeAsRemovable",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[1]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->markNodeAsRemovable(rt, at<0>(args));
      });

  addMethod<1>(
      rt,
      obj,
      "unmarkNodeAsRemovable",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[1]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->unmarkNodeAsRemovable(rt, at<0>(args));
      });

  addMethod<3>(
      rt,
      obj,
      "registerCSSKeyframes",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[3]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->registerCSSKeyframes(rt, at<0>(args), at<1>(args), at<2>(args));
      });

  addMethod<2>(
      rt,
      obj,
      "unregisterCSSKeyframes",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[2]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->unregisterCSSKeyframes(rt, at<0>(args), at<1>(args));
      });

  addMethod<3>(
      rt,
      obj,
      "applyCSSAnimations",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[3]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->applyCSSAnimations(rt, at<0>(args), at<1>(args), at<2>(args));
      });

  addMethod<1>(
      rt,
      obj,
      "setCSSEventHandler",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[1]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->setCSSEventHandler(rt, at<0>(args));
      });

  addMethod<1>(
      rt,
      obj,
      "unregisterCSSAnimations",
      [weakThis = weak_from_this()](jsi::Runtime &, const jsi::Value &, const jsi::Value(&args)[1]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->unregisterCSSAnimations(at<0>(args));
      });

  addMethod<3>(
      rt,
      obj,
      "runCSSTransition",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[3]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->runCSSTransition(rt, at<0>(args), at<1>(args), at<2>(args));
      });

  addMethod<1>(
      rt,
      obj,
      "unregisterCSSTransition",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[1]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->unregisterCSSTransition(rt, at<0>(args));
      });

  addMethod<0>(rt, obj, "getSettledUpdates", [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &) {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return jsi::Value::undefined();
    }
    return strongThis->getSettledUpdates(rt);
  });

  addMethod<2>(
      rt,
      obj,
      "registerPseudoStyles",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[2]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->registerPseudoStyles(rt, at<0>(args), at<1>(args));
      });

  addMethod<1>(
      rt,
      obj,
      "unregisterPseudoStyles",
      [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &, const jsi::Value(&args)[1]) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        strongThis->unregisterPseudoStyles(rt, at<0>(args));
      });

  return obj;
}

} // namespace reanimated
