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

import 'dart:async';
import 'dart:io' show Platform;
import 'dart:ui' show
  FontWeight,
  Offset,
  Size,
  Rect,
  TextAlign,
  TextDirection,
  hashValues;

import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4;

import '../../services.dart' show Clipboard;
import 'autofill.dart';
import 'message_codec.dart';
import 'platform_channel.dart';
import 'system_channels.dart';
import 'system_chrome.dart';
import 'text_editing.dart';
import 'text_editing_delta.dart';

export 'dart:ui' show TextAffinity;

/// Indicates how to handle the intelligent replacement of dashes in text input.
///
/// See also:
///
///  * [TextField.smartDashesType]
///  * [CupertinoTextField.smartDashesType]
///  * [EditableText.smartDashesType]
///  * [SmartQuotesType]
///  * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
enum SmartDashesType {
  /// Smart dashes is disabled.
  ///
  /// This corresponds to the
  /// ["no" value of UITextSmartDashesType](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/no).
  disabled,

  /// Smart dashes is enabled.
  ///
  /// This corresponds to the
  /// ["yes" value of UITextSmartDashesType](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/yes).
  enabled,
}

/// Indicates how to handle the intelligent replacement of quotes in text input.
///
/// See also:
///
///  * [TextField.smartQuotesType]
///  * [CupertinoTextField.smartQuotesType]
///  * [EditableText.smartQuotesType]
///  * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
enum SmartQuotesType {
  /// Smart quotes is disabled.
  ///
  /// This corresponds to the
  /// ["no" value of UITextSmartQuotesType](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/no).
  disabled,

  /// Smart quotes is enabled.
  ///
  /// This corresponds to the
  /// ["yes" value of UITextSmartQuotesType](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/yes).
  enabled,
}

/// The type of information for which to optimize the text input control.
///
/// On Android, behavior may vary across device and keyboard provider.
///
/// This class stays as close to `Enum` interface as possible, and allows
/// for additional flags for some input types. For example, numeric input
/// can specify whether it supports decimal numbers and/or signed numbers.
@immutable
class TextInputType {
  const TextInputType._(this.index)
    : signed = null,
      decimal = null;

  /// Optimize for numerical information.
  ///
  /// Requests a numeric keyboard with additional settings.
  /// The [signed] and [decimal] parameters are optional.
  const TextInputType.numberWithOptions({
    this.signed = false,
    this.decimal = false,
  }) : index = 2;

  /// Enum value index, corresponds to one of the [values].
  final int index;

  /// The number is signed, allowing a positive or negative sign at the start.
  ///
  /// This flag is only used for the [number] input type, otherwise `null`.
  /// Use `const TextInputType.numberWithOptions(signed: true)` to set this.
  final bool? signed;

  /// The number is decimal, allowing a decimal point to provide fractional.
  ///
  /// This flag is only used for the [number] input type, otherwise `null`.
  /// Use `const TextInputType.numberWithOptions(decimal: true)` to set this.
  final bool? decimal;

  /// Optimize for textual information.
  ///
  /// Requests the default platform keyboard.
  static const TextInputType text = TextInputType._(0);

  /// Optimize for multiline textual information.
  ///
  /// Requests the default platform keyboard, but accepts newlines when the
  /// enter key is pressed. This is the input type used for all multiline text
  /// fields.
  static const TextInputType multiline = TextInputType._(1);

  /// Optimize for unsigned numerical information without a decimal point.
  ///
  /// Requests a default keyboard with ready access to the number keys.
  /// Additional options, such as decimal point and/or positive/negative
  /// signs, can be requested using [new TextInputType.numberWithOptions].
  static const TextInputType number = TextInputType.numberWithOptions();

  /// Optimize for telephone numbers.
  ///
  /// Requests a keyboard with ready access to the number keys, "*", and "#".
  static const TextInputType phone = TextInputType._(3);

  /// Optimize for date and time information.
  ///
  /// On iOS, requests the default keyboard.
  ///
  /// On Android, requests a keyboard with ready access to the number keys,
  /// ":", and "-".
  static const TextInputType datetime = TextInputType._(4);

  /// Optimize for email addresses.
  ///
  /// Requests a keyboard with ready access to the "@" and "." keys.
  static const TextInputType emailAddress = TextInputType._(5);

  /// Optimize for URLs.
  ///
  /// Requests a keyboard with ready access to the "/" and "." keys.
  static const TextInputType url = TextInputType._(6);

  /// Optimize for passwords that are visible to the user.
  ///
  /// Requests a keyboard with ready access to both letters and numbers.
  static const TextInputType visiblePassword = TextInputType._(7);

  /// Optimized for a person's name.
  ///
  /// On iOS, requests the
  /// [UIKeyboardType.namePhonePad](https://developer.apple.com/documentation/uikit/uikeyboardtype/namephonepad)
  /// keyboard, a keyboard optimized for entering a person’s name or phone number.
  /// Does not support auto-capitalization.
  ///
  /// On Android, requests a keyboard optimized for
  /// [TYPE_TEXT_VARIATION_PERSON_NAME](https://developer.android.com/reference/android/text/InputType#TYPE_TEXT_VARIATION_PERSON_NAME).
  static const TextInputType name = TextInputType._(8);

  /// Optimized for postal mailing addresses.
  ///
  /// On iOS, requests the default keyboard.
  ///
  /// On Android, requests a keyboard optimized for
  /// [TYPE_TEXT_VARIATION_POSTAL_ADDRESS](https://developer.android.com/reference/android/text/InputType#TYPE_TEXT_VARIATION_POSTAL_ADDRESS).
  static const TextInputType streetAddress = TextInputType._(9);

  /// Prevent the OS from showing the on-screen virtual keyboard.
  static const TextInputType none = TextInputType._(10);

  /// All possible enum values.
  static const List<TextInputType> values = <TextInputType>[
    text, multiline, number, phone, datetime, emailAddress, url, visiblePassword, name, streetAddress, none,
  ];

  // Corresponding string name for each of the [values].
  static const List<String> _names = <String>[
    'text', 'multiline', 'number', 'phone', 'datetime', 'emailAddress', 'url', 'visiblePassword', 'name', 'address', 'none',
  ];

  // Enum value name, this is what enum.toString() would normally return.
  String get _name => 'TextInputType.${_names[index]}';

  /// Returns a representation of this object as a JSON object.
  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'name': _name,
      'signed': signed,
      'decimal': decimal,
    };
  }

  @override
  String toString() {
    return '${objectRuntimeType(this, 'TextInputType')}('
        'name: $_name, '
        'signed: $signed, '
        'decimal: $decimal)';
  }

  @override
  bool operator ==(Object other) {
    return other is TextInputType
        && other.index == index
        && other.signed == signed
        && other.decimal == decimal;
  }

  @override
  int get hashCode => hashValues(index, signed, decimal);
}

