#include <react/renderer/scheduler/Scheduler.h>
#include <react/renderer/uimanager/UIManagerBinding.h>
#include <reanimated/Compat/WorkletsApi.h>
#include <reanimated/Events/UIEventHandler.h>
#include <reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h>
#include <reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h>
#include <reanimated/NativeModules/PropValueProcessor.h>
#include <reanimated/NativeModules/ReanimatedModuleProxy.h>
#include <reanimated/RuntimeDecorators/UIRuntimeDecorator.h>
#include <reanimated/Tools/FeatureFlags.h>
#include <reanimated/Tools/ReanimatedSystraceSection.h>

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

#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 {

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

std::pair<UpdatesBatch, UpdatesBatch> partitionUpdates(
    const UpdatesBatch &updatesBatch,
    const std::unordered_set<std::string> &synchronousPropNames,
    const bool shouldRequireIntegerColors = false,
    const bool allowPartialViews = false) {
  UpdatesBatch synchronousUpdatesBatch;
  UpdatesBatch shadowTreeUpdatesBatch;

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

      for (const auto &[key, value] : props.items()) {
        const auto keyStr = key.asString();
        const bool isColorProp = keyStr == "color" || keyStr.find("Color") != std::string::npos;
        const bool isSynchronous =
            synchronousPropNames.contains(keyStr) && (!shouldRequireIntegerColors || !isColorProp || value.isNumber());
        if (isSynchronous) {
          synchronousProps[keyStr] = value;
        } else {
          shadowTreeProps[keyStr] = value;
        }
      }

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

      if (!shadowTreeProps.empty()) {
        shadowTreeUpdatesBatch.emplace_back(shadowNode, std::move(shadowTreeProps));
      }
    } else {
      bool hasOnlySynchronousProps = true;

      for (const auto &[key, value] : props.items()) {
        const auto keyStr = key.asString();
        const bool isColorProp = keyStr == "color" || keyStr.find("Color") != std::string::npos;
        const bool isSynchronous =
            synchronousPropNames.contains(keyStr) && (!shouldRequireIntegerColors || !isColorProp || value.isNumber());
        if (!isSynchronous) {
          hasOnlySynchronousProps = false;
          break;
        }
      }

      if (hasOnlySynchronousProps) {
        synchronousUpdatesBatch.emplace_back(shadowNode, props);
      } else {
        shadowTreeUpdatesBatch.emplace_back(shadowNode, 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)
    : ReanimatedModuleProxySpec(jsCallInvoker),
      isReducedMotion_(isReducedMotion),
      uiRuntime_(uiRuntime),
      uiScheduler_(uiScheduler),
      eventHandlerRegistry_(std::make_unique<UIEventHandlerRegistry>()),
      requestRender_(platformDepMethodsHolder.requestRender),
      animatedSensorModule_(platformDepMethodsHolder),
      layoutAnimationsManager_(std::make_shared<LayoutAnimationsManager>()),
      getAnimationTimestamp_(platformDepMethodsHolder.getAnimationTimestamp),
#ifdef __APPLE__
      forceScreenSnapshot_(platformDepMethodsHolder.forceScreenSnapshotFunction),
#endif
      animatedPropsRegistry_(std::make_shared<AnimatedPropsRegistry>()),
      staticPropsRegistry_(std::make_shared<StaticPropsRegistry>()),
      updatesRegistryManager_(std::make_shared<UpdatesRegistryManager>(staticPropsRegistry_)),
      viewStylesRepository_(std::make_shared<ViewStylesRepository>(staticPropsRegistry_, animatedPropsRegistry_)),
      cssAnimationKeyframesRegistry_(std::make_shared<CSSKeyframesRegistry>(viewStylesRepository_)),
      cssAnimationsRegistry_(std::make_shared<CSSAnimationsRegistry>()),
      cssTransitionsRegistry_(std::make_shared<CSSTransitionsRegistry>(getAnimationTimestamp_, viewStylesRepository_)),
      synchronouslyUpdateUIPropsFunction_(platformDepMethodsHolder.synchronouslyUpdateUIPropsFunction),
#ifdef ANDROID
      filterUnmountedTagsFunction_(platformDepMethodsHolder.filterUnmountedTagsFunction),
#endif // ANDROID
      subscribeForKeyboardEventsFunction_(platformDepMethodsHolder.subscribeForKeyboardEvents),
      unsubscribeFromKeyboardEventsFunction_(platformDepMethodsHolder.unsubscribeFromKeyboardEvents) {
  auto lock = updatesRegistryManager_->lock();
  // 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_);
}

void ReanimatedModuleProxy::init(const PlatformDepMethodsHolder &platformDepMethodsHolder) {
  auto onRenderCallback = [weakThis = weak_from_this()](const double timestampMs) {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return;
    }

    strongThis->renderRequested_ = false;
    strongThis->onRender(timestampMs);
  };
  onRenderCallback_ = std::move(onRenderCallback);

  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_();
    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) {
        auto strongThis = weakThis.lock();
        if (!strongThis) {
          return;
        }
        auto surfaceId = strongThis->layoutAnimationsProxy_->progressLayoutAnimation(tag, newStyle);
        if (!surfaceId) {
          return;
        }
        strongThis->layoutAnimationFlushRequests_.insert(*surfaceId);
      };

  auto requestLayoutAnimationRender = [weakThis = weak_from_this()](double) {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return;
    }
    strongThis->layoutAnimationRenderRequested_ = false;
  };

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

    auto surfaceId = strongThis->layoutAnimationsProxy_->endLayoutAnimation(tag, shouldRemove);

    if (!strongThis->layoutAnimationRenderRequested_) {
      strongThis->layoutAnimationRenderRequested_ = true;
      // if an animation has duration 0, performOperations would not get
      // called for it so we call requestRender to have it called in the
      // next frame
      strongThis->requestRender_(requestLayoutAnimationRender);
    }

    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);
  };

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

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();
}

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_);
    const auto resultStr = strongThis->obtainPropFromShadowNode(uiRuntime, propNameStr, shadowNode);

    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);
}

