// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Fuse from 'fuse.js';
import { sortBy } from 'lodash';
import { useEffect, useMemo, useState } from 'react';
import { z } from 'zod';
import emojiRegex from 'emoji-regex';
import * as log from '../../../logging/log';
import * as Errors from '../../../types/errors';
import type { LocaleEmojiListType } from '../../../types/emoji';
import type { LocalizerType } from '../../../types/I18N';
import { strictAssert } from '../../../util/assert';
import { drop } from '../../../util/drop';
import { parseUnknown } from '../../../util/schemas';

// Import emoji-datasource dynamically to avoid costly typechecking.
// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires
const RAW_UNTYPED_DATA: unknown = require('emoji-datasource' as string);

/**
 * Types
 */

export enum EmojiUnicodeCategory {
  SmileysAndEmotion = 'EmojiUnicodeCategory.SmileysAndEmotion',
  PeopleAndBody = 'EmojiUnicodeCategory.PeopleAndBody',
  Component = 'EmojiUnicodeCategory.Component',
  AnimalsAndNature = 'EmojiUnicodeCategory.AnimalsAndNature',
  FoodAndDrink = 'EmojiUnicodeCategory.FoodAndDrink',
  TravelAndPlaces = 'EmojiUnicodeCategory.TravelAndPlaces',
  Activities = 'EmojiUnicodeCategory.Activities',
  Objects = 'EmojiUnicodeCategory.Objects',
  Symbols = 'EmojiUnicodeCategory.Symbols',
  Flags = 'EmojiUnicodeCategory.Flags',
}

export enum EmojiPickerCategory {
  SmileysAndPeople = 'EmojiPickerCategory.SmileysAndPeople',
  AnimalsAndNature = 'EmojiPickerCategory.AnimalsAndNature',
  FoodAndDrink = 'EmojiPickerCategory.FoodAndDrink',
  TravelAndPlaces = 'EmojiPickerCategory.TravelAndPlaces',
  Activities = 'EmojiPickerCategory.Activities',
  Objects = 'EmojiPickerCategory.Objects',
  Symbols = 'EmojiPickerCategory.Symbols',
  Flags = 'EmojiPickerCategory.Flags',
}

export enum EmojiSkinTone {
  None = 'EmojiSkinTone.None',
  Type1 = 'EmojiSkinTone.Type1', // 1F3FB
  Type2 = 'EmojiSkinTone.Type2', // 1F3FC
  Type3 = 'EmojiSkinTone.Type3', // 1F3FD
  Type4 = 'EmojiSkinTone.Type4', // 1F3FE
  Type5 = 'EmojiSkinTone.Type5', // 1F3FF
}

export function isValidEmojiSkinTone(value: unknown): value is EmojiSkinTone {
  return (
    typeof value === 'string' &&
    EMOJI_SKIN_TONE_ORDER.includes(value as EmojiSkinTone)
  );
}

export const EMOJI_SKIN_TONE_ORDER: ReadonlyArray<EmojiSkinTone> = [
  EmojiSkinTone.None,
  EmojiSkinTone.Type1,
  EmojiSkinTone.Type2,
  EmojiSkinTone.Type3,
  EmojiSkinTone.Type4,
  EmojiSkinTone.Type5,
];

/** @deprecated We should use `EmojiSkinTone` everywhere */
export const EMOJI_SKIN_TONE_TO_NUMBER: Map<EmojiSkinTone, number> = new Map([
  [EmojiSkinTone.None, 0],
  [EmojiSkinTone.Type1, 1],
  [EmojiSkinTone.Type2, 2],
  [EmojiSkinTone.Type3, 3],
  [EmojiSkinTone.Type4, 4],
  [EmojiSkinTone.Type5, 5],
]);

/** @deprecated We should use `EmojiSkinTone` everywhere */
export const KEY_TO_EMOJI_SKIN_TONE = new Map<string, EmojiSkinTone>([
  ['1F3FB', EmojiSkinTone.Type1],
  ['1F3FC', EmojiSkinTone.Type2],
  ['1F3FD', EmojiSkinTone.Type3],
  ['1F3FE', EmojiSkinTone.Type4],
  ['1F3FF', EmojiSkinTone.Type5],
]);