/// An action the user has requested the text input control to perform.
///
/// Each action represents a logical meaning, and also configures the soft
/// keyboard to display a certain kind of action button. The visual appearance
/// of the action button might differ between versions of the same OS.
///
/// Despite the logical meaning of each action, choosing a particular
/// [TextInputAction] does not necessarily cause any specific behavior to
/// happen, other than changing the focus when appropriate. It is up to the
/// developer to ensure that the behavior that occurs when an action button is
/// pressed is appropriate for the action button chosen.
///
/// For example: If the user presses the keyboard action button on iOS when it
/// reads "Emergency Call", the result should not be a focus change to the next
/// TextField. This behavior is not logically appropriate for a button that says
/// "Emergency Call".
///
/// See [EditableText] for more information about customizing action button
/// behavior.
///
/// Most [TextInputAction]s are supported equally by both Android and iOS.
/// However, there is not a complete, direct mapping between Android's IME input
/// types and iOS's keyboard return types. Therefore, some [TextInputAction]s
/// are inappropriate for one of the platforms. If a developer chooses an
/// inappropriate [TextInputAction] when running in debug mode, an error will be
/// thrown. If the same thing is done in release mode, then instead of sending
/// the inappropriate value, Android will use "unspecified" on the platform
/// side and iOS will use "default" on the platform side.
///
/// See also:
///
///  * [TextInput], which configures the platform's keyboard setup.
///  * [EditableText], which invokes callbacks when the action button is pressed.
enum TextInputAction {
  /// Logical meaning: There is no relevant input action for the current input
  /// source, e.g., [TextField].
  ///
  /// Android: Corresponds to Android's "IME_ACTION_NONE". The keyboard setup
  /// is decided by the OS. The keyboard will likely show a return key.
  ///
  /// iOS: iOS does not have a keyboard return type of "none." It is
  /// inappropriate to choose this [TextInputAction] when running on iOS.
  none,

  /// Logical meaning: Let the OS decide which action is most appropriate.
  ///
  /// Android: Corresponds to Android's "IME_ACTION_UNSPECIFIED". The OS chooses
  /// which keyboard action to display. The decision will likely be a done
  /// button or a return key.
  ///
  /// iOS: Corresponds to iOS's "UIReturnKeyDefault". The title displayed in
  /// the action button is "return".
  unspecified,

  /// Logical meaning: The user is done providing input to a group of inputs
  /// (like a form). Some kind of finalization behavior should now take place.
  ///
  /// Android: Corresponds to Android's "IME_ACTION_DONE". The OS displays a
  /// button that represents completion, e.g., a checkmark button.
  ///
  /// iOS: Corresponds to iOS's "UIReturnKeyDone". The title displayed in the
  /// action button is "Done".
  done,

  /// Logical meaning: The user has entered some text that represents a
  /// destination, e.g., a restaurant name. The "go" button is intended to take
  /// the user to a part of the app that corresponds to this destination.
  ///
  /// Android: Corresponds to Android's "IME_ACTION_GO". The OS displays a
  /// button that represents taking "the user to the target of the text they
  /// typed", e.g., a right-facing arrow button.
  ///
  /// iOS: Corresponds to iOS's "UIReturnKeyGo". The title displayed in the
  /// action button is "Go".
  go,

  /// Logical meaning: Execute a search query.
  ///
  /// Android: Corresponds to Android's "IME_ACTION_SEARCH". The OS displays a
  /// button that represents a search, e.g., a magnifying glass button.
  ///
  /// iOS: Corresponds to iOS's "UIReturnKeySearch". The title displayed in the
  /// action button is "Search".
  search,

  /// Logical meaning: Sends something that the user has composed, e.g., an
  /// email or a text message.
  ///
  /// Android: Corresponds to Android's "IME_ACTION_SEND". The OS displays a
  /// button that represents sending something, e.g., a paper plane button.
  ///
  /// iOS: Corresponds to iOS's "UIReturnKeySend". The title displayed in the
  /// action button is "Send".
  send,

  /// Logical meaning: The user is done with the current input source and wants
  /// to move to the next one.
  ///
  /// Moves the focus to the next focusable item in the same [FocusScope].
  ///
  /// Android: Corresponds to Android's "IME_ACTION_NEXT". The OS displays a
  /// button that represents moving forward, e.g., a right-facing arrow button.
  ///
  /// iOS: Corresponds to iOS's "UIReturnKeyNext". The title displayed in the
  /// action button is "Next".
  next,

  /// Logical meaning: The user wishes to return to the previous input source
  /// in the group, e.g., a form with multiple [TextField]s.
  ///
  /// Moves the focus to the previous focusable item in the same [FocusScope].
  ///
  /// Android: Corresponds to Android's "IME_ACTION_PREVIOUS". The OS displays a
  /// button that represents moving backward, e.g., a left-facing arrow button.
  ///
  /// iOS: iOS does not have a keyboard return type of "previous." It is
  /// inappropriate to choose this [TextInputAction] when running on iOS.
  previous,

  /// Logical meaning: In iOS apps, it is common for a "Back" button and
  /// "Continue" button to appear at the top of the screen. However, when the
  /// keyboard is open, these buttons are often hidden off-screen. Therefore,
  /// the purpose of the "Continue" return key on iOS is to make the "Continue"
  /// button available when the user is entering text.
  ///
  /// Historical context aside, [TextInputAction.continueAction] can be used any
  /// time that the term "Continue" seems most appropriate for the given action.
  ///
  /// Android: Android does not have an IME input type of "continue." It is
  /// inappropriate to choose this [TextInputAction] when running on Android.
  ///
  /// iOS: Corresponds to iOS's "UIReturnKeyContinue". The title displayed in the
  /// action button is "Continue". This action is only available on iOS 9.0+.
  ///
  /// The reason that this value has "Action" post-fixed to it is because
  /// "continue" is a reserved word in Dart, as well as many other languages.
  continueAction,

  /// Logical meaning: The user wants to join something, e.g., a wireless
  /// network.
  ///
  /// Android: Android does not have an IME input type of "join." It is
  /// inappropriate to choose this [TextInputAction] when running on Android.
  ///
  /// iOS: Corresponds to iOS's "UIReturnKeyJoin". The title displayed in the
  /// action button is "Join".
  join,

  /// Logical meaning: The user wants routing options, e.g., driving directions.
  ///
  /// Android: Android does not have an IME input type of "route." It is
  /// inappropriate to choose this [TextInputAction] when running on Android.
  ///
  /// iOS: Corresponds to iOS's "UIReturnKeyRoute". The title displayed in the
  /// action button is "Route".
  route,

  /// Logical meaning: Initiate a call to emergency services.
  ///
  /// Android: Android does not have an IME input type of "emergencyCall." It is
  /// inappropriate to choose this [TextInputAction] when running on Android.
  ///
  /// iOS: Corresponds to iOS's "UIReturnKeyEmergencyCall". The title displayed
  /// in the action button is "Emergency Call".
  emergencyCall,

  /// Logical meaning: Insert a newline character in the focused text input,
  /// e.g., [TextField].
  ///
  /// Android: Corresponds to Android's "IME_ACTION_NONE". The OS displays a
  /// button that represents a new line, e.g., a carriage return button.
  ///
  /// iOS: Corresponds to iOS's "UIReturnKeyDefault". The title displayed in the
  /// action button is "return".
  ///
  /// The term [TextInputAction.newline] exists in Flutter but not in Android
  /// or iOS. The reason for introducing this term is so that developers can
  /// achieve the common result of inserting new lines without needing to
  /// understand the various IME actions on Android and return keys on iOS.
  /// Thus, [TextInputAction.newline] is a convenience term that alleviates the
  /// need to understand the underlying platforms to achieve this common behavior.
  newline,
}

/// Configures how the platform keyboard will select an uppercase or
/// lowercase keyboard.
///
/// Only supports text keyboards, other keyboard types will ignore this
/// configuration. Capitalization is locale-aware.
enum TextCapitalization {
  /// Defaults to an uppercase keyboard for the first letter of each word.
  ///
  /// Corresponds to `InputType.TYPE_TEXT_FLAG_CAP_WORDS` on Android, and
  /// `UITextAutocapitalizationTypeWords` on iOS.
  words,