void ReanimatedModuleProxy::maybeRequestRender() {
  if (!renderRequested_) {
    renderRequested_ = true;
    requestRender_(onRenderCallback_);
  }
}

void ReanimatedModuleProxy::onRender(double timestampMs) {
  ReanimatedSystraceSection s("ReanimatedModuleProxy::onRender");
  // NOOP
}

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) {
  const auto tag = viewTag.asNumber();
  staticPropsRegistry_->set(rt, tag, viewStyle);
  if (staticPropsRegistry_->hasObservers(tag)) {
    maybeRunCSSLoop();
  }
}

void ReanimatedModuleProxy::markNodeAsRemovable(jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper) {
  auto lock = updatesRegistryManager_->lock();
  auto shadowNode = shadowNodeFromValue(rt, shadowNodeWrapper);
  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);
  cssAnimationKeyframesRegistry_->set(
      animationName.asString(rt).utf8(rt),
      compoundComponentNameStr,
      parseCSSAnimationKeyframesConfig(rt, keyframesConfig, compoundComponentNameStr, viewStylesRepository_));
}

void ReanimatedModuleProxy::unregisterCSSKeyframes(
    jsi::Runtime &rt,
    const jsi::Value &animationName,
    const jsi::Value &compoundComponentName) {
  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 timestamp = getCssTimestamp();
  const auto updates = parseCSSAnimationUpdates(rt, animationUpdates);

  CSSAnimationsMap newAnimations;

  if (!updates.newAnimationSettings.empty()) {
    // animationNames always exists when newAnimationSettings is not empty
    const auto animationNames = updates.animationNames.value();
    const auto animationNamesCount = animationNames.size();

    for (const auto &[index, settings] : updates.newAnimationSettings) {
      if (index >= animationNamesCount) {
        throw std::invalid_argument("[Reanimated] index is out of bounds of animationNames");
      }

      const auto &animationName = animationNames[index];
      const auto nativeComponentName = shadowNode->getComponentName();
      const auto compoundComponentNameStr = compoundComponentName.asString(rt).utf8(rt);
      const auto keyframesConfigOpt = cssAnimationKeyframesRegistry_->get(animationName, compoundComponentNameStr);

      if (!keyframesConfigOpt) {
        throw std::runtime_error(
            "[Reanimated] No keyframes with name `" + animationName + "` were registered for component `" +
            splitCompoundComponentName(compoundComponentNameStr).second + "` (" + nativeComponentName + ")");
      }

      newAnimations.emplace(
          index,
          std::make_shared<CSSAnimation>(
              rt, shadowNode, animationName, keyframesConfigOpt->get(), settings, timestamp));
    }
  }

  {
    auto lock = cssAnimationsRegistry_->lock();
    cssAnimationsRegistry_->apply(
        rt, shadowNode, updates.animationNames, newAnimations, updates.settingsUpdates, timestamp);
  }

  maybeRunCSSLoop();
}

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

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

  {
    auto lock = cssTransitionsRegistry_->lock();
    cssTransitionsRegistry_->run(rt, shadowNode, config);
  }

  maybeRunCSSLoop();
}