/** @deprecated We should use `EmojiSkinTone` everywhere */
export const EMOJI_SKIN_TONE_TO_KEY: Map<EmojiSkinTone, string> = new Map([
  [EmojiSkinTone.Type1, '1F3FB'],
  [EmojiSkinTone.Type2, '1F3FC'],
  [EmojiSkinTone.Type3, '1F3FD'],
  [EmojiSkinTone.Type4, '1F3FE'],
  [EmojiSkinTone.Type5, '1F3FF'],
]);

export type EmojiParentKey = string & { EmojiParentKey: never };
export type EmojiVariantKey = string & { EmojiVariantKey: never };

export type EmojiParentValue = string & { EmojiParentValue: never };
export type EmojiVariantValue = string & { EmojiVariantValue: never };

/** @deprecated Prefer EmojiKey for refs, load short names from translations */
export type EmojiEnglishShortName = string & { EmojiEnglishShortName: never };

export type EmojiVariantData = Readonly<{
  key: EmojiVariantKey;
  value: EmojiVariantValue;
  valueNonqualified: EmojiVariantValue | null;
  sheetX: number;
  sheetY: number;
}>;

type EmojiDefaultSkinToneVariants = Record<EmojiSkinTone, EmojiVariantKey>;

export type EmojiParentData = Readonly<{
  key: EmojiParentKey;
  value: EmojiParentValue;
  valueNonqualified: EmojiParentValue | null;
  unicodeCategory: EmojiUnicodeCategory;
  pickerCategory: EmojiPickerCategory | null;
  defaultVariant: EmojiVariantKey;
  defaultSkinToneVariants: EmojiDefaultSkinToneVariants | null;
  /** @deprecated Prefer EmojiKey for refs, load short names from translations */
  englishShortNameDefault: EmojiEnglishShortName;
  /** @deprecated Prefer EmojiKey for refs, load short names from translations */
  englishShortNames: ReadonlyArray<EmojiEnglishShortName>;
  emoticonDefault: string | null;
  emoticons: ReadonlyArray<string>;
}>;

/**
 * Schemas
 */

const RawEmojiSkinToneSchema = z.object({
  unified: z.string(),
  non_qualified: z.union([z.string(), z.null()]),
  sheet_x: z.number(),
  sheet_y: z.number(),
  has_img_apple: z.boolean(),
});

const RawEmojiSkinToneMapSchema = z.record(z.string(), RawEmojiSkinToneSchema);

const RawEmojiSchema = z.object({
  unified: z.string(),
  non_qualified: z.union([z.string(), z.null()]),
  category: z.string(),
  sort_order: z.number(),
  sheet_x: z.number(),
  sheet_y: z.number(),
  has_img_apple: z.boolean(),
  short_name: z.string(),
  short_names: z.array(z.string()),
  text: z.nullable(z.string()),
  texts: z.nullable(z.array(z.string())),
  skin_variations: RawEmojiSkinToneMapSchema.optional(),
});

const RAW_UNICODE_CATEGORY_MAP: Record<string, EmojiUnicodeCategory> = {
  'Smileys & Emotion': EmojiUnicodeCategory.SmileysAndEmotion,
  'People & Body': EmojiUnicodeCategory.PeopleAndBody,
  Component: EmojiUnicodeCategory.Component,
  'Animals & Nature': EmojiUnicodeCategory.AnimalsAndNature,
  'Food & Drink': EmojiUnicodeCategory.FoodAndDrink,
  'Travel & Places': EmojiUnicodeCategory.TravelAndPlaces,
  Activities: EmojiUnicodeCategory.Activities,
  Objects: EmojiUnicodeCategory.Objects,
  Symbols: EmojiUnicodeCategory.Symbols,
  Flags: EmojiUnicodeCategory.Flags,
};