  /// Defaults to an uppercase keyboard for the first letter of each sentence.
  ///
  /// Corresponds to `InputType.TYPE_TEXT_FLAG_CAP_SENTENCES` on Android, and
  /// `UITextAutocapitalizationTypeSentences` on iOS.
  sentences,

  /// Defaults to an uppercase keyboard for each character.
  ///
  /// Corresponds to `InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS` on Android, and
  /// `UITextAutocapitalizationTypeAllCharacters` on iOS.
  characters,

  /// Defaults to a lowercase keyboard.
  none,
}

/// Controls the visual appearance of the text input control.
///
/// Many [TextInputAction]s are common between Android and iOS. However, if an
/// [inputAction] is provided that is not supported by the current
/// platform in debug mode, an error will be thrown when the corresponding
/// text input is attached. For example, providing iOS's "emergencyCall"
/// action when running on an Android device will result in an error when in
/// debug mode. In release mode, incompatible [TextInputAction]s are replaced
/// either with "unspecified" on Android, or "default" on iOS. Appropriate
/// [inputAction]s can be chosen by checking the current platform and then
/// selecting the appropriate action.
///
/// See also:
///
///  * [TextInput.attach]
///  * [TextInputAction]
@immutable
class TextInputConfiguration {
  /// Creates configuration information for a text input control.
  ///
  /// All arguments have default values, except [actionLabel]. Only
  /// [actionLabel] may be null.
  const TextInputConfiguration({
    this.inputType = TextInputType.text,
    this.readOnly = false,
    this.obscureText = false,
    this.autocorrect = true,
    SmartDashesType? smartDashesType,
    SmartQuotesType? smartQuotesType,
    this.enableSuggestions = true,
    this.actionLabel,
    this.inputAction = TextInputAction.done,
    this.keyboardAppearance = Brightness.light,
    this.textCapitalization = TextCapitalization.none,
    this.autofillConfiguration = AutofillConfiguration.disabled,
    this.enableIMEPersonalizedLearning = true,
    this.enableDeltaModel = false,
  }) : assert(inputType != null),
       assert(obscureText != null),
       smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
       smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
       assert(autocorrect != null),
       assert(enableSuggestions != null),
       assert(keyboardAppearance != null),
       assert(inputAction != null),
       assert(textCapitalization != null),
       assert(enableIMEPersonalizedLearning != null),
       assert(enableDeltaModel != null);

  /// The type of information for which to optimize the text input control.
  final TextInputType inputType;

  /// Whether the text field can be edited or not.
  ///
  /// Defaults to false.
  final bool readOnly;

  /// Whether to hide the text being edited (e.g., for passwords).
  ///
  /// Defaults to false.
  final bool obscureText;

  /// Whether to enable autocorrection.
  ///
  /// Defaults to true.
  final bool autocorrect;

  /// The configuration to use for autofill.
  ///
  /// Defaults to null, in which case no autofill information will be provided
  /// to the platform. This will prevent the corresponding input field from
  /// participating in autofills triggered by other fields. Additionally, on
  /// Android and web, setting [autofillConfiguration] to null disables autofill.
  final AutofillConfiguration autofillConfiguration;

  /// {@template flutter.services.TextInputConfiguration.smartDashesType}
  /// Whether to allow the platform to automatically format dashes.
  ///
  /// This flag only affects iOS versions 11 and above. It sets
  /// [`UITextSmartDashesType`](https://developer.apple.com/documentation/uikit/uitextsmartdashestype?language=objc)
  /// in the engine. When true, it passes
  /// [`UITextSmartDashesTypeYes`](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/uitextsmartdashestypeyes?language=objc),
  /// and when false, it passes
  /// [`UITextSmartDashesTypeNo`](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/uitextsmartdashestypeno?language=objc).
  ///
  /// As an example of what this does, two consecutive hyphen characters will be
  /// automatically replaced with one en dash, and three consecutive hyphens
  /// will become one em dash.
  ///
  /// Defaults to true, unless [obscureText] is true, when it defaults to false.
  /// This is to avoid the problem where password fields receive autoformatted
  /// characters.
  ///
  /// See also:
  ///
  ///  * [smartQuotesType]
  ///  * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
  /// {@endtemplate}
  final SmartDashesType smartDashesType;

  /// {@template flutter.services.TextInputConfiguration.smartQuotesType}
  /// Whether to allow the platform to automatically format quotes.
  ///
  /// This flag only affects iOS. It sets
  /// [`UITextSmartQuotesType`](https://developer.apple.com/documentation/uikit/uitextsmartquotestype?language=objc)
  /// in the engine. When true, it passes
  /// [`UITextSmartQuotesTypeYes`](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/uitextsmartquotestypeyes?language=objc),
  /// and when false, it passes
  /// [`UITextSmartQuotesTypeNo`](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/uitextsmartquotestypeno?language=objc).
  ///
  /// As an example of what this does, a standard vertical double quote
  /// character will be automatically replaced by a left or right double quote
  /// depending on its position in a word.
  ///
  /// Defaults to true, unless [obscureText] is true, when it defaults to false.
  /// This is to avoid the problem where password fields receive autoformatted
  /// characters.
  ///
  /// See also:
  ///
  ///  * [smartDashesType]
  ///  * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
  /// {@endtemplate}
  final SmartQuotesType smartQuotesType;

  /// {@template flutter.services.TextInputConfiguration.enableSuggestions}
  /// Whether to show input suggestions as the user types.
  ///
  /// This flag only affects Android. On iOS, suggestions are tied directly to
  /// [autocorrect], so that suggestions are only shown when [autocorrect] is
  /// true. On Android autocorrection and suggestion are controlled separately.
  ///
  /// Defaults to true. Cannot be null.
  ///
  /// See also:
  ///
  ///  * <https://developer.android.com/reference/android/text/InputType.html#TYPE_TEXT_FLAG_NO_SUGGESTIONS>
  /// {@endtemplate}
  final bool enableSuggestions;

  /// What text to display in the text input control's action button.
  final String? actionLabel;

  /// What kind of action to request for the action button on the IME.
  final TextInputAction inputAction;

  /// Specifies how platforms may automatically capitalize text entered by the
  /// user.
  ///
  /// Defaults to [TextCapitalization.none].
  ///
  /// See also:
  ///
  ///  * [TextCapitalization], for a description of each capitalization behavior.
  final TextCapitalization textCapitalization;

  /// The appearance of the keyboard.
  ///
  /// This setting is only honored on iOS devices.
  ///
  /// Defaults to [Brightness.light].
  final Brightness keyboardAppearance;

  /// {@template flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
  /// Whether to enable that the IME update personalized data such as typing
  /// history and user dictionary data.
  ///
  /// This flag only affects Android. On iOS, there is no equivalent flag.
  ///
  /// Defaults to true. Cannot be null.
  ///
  /// See also:
  ///
  ///  * <https://developer.android.com/reference/android/view/inputmethod/EditorInfo#IME_FLAG_NO_PERSONALIZED_LEARNING>
  /// {@endtemplate}
  final bool enableIMEPersonalizedLearning;

