part of dart._engine;
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.




/// Requests that the browser schedule a frame.
///
/// This may be overridden in tests, for example, to pump fake frames.
ui.VoidCallback? scheduleFrameCallback;

/// Signature of functions added as a listener to high contrast changes
typedef HighContrastListener = void Function(bool enabled);
typedef _KeyDataResponseCallback = void Function(bool handled);


/// Determines if high contrast is enabled using media query 'forced-colors: active' for Windows
class HighContrastSupport {
  static HighContrastSupport instance = HighContrastSupport();
  static const String _highContrastMediaQueryString = '(forced-colors: active)';

  final List<HighContrastListener> _listeners = <HighContrastListener>[];

  /// Reference to css media query that indicates whether high contrast is on.
  final DomMediaQueryList _highContrastMediaQuery = domWindow.matchMedia(_highContrastMediaQueryString);
  late final DomEventListener _onHighContrastChangeListener =
      allowInterop(_onHighContrastChange);

  bool get isHighContrastEnabled => _highContrastMediaQuery.matches;

  /// Adds function to the list of listeners on high contrast changes
  void addListener(HighContrastListener listener) {
    if (_listeners.isEmpty) {
      _highContrastMediaQuery.addListener(_onHighContrastChangeListener);
    }
    _listeners.add(listener);
  }

  /// Removes function from the list of listeners on high contrast changes
  void removeListener(HighContrastListener listener) {
    _listeners.remove(listener);
    if (_listeners.isEmpty) {
      _highContrastMediaQuery.removeListener(_onHighContrastChangeListener);
    }
  }

  void _onHighContrastChange(DomEvent event) {
    final DomMediaQueryListEvent mqEvent = event as DomMediaQueryListEvent;
    final bool isHighContrastEnabled = mqEvent.matches!;
    for (final HighContrastListener listener in _listeners) {
      listener(isHighContrastEnabled);
    }
  }
}

/// Platform event dispatcher.
///
/// This is the central entry point for platform messages and configuration
/// events from the platform.
class EnginePlatformDispatcher extends ui.PlatformDispatcher {
  /// Private constructor, since only dart:ui is supposed to create one of
  /// these.
  EnginePlatformDispatcher() {
    _addBrightnessMediaQueryListener();
    HighContrastSupport.instance.addListener(_updateHighContrast);
    _addFontSizeObserver();
    registerHotRestartListener(dispose);
  }

  /// The [EnginePlatformDispatcher] singleton.
  static EnginePlatformDispatcher get instance => _instance;
  static final EnginePlatformDispatcher _instance = EnginePlatformDispatcher();

  /// The current platform configuration.
  @override
  ui.PlatformConfiguration configuration = ui.PlatformConfiguration(
    locales: parseBrowserLanguages(),
    textScaleFactor: findBrowserTextScaleFactor(),
    accessibilityFeatures: computeAccessibilityFeatures(),
  );

  /// Compute accessibility features based on the current value of high contrast flag
  static EngineAccessibilityFeatures computeAccessibilityFeatures() {
    final EngineAccessibilityFeaturesBuilder builder =
        EngineAccessibilityFeaturesBuilder(0);
    if (HighContrastSupport.instance.isHighContrastEnabled) {
      builder.highContrast = true;
    }
    return builder.build();
  }

  void dispose() {
    _removeBrightnessMediaQueryListener();
    _disconnectFontSizeObserver();
    HighContrastSupport.instance.removeListener(_updateHighContrast);
  }

