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

// The following segment is not only used in the generating script, but also
// copied to the generated package.
/*@@@ SHARED SEGMENT START @@@*/

/// Used in the final mapping indicating the logical key should be derived from
/// KeyboardEvent.keyCode.
///
/// This value is chosen because it's a printable character within EASCII that
/// will never be mapped to (checked in the marshalling algorithm).
const int kUseKeyCode = 0xFF;

/// Used in the final mapping indicating the event key is 'Dead', the dead key.
final String _kUseDead = String.fromCharCode(0xFE);

/// The KeyboardEvent.key for a dead key.
const String _kEventKeyDead = 'Dead';

/// A map of all goals from the scan codes to their mapped value in US layout.
const Map<String, String> kLayoutGoals = <String, String>{
  'KeyA': 'a',
  'KeyB': 'b',
  'KeyC': 'c',
  'KeyD': 'd',
  'KeyE': 'e',
  'KeyF': 'f',
  'KeyG': 'g',
  'KeyH': 'h',
  'KeyI': 'i',
  'KeyJ': 'j',
  'KeyK': 'k',
  'KeyL': 'l',
  'KeyM': 'm',
  'KeyN': 'n',
  'KeyO': 'o',
  'KeyP': 'p',
  'KeyQ': 'q',
  'KeyR': 'r',
  'KeyS': 's',
  'KeyT': 't',
  'KeyU': 'u',
  'KeyV': 'v',
  'KeyW': 'w',
  'KeyX': 'x',
  'KeyY': 'y',
  'KeyZ': 'z',
  'Digit1': '1',
  'Digit2': '2',
  'Digit3': '3',
  'Digit4': '4',
  'Digit5': '5',
  'Digit6': '6',
  'Digit7': '7',
  'Digit8': '8',
  'Digit9': '9',
  'Digit0': '0',
  'Minus': '-',
  'Equal': '=',
  'BracketLeft': '[',
  'BracketRight': ']',
  'Backslash': r'\',
  'Semicolon': ';',
  'Quote': "'",
  'Backquote': '`',
  'Comma': ',',
  'Period': '.',
  'Slash': '/',
};

final int _kLowerA = 'a'.codeUnitAt(0);
final int _kUpperA = 'A'.codeUnitAt(0);
final int _kLowerZ = 'z'.codeUnitAt(0);
final int _kUpperZ = 'Z'.codeUnitAt(0);

bool _isAscii(int charCode) {
  // 0x20 is the first printable character in ASCII.
  return charCode >= 0x20 && charCode <= 0x7F;
}

/// Returns whether the `char` is a single character of a letter or a digit.
bool isLetter(int charCode) {
  return (charCode >= _kLowerA && charCode <= _kLowerZ)
      || (charCode >= _kUpperA && charCode <= _kUpperZ);
}

/// A set of rules that can derive a large number of logical keys simply from
/// the event's code and key.
///
/// This greatly reduces the entries needed in the final mapping.
int? heuristicMapper(String code, String key) {
  // Digit code: return the digit by event code.
  if (code.startsWith('Digit')) {
    assert(code.length == 6);
    return code.codeUnitAt(5); // The character immediately after 'Digit'
  }
  final int charCode = key.codeUnitAt(0);
  // Non-ascii: return the goal (i.e. US mapping by event code).
  if (key.length > 1 || !_isAscii(charCode)) {
    return kLayoutGoals[code]?.codeUnitAt(0);
  }
  // Letter key: return the event key letter.
  if (isLetter(charCode)) {
    return key.toLowerCase().codeUnitAt(0);
  }
  return null;
}

// Maps an integer to a printable EASCII character by adding it to this value.
//
// We could've chosen 0x20, the first printable character, for a slightly bigger
// range, but it's prettier this way and sufficient.
final int _kMarshallIntBase = '0'.codeUnitAt(0);

class _StringStream {
  _StringStream(this._data) : _offset = 0;

  final String _data;
  final Map<int, String> _goalToEventCode = Map<int, String>.fromEntries(
    kLayoutGoals
      .entries
      .map((MapEntry<String, String> beforeEntry) =>
          MapEntry<int, String>(beforeEntry.value.codeUnitAt(0), beforeEntry.key))
  );

  int get offest => _offset;
  int _offset;

  int readIntAsVerbatim() {
    final int result = _data.codeUnitAt(_offset);
    _offset += 1;
    assert(result >= _kMarshallIntBase);
    return result - _kMarshallIntBase;
  }