  /// Creates a copy of this [TextInputConfiguration] with the given fields
  /// replaced with new values.
  TextInputConfiguration copyWith({
    TextInputType? inputType,
    bool? readOnly,
    bool? obscureText,
    bool? autocorrect,
    SmartDashesType? smartDashesType,
    SmartQuotesType? smartQuotesType,
    bool? enableSuggestions,
    String? actionLabel,
    TextInputAction? inputAction,
    Brightness? keyboardAppearance,
    TextCapitalization? textCapitalization,
    bool? enableIMEPersonalizedLearning,
    AutofillConfiguration? autofillConfiguration,
    bool? enableDeltaModel,
  }) {
    return TextInputConfiguration(
      inputType: inputType ?? this.inputType,
      readOnly: readOnly ?? this.readOnly,
      obscureText: obscureText ?? this.obscureText,
      autocorrect: autocorrect ?? this.autocorrect,
      smartDashesType: smartDashesType ?? this.smartDashesType,
      smartQuotesType: smartQuotesType ?? this.smartQuotesType,
      enableSuggestions: enableSuggestions ?? this.enableSuggestions,
      inputAction: inputAction ?? this.inputAction,
      textCapitalization: textCapitalization ?? this.textCapitalization,
      keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance,
      enableIMEPersonalizedLearning: enableIMEPersonalizedLearning?? this.enableIMEPersonalizedLearning,
      autofillConfiguration: autofillConfiguration ?? this.autofillConfiguration,
      enableDeltaModel: enableDeltaModel ?? this.enableDeltaModel,
    );
  }

  /// Whether to enable that the engine sends text input updates to the
  /// framework as [TextEditingDelta]'s or as one [TextEditingValue].
  ///
  /// Enabling this flag results in granular text updates being received from the
  /// platform's text input control.
  ///
  /// When this is enabled:
  ///  * You must implement [DeltaTextInputClient] and not [TextInputClient] to
  ///    receive granular updates from the platform's text input.
  ///  * Platform text input updates will come through
  ///    [DeltaTextInputClient.updateEditingValueWithDeltas].
  ///  * If [TextInputClient] is implemented with this property enabled then
  ///    you will experience unexpected behavior as [TextInputClient] does not implement
  ///    a delta channel.
  ///
  /// When this is disabled:
  ///  * If [DeltaTextInputClient] is implemented then updates for the
  ///    editing state will continue to come through the
  ///    [DeltaTextInputClient.updateEditingValue] channel.
  ///  * If [TextInputClient] is implemented then updates for the editing
  ///    state will come through [TextInputClient.updateEditingValue].
  ///
  /// Defaults to false. Cannot be null.
  final bool enableDeltaModel;

  /// Returns a representation of this object as a JSON object.
  Map<String, dynamic> toJson() {
    final Map<String, dynamic>? autofill = autofillConfiguration.toJson();
    return <String, dynamic>{
      'inputType': inputType.toJson(),
      'readOnly': readOnly,
      'obscureText': obscureText,
      'autocorrect': autocorrect,
      'smartDashesType': smartDashesType.index.toString(),
      'smartQuotesType': smartQuotesType.index.toString(),
      'enableSuggestions': enableSuggestions,
      'actionLabel': actionLabel,
      'inputAction': inputAction.toString(),
      'textCapitalization': textCapitalization.toString(),
      'keyboardAppearance': keyboardAppearance.toString(),
      'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning,
      if (autofill != null) 'autofill': autofill,
      'enableDeltaModel' : enableDeltaModel,
    };
  }
}

TextAffinity? _toTextAffinity(String? affinity) {
  switch (affinity) {
    case 'TextAffinity.downstream':
      return TextAffinity.downstream;
    case 'TextAffinity.upstream':
      return TextAffinity.upstream;
  }
  return null;
}

/// A floating cursor state the user has induced by force pressing an iOS
/// keyboard.
enum FloatingCursorDragState {
  /// A user has just activated a floating cursor.
  Start,

  /// A user is dragging a floating cursor.
  Update,

  /// A user has lifted their finger off the screen after using a floating
  /// cursor.
  End,
}

/// The current state and position of the floating cursor.
class RawFloatingCursorPoint {
  /// Creates information for setting the position and state of a floating
  /// cursor.
  ///
  /// [state] must not be null and [offset] must not be null if the state is
  /// [FloatingCursorDragState.Update].
  RawFloatingCursorPoint({
    this.offset,
    required this.state,
  }) : assert(state != null),
       assert(state != FloatingCursorDragState.Update || offset != null);

  /// The raw position of the floating cursor as determined by the iOS sdk.
  final Offset? offset;

  /// The state of the floating cursor.
  final FloatingCursorDragState state;
}

/// The current text, selection, and composing state for editing a run of text.
@immutable
class TextEditingValue {
  /// Creates information for editing a run of text.
  ///
  /// The selection and composing range must be within the text.
  ///
  /// The [text], [selection], and [composing] arguments must not be null but
  /// each have default values.
  const TextEditingValue({
    this.text = '',
    this.selection = const TextSelection.collapsed(offset: -1),
    this.composing = TextRange.empty,
  }) : assert(text != null),
       assert(selection != null),
       assert(composing != null);

  /// Creates an instance of this class from a JSON object.
  factory TextEditingValue.fromJSON(Map<String, dynamic> encoded) {
    return TextEditingValue(
      text: encoded['text'] as String,
      selection: TextSelection(
        baseOffset: encoded['selectionBase'] as int? ?? -1,
        extentOffset: encoded['selectionExtent'] as int? ?? -1,
        affinity: _toTextAffinity(encoded['selectionAffinity'] as String?) ?? TextAffinity.downstream,
        isDirectional: encoded['selectionIsDirectional'] as bool? ?? false,
      ),
      composing: TextRange(
        start: encoded['composingBase'] as int? ?? -1,
        end: encoded['composingExtent'] as int? ?? -1,
      ),
    );
  }

  /// The current text being edited.
  final String text;

  /// The range of text that is currently selected.
  ///
  /// When [selection] is a [TextSelection] that has the same non-negative
  /// `baseOffset` and `extentOffset`, the [selection] property represents the
  /// caret position.
  ///
  /// If the current [selection] has a negative `baseOffset` or `extentOffset`,
  /// then the text currently does not have a selection or a caret location, and
  /// most text editing operations that rely on the current selection (for
  /// instance, insert a character at the caret location) will do nothing.
  final TextSelection selection;

  /// The range of text that is still being composed.
  ///
  /// Composing regions are created by input methods (IMEs) to indicate the text
  /// within a certain range is provisional. For instance, the Android Gboard
  /// app's English keyboard puts the current word under the caret into a
  /// composing region to indicate the word is subject to autocorrect or
  /// prediction changes.
  ///
  /// Composing regions can also be used for performing multistage input, which
  /// is typically used by IMEs designed for phoetic keyboard to enter
  /// ideographic symbols. As an example, many CJK keyboards require the user to
  /// enter a latin alphabet sequence and then convert it to CJK characters. On
  /// iOS, the default software keyboards do not have a dedicated view to show
  /// the unfinished latin sequence, so it's displayed directly in the text
  /// field, inside of a composing region.
  ///
  /// The composing region should typically only be changed by the IME, or the
  /// user via interacting with the IME.
  ///
  /// If the range represented by this property is [TextRange.empty], then the
  /// text is not currently being composed.
  final TextRange composing;

  /// A value that corresponds to the empty string with no selection and no composing range.
  static const TextEditingValue empty = TextEditingValue();

  /// Creates a copy of this value but with the given fields replaced with the new values.
  TextEditingValue copyWith({
    String? text,
    TextSelection? selection,
    TextRange? composing,
  }) {
    return TextEditingValue(
      text: text ?? this.text,
      selection: selection ?? this.selection,
      composing: composing ?? this.composing,
    );
  }