const RAW_PICKER_CATEGORY_MAP: Record<string, EmojiPickerCategory | null> = {
  'Smileys & Emotion': EmojiPickerCategory.SmileysAndPeople, // merged
  'People & Body': EmojiPickerCategory.SmileysAndPeople, // merged
  Component: null, // dropped
  'Animals & Nature': EmojiPickerCategory.AnimalsAndNature,
  'Food & Drink': EmojiPickerCategory.FoodAndDrink,
  'Travel & Places': EmojiPickerCategory.TravelAndPlaces,
  Activities: EmojiPickerCategory.Activities,
  Objects: EmojiPickerCategory.Objects,
  Symbols: EmojiPickerCategory.Symbols,
  Flags: EmojiPickerCategory.Flags,
};

/**
 * Data Normalization
 */

function toEmojiUnicodeCategory(category: string): EmojiUnicodeCategory {
  const result = RAW_UNICODE_CATEGORY_MAP[category];
  strictAssert(result != null, `Unknown category: ${category}`);
  return result;
}

function toEmojiPickerCategory(category: string): EmojiPickerCategory | null {
  const result = RAW_PICKER_CATEGORY_MAP[category];
  strictAssert(
    typeof result !== 'undefined',
    `Unknown picker category: ${category}`
  );
  return result;
}

function toEmojiParentKey(unified: string): EmojiParentKey {
  return unified as EmojiParentKey;
}

function toEmojiVariantKey(unified: string): EmojiVariantKey {
  return unified as EmojiVariantKey;
}

function encodeUnified(unified: string): string {
  return unified
    .split('-')
    .map(char => String.fromCodePoint(Number.parseInt(char, 16)))
    .join('');
}

function toEmojiParentValue(unified: string): EmojiParentValue {
  return encodeUnified(unified) as EmojiParentValue;
}

function toEmojiVariantValue(unified: string): EmojiVariantValue {
  return encodeUnified(unified) as EmojiVariantValue;
}

const RAW_EMOJI_DATA = parseUnknown(
  z.array(RawEmojiSchema),
  RAW_UNTYPED_DATA
).sort((a, b) => {
  return a.sort_order - b.sort_order;
});

type EmojiSearchIndexEntry = Readonly<{
  key: EmojiParentKey;
  rank: number | null;
  shortName: string;
  shortNames: ReadonlyArray<string>;
  emoticon: string | null;
  emoticons: ReadonlyArray<string>;
}>;

type EmojiSearchIndex = ReadonlyArray<EmojiSearchIndexEntry>;

type EmojiIndex = Readonly<{
  // raw data
  parentByKey: Record<EmojiParentKey, EmojiParentData>;
  parentKeysByName: Record<EmojiEnglishShortName, EmojiParentKey>;
  parentKeysByValue: Record<EmojiParentValue, EmojiParentKey>;
  parentKeysByValueNonQualified: Record<EmojiParentValue, EmojiParentKey>;
  parentKeysByVariantKeys: Record<EmojiVariantKey, EmojiParentKey>;

  variantByKey: Record<EmojiVariantKey, EmojiVariantData>;
  variantKeysByValue: Record<EmojiVariantValue, EmojiVariantKey>;
  variantKeysByValueNonQualified: Record<EmojiVariantValue, EmojiVariantKey>;

  unicodeCategories: Record<EmojiUnicodeCategory, Array<EmojiParentKey>>;
  pickerCategories: Record<EmojiPickerCategory, Array<EmojiParentKey>>;

  defaultEnglishSearchIndex: Array<EmojiSearchIndexEntry>;
}>;