  int readIntAsChar() {
    final int result = _data.codeUnitAt(_offset);
    _offset += 1;
    return result;
  }

  String readEventKey() {
    final String char = String.fromCharCode(readIntAsChar());
    if (char == _kUseDead) {
      return _kEventKeyDead;
    } else {
      return char;
    }
  }

  String readEventCode() {
    final int charCode = _data.codeUnitAt(_offset);
    _offset += 1;
    return _goalToEventCode[charCode]!;
  }
}

Map<String, int> _unmarshallCodeMap(_StringStream stream) {
  final int entryNum = stream.readIntAsVerbatim();
  return <String, int>{
    for (int i = 0; i < entryNum; i++)
      stream.readEventKey(): stream.readIntAsChar(),
  };
}

/// Decode a key mapping data out of the string.
Map<String, Map<String, int>> unmarshallMappingData(String compressed) {
  final _StringStream stream = _StringStream(compressed);
  final int eventCodeNum = stream.readIntAsVerbatim();
  return <String, Map<String, int>>{
    for (int i = 0; i < eventCodeNum; i++)
      stream.readEventCode() : _unmarshallCodeMap(stream),
  };
}

/*@@@ SHARED SEGMENT END @@@*/

/// Whether the given charCode is a ASCII letter.
bool isLetterChar(int charCode) {
  return (charCode >= _kLowerA && charCode <= _kLowerZ)
      || (charCode >= _kUpperA && charCode <= _kUpperZ);
}

bool _isPrintableEascii(int charCode) {
  return charCode >= 0x20 && charCode <= 0xFF;
}

typedef _ForEachAction<V> = void Function(String key, V value);
void _sortedForEach<V>(Map<String, V> map, _ForEachAction<V> action) {
  map
    .entries
    .toList()
    ..sort((MapEntry<String, V> a, MapEntry<String, V> b) => a.key.compareTo(b.key))
    ..forEach((MapEntry<String, V> entry) {
      action(entry.key, entry.value);
    });
}

// Encode a small integer as a character by its value.
//
// For example, 0x48 is encoded as '0'. This means that values within 0x0 - 0x19
// or greater than 0xFF are forbidden.
void _marshallIntAsChar(StringBuffer builder, int value) {
  assert(_isPrintableEascii(value), '$value');
  builder.writeCharCode(value);
}

const int _kMarshallIntEnd = 0xFF; // The last printable EASCII.
// Encode a small integer as a character based on a certain printable codepoint.
//
// For example, 0x0 is encoded as '0', and 0x1 is encoded as '1'. This function
// allows smaller values than _marshallIntAsChar.
void _marshallIntAsVerbatim(StringBuffer builder, int value) {
  final int result = value + _kMarshallIntBase;
  assert(result <= _kMarshallIntEnd);
  builder.writeCharCode(result);
}

void _marshallEventCode(StringBuffer builder, String value) {
  // Instead of recording the entire eventCode, since the eventCode is mapped
  // 1-to-1 to a character in kLayoutGoals, we record the goal instead.
  final String char = kLayoutGoals[value]!;
  builder.write(char);
}

void _marshallEventKey(StringBuffer builder, String value) {
  if (value == _kEventKeyDead) {
    builder.write(_kUseDead);
  } else {
    assert(value.length == 1, value);
    assert(value != _kUseDead);
    builder.write(value);
  }
}

/// Encode a key mapping data into a list of strings.
///
/// The list of strings should be used concatenated, but is returned this way
/// for aesthetic purposes (one entry per line).
///
/// The algorithm aims at encoding the map directly into a printable string
/// (instead of a binary stream converted by base64). Some characters in the
/// string can be multi-byte, which means the decoder should parse the string
/// using substr instead of as a binary stream.
List<String> marshallMappingData(Map<String, Map<String, int>> mappingData) {
  final StringBuffer builder = StringBuffer();
  _marshallIntAsVerbatim(builder, mappingData.length);
  _sortedForEach(mappingData, (String eventCode, Map<String, int> codeMap) {
    builder.write('\n');
    _marshallEventCode(builder, eventCode);
    _marshallIntAsVerbatim(builder, codeMap.length);
    _sortedForEach(codeMap, (String eventKey, int logicalKey) {
      _marshallEventKey(builder, eventKey);
      _marshallIntAsChar(builder, logicalKey);
    });
  });
  return builder.toString().split('\n');
}