  /// Whether the [composing] range is a valid range within [text].
  ///
  /// Returns true if and only if the [composing] range is normalized, its start
  /// is greater than or equal to 0, and its end is less than or equal to
  /// [text]'s length.
  ///
  /// If this property is false while the [composing] range's `isValid` is true,
  /// it usually indicates the current [composing] range is invalid because of a
  /// programming error.
  bool get isComposingRangeValid => composing.isValid && composing.isNormalized && composing.end <= text.length;

  /// Returns a representation of this object as a JSON object.
  Map<String, dynamic> toJSON() {
    return <String, dynamic>{
      'text': text,
      'selectionBase': selection.baseOffset,
      'selectionExtent': selection.extentOffset,
      'selectionAffinity': selection.affinity.toString(),
      'selectionIsDirectional': selection.isDirectional,
      'composingBase': composing.start,
      'composingExtent': composing.end,
    };
  }

  @override
  String toString() => '${objectRuntimeType(this, 'TextEditingValue')}(text: \u2524$text\u251C, selection: $selection, composing: $composing)';

  @override
  bool operator ==(Object other) {
    if (identical(this, other))
      return true;
    return other is TextEditingValue
        && other.text == text
        && other.selection == selection
        && other.composing == composing;
  }

  @override
  int get hashCode => hashValues(
    text.hashCode,
    selection.hashCode,
    composing.hashCode,
  );
}

/// Indicates what triggered the change in selected text (including changes to
/// the cursor location).
enum SelectionChangedCause {
  /// The user tapped on the text and that caused the selection (or the location
  /// of the cursor) to change.
  tap,

  /// The user tapped twice in quick succession on the text and that caused
  /// the selection (or the location of the cursor) to change.
  doubleTap,

  /// The user long-pressed the text and that caused the selection (or the
  /// location of the cursor) to change.
  longPress,

  /// The user force-pressed the text and that caused the selection (or the
  /// location of the cursor) to change.
  forcePress,

  /// The user used the keyboard to change the selection or the location of the
  /// cursor.
  ///
  /// Keyboard-triggered selection changes may be caused by the IME as well as
  /// by accessibility tools (e.g. TalkBack on Android).
  keyboard,

  /// The user used the selection toolbar to change the selection or the
  /// location of the cursor.
  ///
  /// An example is when the user taps on select all in the tool bar.
  toolbar,

  /// The user used the mouse to change the selection by dragging over a piece
  /// of text.
  drag,
}

/// A mixin for manipulating the selection, provided for toolbar or shortcut
/// keys.
mixin TextSelectionDelegate {
  /// Gets the current text input.
  TextEditingValue get textEditingValue;

  /// Indicates that the user has requested the delegate to replace its current
  /// text editing state with [value].
  ///
  /// The new [value] is treated as user input and thus may subject to input
  /// formatting.
  @Deprecated(
    'Use the userUpdateTextEditingValue instead. '
    'This feature was deprecated after v1.26.0-17.2.pre.',
  )
  set textEditingValue(TextEditingValue value) {}

  /// Indicates that the user has requested the delegate to replace its current
  /// text editing state with [value].
  ///
  /// The new [value] is treated as user input and thus may subject to input
  /// formatting.
  ///
  /// See also:
  ///
  /// * [EditableTextState.userUpdateTextEditingValue]: an implementation that
  ///   applies additional pre-processing to the specified [value], before
  ///   updating the text editing state.
  void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause);

  /// Hides the text selection toolbar.
  ///
  /// By default, hideHandles is true, and the toolbar is hidden along with its
  /// handles. If hideHandles is set to false, then the toolbar will be hidden
  /// but the handles will remain.
  void hideToolbar([bool hideHandles = true]);

  /// Brings the provided [TextPosition] into the visible area of the text
  /// input.
  void bringIntoView(TextPosition position);

  /// Whether cut is enabled, must not be null.
  bool get cutEnabled => true;

  /// Whether copy is enabled, must not be null.
  bool get copyEnabled => true;

  /// Whether paste is enabled, must not be null.
  bool get pasteEnabled => true;

  /// Whether select all is enabled, must not be null.
  bool get selectAllEnabled => true;

  /// Cut current selection to [Clipboard].
  ///
  /// If and only if [cause] is [SelectionChangedCause.toolbar], the toolbar
  /// will be hidden and the current selection will be scrolled into view.
  void cutSelection(SelectionChangedCause cause);

  /// Paste text from [Clipboard].
  ///
  /// If there is currently a selection, it will be replaced.
  ///
  /// If and only if [cause] is [SelectionChangedCause.toolbar], the toolbar
  /// will be hidden and the current selection will be scrolled into view.
  Future<void> pasteText(SelectionChangedCause cause);

  /// Set the current selection to contain the entire text value.
  ///
  /// If and only if [cause] is [SelectionChangedCause.toolbar], the selection
  /// will be scrolled into view.
  void selectAll(SelectionChangedCause cause);

  /// Copy current selection to [Clipboard].
  ///
  /// If [cause] is [SelectionChangedCause.toolbar], the position of
  /// [bringIntoView] to selection will be called and hide toolbar.
  void copySelection(SelectionChangedCause cause);
}

/// An interface to receive information from [TextInput].
///
/// If [TextInputConfiguration.enableDeltaModel] is set to true,
/// [DeltaTextInputClient] must be implemented instead of this class.
///
/// See also:
///
///  * [TextInput.attach]
///  * [EditableText], a [TextInputClient] implementation.
///  * [DeltaTextInputClient], a [TextInputClient] extension that receives
///    granular information from the platform's text input.
abstract class TextInputClient {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const TextInputClient();

  /// The current state of the [TextEditingValue] held by this client.
  TextEditingValue? get currentTextEditingValue;

  /// The [AutofillScope] this [TextInputClient] belongs to, if any.
  ///
  /// It should return null if this [TextInputClient] does not need autofill
  /// support. For a [TextInputClient] that supports autofill, returning null
  /// causes it to participate in autofill alone.
  ///
  /// See also:
  ///
  /// * [AutofillGroup], a widget that creates an [AutofillScope] for its
  ///   descendent autofillable [TextInputClient]s.
  AutofillScope? get currentAutofillScope;

  /// Requests that this client update its editing state to the given value.
  ///
  /// The new [value] is treated as user input and thus may subject to input
  /// formatting.
  void updateEditingValue(TextEditingValue value);

  /// Requests that this client perform the given action.
  void performAction(TextInputAction action);

  /// Request from the input method that this client perform the given private
  /// command.
  ///
  /// This can be used to provide domain-specific features that are only known
  /// between certain input methods and their clients.
  ///
  /// See also:
  ///   * [performPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputConnection#performPrivateCommand\(java.lang.String,%20android.os.Bundle\)),
  ///     which is the Android documentation for performPrivateCommand, used to
  ///     send a command from the input method.
  ///   * [sendAppPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#sendAppPrivateCommand),
  ///     which is the Android documentation for sendAppPrivateCommand, used to
  ///     send a command to the input method.
  void performPrivateCommand(String action, Map<String, dynamic> data);

  /// Updates the floating cursor position and state.
  void updateFloatingCursor(RawFloatingCursorPoint point);

  /// Requests that this client display a prompt rectangle for the given text range,
  /// to indicate the range of text that will be changed by a pending autocorrection.
  ///
  /// This method will only be called on iOS.
  void showAutocorrectionPromptRect(int start, int end);