const EMOJI_INDEX: EmojiIndex = {
  parentByKey: {},
  parentKeysByValue: {},
  parentKeysByValueNonQualified: {},
  parentKeysByName: {},
  parentKeysByVariantKeys: {},
  variantByKey: {},
  variantKeysByValue: {},
  variantKeysByValueNonQualified: {},
  unicodeCategories: {
    [EmojiUnicodeCategory.SmileysAndEmotion]: [],
    [EmojiUnicodeCategory.PeopleAndBody]: [],
    [EmojiUnicodeCategory.Component]: [],
    [EmojiUnicodeCategory.AnimalsAndNature]: [],
    [EmojiUnicodeCategory.FoodAndDrink]: [],
    [EmojiUnicodeCategory.TravelAndPlaces]: [],
    [EmojiUnicodeCategory.Activities]: [],
    [EmojiUnicodeCategory.Objects]: [],
    [EmojiUnicodeCategory.Symbols]: [],
    [EmojiUnicodeCategory.Flags]: [],
  },
  pickerCategories: {
    [EmojiPickerCategory.SmileysAndPeople]: [],
    [EmojiPickerCategory.AnimalsAndNature]: [],
    [EmojiPickerCategory.FoodAndDrink]: [],
    [EmojiPickerCategory.TravelAndPlaces]: [],
    [EmojiPickerCategory.Activities]: [],
    [EmojiPickerCategory.Objects]: [],
    [EmojiPickerCategory.Symbols]: [],
    [EmojiPickerCategory.Flags]: [],
  },
  defaultEnglishSearchIndex: [],
};

function addParent(parent: EmojiParentData, rank: number) {
  EMOJI_INDEX.parentByKey[parent.key] = parent;
  EMOJI_INDEX.parentKeysByValue[parent.value] = parent.key;
  if (parent.valueNonqualified != null) {
    EMOJI_INDEX.parentKeysByValue[parent.valueNonqualified] = parent.key;
    EMOJI_INDEX.parentKeysByValueNonQualified[parent.valueNonqualified] =
      parent.key;
  }
  EMOJI_INDEX.parentKeysByName[parent.englishShortNameDefault] = parent.key;
  EMOJI_INDEX.unicodeCategories[parent.unicodeCategory].push(parent.key);
  if (parent.pickerCategory != null) {
    EMOJI_INDEX.pickerCategories[parent.pickerCategory].push(parent.key);
  }

  for (const englishShortName of parent.englishShortNames) {
    EMOJI_INDEX.parentKeysByName[englishShortName] = parent.key;
  }

  EMOJI_INDEX.defaultEnglishSearchIndex.push({
    key: parent.key,
    rank,
    shortName: parent.englishShortNameDefault,
    shortNames: parent.englishShortNames,
    emoticon: parent.emoticonDefault,
    emoticons: parent.emoticons,
  });
}

function addVariant(parentKey: EmojiParentKey, variant: EmojiVariantData) {
  EMOJI_INDEX.parentKeysByVariantKeys[variant.key] = parentKey;
  EMOJI_INDEX.variantByKey[variant.key] = variant;
  EMOJI_INDEX.variantKeysByValue[variant.value] = variant.key;
  if (variant.valueNonqualified) {
    EMOJI_INDEX.variantKeysByValue[variant.valueNonqualified] = variant.key;
    EMOJI_INDEX.variantKeysByValueNonQualified[variant.valueNonqualified] =
      variant.key;
  }
}

