// Copyright 2014 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.

/// @docImport 'package:flutter/widgets.dart';
///
/// @docImport 'semantics.dart';
library;

import 'dart:ui' as ui show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilder;

import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';

import 'debug.dart';

export 'dart:ui' show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilder;

/// The glue between the semantics layer and the Flutter engine.
mixin SemanticsBinding on BindingBase {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    _accessibilityFeatures = platformDispatcher.accessibilityFeatures;
    platformDispatcher
      ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
      ..onSemanticsActionEvent = _handleSemanticsActionEvent
      ..onAccessibilityFeaturesChanged = () {
        // TODO(chunhtai): Web should not notify accessibility feature changes during updateSemantics
        // https://github.com/flutter/flutter/issues/158399
        if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
          SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
            handleAccessibilityFeaturesChanged();
          }, debugLabel: 'SemanticsBinding.handleAccessibilityFeaturesChanged');
        } else {
          handleAccessibilityFeaturesChanged();
        }
      };
    _handleSemanticsEnabledChanged();
    addSemanticsEnabledListener(_handleFrameworkSemanticsEnabledChanged);
    // Ensure the initial value is set.
    if (semanticsEnabled) {
      _handleFrameworkSemanticsEnabledChanged();
    }
  }

  /// The current [SemanticsBinding], if one has been created.
  ///
  /// Provides access to the features exposed by this mixin. The binding must
  /// be initialized before using this getter; this is typically done by calling
  /// [runApp] or [WidgetsFlutterBinding.ensureInitialized].
  static SemanticsBinding get instance => BindingBase.checkInstance(_instance);
  static SemanticsBinding? _instance;

  /// Whether semantics information must be collected.
  ///
  /// Returns true if either the platform has requested semantics information
  /// to be generated or if [ensureSemantics] has been called otherwise.
  ///
  /// To get notified when this value changes register a listener with
  /// [addSemanticsEnabledListener].
  bool get semanticsEnabled {
    assert(_semanticsEnabled.value == (_outstandingHandles > 0));
    return _semanticsEnabled.value;
  }

  late final ValueNotifier<bool> _semanticsEnabled = ValueNotifier<bool>(
    platformDispatcher.semanticsEnabled,
  );

  /// Adds a `listener` to be called when [semanticsEnabled] changes.
  ///
  /// See also:
  ///
  ///  * [removeSemanticsEnabledListener] to remove the listener again.
  ///  * [ValueNotifier.addListener], which documents how and when listeners are
  ///    called.
  void addSemanticsEnabledListener(VoidCallback listener) {
    _semanticsEnabled.addListener(listener);
  }

  /// Removes a `listener` added by [addSemanticsEnabledListener].
  ///
  /// See also:
  ///
  ///  * [ValueNotifier.removeListener], which documents how listeners are
  ///    removed.
  void removeSemanticsEnabledListener(VoidCallback listener) {
    _semanticsEnabled.removeListener(listener);
  }

  final ObserverList<ValueSetter<ui.SemanticsActionEvent>> _semanticsActionListeners =
      ObserverList<ValueSetter<ui.SemanticsActionEvent>>();

  /// Adds a listener that is called for every [ui.SemanticsActionEvent] received.
  ///
  /// The listeners are called before [performSemanticsAction] is invoked.
  ///
  /// To remove the listener, call [removeSemanticsActionListener].
  void addSemanticsActionListener(ValueSetter<ui.SemanticsActionEvent> listener) {
    _semanticsActionListeners.add(listener);
  }

  /// Removes a listener previously added with [addSemanticsActionListener].
  void removeSemanticsActionListener(ValueSetter<ui.SemanticsActionEvent> listener) {
    _semanticsActionListeners.remove(listener);
  }

  /// The number of clients registered to listen for semantics.
  ///
  /// The number is increased whenever [ensureSemantics] is called and decreased
  /// when [SemanticsHandle.dispose] is called.
  int get debugOutstandingSemanticsHandles => _outstandingHandles;
  int _outstandingHandles = 0;

  /// Creates a new [SemanticsHandle] and requests the collection of semantics
  /// information.
  ///
  /// Semantics information are only collected when there are clients interested
  /// in them. These clients express their interest by holding a
  /// [SemanticsHandle].
  ///
  /// Clients can close their [SemanticsHandle] by calling
  /// [SemanticsHandle.dispose]. Once all outstanding [SemanticsHandle] objects
  /// are closed, semantics information are no longer collected.
  SemanticsHandle ensureSemantics() {
    assert(_outstandingHandles >= 0);
    _outstandingHandles++;
    assert(_outstandingHandles > 0);
    _semanticsEnabled.value = true;
    return SemanticsHandle._(_didDisposeSemanticsHandle);
  }

  void _didDisposeSemanticsHandle() {
    assert(_outstandingHandles > 0);
    _outstandingHandles--;
    assert(_outstandingHandles >= 0);
    _semanticsEnabled.value = _outstandingHandles > 0;
  }

  // Handle for semantics request from the platform.
  SemanticsHandle? _semanticsHandle;

  void _handleSemanticsEnabledChanged() {
    if (platformDispatcher.semanticsEnabled) {
      _semanticsHandle ??= ensureSemantics();
    } else {
      _semanticsHandle?.dispose();
      _semanticsHandle = null;
    }
  }

  void _handleSemanticsActionEvent(ui.SemanticsActionEvent action) {
    final Object? arguments = action.arguments;
    final ui.SemanticsActionEvent decodedAction = arguments is ByteData
        ? action.copyWith(arguments: const StandardMessageCodec().decodeMessage(arguments))
        : action;
    // Listeners may get added/removed while the iteration is in progress. Since the list cannot
    // be modified while iterating, we are creating a local copy for the iteration.
    final List<ValueSetter<ui.SemanticsActionEvent>> localListeners = _semanticsActionListeners
        .toList(growable: false);
    for (final listener in localListeners) {
      if (_semanticsActionListeners.contains(listener)) {
        listener(decodedAction);
      }
    }
    performSemanticsAction(decodedAction);
  }

  void _handleFrameworkSemanticsEnabledChanged() {
    platformDispatcher.setSemanticsTreeEnabled(semanticsEnabled);
  }

  /// Called whenever the platform requests an action to be performed on a
  /// [SemanticsNode].
  ///
  /// This callback is invoked when a user interacts with the app via an
  /// accessibility service (e.g. TalkBack and VoiceOver) and initiates an
  /// action on the focused node.
  ///
  /// Bindings that mixin the [SemanticsBinding] must implement this method and
  /// perform the given `action` on the [SemanticsNode] specified by
  /// [ui.SemanticsActionEvent.nodeId].
  ///
  /// See [dart:ui.PlatformDispatcher.onSemanticsActionEvent].
  @protected
  void performSemanticsAction(ui.SemanticsActionEvent action);

  /// The currently active set of [ui.AccessibilityFeatures].
  ///
  /// This is set when the binding is first initialized and updated whenever a
  /// flag is changed.
  ///
  /// To listen to changes to accessibility features, create a
  /// [WidgetsBindingObserver] and listen to
  /// [WidgetsBindingObserver.didChangeAccessibilityFeatures].
  ui.AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures;
  late ui.AccessibilityFeatures _accessibilityFeatures;

  /// Called when the platform accessibility features change.
  ///
  /// See [dart:ui.PlatformDispatcher.onAccessibilityFeaturesChanged].
  @protected
  @mustCallSuper
  void handleAccessibilityFeaturesChanged() {
    _accessibilityFeatures = platformDispatcher.accessibilityFeatures;
  }

  /// Creates an empty semantics update builder.
  ///
  /// The caller is responsible for filling out the semantics node updates.
  ///
  /// This method is used by the [SemanticsOwner] to create builder for all its
  /// semantics updates.
  ui.SemanticsUpdateBuilder createSemanticsUpdateBuilder() {
    return ui.SemanticsUpdateBuilder();
  }

  /// The platform is requesting that animations be disabled or simplified.
  ///
  /// This setting can be overridden for testing or debugging by setting
  /// [debugSemanticsDisableAnimations].
  bool get disableAnimations {
    bool value = _accessibilityFeatures.disableAnimations;
    assert(() {
      if (debugSemanticsDisableAnimations != null) {
        value = debugSemanticsDisableAnimations!;
      }
      return true;
    }());
    return value;
  }
}

/// A reference to the semantics information generated by the framework.
///
/// Semantics information are only collected when there are clients interested
/// in them. These clients express their interest by holding a
/// [SemanticsHandle]. When the client no longer needs the
/// semantics information, it must call [dispose] on the [SemanticsHandle] to
/// close it. When all open [SemanticsHandle]s are disposed, the framework will
/// stop updating the semantics information.
///
/// To obtain a [SemanticsHandle], call [SemanticsBinding.ensureSemantics].
class SemanticsHandle {
  SemanticsHandle._(this._onDispose) {
    assert(debugMaybeDispatchCreated('semantics', 'SemanticsHandle', this));
  }

  final VoidCallback _onDispose;

  /// Closes the semantics handle.
  ///
  /// When all the outstanding [SemanticsHandle] objects are closed, the
  /// framework will stop generating semantics information.
  @mustCallSuper
  void dispose() {
    assert(debugMaybeDispatchDisposed(this));
    _onDispose();
  }
}