  /// Platform notified framework of closed connection.
  ///
  /// [TextInputClient] should cleanup its connection and finalize editing.
  void connectionClosed();
}

/// An interface to receive granular information from [TextInput].
///
/// See also:
///
///  * [TextInput.attach]
///  * [TextInputConfiguration], to opt-in to receive [TextEditingDelta]'s from
///    the platforms [TextInput] you must set [TextInputConfiguration.enableDeltaModel]
///    to true.
abstract class DeltaTextInputClient extends TextInputClient {
  /// Requests that this client update its editing state by applying the deltas
  /// received from the engine.
  ///
  /// The list of [TextEditingDelta]'s are treated as changes that will be applied
  /// to the client's editing state. A change is any mutation to the raw text
  /// value, or any updates to the selection and/or composing region.
  ///
  /// Here is an example of what implementation of this method could look like:
  /// {@tool snippet}
  /// @override
  /// void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
  ///   TextEditingValue newValue = _previousValue;
  ///   for (final TextEditingDelta delta in textEditingDeltas) {
  ///     newValue = delta.apply(newValue);
  ///   }
  ///   _localValue = newValue;
  /// }
  /// {@end-tool}
  void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas);
}

/// An interface for interacting with a text input control.
///
/// See also:
///
///  * [TextInput.attach], a method used to establish a [TextInputConnection]
///    between the system's text input and a [TextInputClient].
///  * [EditableText], a [TextInputClient] that connects to and interacts with
///    the system's text input using a [TextInputConnection].
class TextInputConnection {
  TextInputConnection._(this._client)
      : assert(_client != null),
        _id = _nextId++;

  Size? _cachedSize;
  Matrix4? _cachedTransform;
  Rect? _cachedRect;
  Rect? _cachedCaretRect;

  static int _nextId = 1;
  final int _id;

  /// Resets the internal ID counter for testing purposes.
  ///
  /// This call has no effect when asserts are disabled. Calling it from
  /// application code will likely break text input for the application.
  @visibleForTesting
  static void debugResetId({int to = 1}) {
    assert(to != null);
    assert(() {
      _nextId = to;
      return true;
    }());
  }

  final TextInputClient _client;

  /// Whether this connection is currently interacting with the text input control.
  bool get attached => TextInput._instance._currentConnection == this;

  /// Requests that the text input control become visible.
  void show() {
    assert(attached);
    TextInput._instance._show();
  }

  /// Requests the system autofill UI to appear.
  ///
  /// Currently only works on Android. Other platforms do not respond to this
  /// message.
  ///
  /// See also:
  ///
  ///  * [EditableText], a [TextInputClient] that calls this method when focused.
  void requestAutofill() {
    assert(attached);
    TextInput._instance._requestAutofill();
  }

  /// Requests that the text input control update itself according to the new
  /// [TextInputConfiguration].
  void updateConfig(TextInputConfiguration configuration) {
    assert(attached);
    TextInput._instance._updateConfig(configuration);
  }

  /// Requests that the text input control change its internal state to match
  /// the given state.
  void setEditingState(TextEditingValue value) {
    assert(attached);
    TextInput._instance._setEditingState(value);
  }

  /// Send the size and transform of the editable text to engine.
  ///
  /// The values are sent as platform messages so they can be used on web for
  /// example to correctly position and size the html input field.
  ///
  /// 1. [editableBoxSize]: size of the render editable box.
  ///
  /// 2. [transform]: a matrix that maps the local paint coordinate system
  ///                 to the [PipelineOwner.rootNode].
  void setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) {
    if (editableBoxSize != _cachedSize || transform != _cachedTransform) {
      _cachedSize = editableBoxSize;
      _cachedTransform = transform;
      TextInput._instance._setEditableSizeAndTransform(
        <String, dynamic>{
          'width': editableBoxSize.width,
          'height': editableBoxSize.height,
          'transform': transform.storage,
        },
      );
    }
  }

  /// Send the smallest rect that covers the text in the client that's currently
  /// being composed.
  ///
  /// The given `rect` can not be null. If any of the 4 coordinates of the given
  /// [Rect] is not finite, a [Rect] of size (-1, -1) will be sent instead.
  ///
  /// This information is used for positioning the IME candidates menu on each
  /// platform.
  void setComposingRect(Rect rect) {
    assert(rect != null);
    if (rect == _cachedRect)
      return;
    _cachedRect = rect;
    final Rect validRect = rect.isFinite ? rect : Offset.zero & const Size(-1, -1);
    TextInput._instance._setComposingTextRect(
      <String, dynamic>{
        'width': validRect.width,
        'height': validRect.height,
        'x': validRect.left,
        'y': validRect.top,
      },
    );
  }

  /// Sends the coordinates of caret rect. This is used on macOS for positioning
  /// the accent selection menu.
  void setCaretRect(Rect rect) {
    assert(rect != null);
    if (rect == _cachedCaretRect)
      return;
    _cachedCaretRect = rect;
    final Rect validRect = rect.isFinite ? rect : Offset.zero & const Size(-1, -1);
    TextInput._instance._setCaretRect(
      <String, dynamic>{
        'width': validRect.width,
        'height': validRect.height,
        'x': validRect.left,
        'y': validRect.top,
      },
    );
  }

  /// Send text styling information.
  ///
  /// This information is used by the Flutter Web Engine to change the style
  /// of the hidden native input's content. Hence, the content size will match
  /// to the size of the editable widget's content.
  void setStyle({
    required String? fontFamily,
    required double? fontSize,
    required FontWeight? fontWeight,
    required TextDirection textDirection,
    required TextAlign textAlign,
  }) {
    assert(attached);

    TextInput._instance._setStyle(
      <String, dynamic>{
        'fontFamily': fontFamily,
        'fontSize': fontSize,
        'fontWeightIndex': fontWeight?.index,
        'textAlignIndex': textAlign.index,
        'textDirectionIndex': textDirection.index,
      },
    );
  }

  /// Stop interacting with the text input control.
  ///
  /// After calling this method, the text input control might disappear if no
  /// other client attaches to it within this animation frame.
  void close() {
    if (attached) {
      TextInput._instance._clearClient();
    }
    assert(!attached);
  }

  /// Platform sent a notification informing the connection is closed.
  ///
  /// [TextInputConnection] should clean current client connection.
  void connectionClosedReceived() {
    TextInput._instance._currentConnection = null;
    assert(!attached);
  }
}

TextInputAction _toTextInputAction(String action) {
  switch (action) {
    case 'TextInputAction.none':
      return TextInputAction.none;
    case 'TextInputAction.unspecified':
      return TextInputAction.unspecified;
    case 'TextInputAction.go':
      return TextInputAction.go;
    case 'TextInputAction.search':
      return TextInputAction.search;
    case 'TextInputAction.send':
      return TextInputAction.send;
    case 'TextInputAction.next':
      return TextInputAction.next;
    case 'TextInputAction.previous':
      return TextInputAction.previous;
    case 'TextInputAction.continue_action':
      return TextInputAction.continueAction;
    case 'TextInputAction.join':
      return TextInputAction.join;
    case 'TextInputAction.route':
      return TextInputAction.route;
    case 'TextInputAction.emergencyCall':
      return TextInputAction.emergencyCall;
    case 'TextInputAction.done':
      return TextInputAction.done;
    case 'TextInputAction.newline':
      return TextInputAction.newline;
  }
  throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Unknown text input action: $action')]);
}