void ReanimatedModuleProxy::unregisterCSSTransition(jsi::Runtime &rt, const jsi::Value &viewTag) {
  auto lock = cssTransitionsRegistry_->lock();
  cssTransitionsRegistry_->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");

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

  const auto lock = animatedPropsRegistry_->lock();

  // TODO: fix bug when threshold difference is smaller than 1 second
  // TODO(future): flush updates from CSS animations and CSS transitions registries
  animatedPropsRegistry_->removeUpdatesOlderThanTimestamp(currentTimestamp - 2000); // 2 seconds

  // TODO(future): find a better way to obtain timestamp for removing updates
  // TODO(future): move removing old updates to separate method
  return animatedPropsRegistry_->getUpdatesOlderThanTimestamp(rt, currentTimestamp - 1000); // 1 second
}

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;
  }

  int tag = eventTarget->getTag();
  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 (!layoutAnimationsProxy_) {
        return false;
      }
      auto surfaceId = layoutAnimationsProxy_->onTransitionProgress(tag, progress, closing, goingForward);
      if (!surfaceId) {
        return false;
      }
      // TODO (future): enumerate -> visit
      uiManager_->getShadowTreeRegistry().enumerate(
          [](const ShadowTree &shadowTree, bool &) { shadowTree.notifyDelegatesOfUpdates(); });
      return false;
    } else if (eventType == "onGestureCancel") {
      if (!layoutAnimationsProxy_) {
        return false;
      }
      auto surfaceId = layoutAnimationsProxy_->onGestureCancel();
      if (!surfaceId) {
        return false;
      }
      // TODO (future): enumerate -> visit
      uiManager_->getShadowTreeRegistry().enumerate(
          [](const ShadowTree &shadowTree, bool &) { 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);

  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::cssLoopCallback(const double /*timestampMs*/) {
  shouldUpdateCssAnimations_ = true;
  if (cssAnimationsRegistry_->hasUpdates() || cssTransitionsRegistry_->hasUpdates()
#ifdef ANDROID
      || updatesRegistryManager_->hasPropsToRevert()
#endif // ANDROID
  ) {
    requestRender_([weakThis = weak_from_this()](const double newTimestampMs) {
      if (auto strongThis = weakThis.lock()) {
        strongThis->cssLoopCallback(newTimestampMs);
      }
    });
  } else {
    cssLoopRunning_ = false;
  }
}

void ReanimatedModuleProxy::maybeRunCSSLoop() {
  if (cssLoopRunning_) {
    return;
  }

  cssLoopRunning_ = true;

  scheduleOnUI(uiScheduler_, [=, weakThis = weak_from_this()]() {
    auto strongThis = weakThis.lock();
    if (!strongThis) {
      return;
    }
    strongThis->requestRender_([weakThis](const double timestampMs) {
      if (auto strongThis = weakThis.lock()) {
        strongThis->cssLoopCallback(timestampMs);
      }
    });
  });
}

double ReanimatedModuleProxy::getCssTimestamp() {
  if (cssLoopRunning_) {
    return currentCssTimestamp_;
  }
  currentCssTimestamp_ = getAnimationTimestamp_();
  return currentCssTimestamp_;
}

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

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

  jsi::Runtime &uiRuntime = getJSIRuntimeFromWorkletRuntime(uiRuntime_);

  UpdatesBatch updatesBatch;
  {
    ReanimatedSystraceSection s2("ReanimatedModuleProxy::flushUpdates");

    auto lock = updatesRegistryManager_->lock();

    if (shouldUpdateCssAnimations_) {
      currentCssTimestamp_ = getAnimationTimestamp_();
      auto lock = cssTransitionsRegistry_->lock();
      // Update CSS transitions and flush updates
      cssTransitionsRegistry_->update(currentCssTimestamp_);
      cssTransitionsRegistry_->flushUpdates(updatesBatch);
    }

    {
      auto lock = animatedPropsRegistry_->lock();
      // Flush all animated props updates
      animatedPropsRegistry_->flushUpdates(updatesBatch);
    }

    if (shouldUpdateCssAnimations_) {
      auto lock = cssAnimationsRegistry_->lock();
      // Update CSS animations and flush updates
      cssAnimationsRegistry_->update(currentCssTimestamp_);
      cssAnimationsRegistry_->flushUpdates(updatesBatch);
    }

    shouldUpdateCssAnimations_ = false;

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

    if ((updatesBatch.size() > 0) && updatesRegistryManager_->shouldReanimatedSkipCommit()) {
      updatesRegistryManager_->pleaseCommitAfterPause();
    }
  }

  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.
    return;
  }

  commitUpdates(uiRuntime, updatesBatch);

  // Clear the entire cache after the commit
  // (we don't know if the view is updated from outside of Reanimated
  // so we have to clear the entire cache)
  viewStylesRepository_->clearNodesCache();
}

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

  UpdatesBatch updatesBatch = animatedPropsRegistry_->getPendingUpdates();

  applySynchronousUpdates(updatesBatch, true);
}

