// 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 [KeyEventType.synchronize] and [KeyEventType.cancel] 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 Flutter detects an inconsistency between the state Flutter records and
  /// the state returned by the system, Flutter 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. Flutter normalizes 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;

  String _logicalToString() {
    final String result = '0x${logical.toRadixString(16)}';
    // Find the bits that are not included in `valueMask`, shifted to the right.
    // For example, if [logical] 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.
    final int planeNum = (logical / 0x100000000).floor();
    final String planeDescription = (() {
      switch (planeNum) {
        case 0x000:
          return ' (Unicode)';
        case 0x001:
          return ' (Unprintable)';
        case 0x002:
          return ' (Flutter)';
        case 0x017:
          return ' (Web)';
      }
      return '';
    })();
    return '$result$planeDescription';
  }

  String? _escapeCharacter() {
    if (character == null) {
      return character ?? '<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(type: ${_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: $character, '
            'synthesized: $synthesized'
           ')';
  }

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