for (const rawEmoji of RAW_EMOJI_DATA) {
  const parentKey = toEmojiParentKey(rawEmoji.unified);

  const defaultVariant: EmojiVariantData = {
    key: toEmojiVariantKey(rawEmoji.unified),
    value: toEmojiVariantValue(rawEmoji.unified),
    valueNonqualified:
      rawEmoji.non_qualified != null
        ? toEmojiVariantValue(rawEmoji.non_qualified)
        : null,
    sheetX: rawEmoji.sheet_x,
    sheetY: rawEmoji.sheet_y,
  };

  addVariant(parentKey, defaultVariant);

  let defaultSkinToneVariants: EmojiDefaultSkinToneVariants | null = null;
  if (rawEmoji.skin_variations != null) {
    const map = new Map<string, EmojiVariantKey>();

    for (const [key, value] of Object.entries(rawEmoji.skin_variations)) {
      const variantKey = toEmojiVariantKey(value.unified);
      map.set(key, variantKey);

      const skinToneVariant: EmojiVariantData = {
        key: variantKey,
        value: toEmojiVariantValue(value.unified),
        valueNonqualified:
          rawEmoji.non_qualified != null
            ? toEmojiVariantValue(rawEmoji.non_qualified)
            : null,
        sheetX: value.sheet_x,
        sheetY: value.sheet_y,
      };

      addVariant(parentKey, skinToneVariant);
    }

    const result: Partial<EmojiDefaultSkinToneVariants> = {};
    for (const [key, skinTone] of KEY_TO_EMOJI_SKIN_TONE) {
      const one = map.get(key) ?? null;
      const two = map.get(`${key}-${key}`) ?? null;
      const variantKey = one ?? two;
      if (variantKey == null) {
        const keys = Object.keys(rawEmoji.skin_variations);
        throw new Error(`Missing variant key ${parentKey} -> ${key} (${keys})`);
      }
      result[skinTone] = variantKey;
    }

    defaultSkinToneVariants = result as EmojiDefaultSkinToneVariants;
  }

  const parent: EmojiParentData = {
    key: toEmojiParentKey(rawEmoji.unified),
    value: toEmojiParentValue(rawEmoji.unified),
    valueNonqualified:
      rawEmoji.non_qualified != null
        ? toEmojiParentValue(rawEmoji.non_qualified)
        : null,
    unicodeCategory: toEmojiUnicodeCategory(rawEmoji.category),
    pickerCategory: toEmojiPickerCategory(rawEmoji.category),
    defaultVariant: defaultVariant.key,
    defaultSkinToneVariants,
    englishShortNameDefault: rawEmoji.short_name as EmojiEnglishShortName,
    englishShortNames: rawEmoji.short_names as Array<EmojiEnglishShortName>,
    emoticonDefault: rawEmoji.text ?? null,
    emoticons: rawEmoji.texts ?? [],
  };

  addParent(parent, rawEmoji.sort_order);
}

export function isEmojiParentKey(input: string): input is EmojiParentKey {
  return Object.hasOwn(EMOJI_INDEX.parentByKey, input);
}

export function isEmojiVariantKey(input: string): input is EmojiVariantKey {
  return Object.hasOwn(EMOJI_INDEX.variantByKey, input);
}

export function isEmojiParentValue(input: string): input is EmojiParentValue {
  return Object.hasOwn(EMOJI_INDEX.parentKeysByValue, input);
}

export function isEmojiVariantValue(input: string): input is EmojiVariantValue {
  return Object.hasOwn(EMOJI_INDEX.variantKeysByValue, input);
}

export function isEmojiVariantValueNonQualified(
  input: EmojiVariantValue
): boolean {
  return Object.hasOwn(EMOJI_INDEX.variantKeysByValueNonQualified, input);
}

/** @deprecated Prefer EmojiKey for refs, load short names from translations */
export function isEmojiEnglishShortName(
  input: string
): input is EmojiEnglishShortName {
  return Object.hasOwn(EMOJI_INDEX.parentKeysByName, input);
}

export function getEmojiParentByKey(key: EmojiParentKey): EmojiParentData {
  const data = EMOJI_INDEX.parentByKey[key];
  strictAssert(data, `Missing emoji parent data for key "${key}"`);
  return data;
}

export function getEmojiVariantByKey(key: EmojiVariantKey): EmojiVariantData {
  const data = EMOJI_INDEX.variantByKey[key];
  strictAssert(data, `Missing emoji variant data for key "${key}"`);
  return data;
}

export function getEmojiParentKeyByValue(
  value: EmojiParentValue
): EmojiParentKey {
  const key = EMOJI_INDEX.parentKeysByValue[value];
  strictAssert(key, `Missing emoji parent key for value "${value}"`);
  return key;
}

export function getEmojiVariantKeyByValue(
  value: EmojiVariantValue
): EmojiVariantKey {
  const key = EMOJI_INDEX.variantKeysByValue[value];
  strictAssert(key, `Missing emoji variant key for value "${value}"`);
  return key;
}

export function getEmojiParentKeyByVariantKey(
  key: EmojiVariantKey
): EmojiParentKey {
  const parentKey = EMOJI_INDEX.parentKeysByVariantKeys[key];
  strictAssert(parentKey, `Missing parent key for variant key "${key}"`);
  return parentKey;
}