FloatingCursorDragState _toTextCursorAction(String state) {
  switch (state) {
    case 'FloatingCursorDragState.start':
      return FloatingCursorDragState.Start;
    case 'FloatingCursorDragState.update':
      return FloatingCursorDragState.Update;
    case 'FloatingCursorDragState.end':
      return FloatingCursorDragState.End;
  }
  throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Unknown text cursor action: $state')]);
}

RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, dynamic> encoded) {
  assert(state != null, 'You must provide a state to set a new editing point.');
  assert(encoded['X'] != null, 'You must provide a value for the horizontal location of the floating cursor.');
  assert(encoded['Y'] != null, 'You must provide a value for the vertical location of the floating cursor.');
  final Offset offset = state == FloatingCursorDragState.Update
    ? Offset(encoded['X'] as double, encoded['Y'] as double)
    : Offset.zero;
  return RawFloatingCursorPoint(offset: offset, state: state);
}

/// An low-level interface to the system's text input control.
///
/// To start interacting with the system's text input control, call [attach] to
/// establish a [TextInputConnection] between the system's text input control
/// and a [TextInputClient]. The majority of commands available for
/// interacting with the text input control reside in the returned
/// [TextInputConnection]. The communication between the system text input and
/// the [TextInputClient] is asynchronous.
///
/// The platform text input plugin (which represents the system's text input)
/// and the [TextInputClient] usually maintain their own text editing states
/// ([TextEditingValue]) separately. They must be kept in sync as long as the
/// [TextInputClient] is connected. The following methods can be used to send
/// [TextEditingValue] to update the other party, when either party's text
/// editing states change:
///
/// * The [TextInput.attach] method allows a [TextInputClient] to establish a
///   connection to the text input. An optional field in its `configuration`
///   parameter can be used to specify an initial value for the platform text
///   input plugin's [TextEditingValue].
///
/// * The [TextInputClient] sends its [TextEditingValue] to the platform text
///   input plugin using [TextInputConnection.setEditingState].
///
/// * The platform text input plugin sends its [TextEditingValue] to the
///   connected [TextInputClient] via a "TextInput.setEditingState" message.
///
/// * When autofill happens on a disconnected [TextInputClient], the platform
///   text input plugin sends the [TextEditingValue] to the connected
///   [TextInputClient]'s [AutofillScope], and the [AutofillScope] will further
///   relay the value to the correct [TextInputClient].
///
/// When synchronizing the [TextEditingValue]s, the communication may get stuck
/// in an infinite when both parties are trying to send their own update. To
/// mitigate the problem, only [TextInputClient]s are allowed to alter the
/// received [TextEditingValue]s while platform text input plugins are to accept
/// the received [TextEditingValue]s unmodified. More specifically:
///
/// * When a [TextInputClient] receives a new [TextEditingValue] from the
///   platform text input plugin, it's allowed to modify the value (for example,
///   apply [TextInputFormatter]s). If it decides to do so, it must send the
///   updated [TextEditingValue] back to the platform text input plugin to keep
///   the [TextEditingValue]s in sync.
///
/// * When the platform text input plugin receives a new value from the
///   connected [TextInputClient], it must accept the new value as-is, to avoid
///   sending back an updated value.
///
/// See also:
///
///  * [TextField], a widget in which the user may enter text.
///  * [EditableText], a [TextInputClient] that connects to [TextInput] when it
///    wants to take user input from the keyboard.
class TextInput {
  TextInput._() {
    _channel = SystemChannels.textInput;
    _channel.setMethodCallHandler(_handleTextInputInvocation);
  }

  /// Set the [MethodChannel] used to communicate with the system's text input
  /// control.
  ///
  /// This is only meant for testing within the Flutter SDK. Changing this
  /// will break the ability to input text. This has no effect if asserts are
  /// disabled.
  @visibleForTesting
  static void setChannel(MethodChannel newChannel) {
    assert(() {
      _instance._channel = newChannel..setMethodCallHandler(_instance._handleTextInputInvocation);
      return true;
    }());
  }

  static final TextInput _instance = TextInput._();

  static const List<TextInputAction> _androidSupportedInputActions = <TextInputAction>[
    TextInputAction.none,
    TextInputAction.unspecified,
    TextInputAction.done,
    TextInputAction.send,
    TextInputAction.go,
    TextInputAction.search,
    TextInputAction.next,
    TextInputAction.previous,
    TextInputAction.newline,
  ];

  static const List<TextInputAction> _iOSSupportedInputActions = <TextInputAction>[
    TextInputAction.unspecified,
    TextInputAction.done,
    TextInputAction.send,
    TextInputAction.go,
    TextInputAction.search,
    TextInputAction.next,
    TextInputAction.newline,
    TextInputAction.continueAction,
    TextInputAction.join,
    TextInputAction.route,
    TextInputAction.emergencyCall,
  ];

  /// Begin interacting with the text input control.
  ///
  /// Calling this function helps multiple clients coordinate about which one is
  /// currently interacting with the text input control. The returned
  /// [TextInputConnection] provides an interface for actually interacting with
  /// the text input control.
  ///
  /// A client that no longer wishes to interact with the text input control
  /// should call [TextInputConnection.close] on the returned
  /// [TextInputConnection].
  static TextInputConnection attach(TextInputClient client, TextInputConfiguration configuration) {
    assert(client != null);
    assert(configuration != null);
    final TextInputConnection connection = TextInputConnection._(client);
    _instance._attach(connection, configuration);
    return connection;
  }

  /// This method actually notifies the embedding of the client. It is utilized
  /// by [attach] and by [_handleTextInputInvocation] for the
  /// `TextInputClient.requestExistingInputState` method.
  void _attach(TextInputConnection connection, TextInputConfiguration configuration) {
    assert(connection != null);
    assert(connection._client != null);
    assert(configuration != null);
    assert(_debugEnsureInputActionWorksOnPlatform(configuration.inputAction));
    _channel.invokeMethod<void>(
      'TextInput.setClient',
      <dynamic>[ connection._id, configuration.toJson() ],
    );
    _currentConnection = connection;
    _currentConfiguration = configuration;
  }

  static bool _debugEnsureInputActionWorksOnPlatform(TextInputAction inputAction) {
    assert(() {
      if (kIsWeb) {
        // TODO(flutterweb): what makes sense here?
        return true;
      }
      if (Platform.isIOS) {
        assert(
          _iOSSupportedInputActions.contains(inputAction),
          'The requested TextInputAction "$inputAction" is not supported on iOS.',
        );
      } else if (Platform.isAndroid) {
        assert(
          _androidSupportedInputActions.contains(inputAction),
          'The requested TextInputAction "$inputAction" is not supported on Android.',
        );
      }
      return true;
    }());
    return true;
  }

  late MethodChannel _channel;

  TextInputConnection? _currentConnection;
  late TextInputConfiguration _currentConfiguration;

  Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
    if (_currentConnection == null)
      return;
    final String method = methodCall.method;

    // The requestExistingInputState request needs to be handled regardless of
    // the client ID, as long as we have a _currentConnection.
    if (method == 'TextInputClient.requestExistingInputState') {
      assert(_currentConnection!._client != null);
      _attach(_currentConnection!, _currentConfiguration);
      final TextEditingValue? editingValue = _currentConnection!._client.currentTextEditingValue;
      if (editingValue != null) {
        _setEditingState(editingValue);
      }
      return;
    }

    final List<dynamic> args = methodCall.arguments as List<dynamic>;

