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

part of dart.ui;

/// The type of a key event.
// Must match the KeyEventType enum in ui/window/key_data.h.
enum KeyEventType {
  /// The key is pressed.
  down,

  /// The key is released.
  up,

  /// The key is held, causing a repeated key input.
  repeat,
}

/// Information about a key event.
class KeyData {
  /// Creates an object that represents a key event.
  const KeyData({
    required this.timeStamp,
    required this.type,
    required this.physical,
    required this.logical,
    required this.character,
    required this.synthesized,
  });

  /// Time of event dispatch, relative to an arbitrary timeline.
  ///
  /// For synthesized events, the [timeStamp] might not be the actual time that
  /// the key press or release happens.
  final Duration timeStamp;

  /// The type of the event.
  final KeyEventType type;

  /// The key code for the physical key that has changed.
  final int physical;

  /// The key code for the logical key that has changed.
  final int logical;

  /// Character input from the event.
  ///
  /// Ignored for up events.
  final String? character;

  /// If [synthesized] is true, this event does not correspond to a native event.
  ///
  /// Although most of Flutter's keyboard events are transformed from native
  /// events, some events are not based on native events, and are synthesized
  /// only to conform Flutter's key event model (as documented in
  /// the `HardwareKeyboard` class in the framework).
  ///
  /// For example, some key downs or ups might be lost when the window loses
  /// focus. Some platforms provides ways to query whether a key is being held.
  /// If the embedder detects an inconsistency between its internal record and
  /// the state returned by the system, the embedder will synthesize a
  /// corresponding event to synchronize the state without breaking the event
  /// model.
  ///
  /// As another example, macOS treats CapsLock in a special way by sending
  /// down and up events at the down of alterate presses to indicate the
  /// direction in which the lock is toggled instead of that the physical key is
  /// going. A macOS embedder should normalize the behavior by converting a
  /// native down event into a down event followed immediately by a synthesized
  /// up event, and the native up event also into a down event followed
  /// immediately by a synthesized up event.
  ///
  /// Synthesized events do not have a trustworthy [timeStamp], and should not be
  /// processed as if the key actually went down or up at the time of the
  /// callback.
  ///
  /// [KeyRepeatEvent] is never synthesized.
  final bool synthesized;

  // Returns the bits that are not included in [valueMask], shifted to the
  // right.
  //
  // For example, if the input is 0x12abcdabcd, then the result is 0x12.
  //
  // This is mostly equivalent to a right shift, resolving the problem that
  // JavaScript only support 32-bit bitwise operations and needs to use division
  // instead.
  static int _nonValueBits(int n) {
    const int valueMask = 0x000FFFFFFFF;
    // `n >> valueMaskWidth` is equivalent to `n / divisorForValueMask`.
    const int divisorForValueMask = valueMask + 1;
    const int valueMaskWidth = 32;

    // Equivalent to assert(divisorForValueMask == (1 << valueMaskWidth)).
    const int firstDivisorWidth = 28;
    assert(divisorForValueMask ==
      (1 << firstDivisorWidth) * (1 << (valueMaskWidth - firstDivisorWidth)));

    // JS only supports up to 2^53 - 1, therefore non-value bits can only
    // contain (maxSafeIntegerWidth - valueMaskWidth) bits.
    const int maxSafeIntegerWidth = 52;
    const int nonValueMask = (1 << (maxSafeIntegerWidth - valueMaskWidth)) - 1;

    if (identical(0, 0.0)) { // Detects if we are on the web.
      return (n / divisorForValueMask).floor() & nonValueMask;
    } else {
      return (n >> valueMaskWidth) & nonValueMask;
    }
  }

  String _logicalToString() {
    final String result = '0x${logical.toRadixString(16)}';
    final int planeNum = _nonValueBits(logical) & 0x0FF;
    final String planeDescription = (() {
      switch (planeNum) {
        case 0x000:
          return ' (Unicode)';
        case 0x001:
          return ' (Unprintable)';
        case 0x002:
          return ' (Flutter)';
        case 0x011:
          return ' (Android)';
        case 0x012:
          return ' (Fuchsia)';
        case 0x013:
          return ' (iOS)';
        case 0x014:
          return ' (macOS)';
        case 0x015:
          return ' (GTK)';
        case 0x016:
          return ' (Windows)';
        case 0x017:
          return ' (Web)';
        case 0x018:
          return ' (GLFW)';
      }
      return '';
    })();
    return '$result$planeDescription';
  }

  String? _escapeCharacter() {
    if (character == null) {
      return '<none>';
    }
    switch (character!) {
      case '\n':
        return r'"\n"';
      case '\t':
        return r'"\t"';
      case '\r':
        return r'"\r"';
      case '\b':
        return r'"\b"';
      case '\f':
        return r'"\f"';
      default:
        return '"$character"';
    }
  }

  String? _quotedCharCode() {
    if (character == null) {
      return '';
    }
    final Iterable<String> hexChars = character!.codeUnits
        .map((int code) => code.toRadixString(16).padLeft(2, '0'));
    return ' (0x${hexChars.join(' ')})';
  }

  @override
  String toString() => 'KeyData(key ${_typeToString(type)}, physical: 0x${physical.toRadixString(16)}, '
    'logical: ${_logicalToString()}, character: ${_escapeCharacter()}${_quotedCharCode()}${synthesized ? ', synthesized' : ''})';

  /// Returns a complete textual description of the information in this object.
  String toStringFull() {
    return '$runtimeType('
            'type: ${_typeToString(type)}, '
            'timeStamp: $timeStamp, '
            'physical: 0x${physical.toRadixString(16)}, '
            'logical: 0x${logical.toRadixString(16)}, '
            'character: ${_escapeCharacter()}, '
            'synthesized: $synthesized'
           ')';
  }

  static String _typeToString(KeyEventType type) {
    switch (type) {
      case KeyEventType.up:
        return 'up';
      case KeyEventType.down:
        return 'down';
      case KeyEventType.repeat:
        return 'repeat';
    }
  }
}