export function getEmojiUnicodeCategoryParentKeys(
  category: EmojiUnicodeCategory
): ReadonlyArray<EmojiParentKey> {
  const parents = EMOJI_INDEX.unicodeCategories[category];
  strictAssert(parents, `Missing category emojis for ${category}`);
  return parents;
}

export function getEmojiPickerCategoryParentKeys(
  category: EmojiPickerCategory
): ReadonlyArray<EmojiParentKey> {
  const parents = EMOJI_INDEX.pickerCategories[category];
  strictAssert(parents, `Missing category emojis for ${category}`);
  return parents;
}

/**
 * Apply a skin tone (if possible) to any parent key.
 */
export function getEmojiVariantByParentKeyAndSkinTone(
  key: EmojiParentKey,
  skinTone: EmojiSkinTone | null
): EmojiVariantData {
  const parent = getEmojiParentByKey(key);
  const skinToneVariants = parent.defaultSkinToneVariants;

  if (
    skinTone == null ||
    skinTone === EmojiSkinTone.None ||
    skinToneVariants == null
  ) {
    return getEmojiVariantByKey(parent.defaultVariant);
  }

  const variantKey = skinToneVariants[skinTone];
  strictAssert(variantKey, `Missing skin tone variant for ${skinTone}`);

  return getEmojiVariantByKey(variantKey);
}

/** @deprecated */
export function getEmojiParentKeyByEnglishShortName(
  englishShortName: EmojiEnglishShortName
): EmojiParentKey {
  const emojiKey = EMOJI_INDEX.parentKeysByName[englishShortName];
  strictAssert(emojiKey, `Missing emoji info for ${englishShortName}`);
  return emojiKey;
}

/** Exported for testing */
export function* _allEmojiVariantKeys(): Iterable<EmojiVariantKey> {
  yield* Object.keys(EMOJI_INDEX.variantByKey) as Array<EmojiVariantKey>;
}

export function emojiParentKeyConstant(input: string): EmojiParentKey {
  strictAssert(
    isEmojiParentValue(input),
    `Missing emoji parent for value "${input}"`
  );
  return getEmojiParentKeyByValue(input);
}

export function emojiVariantConstant(input: string): EmojiVariantData {
  strictAssert(
    isEmojiVariantValue(input),
    `Missing emoji variant for value "${input}"`
  );
  const key = getEmojiVariantKeyByValue(input);
  return getEmojiVariantByKey(key);
}

/**
 * Search
 */

export type EmojiSearch = (
  query: string,
  limit?: number
) => Array<EmojiParentKey>;

function createEmojiSearchIndex(
  localeEmojiList: LocaleEmojiListType
): EmojiSearchIndex {
  const results: Array<EmojiSearchIndexEntry> = [];

  for (const localeEmoji of localeEmojiList) {
    if (!isEmojiParentValue(localeEmoji.emoji)) {
      // Skipping unknown emoji, most likely apple doesn't support it
      continue;
    }

    const parentKey = getEmojiParentKeyByValue(localeEmoji.emoji);
    const emoji = getEmojiParentByKey(parentKey);
    results.push({
      key: parentKey,
      rank: localeEmoji.rank,
      shortName: localeEmoji.shortName,
      shortNames: localeEmoji.tags,
      emoticon: emoji.emoticonDefault,
      emoticons: emoji.emoticons,
    });
  }

  return results;
}

const FuseKeys: Array<Fuse.FuseOptionKey> = [
  { name: 'shortName', weight: 100 },
  { name: 'shortNames', weight: 1 },
  { name: 'emoticon', weight: 50 },
  { name: 'emoticons', weight: 1 },
];

const FuseFuzzyOptions: Fuse.IFuseOptions<EmojiSearchIndexEntry> = {
  shouldSort: false,
  threshold: 0.2,
  minMatchCharLength: 1,
  keys: FuseKeys,
  includeScore: true,
};

const FuseExactOptions: Fuse.IFuseOptions<EmojiSearchIndexEntry> = {
  shouldSort: false,
  threshold: 0,
  minMatchCharLength: 1,
  keys: FuseKeys,
  includeScore: true,
};