    // The updateEditingStateWithTag request (autofill) can come up even to a
    // text field that doesn't have a connection.
    if (method == 'TextInputClient.updateEditingStateWithTag') {
      assert(_currentConnection!._client != null);
      final TextInputClient client = _currentConnection!._client;
      final AutofillScope? scope = client.currentAutofillScope;
      final Map<String, dynamic> editingValue = args[1] as Map<String, dynamic>;
      for (final String tag in editingValue.keys) {
        final TextEditingValue textEditingValue = TextEditingValue.fromJSON(
          editingValue[tag] as Map<String, dynamic>,
        );
        final AutofillClient? client = scope?.getAutofillClient(tag);
        if (client != null && client.textInputConfiguration.autofillConfiguration.enabled) {
          client.autofill(textEditingValue);
        }
      }

      return;
    }

    final int client = args[0] as int;
    if (client != _currentConnection!._id) {
      // If the client IDs don't match, the incoming message was for a different
      // client.
      bool debugAllowAnyway = false;
      assert(() {
        // In debug builds we allow "-1" as a magical client ID that ignores
        // this verification step so that tests can always get through, even
        // when they are not mocking the engine side of text input.
        if (client == -1)
          debugAllowAnyway = true;
        return true;
      }());
      if (!debugAllowAnyway)
        return;
    }

    switch (method) {
      case 'TextInputClient.updateEditingState':
        _currentConnection!._client.updateEditingValue(TextEditingValue.fromJSON(args[1] as Map<String, dynamic>));
        break;
      case 'TextInputClient.updateEditingStateWithDeltas':
        assert(_currentConnection!._client is DeltaTextInputClient, 'You must be using a DeltaTextInputClient if TextInputConfiguration.enableDeltaModel is set to true');
        final List<TextEditingDelta> deltas = <TextEditingDelta>[];

        final Map<String, dynamic> encoded = args[1] as Map<String, dynamic>;

        for (final dynamic encodedDelta in encoded['deltas']) {
          final TextEditingDelta delta = TextEditingDelta.fromJSON(encodedDelta as Map<String, dynamic>);
          deltas.add(delta);
        }

        (_currentConnection!._client as DeltaTextInputClient).updateEditingValueWithDeltas(deltas);
        break;
      case 'TextInputClient.performAction':
        _currentConnection!._client.performAction(_toTextInputAction(args[1] as String));
        break;
      case 'TextInputClient.performPrivateCommand':
        final Map<String, dynamic> firstArg = args[1] as Map<String, dynamic>;
        _currentConnection!._client.performPrivateCommand(
          firstArg['action'] as String,
          firstArg['data'] as Map<String, dynamic>,
        );
        break;
      case 'TextInputClient.updateFloatingCursor':
        _currentConnection!._client.updateFloatingCursor(_toTextPoint(
          _toTextCursorAction(args[1] as String),
          args[2] as Map<String, dynamic>,
        ));
        break;
      case 'TextInputClient.onConnectionClosed':
        _currentConnection!._client.connectionClosed();
        break;
      case 'TextInputClient.showAutocorrectionPromptRect':
        _currentConnection!._client.showAutocorrectionPromptRect(args[1] as int, args[2] as int);
        break;
      default:
        throw MissingPluginException();
    }
  }

  bool _hidePending = false;

  void _scheduleHide() {
    if (_hidePending)
      return;
    _hidePending = true;

    // Schedule a deferred task that hides the text input. If someone else
    // shows the keyboard during this update cycle, then the task will do
    // nothing.
    scheduleMicrotask(() {
      _hidePending = false;
      if (_currentConnection == null)
        _channel.invokeMethod<void>('TextInput.hide');
    });
  }

  void _clearClient() {
    _channel.invokeMethod<void>('TextInput.clearClient');
    _currentConnection = null;
    _scheduleHide();
  }

  void _updateConfig(TextInputConfiguration configuration) {
    assert(configuration != null);
    _channel.invokeMethod<void>(
      'TextInput.updateConfig',
      configuration.toJson(),
    );
  }

  void _setEditingState(TextEditingValue value) {
    assert(value != null);
    _channel.invokeMethod<void>(
      'TextInput.setEditingState',
      value.toJSON(),
    );
  }

  void _show() {
    _channel.invokeMethod<void>('TextInput.show');
  }

  void _requestAutofill() {
    _channel.invokeMethod<void>('TextInput.requestAutofill');
  }

  void _setEditableSizeAndTransform(Map<String, dynamic> args) {
    _channel.invokeMethod<void>(
      'TextInput.setEditableSizeAndTransform',
      args,
    );
  }

  void _setComposingTextRect(Map<String, dynamic> args) {
    _channel.invokeMethod<void>(
      'TextInput.setMarkedTextRect',
      args,
    );
  }

  void _setCaretRect(Map<String, dynamic> args) {
    _channel.invokeMethod<void>(
      'TextInput.setCaretRect',
      args,
    );
  }

  void _setStyle(Map<String, dynamic> args) {
    _channel.invokeMethod<void>(
      'TextInput.setStyle',
      args,
    );
  }

  /// Finishes the current autofill context, and potentially saves the user
  /// input for future use if `shouldSave` is true.
  ///
  /// Typically, this method should be called when the user has finalized their
  /// input. For example, in a [Form], it's typically done immediately before or
  /// after its content is submitted.
  ///
  /// The topmost [AutofillGroup]s also call [finishAutofillContext]
  /// automatically when they are disposed. The default behavior can be
  /// overridden in [AutofillGroup.onDisposeAction].
  ///
  /// {@template flutter.services.TextInput.finishAutofillContext}
  /// An autofill context is a collection of input fields that live in the
  /// platform's text input plugin. The platform is encouraged to save the user
  /// input stored in the current autofill context before the context is
  /// destroyed, when [TextInput.finishAutofillContext] is called with
  /// `shouldSave` set to true.
  ///
  /// Currently, there can only be at most one autofill context at any given
  /// time. When any input field in an [AutofillGroup] requests for autofill
  /// (which is done automatically when an autofillable [EditableText] gains
  /// focus), the current autofill context will merge the content of that
  /// [AutofillGroup] into itself. When there isn't an existing autofill context,
  /// one will be created to hold the newly added input fields from the group.
  ///
  /// Once added to an autofill context, an input field will stay in the context
  /// until the context is destroyed. To prevent leaks, call
  /// [TextInput.finishAutofillContext] to signal the text input plugin that the
  /// user has finalized their input in the current autofill context. The
  /// platform text input plugin either encourages or discourages the platform
  /// from saving the user input based on the value of the `shouldSave`
  /// parameter. The platform usually shows a "Save for autofill?" prompt for
  /// user confirmation.
  /// {@endtemplate}
  ///
  /// On many platforms, calling [finishAutofillContext] shows the save user
  /// input dialog and disrupts the user's flow. Ideally the dialog should only
  /// be shown no more than once for every screen. Consider removing premature
  /// [finishAutofillContext] calls to prevent showing the save user input UI
  /// too frequently. However, calling [finishAutofillContext] when there's no
  /// existing autofill context usually does not bring up the save user input
  /// UI.
  ///
  /// See also:
  ///
  /// * [EditableText.autofillHints] for autofill save troubleshooting tips.
  /// * [AutofillGroup.onDisposeAction], a configurable action that runs when a
  ///   topmost [AutofillGroup] is getting disposed.
  static void finishAutofillContext({ bool shouldSave = true }) {
    assert(shouldSave != null);
    TextInput._instance._channel.invokeMethod<void>(
      'TextInput.finishAutofillContext',
      shouldSave,
    );
  }
}