  /// Receives all events related to platform configuration changes.
  @override
  ui.VoidCallback? get onPlatformConfigurationChanged =>
      _onPlatformConfigurationChanged;
  ui.VoidCallback? _onPlatformConfigurationChanged;
  Zone? _onPlatformConfigurationChangedZone;
  @override
  set onPlatformConfigurationChanged(ui.VoidCallback? callback) {
    _onPlatformConfigurationChanged = callback;
    _onPlatformConfigurationChangedZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnPlatformConfigurationChanged() {
    invoke(
        _onPlatformConfigurationChanged, _onPlatformConfigurationChangedZone);
  }

  /// The current list of windows,
  @override
  Iterable<ui.FlutterView> get views => _windows.values;
  Map<Object, ui.FlutterWindow> get windows => _windows;
  final Map<Object, ui.FlutterWindow> _windows = <Object, ui.FlutterWindow>{};

  /// A map of opaque platform window identifiers to window configurations.
  ///
  /// This should be considered a protected member, only to be used by
  /// [PlatformDispatcher] subclasses.
  Map<Object, ui.ViewConfiguration> get windowConfigurations => _windowConfigurations;
  final Map<Object, ui.ViewConfiguration> _windowConfigurations =
      <Object, ui.ViewConfiguration>{};

  /// A callback that is invoked whenever the platform's [devicePixelRatio],
  /// [physicalSize], [padding], [viewInsets], or [systemGestureInsets]
  /// values change, for example when the device is rotated or when the
  /// application is resized (e.g. when showing applications side-by-side
  /// on Android).
  ///
  /// The engine invokes this callback in the same zone in which the callback
  /// was set.
  ///
  /// The framework registers with this callback and updates the layout
  /// appropriately.
  ///
  /// See also:
  ///
  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
  ///    register for notifications when this is called.
  ///  * [MediaQuery.of], a simpler mechanism for the same.
  @override
  ui.VoidCallback? get onMetricsChanged => _onMetricsChanged;
  ui.VoidCallback? _onMetricsChanged;
  Zone? _onMetricsChangedZone;
  @override
  set onMetricsChanged(ui.VoidCallback? callback) {
    _onMetricsChanged = callback;
    _onMetricsChangedZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnMetricsChanged() {
    if (_onMetricsChanged != null) {
      invoke(_onMetricsChanged, _onMetricsChangedZone);
    }
  }

  /// Returns device pixel ratio returned by browser.
  static double get browserDevicePixelRatio {
    final double ratio = domWindow.devicePixelRatio;
    // Guard against WebOS returning 0.
    return (ratio == 0.0) ? 1.0 : ratio;
  }

  /// A callback invoked when any window begins a frame.
  ///
  /// A callback that is invoked to notify the application that it is an
  /// appropriate time to provide a scene using the [SceneBuilder] API and the
  /// [PlatformWindow.render] method.
  /// When possible, this is driven by the hardware VSync signal of the attached
  /// screen with the highest VSync rate. This is only called if
  /// [PlatformWindow.scheduleFrame] has been called since the last time this
  /// callback was invoked.
  @override
  ui.FrameCallback? get onBeginFrame => _onBeginFrame;
  ui.FrameCallback? _onBeginFrame;
  Zone? _onBeginFrameZone;
  @override
  set onBeginFrame(ui.FrameCallback? callback) {
    _onBeginFrame = callback;
    _onBeginFrameZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnBeginFrame(Duration duration) {
    invoke1<Duration>(_onBeginFrame, _onBeginFrameZone, duration);
  }

  /// A callback that is invoked for each frame after [onBeginFrame] has
  /// completed and after the microtask queue has been drained.
  ///
  /// This can be used to implement a second phase of frame rendering that
  /// happens after any deferred work queued by the [onBeginFrame] phase.
  @override
  ui.VoidCallback? get onDrawFrame => _onDrawFrame;
  ui.VoidCallback? _onDrawFrame;
  Zone? _onDrawFrameZone;
  @override
  set onDrawFrame(ui.VoidCallback? callback) {
    _onDrawFrame = callback;
    _onDrawFrameZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnDrawFrame() {
    invoke(_onDrawFrame, _onDrawFrameZone);
  }

  /// A callback that is invoked when pointer data is available.
  ///
  /// The framework invokes this callback in the same zone in which the
  /// callback was set.
  ///
  /// See also:
  ///
  ///  * [GestureBinding], the Flutter framework class which manages pointer
  ///    events.
  @override
  ui.PointerDataPacketCallback? get onPointerDataPacket => _onPointerDataPacket;
  ui.PointerDataPacketCallback? _onPointerDataPacket;
  Zone? _onPointerDataPacketZone;
  @override
  set onPointerDataPacket(ui.PointerDataPacketCallback? callback) {
    _onPointerDataPacket = callback;
    _onPointerDataPacketZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnPointerDataPacket(ui.PointerDataPacket dataPacket) {
    invoke1<ui.PointerDataPacket>(
        _onPointerDataPacket, _onPointerDataPacketZone, dataPacket);
  }

  /// A callback that is invoked when key data is available.
  ///
  /// The framework invokes this callback in the same zone in which the
  /// callback was set.
  ///
  /// See also:
  ///
  ///  * [GestureBinding], the Flutter framework class which manages pointer
  ///    events.
  @override
  ui.KeyDataCallback? get onKeyData => _onKeyData;
  ui.KeyDataCallback? _onKeyData;
  Zone? _onKeyDataZone;
  @override
  set onKeyData(ui.KeyDataCallback? callback) {
    _onKeyData = callback;
    _onKeyDataZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnKeyData(ui.KeyData data, _KeyDataResponseCallback callback) {
    final ui.KeyDataCallback? onKeyData = _onKeyData;
    if (onKeyData != null) {
      invoke(
        () => callback(onKeyData(data)),
        _onKeyDataZone,
      );
    } else {
      callback(false);
    }
  }

  /// A callback that is invoked to report the [FrameTiming] of recently
  /// rasterized frames.
  ///
  /// It's preferred to use [SchedulerBinding.addTimingsCallback] than to use
  /// [PlatformDispatcher.onReportTimings] directly because
  /// [SchedulerBinding.addTimingsCallback] allows multiple callbacks.
  ///
  /// This can be used to see if the application has missed frames (through
  /// [FrameTiming.buildDuration] and [FrameTiming.rasterDuration]), or high
  /// latencies (through [FrameTiming.totalSpan]).
  ///
  /// Unlike [Timeline], the timing information here is available in the release
  /// mode (additional to the profile and the debug mode). Hence this can be
  /// used to monitor the application's performance in the wild.
  ///
  /// {@macro dart.ui.TimingsCallback.list}
  ///
  /// If this is null, no additional work will be done. If this is not null,
  /// Flutter spends less than 0.1ms every 1 second to report the timings
  /// (measured on iPhone6S). The 0.1ms is about 0.6% of 16ms (frame budget for
  /// 60fps), or 0.01% CPU usage per second.
  @override
  ui.TimingsCallback? get onReportTimings => _onReportTimings;
  ui.TimingsCallback? _onReportTimings;
  Zone? _onReportTimingsZone;
  @override
  set onReportTimings(ui.TimingsCallback? callback) {
    _onReportTimings = callback;
    _onReportTimingsZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnReportTimings(List<ui.FrameTiming> timings) {
    invoke1<List<ui.FrameTiming>>(
        _onReportTimings, _onReportTimingsZone, timings);
  }

  @override
  void sendPlatformMessage(
    String name,
    ByteData? data,
    ui.PlatformMessageResponseCallback? callback,
  ) {
    _sendPlatformMessage(
        name, data, _zonedPlatformMessageResponseCallback(callback));
  }

  @override
  void sendPortPlatformMessage(
    String name,
    ByteData? data,
    int identifier,
    Object port,
  ) {
    throw Exception("Isolates aren't supported in web.");
  }

  @override
  void registerBackgroundIsolate(ui.RootIsolateToken token) {
    throw Exception("Isolates aren't supported in web.");
  }

  // TODO(ianh): Deprecate onPlatformMessage once the framework is moved over
  // to using channel buffers exclusively.
  @override
  ui.PlatformMessageCallback? get onPlatformMessage => _onPlatformMessage;
  ui.PlatformMessageCallback? _onPlatformMessage;
  Zone? _onPlatformMessageZone;
  @override
  set onPlatformMessage(ui.PlatformMessageCallback? callback) {
    _onPlatformMessage = callback;
    _onPlatformMessageZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnPlatformMessage(
    String name,
    ByteData? data,
    ui.PlatformMessageResponseCallback callback,
  ) {
    if (name == ui.ChannelBuffers.kControlChannelName) {
      // TODO(ianh): move this logic into ChannelBuffers once we remove onPlatformMessage
      try {
        ui.channelBuffers.handleMessage(data!);
      } finally {
        callback(null);
      }
    } else if (_onPlatformMessage != null) {
      invoke3<String, ByteData?, ui.PlatformMessageResponseCallback>(
        _onPlatformMessage,
        _onPlatformMessageZone,
        name,
        data,
        callback,
      );
    } else {
      ui.channelBuffers.push(name, data, callback);
    }
  }

  /// Wraps the given [callback] in another callback that ensures that the
  /// original callback is called in the zone it was registered in.
  static ui.PlatformMessageResponseCallback?
      _zonedPlatformMessageResponseCallback(
          ui.PlatformMessageResponseCallback? callback) {
    if (callback == null) {
      return null;
    }

    // Store the zone in which the callback is being registered.
    final Zone registrationZone = Zone.current;

    return (ByteData? data) {
      registrationZone.runUnaryGuarded(callback, data);
    };
  }

  PlatformViewMessageHandler? _platformViewMessageHandler;

  void _sendPlatformMessage(
    String name,
    ByteData? data,
    ui.PlatformMessageResponseCallback? callback,
  ) {
    // In widget tests we want to bypass processing of platform messages.
    if (assertionsEnabled && ui.debugEmulateFlutterTesterEnvironment) {
      return;
    }

    if (debugPrintPlatformMessages) {
      print('Sent platform message on channel: "$name"');
    }

    if (assertionsEnabled && name == 'flutter/debug-echo') {
      // Echoes back the data unchanged. Used for testing purposes.
      replyToPlatformMessage(callback, data);
      return;
    }

    switch (name) {

      /// This should be in sync with shell/common/shell.cc
      case 'flutter/skia':
        const MethodCodec codec = JSONMethodCodec();
        final MethodCall decoded = codec.decodeMethodCall(data);
        switch (decoded.method) {
          case 'Skia.setResourceCacheMaxBytes':
            if (renderer is CanvasKitRenderer) {
              assert(
                decoded.arguments is int,
                'Argument to Skia.setResourceCacheMaxBytes must be an int, but was ${decoded.arguments.runtimeType}',
              );
              final int cacheSizeInBytes = decoded.arguments as int;
              CanvasKitRenderer.instance.resourceCacheMaxBytes = cacheSizeInBytes;
            }

            // Also respond in HTML mode. Otherwise, apps would have to detect
            // CanvasKit vs HTML before invoking this method.
            replyToPlatformMessage(
                callback, codec.encodeSuccessEnvelope(<bool>[true]));
            break;
        }
        return;

      case 'flutter/assets':
        final String url = utf8.decode(data!.buffer.asUint8List());
        ui.webOnlyAssetManager.load(url).then((ByteData assetData) {
          replyToPlatformMessage(callback, assetData);
        }, onError: (dynamic error) {
          printWarning('Error while trying to load an asset: $error');
          replyToPlatformMessage(callback, null);
        });
        return;

      case 'flutter/platform':
        const MethodCodec codec = JSONMethodCodec();
        final MethodCall decoded = codec.decodeMethodCall(data);
        switch (decoded.method) {
          case 'SystemNavigator.pop':
            // TODO(gspencergoog): As multi-window support expands, the pop call
            // will need to include the window ID. Right now only one window is
            // supported.
            (_windows[0]! as EngineFlutterWindow)
                .browserHistory
                .exit()
                .then((_) {
              replyToPlatformMessage(
                  callback, codec.encodeSuccessEnvelope(true));
            });
            return;
          case 'HapticFeedback.vibrate':
            final String? type = decoded.arguments as String?;
            vibrate(_getHapticFeedbackDuration(type));
            replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
            return;
          case 'SystemChrome.setApplicationSwitcherDescription':
            final Map<String, dynamic> arguments = decoded.arguments as Map<String, dynamic>;
            // TODO(ferhat): Find more appropriate defaults? Or noop when values are null?
            final String label = arguments['label'] as String? ?? '';
            final int primaryColor = arguments['primaryColor'] as int? ?? 0xFF000000;
            domDocument.title = label;
            setThemeColor(ui.Color(primaryColor));
            replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
            return;
          case 'SystemChrome.setPreferredOrientations':
            final List<dynamic> arguments = decoded.arguments as List<dynamic>;
            flutterViewEmbedder.setPreferredOrientation(arguments).then((bool success) {
              replyToPlatformMessage(
                  callback, codec.encodeSuccessEnvelope(success));
            });
            return;
          case 'SystemSound.play':
            // There are no default system sounds on web.
            replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
            return;
          case 'Clipboard.setData':
            ClipboardMessageHandler().setDataMethodCall(decoded, callback);
            return;
          case 'Clipboard.getData':
            ClipboardMessageHandler().getDataMethodCall(callback);
            return;
        }
        break;

      // Dispatched by the bindings to delay service worker initialization.
      case 'flutter/service_worker':
        domWindow.dispatchEvent(createDomEvent('Event', 'flutter-first-frame'));
        return;

      case 'flutter/textinput':
        textEditing.channel.handleTextInput(data, callback);
        return;

      case 'flutter/mousecursor':
        const MethodCodec codec = StandardMethodCodec();
        final MethodCall decoded = codec.decodeMethodCall(data);
        final Map<dynamic, dynamic> arguments = decoded.arguments as Map<dynamic, dynamic>;
        switch (decoded.method) {
          case 'activateSystemCursor':
            MouseCursor.instance!.activateSystemCursor(arguments.tryString('kind'));
        }
        return;

      case 'flutter/web_test_e2e':
        const MethodCodec codec = JSONMethodCodec();
        replyToPlatformMessage(
            callback,
            codec.encodeSuccessEnvelope(
                _handleWebTestEnd2EndMessage(codec, data)));
        return;

      case 'flutter/platform_views':
        _platformViewMessageHandler ??= PlatformViewMessageHandler(
          contentManager: platformViewManager,
          contentHandler: (DomElement content) {
            flutterViewEmbedder.glassPaneElement!.append(content);
          },
        );
        _platformViewMessageHandler!.handlePlatformViewCall(data, callback!);
        return;

      case 'flutter/accessibility':
        // In widget tests we want to bypass processing of platform messages.
        const StandardMessageCodec codec = StandardMessageCodec();
        accessibilityAnnouncements.handleMessage(codec, data);
        replyToPlatformMessage(callback, codec.encodeMessage(true));
        return;

      case 'flutter/navigation':
        // TODO(gspencergoog): As multi-window support expands, the navigation call
        // will need to include the window ID. Right now only one window is
        // supported.
        (_windows[0]! as EngineFlutterWindow)
            .handleNavigationMessage(data)
            .then((bool handled) {
          if (handled) {
            const MethodCodec codec = JSONMethodCodec();
            replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
          } else {
            callback?.call(null);
          }
        });

        // As soon as Flutter starts taking control of the app navigation, we
        // should reset _defaultRouteName to "/" so it doesn't have any
        // further effect after this point.
        _defaultRouteName = '/';
        return;
    }

    if (pluginMessageCallHandler != null) {
      pluginMessageCallHandler!(name, data, callback);
      return;
    }

    // Passing [null] to [callback] indicates that the platform message isn't
    // implemented. Look at [MethodChannel.invokeMethod] to see how [null] is
    // handled.
    replyToPlatformMessage(callback, null);
  }

  int _getHapticFeedbackDuration(String? type) {
    const int vibrateLongPress = 50;
    const int vibrateLightImpact = 10;
    const int vibrateMediumImpact = 20;
    const int vibrateHeavyImpact = 30;
    const int vibrateSelectionClick = 10;

    switch (type) {
      case 'HapticFeedbackType.lightImpact':
        return vibrateLightImpact;
      case 'HapticFeedbackType.mediumImpact':
        return vibrateMediumImpact;
      case 'HapticFeedbackType.heavyImpact':
        return vibrateHeavyImpact;
      case 'HapticFeedbackType.selectionClick':
        return vibrateSelectionClick;
      default:
        return vibrateLongPress;
    }
  }

  /// Requests that, at the next appropriate opportunity, the [onBeginFrame]
  /// and [onDrawFrame] callbacks be invoked.
  ///
  /// See also:
  ///
  ///  * [SchedulerBinding], the Flutter framework class which manages the
  ///    scheduling of frames.
  @override
  void scheduleFrame() {
    if (scheduleFrameCallback == null) {
      throw Exception('scheduleFrameCallback must be initialized first.');
    }
    scheduleFrameCallback!();
  }

  /// Updates the application's rendering on the GPU with the newly provided
  /// [Scene]. This function must be called within the scope of the
  /// [onBeginFrame] or [onDrawFrame] callbacks being invoked. If this function
  /// is called a second time during a single [onBeginFrame]/[onDrawFrame]
  /// callback sequence or called outside the scope of those callbacks, the call
  /// will be ignored.
  ///
  /// To record graphical operations, first create a [PictureRecorder], then
  /// construct a [Canvas], passing that [PictureRecorder] to its constructor.
  /// After issuing all the graphical operations, call the
  /// [PictureRecorder.endRecording] function on the [PictureRecorder] to obtain
  /// the final [Picture] that represents the issued graphical operations.
  ///
  /// Next, create a [SceneBuilder], and add the [Picture] to it using
  /// [SceneBuilder.addPicture]. With the [SceneBuilder.build] method you can
  /// then obtain a [Scene] object, which you can display to the user via this
  /// [render] function.
  ///
  /// See also:
  ///
  ///  * [SchedulerBinding], the Flutter framework class which manages the
  ///    scheduling of frames.
  ///  * [RendererBinding], the Flutter framework class which manages layout and
  ///    painting.
  @override
  void render(ui.Scene scene, [ui.FlutterView? view]) {
    renderer.renderScene(scene);
  }

  /// Additional accessibility features that may be enabled by the platform.
  @override
  ui.AccessibilityFeatures get accessibilityFeatures =>
      configuration.accessibilityFeatures;

  /// A callback that is invoked when the value of [accessibilityFeatures] changes.
  ///
  /// The framework invokes this callback in the same zone in which the
  /// callback was set.
  @override
  ui.VoidCallback? get onAccessibilityFeaturesChanged =>
      _onAccessibilityFeaturesChanged;
  ui.VoidCallback? _onAccessibilityFeaturesChanged;
  Zone? _onAccessibilityFeaturesChangedZone;
  @override
  set onAccessibilityFeaturesChanged(ui.VoidCallback? callback) {
    _onAccessibilityFeaturesChanged = callback;
    _onAccessibilityFeaturesChangedZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnAccessibilityFeaturesChanged() {
    invoke(
        _onAccessibilityFeaturesChanged, _onAccessibilityFeaturesChangedZone);
  }

  /// Change the retained semantics data about this window.
  ///
  /// If [semanticsEnabled] is true, the user has requested that this function
  /// be called whenever the semantic content of this window changes.
  ///
  /// In either case, this function disposes the given update, which means the
  /// semantics update cannot be used further.
  @override
  @Deprecated('''
    In a multi-view world, the platform dispatcher can no longer provide apis
    to update semantics since each view will host its own semantics tree.

    Semantics updates must be passed to an individual [FlutterView]. To update
    semantics, use PlatformDispatcher.instance.views to get a [FlutterView] and
    call `updateSemantics`.
  ''')
  void updateSemantics(ui.SemanticsUpdate update) {
    EngineSemanticsOwner.instance.updateSemantics(update);
  }

  /// This is equivalent to `locales.first`, except that it will provide an
  /// undefined (using the language tag "und") non-null locale if the [locales]
  /// list has not been set or is empty.
  ///
  /// We use the first locale in the [locales] list instead of the browser's
  /// built-in `navigator.language` because browsers do not agree on the
  /// implementation.
  ///
  /// See also:
  ///
  /// * https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/languages,
  ///   which explains browser quirks in the implementation notes.
  @override
  ui.Locale get locale =>
      locales.isEmpty ? const ui.Locale.fromSubtags() : locales.first;

  /// The full system-reported supported locales of the device.
  ///
  /// This establishes the language and formatting conventions that application
  /// should, if possible, use to render their user interface.
  ///
  /// The list is ordered in order of priority, with lower-indexed locales being
  /// preferred over higher-indexed ones. The first element is the primary [locale].
  ///
  /// The [onLocaleChanged] callback is called whenever this value changes.
  ///
  /// See also:
  ///
  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
  ///    observe when this value changes.
  @override
  List<ui.Locale> get locales => configuration.locales;

  /// Performs the platform-native locale resolution.
  ///
  /// Each platform may return different results.
  ///
  /// If the platform fails to resolve a locale, then this will return null.
  ///
  /// This method returns synchronously and is a direct call to
  /// platform specific APIs without invoking method channels.
  @override
  ui.Locale? computePlatformResolvedLocale(List<ui.Locale> supportedLocales) {
    // TODO(garyq): Implement on web.
    return null;
  }

  /// A callback that is invoked whenever [locale] changes value.
  ///
  /// The framework invokes this callback in the same zone in which the
  /// callback was set.
  ///
  /// See also:
  ///
  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
  ///    observe when this callback is invoked.
  @override
  ui.VoidCallback? get onLocaleChanged => _onLocaleChanged;
  ui.VoidCallback? _onLocaleChanged;
  Zone? _onLocaleChangedZone;
  @override
  set onLocaleChanged(ui.VoidCallback? callback) {
    _onLocaleChanged = callback;
    _onLocaleChangedZone = Zone.current;
  }

  /// The locale used when we fail to get the list from the browser.
  static const ui.Locale _defaultLocale = ui.Locale('en', 'US');

  /// Sets locales to an empty list.
  ///
  /// The empty list is not a valid value for locales. This is only used for
  /// testing locale update logic.
  void debugResetLocales() {
    configuration = configuration.copyWith(locales: const <ui.Locale>[]);
  }

  // Called by FlutterViewEmbedder when browser languages change.
  void updateLocales() {
    configuration = configuration.copyWith(locales: parseBrowserLanguages());
  }

  static List<ui.Locale> parseBrowserLanguages() {
    // TODO(yjbanov): find a solution for IE
    final List<String>? languages = domWindow.navigator.languages;
    if (languages == null || languages.isEmpty) {
      // To make it easier for the app code, let's not leave the locales list
      // empty. This way there's fewer corner cases for apps to handle.
      return const <ui.Locale>[_defaultLocale];
    }

    final List<ui.Locale> locales = <ui.Locale>[];
    for (final String language in languages) {
      final List<String> parts = language.split('-');
      if (parts.length > 1) {
        locales.add(ui.Locale(parts.first, parts.last));
      } else {
        locales.add(ui.Locale(language));
      }
    }

    assert(locales.isNotEmpty);
    return locales;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnLocaleChanged() {
    invoke(_onLocaleChanged, _onLocaleChangedZone);
  }

  /// The system-reported text scale.
  ///
  /// This establishes the text scaling factor to use when rendering text,
  /// according to the user's platform preferences.
  ///
  /// The [onTextScaleFactorChanged] callback is called whenever this value
  /// changes.
  ///
  /// See also:
  ///
  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
  ///    observe when this value changes.
  @override
  double get textScaleFactor => configuration.textScaleFactor;

  /// The setting indicating whether time should always be shown in the 24-hour
  /// format.
  ///
  /// This option is used by [showTimePicker].
  @override
  bool get alwaysUse24HourFormat => configuration.alwaysUse24HourFormat;

  /// Updates [textScaleFactor] and invokes [onTextScaleFactorChanged] and
  /// [onPlatformConfigurationChanged] callbacks if [textScaleFactor] changed.
  void _updateTextScaleFactor(double value) {
    if (configuration.textScaleFactor != value) {
      configuration = configuration.copyWith(textScaleFactor: value);
      invokeOnPlatformConfigurationChanged();
      invokeOnTextScaleFactorChanged();
    }
  }

  /// Watches for font-size changes in the browser's <html> element to
  /// recalculate [textScaleFactor].
  ///
  /// Updates [textScaleFactor] with the new value.
  DomMutationObserver? _fontSizeObserver;

  /// Set the callback function for updating [textScaleFactor] based on
  /// font-size changes in the browser's <html> element.
  void _addFontSizeObserver() {
    const String styleAttribute = 'style';

    _fontSizeObserver = createDomMutationObserver(allowInterop(
        (List<dynamic> mutations, DomMutationObserver _) {
      for (final dynamic mutation in mutations) {
        final DomMutationRecord record = mutation as DomMutationRecord;
        if (record.type == 'attributes' &&
            record.attributeName == styleAttribute) {
          final double newTextScaleFactor = findBrowserTextScaleFactor();
          _updateTextScaleFactor(newTextScaleFactor);
        }
      }
    }));
    _fontSizeObserver!.observe(
      domDocument.documentElement!,
      attributes: true,
      attributeFilter: <String>[styleAttribute],
    );
  }

  /// Remove the observer for font-size changes in the browser's <html> element.
  void _disconnectFontSizeObserver() {
    _fontSizeObserver?.disconnect();
    _fontSizeObserver = null;
  }

  /// A callback that is invoked whenever [textScaleFactor] changes value.
  ///
  /// The framework invokes this callback in the same zone in which the
  /// callback was set.
  ///
  /// See also:
  ///
  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
  ///    observe when this callback is invoked.
  @override
  ui.VoidCallback? get onTextScaleFactorChanged => _onTextScaleFactorChanged;
  ui.VoidCallback? _onTextScaleFactorChanged;
  Zone? _onTextScaleFactorChangedZone;
  @override
  set onTextScaleFactorChanged(ui.VoidCallback? callback) {
    _onTextScaleFactorChanged = callback;
    _onTextScaleFactorChangedZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnTextScaleFactorChanged() {
    invoke(_onTextScaleFactorChanged, _onTextScaleFactorChangedZone);
  }

  void updateSemanticsEnabled(bool semanticsEnabled) {
    if (semanticsEnabled != this.semanticsEnabled) {
      configuration = configuration.copyWith(semanticsEnabled: semanticsEnabled);
      if (_onSemanticsEnabledChanged != null) {
        invokeOnSemanticsEnabledChanged();
      }
    }
  }

  /// The setting indicating the current brightness mode of the host platform.
  /// If the platform has no preference, [platformBrightness] defaults to [Brightness.light].
  @override
  ui.Brightness get platformBrightness => configuration.platformBrightness;

  /// Updates [_platformBrightness] and invokes [onPlatformBrightnessChanged]
  /// callback if [_platformBrightness] changed.
  void _updatePlatformBrightness(ui.Brightness value) {
    if (configuration.platformBrightness != value) {
      configuration = configuration.copyWith(platformBrightness: value);
      invokeOnPlatformConfigurationChanged();
      invokeOnPlatformBrightnessChanged();
    }
  }

  /// The setting indicating the current system font of the host platform.
  @override
  String? get systemFontFamily => configuration.systemFontFamily;

  /// Updates [_highContrast] and invokes [onHighContrastModeChanged]
  /// callback if [_highContrast] changed.
  void _updateHighContrast(bool value) {
    if (configuration.accessibilityFeatures.highContrast != value) {
      final EngineAccessibilityFeatures original =
          configuration.accessibilityFeatures as EngineAccessibilityFeatures;
      configuration = configuration.copyWith(
          accessibilityFeatures: original.copyWith(highContrast: value));
      invokeOnPlatformConfigurationChanged();
    }
  }

  /// Reference to css media query that indicates the user theme preference on the web.
  final DomMediaQueryList _brightnessMediaQuery =
      domWindow.matchMedia('(prefers-color-scheme: dark)');

  /// A callback that is invoked whenever [_brightnessMediaQuery] changes value.
  ///
  /// Updates the [_platformBrightness] with the new user preference.
  DomEventListener? _brightnessMediaQueryListener;

  /// Set the callback function for listening changes in [_brightnessMediaQuery] value.
  void _addBrightnessMediaQueryListener() {
    _updatePlatformBrightness(_brightnessMediaQuery.matches
        ? ui.Brightness.dark
        : ui.Brightness.light);

    _brightnessMediaQueryListener = allowInterop((DomEvent event) {
      final DomMediaQueryListEvent mqEvent =
          event as DomMediaQueryListEvent;
      _updatePlatformBrightness(
          mqEvent.matches! ? ui.Brightness.dark : ui.Brightness.light);
    });
    _brightnessMediaQuery.addListener(_brightnessMediaQueryListener);
  }

  /// Remove the callback function for listening changes in [_brightnessMediaQuery] value.
  void _removeBrightnessMediaQueryListener() {
    _brightnessMediaQuery.removeListener(_brightnessMediaQueryListener);
    _brightnessMediaQueryListener = null;
  }

  /// A callback that is invoked whenever [platformBrightness] changes value.
  ///
  /// The framework invokes this callback in the same zone in which the
  /// callback was set.
  ///
  /// See also:
  ///
  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
  ///    observe when this callback is invoked.
  @override
  ui.VoidCallback? get onPlatformBrightnessChanged =>
      _onPlatformBrightnessChanged;
  ui.VoidCallback? _onPlatformBrightnessChanged;
  Zone? _onPlatformBrightnessChangedZone;
  @override
  set onPlatformBrightnessChanged(ui.VoidCallback? callback) {
    _onPlatformBrightnessChanged = callback;
    _onPlatformBrightnessChangedZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnPlatformBrightnessChanged() {
    invoke(_onPlatformBrightnessChanged, _onPlatformBrightnessChangedZone);
  }

  /// A callback that is invoked whenever [systemFontFamily] changes value.
  ///
  /// The framework invokes this callback in the same zone in which the
  /// callback was set.
  ///
  /// See also:
  ///
  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
  ///    observe when this callback is invoked.
  @override
  ui.VoidCallback? get onSystemFontFamilyChanged =>
      _onSystemFontFamilyChanged;
  ui.VoidCallback? _onSystemFontFamilyChanged;
  Zone? _onSystemFontFamilyChangedZone;
  @override
  set onSystemFontFamilyChanged(ui.VoidCallback? callback) {
    _onSystemFontFamilyChanged = callback;
    _onSystemFontFamilyChangedZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnSystemFontFamilyChanged() {
    invoke(_onSystemFontFamilyChanged, _onSystemFontFamilyChangedZone);
  }

  /// Whether the user has requested that [updateSemantics] be called when
  /// the semantic contents of window changes.
  ///
  /// The [onSemanticsEnabledChanged] callback is called whenever this value
  /// changes.
  @override
  bool get semanticsEnabled => configuration.semanticsEnabled;

  /// A callback that is invoked when the value of [semanticsEnabled] changes.
  ///
  /// The framework invokes this callback in the same zone in which the
  /// callback was set.
  @override
  ui.VoidCallback? get onSemanticsEnabledChanged => _onSemanticsEnabledChanged;
  ui.VoidCallback? _onSemanticsEnabledChanged;
  Zone? _onSemanticsEnabledChangedZone;
  @override
  set onSemanticsEnabledChanged(ui.VoidCallback? callback) {
    _onSemanticsEnabledChanged = callback;
    _onSemanticsEnabledChangedZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnSemanticsEnabledChanged() {
    invoke(_onSemanticsEnabledChanged, _onSemanticsEnabledChangedZone);
  }

  /// A callback that is invoked whenever the user requests an action to be
  /// performed.
  ///
  /// This callback is used when the user expresses the action they wish to
  /// perform based on the semantics supplied by [updateSemantics].
  ///
  /// The framework invokes this callback in the same zone in which the
  /// callback was set.
  @override
  ui.SemanticsActionCallback? get onSemanticsAction => _onSemanticsAction;
  ui.SemanticsActionCallback? _onSemanticsAction;
  Zone? _onSemanticsActionZone;
  @override
  set onSemanticsAction(ui.SemanticsActionCallback? callback) {
    _onSemanticsAction = callback;
    _onSemanticsActionZone = Zone.current;
  }

  /// Engine code should use this method instead of the callback directly.
  /// Otherwise zones won't work properly.
  void invokeOnSemanticsAction(
      int id, ui.SemanticsAction action, ByteData? args) {
    invoke3<int, ui.SemanticsAction, ByteData?>(
        _onSemanticsAction, _onSemanticsActionZone, id, action, args);
  }

  // TODO(dnfield): make this work on web.
  // https://github.com/flutter/flutter/issues/100277
  ui.ErrorCallback? _onError;
  // ignore: unused_field
  late Zone _onErrorZone;
  @override
  ui.ErrorCallback? get onError => _onError;
  @override
  set onError(ui.ErrorCallback? callback) {
    _onError = callback;
    _onErrorZone = Zone.current;
  }

  /// The route or path that the embedder requested when the application was
  /// launched.
  ///
  /// This will be the string "`/`" if no particular route was requested.
  ///
  /// ## Android
  ///
  /// On Android, calling
  /// [`FlutterView.setInitialRoute`](/javadoc/io/flutter/view/FlutterView.html#setInitialRoute-java.lang.String-)
  /// will set this value. The value must be set sufficiently early, i.e. before
  /// the [runApp] call is executed in Dart, for this to have any effect on the
  /// framework. The `createFlutterView` method in your `FlutterActivity`
  /// subclass is a suitable time to set the value. The application's
  /// `AndroidManifest.xml` file must also be updated to have a suitable
  /// [`<intent-filter>`](https://developer.android.com/guide/topics/manifest/intent-filter-element.html).
  ///
  /// ## iOS
  ///
  /// On iOS, calling
  /// [`FlutterViewController.setInitialRoute`](/objcdoc/Classes/FlutterViewController.html#/c:objc%28cs%29FlutterViewController%28im%29setInitialRoute:)
  /// will set this value. The value must be set sufficiently early, i.e. before
  /// the [runApp] call is executed in Dart, for this to have any effect on the
  /// framework. The `application:didFinishLaunchingWithOptions:` method is a
  /// suitable time to set this value.
  ///
  /// See also:
  ///
  ///  * [Navigator], a widget that handles routing.
  ///  * [SystemChannels.navigation], which handles subsequent navigation
  ///    requests from the embedder.
  @override
  String get defaultRouteName {
    return _defaultRouteName ??=
        (_windows[0]! as EngineFlutterWindow).browserHistory.currentPath;
  }

  /// Lazily initialized when the `defaultRouteName` getter is invoked.
  ///
  /// The reason for the lazy initialization is to give enough time for the app
  /// to set [locationStrategy] in `lib/initialization.dart`.
  String? _defaultRouteName;

  /// In Flutter, platform messages are exchanged between threads so the
  /// messages and responses have to be exchanged asynchronously. We simulate
  /// that by adding a zero-length delay to the reply.
  void replyToPlatformMessage(
    ui.PlatformMessageResponseCallback? callback,
    ByteData? data,
  ) {
    Future<void>.delayed(Duration.zero).then((_) {
      if (callback != null) {
        callback(data);
      }
    });
  }

  @override
  ui.FrameData get frameData => const ui.FrameData.webOnly();
}

bool _handleWebTestEnd2EndMessage(MethodCodec codec, ByteData? data) {
  final MethodCall decoded = codec.decodeMethodCall(data);
  final double ratio = double.parse(decoded.arguments as String);
  switch (decoded.method) {
    case 'setDevicePixelRatio':
      window.debugOverrideDevicePixelRatio(ratio);
      EnginePlatformDispatcher.instance.onMetricsChanged!();
      return true;
  }
  return false;
}

/// Invokes [callback] inside the given [zone].
void invoke(void Function()? callback, Zone? zone) {
  if (callback == null) {
    return;
  }

  assert(zone != null);

  if (identical(zone, Zone.current)) {
    callback();
  } else {
    zone!.runGuarded(callback);
  }
}

/// Invokes [callback] inside the given [zone] passing it [arg].
void invoke1<A>(void Function(A a)? callback, Zone? zone, A arg) {
  if (callback == null) {
    return;
  }

  assert(zone != null);

  if (identical(zone, Zone.current)) {
    callback(arg);
  } else {
    zone!.runUnaryGuarded<A>(callback, arg);
  }
}

/// Invokes [callback] inside the given [zone] passing it [arg1] and [arg2].
void invoke2<A1, A2>(
    void Function(A1 a1, A2 a2)? callback, Zone? zone, A1 arg1, A2 arg2) {
  if (callback == null) {
    return;
  }

  assert(zone != null);

  if (identical(zone, Zone.current)) {
    callback(arg1, arg2);
  } else {
    zone!.runGuarded(() {
      callback(arg1, arg2);
    });
  }
}

/// Invokes [callback] inside the given [zone] passing it [arg1], [arg2], and [arg3].
void invoke3<A1, A2, A3>(void Function(A1 a1, A2 a2, A3 a3)? callback,
    Zone? zone, A1 arg1, A2 arg2, A3 arg3) {
  if (callback == null) {
    return;
  }

  assert(zone != null);

  if (identical(zone, Zone.current)) {
    callback(arg1, arg2, arg3);
  } else {
    zone!.runGuarded(() {
      callback(arg1, arg2, arg3);
    });
  }
}

const double _defaultRootFontSize = 16.0;

/// Finds the text scale factor of the browser by looking at the computed style
/// of the browser's <html> element.
double findBrowserTextScaleFactor() {
  final num fontSize = parseFontSize(domDocument.documentElement!) ?? _defaultRootFontSize;
  return fontSize / _defaultRootFontSize;
}