function createEmojiSearch(emojiSearchIndex: EmojiSearchIndex): EmojiSearch {
  const fuseIndex = Fuse.createIndex(FuseKeys, emojiSearchIndex);
  const fuseFuzzy = new Fuse(emojiSearchIndex, FuseFuzzyOptions, fuseIndex);
  const fuseExact = new Fuse(emojiSearchIndex, FuseExactOptions, fuseIndex);

  return function emojiSearch(query, limit = 200) {
    // Prefer exact matches at 2 characters
    const fuse = query.length < 2 ? fuseExact : fuseFuzzy;

    const rawResults = fuse.search(query.substring(0, 32), {
      limit: limit * 2,
    });

    const rankedResults = rawResults.map(result => {
      const rank = result.item.rank ?? 1e9;

      // Exact prefix matches in [0,1] range
      if (result.item.shortName.startsWith(query)) {
        return {
          score: result.item.shortName.length / query.length,
          item: result.item,
        };
      }

      // Other matches in [1,], ordered by score and rank
      return {
        score: 1 + (result.score ?? 0) + rank / emojiSearchIndex.length,
        item: result.item,
      };
    });

    const sortedResults = sortBy(rankedResults, result => {
      return result.score;
    });

    const truncatedResults = sortedResults.slice(0, limit);

    return truncatedResults.map(result => {
      return result.item.key;
    });
  };
}

export function useEmojiSearch(i18n: LocalizerType): EmojiSearch {
  const locale = i18n.getLocale();
  const [localeIndex, setLocaleIndex] = useState<EmojiSearchIndex | null>(null);

  useEffect(() => {
    let canceled = false;

    async function run() {
      try {
        const list = await window.SignalContext.getLocalizedEmojiList(locale);
        if (!canceled) {
          const result = createEmojiSearchIndex(list);
          setLocaleIndex(result);
        }
      } catch (error) {
        log.error(
          `Failed to get localized emoji list for ${locale}`,
          Errors.toLogFormat(error)
        );
      }
    }

    drop(run());

    return () => {
      canceled = true;
    };
  }, [locale]);

  const searchIndex = useMemo(() => {
    return localeIndex ?? EMOJI_INDEX.defaultEnglishSearchIndex;
  }, [localeIndex]);

  const emojiSearch = useMemo(() => {
    return createEmojiSearch(searchIndex);
  }, [searchIndex]);

  return emojiSearch;
}

/**
 *
 */

function isSafeEmojifyEmoji(value: string): value is EmojiVariantValue {
  return isEmojiVariantValue(value) && !isEmojiVariantValueNonQualified(value);
}

export type EmojiSpan = Readonly<{
  index: number;
  length: number;
  emoji: EmojiVariantValue;
}>;

export type EmojifyData = Readonly<{
  text: string;
  emojiCount: number;
  isEmojiOnlyText: boolean;
}>;

export function getEmojifyData(input: string): EmojifyData {
  // Fast path, and treat empty strings like they have non-emoji text
  if (input === '' || input.trim() === '') {
    return { text: input, emojiCount: 0, isEmojiOnlyText: false };
  }

  let hasEmojis = false;
  let hasNonEmojis = false;
  let emojiCount = 0;

  const regex = emojiRegex();

  let match = regex.exec(input);
  let lastIndex = 0;
  while (match) {
    const value = match[0];

    // Only consider safe emojis as matches
    if (isSafeEmojifyEmoji(value)) {
      const { index } = match;
      hasEmojis = true;
      // Track if we skipped over any text
      if (index > lastIndex) {
        hasNonEmojis = true;
        lastIndex += index;
      }
      emojiCount += 1;
      // Needs to be the value.length not the match.length
      lastIndex = index + value.length;
    }

    match = regex.exec(input);
  }

  // Track if we had any remaining text
  if (lastIndex === 0 || lastIndex < input.length) {
    hasNonEmojis = true;
  }

  return {
    text: input,
    emojiCount,
    isEmojiOnlyText: hasEmojis && !hasNonEmojis,
  };
}