void ReanimatedModuleProxy::applySynchronousUpdates(UpdatesBatch &updatesBatch, const bool allowPartialUpdates) {
#ifdef ANDROID
  static const std::unordered_set<std::string> synchronousProps = {
      "opacity",
      "elevation",
      "zIndex",
      // "shadowOpacity", // not supported on Android
      // "shadowRadius", // not supported on Android
      "backgroundColor",
      // "color", // TODO: fix animating color of Animated.Text,
      "tintColor",
      "borderRadius",
      "borderTopLeftRadius",
      "borderTopRightRadius",
      "borderTopStartRadius",
      "borderTopEndRadius",
      "borderBottomLeftRadius",
      "borderBottomRightRadius",
      "borderBottomStartRadius",
      "borderBottomEndRadius",
      "borderStartStartRadius",
      "borderStartEndRadius",
      "borderEndStartRadius",
      "borderEndEndRadius",
      "borderColor",
      "borderTopColor",
      "borderBottomColor",
      "borderLeftColor",
      "borderRightColor",
      "borderStartColor",
      "borderEndColor",
      "transform",
  };

  // NOTE: Keep in sync with NativeProxy.java
  static constexpr auto CMD_START_OF_VIEW = 1;
  static constexpr auto CMD_START_OF_TRANSFORM = 2;
  static constexpr auto CMD_END_OF_TRANSFORM = 3;
  static constexpr auto CMD_END_OF_VIEW = 4;

  static constexpr auto CMD_OPACITY = 10;
  static constexpr auto CMD_ELEVATION = 11;
  static constexpr auto CMD_Z_INDEX = 12;
  static constexpr auto CMD_SHADOW_OPACITY = 13;
  static constexpr auto CMD_SHADOW_RADIUS = 14;
  static constexpr auto CMD_BACKGROUND_COLOR = 15;
  static constexpr auto CMD_COLOR = 16;
  static constexpr auto CMD_TINT_COLOR = 17;

  static constexpr auto CMD_BORDER_RADIUS = 20;
  static constexpr auto CMD_BORDER_TOP_LEFT_RADIUS = 21;
  static constexpr auto CMD_BORDER_TOP_RIGHT_RADIUS = 22;
  static constexpr auto CMD_BORDER_TOP_START_RADIUS = 23;
  static constexpr auto CMD_BORDER_TOP_END_RADIUS = 24;
  static constexpr auto CMD_BORDER_BOTTOM_LEFT_RADIUS = 25;
  static constexpr auto CMD_BORDER_BOTTOM_RIGHT_RADIUS = 26;
  static constexpr auto CMD_BORDER_BOTTOM_START_RADIUS = 27;
  static constexpr auto CMD_BORDER_BOTTOM_END_RADIUS = 28;
  static constexpr auto CMD_BORDER_START_START_RADIUS = 29;
  static constexpr auto CMD_BORDER_START_END_RADIUS = 30;
  static constexpr auto CMD_BORDER_END_START_RADIUS = 31;
  static constexpr auto CMD_BORDER_END_END_RADIUS = 32;

  static constexpr auto CMD_BORDER_COLOR = 40;
  static constexpr auto CMD_BORDER_TOP_COLOR = 41;
  static constexpr auto CMD_BORDER_BOTTOM_COLOR = 42;
  static constexpr auto CMD_BORDER_LEFT_COLOR = 43;
  static constexpr auto CMD_BORDER_RIGHT_COLOR = 44;
  static constexpr auto CMD_BORDER_START_COLOR = 45;
  static constexpr auto CMD_BORDER_END_COLOR = 46;

  static constexpr auto CMD_TRANSFORM_TRANSLATE_X = 100;
  static constexpr auto CMD_TRANSFORM_TRANSLATE_Y = 101;
  static constexpr auto CMD_TRANSFORM_SCALE = 102;
  static constexpr auto CMD_TRANSFORM_SCALE_X = 103;
  static constexpr auto CMD_TRANSFORM_SCALE_Y = 104;
  static constexpr auto CMD_TRANSFORM_ROTATE = 105;
  static constexpr auto CMD_TRANSFORM_ROTATE_X = 106;
  static constexpr auto CMD_TRANSFORM_ROTATE_Y = 107;
  static constexpr auto CMD_TRANSFORM_ROTATE_Z = 108;
  static constexpr auto CMD_TRANSFORM_SKEW_X = 109;
  static constexpr auto CMD_TRANSFORM_SKEW_Y = 110;
  static constexpr auto CMD_TRANSFORM_MATRIX = 111;
  static constexpr auto CMD_TRANSFORM_PERSPECTIVE = 112;

  static constexpr auto CMD_UNIT_DEG = 200;
  static constexpr auto CMD_UNIT_RAD = 201;
  static constexpr auto CMD_UNIT_PX = 202;
  static constexpr auto CMD_UNIT_PERCENT = 203;

  const auto propNameToCommand = [](const std::string &name) {
    if (name == "opacity")
      return CMD_OPACITY;

    if (name == "elevation")
      return CMD_ELEVATION;

    if (name == "zIndex")
      return CMD_Z_INDEX;

    if (name == "shadowOpacity")
      return CMD_SHADOW_OPACITY;

    if (name == "shadowRadius")
      return CMD_SHADOW_RADIUS;

    if (name == "backgroundColor")
      return CMD_BACKGROUND_COLOR;

    if (name == "color")
      return CMD_COLOR;

    if (name == "tintColor")
      return CMD_TINT_COLOR;

    if (name == "borderRadius")
      return CMD_BORDER_RADIUS;

    if (name == "borderTopLeftRadius")
      return CMD_BORDER_TOP_LEFT_RADIUS;

    if (name == "borderTopRightRadius")
      return CMD_BORDER_TOP_RIGHT_RADIUS;

    if (name == "borderTopStartRadius")
      return CMD_BORDER_TOP_START_RADIUS;

    if (name == "borderTopEndRadius")
      return CMD_BORDER_TOP_END_RADIUS;

    if (name == "borderBottomLeftRadius")
      return CMD_BORDER_BOTTOM_LEFT_RADIUS;

    if (name == "borderBottomRightRadius")
      return CMD_BORDER_BOTTOM_RIGHT_RADIUS;

    if (name == "borderBottomStartRadius")
      return CMD_BORDER_BOTTOM_START_RADIUS;

    if (name == "borderBottomEndRadius")
      return CMD_BORDER_BOTTOM_END_RADIUS;

    if (name == "borderStartStartRadius")
      return CMD_BORDER_START_START_RADIUS;

    if (name == "borderStartEndRadius")
      return CMD_BORDER_START_END_RADIUS;

    if (name == "borderEndStartRadius")
      return CMD_BORDER_END_START_RADIUS;

    if (name == "borderEndEndRadius")
      return CMD_BORDER_END_END_RADIUS;

    if (name == "borderColor")
      return CMD_BORDER_COLOR;

    if (name == "borderTopColor")
      return CMD_BORDER_TOP_COLOR;

    if (name == "borderBottomColor")
      return CMD_BORDER_BOTTOM_COLOR;

    if (name == "borderLeftColor")
      return CMD_BORDER_LEFT_COLOR;

    if (name == "borderRightColor")
      return CMD_BORDER_RIGHT_COLOR;

    if (name == "borderStartColor")
      return CMD_BORDER_START_COLOR;

    if (name == "borderEndColor")
      return CMD_BORDER_END_COLOR;

    if (name == "transform")
      return CMD_START_OF_TRANSFORM; // TODO: use CMD_TRANSFORM?

    throw std::runtime_error("[Reanimated] Unsupported style: " + name);
  };

  const auto transformNameToCommand = [](const std::string &name) {
    if (name == "translateX")
      return CMD_TRANSFORM_TRANSLATE_X;

    if (name == "translateY")
      return CMD_TRANSFORM_TRANSLATE_Y;

    if (name == "scale")
      return CMD_TRANSFORM_SCALE;

    if (name == "scaleX")
      return CMD_TRANSFORM_SCALE_X;

    if (name == "scaleY")
      return CMD_TRANSFORM_SCALE_Y;

    if (name == "rotate")
      return CMD_TRANSFORM_ROTATE;

    if (name == "rotateX")
      return CMD_TRANSFORM_ROTATE_X;

    if (name == "rotateY")
      return CMD_TRANSFORM_ROTATE_Y;

    if (name == "rotateZ")
      return CMD_TRANSFORM_ROTATE_Z;

    if (name == "skewX")
      return CMD_TRANSFORM_SKEW_X;

    if (name == "skewY")
      return CMD_TRANSFORM_SKEW_Y;

    if (name == "matrix")
      return CMD_TRANSFORM_MATRIX;

    if (name == "perspective")
      return CMD_TRANSFORM_PERSPECTIVE;

    throw std::runtime_error("[Reanimated] Unsupported transform: " + name);
  };

  auto [synchronousUpdatesBatch, shadowTreeUpdatesBatch] =
      partitionUpdates(updatesBatch, synchronousProps, true, allowPartialUpdates);

  if (!synchronousUpdatesBatch.empty()) {
    std::vector<int> intBuffer;
    std::vector<double> doubleBuffer;
    intBuffer.reserve(1024);
    doubleBuffer.reserve(1024);

    for (const auto &[shadowNode, props] : synchronousUpdatesBatch) {
      intBuffer.push_back(CMD_START_OF_VIEW);
      intBuffer.push_back(shadowNode->getTag());
      for (const auto &[key, value] : props.items()) {
        const auto command = propNameToCommand(key.getString());
        switch (command) {
          case CMD_OPACITY:
          case CMD_ELEVATION:
          case CMD_Z_INDEX:
          case CMD_SHADOW_OPACITY:
          case CMD_SHADOW_RADIUS:
            intBuffer.push_back(command);
            doubleBuffer.push_back(value.asDouble());
            break;

          case CMD_BACKGROUND_COLOR:
          case CMD_COLOR:
          case CMD_TINT_COLOR:
          case CMD_BORDER_COLOR:
          case CMD_BORDER_TOP_COLOR:
          case CMD_BORDER_BOTTOM_COLOR:
          case CMD_BORDER_LEFT_COLOR:
          case CMD_BORDER_RIGHT_COLOR:
          case CMD_BORDER_START_COLOR:
          case CMD_BORDER_END_COLOR:
            intBuffer.push_back(command);
            intBuffer.push_back(value.asInt());
            break;

          case CMD_BORDER_RADIUS:
          case CMD_BORDER_TOP_LEFT_RADIUS:
          case CMD_BORDER_TOP_RIGHT_RADIUS:
          case CMD_BORDER_TOP_START_RADIUS:
          case CMD_BORDER_TOP_END_RADIUS:
          case CMD_BORDER_BOTTOM_LEFT_RADIUS:
          case CMD_BORDER_BOTTOM_RIGHT_RADIUS:
          case CMD_BORDER_BOTTOM_START_RADIUS:
          case CMD_BORDER_BOTTOM_END_RADIUS:
          case CMD_BORDER_START_START_RADIUS:
          case CMD_BORDER_START_END_RADIUS:
          case CMD_BORDER_END_START_RADIUS:
          case CMD_BORDER_END_END_RADIUS:
            intBuffer.push_back(command);
            if (value.isDouble()) {
              intBuffer.push_back(CMD_UNIT_PX);
              doubleBuffer.push_back(value.getDouble());
            } else if (value.isString()) {
              const auto &valueStr = value.getString();
              if (!valueStr.ends_with("%")) {
                throw std::runtime_error("[Reanimated] Border radius string must be a percentage");
              }
              intBuffer.push_back(CMD_UNIT_PERCENT);
              doubleBuffer.push_back(std::stof(valueStr.substr(0, -1)));
            } else {
              throw std::runtime_error("[Reanimated] Border radius value must be either a number or a string");
            }
            break;

          case CMD_START_OF_TRANSFORM:
            intBuffer.push_back(command);
            react_native_assert(value.isArray() && "[Reanimated] Transform value must be an array");
            for (const auto &item : value) {
              react_native_assert(item.isObject() && "[Reanimated] Transform array item must be an object");
              react_native_assert(
                  item.size() == 1 && "[Reanimated] Transform array item must have exactly one key-value pair");
              const auto transformCommand = transformNameToCommand(item.keys().begin()->getString());
              const auto &transformValue = *item.values().begin();
              switch (transformCommand) {
                case CMD_TRANSFORM_SCALE:
                case CMD_TRANSFORM_SCALE_X:
                case CMD_TRANSFORM_SCALE_Y:
                case CMD_TRANSFORM_PERSPECTIVE: {
                  intBuffer.push_back(transformCommand);
                  doubleBuffer.push_back(transformValue.asDouble());
                  break;
                }
                case CMD_TRANSFORM_TRANSLATE_X:
                case CMD_TRANSFORM_TRANSLATE_Y: {
                  intBuffer.push_back(transformCommand);
                  if (transformValue.isDouble()) {
                    intBuffer.push_back(CMD_UNIT_PX);
                    doubleBuffer.push_back(transformValue.getDouble());
                  } else if (transformValue.isString()) {
                    const auto &transformValueStr = transformValue.getString();
                    if (!transformValueStr.ends_with("%")) {
                      throw std::runtime_error("[Reanimated] String translate must be a percentage");
                    }
                    intBuffer.push_back(CMD_UNIT_PERCENT);
                    doubleBuffer.push_back(std::stof(transformValueStr.substr(0, -1)));
                  } else {
                    throw std::runtime_error("[Reanimated] Translate value must be either a number or a string");
                  }
                  break;
                }
                case CMD_TRANSFORM_ROTATE:
                case CMD_TRANSFORM_ROTATE_X:
                case CMD_TRANSFORM_ROTATE_Y:
                case CMD_TRANSFORM_ROTATE_Z:
                case CMD_TRANSFORM_SKEW_X:
                case CMD_TRANSFORM_SKEW_Y: {
                  const auto &transformValueStr = transformValue.getString();
                  intBuffer.push_back(transformCommand);
                  if (transformValueStr.ends_with("deg")) {
                    intBuffer.push_back(CMD_UNIT_DEG);
                  } else if (transformValueStr.ends_with("rad")) {
                    intBuffer.push_back(CMD_UNIT_RAD);
                  } else {
                    throw std::runtime_error("[Reanimated] Unsupported rotation unit: " + transformValueStr);
                  }
                  doubleBuffer.push_back(std::stof(transformValueStr.substr(0, -3)));
                  break;
                }
                case CMD_TRANSFORM_MATRIX: {
                  intBuffer.push_back(transformCommand);
                  react_native_assert(transformValue.isArray() && "[Reanimated] Matrix must be an array");
                  int size = transformValue.size();
                  intBuffer.push_back(size);
                  for (int i = 0; i < size; i++) {
                    doubleBuffer.push_back(transformValue[i].asDouble());
                  }
                  break;
                }
              }
            }
            intBuffer.push_back(CMD_END_OF_TRANSFORM);
            break;
        }
      }
      intBuffer.push_back(CMD_END_OF_VIEW);
    }
    synchronouslyUpdateUIPropsFunction_(intBuffer, doubleBuffer);
  }

  updatesBatch = std::move(shadowTreeUpdatesBatch);
#endif // ANDROID

#if __APPLE__
  static const std::unordered_set<std::string> synchronousProps = {
      "opacity",
      "elevation",
      "zIndex",
      "shadowOpacity",
      "shadowRadius",
      "backgroundColor",
      // "color", // TODO: fix animating color of Animated.Text
      "tintColor",
      "borderRadius",
      "borderTopLeftRadius",
      "borderTopRightRadius",
      "borderTopStartRadius",
      "borderTopEndRadius",
      "borderBottomLeftRadius",
      "borderBottomRightRadius",
      "borderBottomStartRadius",
      "borderBottomEndRadius",
      "borderStartStartRadius",
      "borderStartEndRadius",
      "borderEndStartRadius",
      "borderEndEndRadius",
      "borderColor",
      "borderTopColor",
      "borderBottomColor",
      "borderLeftColor",
      "borderRightColor",
      "borderStartColor",
      "borderEndColor",
      "transform",
  };

  auto [synchronousUpdatesBatch, shadowTreeUpdatesBatch] =
      partitionUpdates(updatesBatch, synchronousProps, false, allowPartialUpdates);

  for (const auto &[shadowNode, props] : synchronousUpdatesBatch) {
    synchronouslyUpdateUIPropsFunction_(shadowNode->getTag(), props);
  }

  updatesBatch = std::move(shadowTreeUpdatesBatch);
#endif // __APPLE__
}

