import 'package:characters/characters.dart';

/// Detects chat messages that consist solely of emoji so the UI can render
/// them enlarged (à la WhatsApp/iMessage).
///
/// Returns the font size to use for such a message, or `null` when [text]
/// contains any non-emoji, non-whitespace character (i.e. render normally).

const int _zwj = 0x200D;
const int _vs15 = 0xFE0E;
const int _vs16 = 0xFE0F;
const int _keycap = 0x20E3;
const int _skinToneStart = 0x1F3FB;
const int _skinToneEnd = 0x1F3FF;

/// Codepoints that only ever glue an emoji sequence together and never stand
/// on their own — they don't disqualify a cluster from counting as emoji.
bool _isModifier(int r) =>
    r == _zwj ||
    r == _vs15 ||
    r == _vs16 ||
    r == _keycap ||
    (r >= _skinToneStart && r <= _skinToneEnd) || // skin-tone modifiers
    (r >= 0xE0020 && r <= 0xE007F); // tag characters (flag sequences)

/// The digits, `#` and `*` that become emoji only when combined with the
/// keycap combining mark.
bool _isKeycapBase(int r) =>
    r == 0x23 || r == 0x2A || (r >= 0x30 && r <= 0x39);

bool _isEmojiRune(int r) {
  // Bulk of modern emoji live in the astral pictograph block: emoticons,
  // transport, supplemental symbols, extended-A, regional indicators, …
  if (r >= 0x1F000 && r <= 0x1FAFF) return true;
  // Scattered BMP emoji ranges (misc symbols, dingbats, arrows, stars, …).
  if (r >= 0x2600 && r <= 0x27BF) return true;
  if (r >= 0x2300 && r <= 0x23FF) return true;
  if (r >= 0x2B00 && r <= 0x2BFF) return true;
  if (r >= 0x2190 && r <= 0x21FF) return true;
  if (r >= 0x25A0 && r <= 0x25FF) return true;
  const singles = {
    0x00A9, 0x00AE, 0x203C, 0x2049, 0x2122, 0x2139,
    0x2934, 0x2935, 0x3030, 0x303D, 0x3297, 0x3299, 0x24C2,
  };
  return singles.contains(r);
}

bool _isEmojiCluster(String cluster) {
  final runes = cluster.runes.toList(growable: false);
  final hasKeycap = runes.contains(_keycap);
  var hasEmoji = false;
  for (final r in runes) {
    if (_isModifier(r)) continue;
    if (_isEmojiRune(r)) {
      hasEmoji = true;
      continue;
    }
    if (hasKeycap && _isKeycapBase(r)) {
      hasEmoji = true;
      continue;
    }
    return false; // a plain text character → not an emoji cluster
  }
  return hasEmoji;
}

/// Maximum number of emojis a message may contain to still be enlarged; longer
/// emoji strings would turn into a wall of giant glyphs and render normally.
const int _maxEnlargedEmojis = 3;

/// Font size for an emoji-only [text] of at most [_maxEnlargedEmojis] emojis.
/// `null` means the message is not emoji-only (or has too many emojis) and
/// should render at the normal body size.
double? standaloneEmojiFontSize(String text) {
  final trimmed = text.trim();
  if (trimmed.isEmpty) return null;

  var count = 0;
  for (final cluster in trimmed.characters) {
    if (cluster.trim().isEmpty) continue; // whitespace between emojis
    if (!_isEmojiCluster(cluster)) return null;
    count++;
    if (count > _maxEnlargedEmojis) return null;
  }

  if (count == 0) return null;
  return 34;
}

/// [emoji] with any Fitzpatrick skin-tone modifier stripped, so tone variants
/// of the same emoji compare equal (👍🏻 → 👍). Used to dedupe reactions that
/// only differ in skin tone.
String emojiSkinToneNeutral(String emoji) => String.fromCharCodes(
  emoji.runes.where((r) => r < _skinToneStart || r > _skinToneEnd),
);

/// Distinct emojis contained in [text], most-frequent first with ties broken
/// by first appearance. Used to offer a message's own emojis as quick
/// reactions.
List<String> extractEmojis(String text) {
  final counts = <String, int>{};
  final firstSeen = <String, int>{};
  var order = 0;
  for (final cluster in text.characters) {
    if (!_isEmojiCluster(cluster)) continue;
    counts.update(cluster, (value) => value + 1, ifAbsent: () => 1);
    firstSeen.putIfAbsent(cluster, () => order++);
  }

  final emojis = counts.keys.toList();
  emojis.sort((a, b) {
    final byCount = counts[b]!.compareTo(counts[a]!);
    return byCount != 0 ? byCount : firstSeen[a]!.compareTo(firstSeen[b]!);
  });
  return emojis;
}