void ReanimatedModuleProxy::requestFlushRegistry() {
  requestRender_([weakThis = weak_from_this()](const double timestamp) {
    if (auto strongThis = weakThis.lock()) {
      strongThis->shouldFlushRegistry_ = true;
    }
  });
}

void ReanimatedModuleProxy::commitUpdates(jsi::Runtime &rt, const UpdatesBatch &updatesBatch) {
  ReanimatedSystraceSection s("ReanimatedModuleProxy::commitUpdates");
  react_native_assert(uiManager_ != nullptr);
  const auto &shadowTreeRegistry = uiManager_->getShadowTreeRegistry();

  std::unordered_map<SurfaceId, PropsMap> propsMapBySurface;

#ifdef ANDROID
  updatesRegistryManager_->collectPropsToRevertBySurface(propsMapBySurface);
#endif

  if (shouldFlushRegistry_) {
    shouldFlushRegistry_ = false;
    const auto propsMap = updatesRegistryManager_->collectProps();
    for (auto const &[family, props] : propsMap) {
      const auto surfaceId = family->getSurfaceId();
      auto &propsVector = propsMapBySurface[surfaceId][family];
      for (const auto &prop : props) {
        propsVector.emplace_back(prop);
      }
    }
  } else {
    for (auto const &[shadowNode, props] : updatesBatch) {
      SurfaceId surfaceId = shadowNode->getSurfaceId();
      auto family = &shadowNode->getFamily();
      propsMapBySurface[surfaceId][family].emplace_back(props);
    }
  }

  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) {
        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_);

  initializeLayoutAnimationsProxy();

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

    strongThis->requestFlushRegistry();
  };
  mountHook_ = std::make_shared<ReanimatedMountHook>(uiManager_, updatesRegistryManager_, request);
  commitHook_ = std::make_shared<ReanimatedCommitHook>(uiManager_, updatesRegistryManager_, layoutAnimationsProxy_);
}

void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() {
  auto scheduler = reinterpret_cast<Scheduler *>(uiManager_->getDelegate());
  auto componentDescriptorRegistry =
      scheduler->getContextContainer()
          ->at<std::weak_ptr<const ComponentDescriptorRegistry>>("ComponentDescriptorRegistry_DO_NOT_USE_PRETTY_PLEASE")
          .lock();

  if (componentDescriptorRegistry) {
    if constexpr (StaticFeatureFlags::getFlag("ENABLE_SHARED_ELEMENT_TRANSITIONS")) {
      auto layoutAnimationsProxyExperimental = std::make_shared<LayoutAnimationsProxy_Experimental>(
          layoutAnimationsManager_,
          componentDescriptorRegistry,
          scheduler->getContextContainer(),
          getJSIRuntimeFromWorkletRuntime(uiRuntime_),
          uiScheduler_
#ifdef ANDROID
          ,
          filterUnmountedTagsFunction_,
          uiManager_,
          jsInvoker_
#endif
      );
#ifdef __APPLE__
      layoutAnimationsProxyExperimental->setForceScreenSnapshotFunction(forceScreenSnapshot_);
#endif
      layoutAnimationsProxy_ = std::move(layoutAnimationsProxyExperimental);
    } else {
      auto layoutAnimationsProxyLegacy = std::make_shared<LayoutAnimationsProxy_Legacy>(
          layoutAnimationsManager_,
          componentDescriptorRegistry,
          scheduler->getContextContainer(),
          getJSIRuntimeFromWorkletRuntime(uiRuntime_),
          uiScheduler_
#ifdef ANDROID
          ,
          filterUnmountedTagsFunction_,
          uiManager_,
          jsInvoker_
#endif
      );
      // TODO (future): support in experimental
      uiManager_->setAnimationDelegate(layoutAnimationsProxyLegacy.get());
      layoutAnimationsProxy_ = std::move(layoutAnimationsProxyLegacy);
    }
  }
}

#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 = "";

    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());
}

} // namespace reanimated
