import {
  CustomEmoji,
  CustomEmojiSummaryItem,
  EmojiShortcutEntry
} from '../../../shared-kernel';

export const CUSTOM_EMOJI_MAX_SIZE_BYTES = 1024 * 1024;
export const CUSTOM_EMOJI_ALLOWED_MIME_TYPES = [
  'image/webp',
  'image/gif',
  'image/jpeg'
] as const;
export const CUSTOM_EMOJI_ACCEPT_ATTRIBUTE = '.webp,.gif,.jpg,.jpeg,image/webp,image/gif,image/jpeg';
export const CUSTOM_EMOJI_DATA_CHANNEL_CHUNK_SIZE_BYTES = 4 * 1024;
export const CUSTOM_EMOJI_INLINE_MAX_JSON_BYTES = 48 * 1024;
export const CUSTOM_EMOJI_MAX_TRANSFER_CHUNKS = Math.ceil((CUSTOM_EMOJI_MAX_SIZE_BYTES * 4 / 3) / CUSTOM_EMOJI_DATA_CHANNEL_CHUNK_SIZE_BYTES) + 2;
export interface UnicodeEmojiPickerEntry {
  emoji: string;
  searchTerms: readonly string[];
}

export const UNICODE_EMOJI_PICKER_ENTRIES: readonly UnicodeEmojiPickerEntry[] = [
  { emoji: '👍',
    searchTerms: [
      'thumbs',
      'up',
      'like',
      'thumb',
      '+1'
    ] },
  { emoji: '❤️',
    searchTerms: [
      'heart',
      'love',
      'red'
    ] },
  { emoji: '😂',
    searchTerms: [
      'joy',
      'laugh',
      'lol',
      'tears',
      'funny'
    ] },
  { emoji: '😮',
    searchTerms: [
      'wow',
      'surprised',
      'shock',
      'open',
      'mouth'
    ] },
  { emoji: '😢',
    searchTerms: [
      'cry',
      'sad',
      'tear'
    ] },
  { emoji: '🎉',
    searchTerms: [
      'party',
      'celebrate',
      'tada',
      'confetti'
    ] },
  { emoji: '🔥',
    searchTerms: [
      'fire',
      'lit',
      'hot',
      'flame'
    ] },
  { emoji: '👀',
    searchTerms: [
      'eyes',
      'look',
      'watch',
      'see'
    ] }
];

export const DEFAULT_UNICODE_EMOJIS = UNICODE_EMOJI_PICKER_ENTRIES.map((entry) => entry.emoji);

export interface ChatTextSegment {
  kind: 'text' | 'emoji';
  value: string;
}

const UNICODE_EMOJI_IN_TEXT_PATTERN =
  /\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?)*|[#*0-9]\uFE0F?\u20E3/gu;

export function splitTextIntoEmojiSegments(text: string): ChatTextSegment[] {
  if (!text) {
    return [];
  }

  const segments: ChatTextSegment[] = [];

  let lastIndex = 0;

  for (const match of text.matchAll(UNICODE_EMOJI_IN_TEXT_PATTERN)) {
    const index = match.index ?? 0;

    if (index > lastIndex) {
      segments.push({ kind: 'text',
        value: text.slice(lastIndex, index) });
    }

    segments.push({ kind: 'emoji',
      value: match[0] });

    lastIndex = index + match[0].length;
  }

  if (lastIndex < text.length) {
    segments.push({ kind: 'text',
      value: text.slice(lastIndex) });
  }

  return segments.length > 0 ? segments : [
    { kind: 'text',
      value: text }
  ];
}

export interface CustomEmojiFileLike {
  name: string;
  size: number;
  type: string;
}

export interface CustomEmojiValidationResult {
  valid: boolean;
  reason?: string;
}

export interface SelectEmojiShortcutEntriesInput {
  customEmojis: readonly CustomEmoji[];
  usage: ReadonlyMap<string, number>;
  limit?: number;
}

const CUSTOM_EMOJI_TOKEN_PATTERN = /:emoji\[([^\]\s]+)]\(([^)]+)\)/g;
const CUSTOM_EMOJI_TOKEN_PRESENT_PATTERN = /:emoji\[[^\]\s]+]\([^)]+\)/;
const CUSTOM_EMOJI_TEXT_ALIAS_PATTERN = /:([a-z0-9_-]+):/g;
const CUSTOM_EMOJI_DATA_URL_PATTERN = /^data:([^;,]+);base64,(.*)$/;
const CUSTOM_EMOJI_MARKDOWN_ALT_PREFIX = 'custom-emoji:';
const CUSTOM_EMOJI_PENDING_DATA_URL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

export interface CustomEmojiDataUrlTransfer {
  mime: string;
  chunks: string[];
  total: number;
}

export function validateCustomEmojiFile(file: CustomEmojiFileLike): CustomEmojiValidationResult {
  if (file.size > CUSTOM_EMOJI_MAX_SIZE_BYTES) {
    return { valid: false,
      reason: 'Emoji images can be max 1 MB.' };
  }

  if (!CUSTOM_EMOJI_ALLOWED_MIME_TYPES.includes(file.type.toLowerCase() as typeof CUSTOM_EMOJI_ALLOWED_MIME_TYPES[number])) {
    return { valid: false,
      reason: 'Emoji images must be WebP, GIF, JPG, or JPEG.' };
  }

  return { valid: true };
}

export function selectEmojiShortcutEntries(input: SelectEmojiShortcutEntriesInput): EmojiShortcutEntry[] {
  const limit = input.limit ?? 7;
  const customByKey = new Map(input.customEmojis.map((emoji) => [customEmojiKey(emoji.id), emoji]));
  const entries: EmojiShortcutEntry[] = [];
  const usedKeys = new Set<string>();
  const rankedKeys = [...input.usage.entries()]
    .filter(([, count]) => count > 0)
    .sort((first, second) => second[1] - first[1] || first[0].localeCompare(second[0]));

  for (const [key] of rankedKeys) {
    const entry = entryFromKey(key, customByKey);

    if (!entry || usedKeys.has(entry.key)) {
      continue;
    }

    entries.push(entry);
    usedKeys.add(entry.key);

    if (entries.length >= limit) {
      return entries;
    }
  }

  for (const emoji of DEFAULT_UNICODE_EMOJIS) {
    const key = unicodeEmojiKey(emoji);

    if (usedKeys.has(key)) {
      continue;
    }

    entries.push({ kind: 'unicode',
      key,
      emoji,
      label: emoji });

    usedKeys.add(key);

    if (entries.length >= limit) {
      return entries;
    }
  }

  for (const emoji of input.customEmojis) {
    const key = customEmojiKey(emoji.id);

    if (usedKeys.has(key)) {
      continue;
    }

    entries.push({ kind: 'custom',
      key,
      emoji,
      label: emoji.name });

    usedKeys.add(key);

    if (entries.length >= limit) {
      return entries;
    }
  }

  return entries;
}

export function buildCustomEmojiMessageToken(emoji: CustomEmoji): string {
  return `:emoji[${emoji.id}](${emoji.name})`;
}

export function buildCustomEmojiTextAlias(emoji: CustomEmoji): string {
  return `:${emoji.name}:`;
}

export function replaceCustomEmojiTextAliases(
  content: string,
  resolveEmoji: (name: string) => CustomEmoji | null | undefined
): string {
  return content.replace(CUSTOM_EMOJI_TEXT_ALIAS_PATTERN, (token, name: string) => {
    const emoji = resolveEmoji(name);

    return emoji ? buildCustomEmojiMessageToken(emoji) : token;
  });
}

export function replaceCustomEmojiTokensForPreview(content: string): string {
  return content.replace(CUSTOM_EMOJI_TOKEN_PATTERN, (_token, _id: string, name: string) => `:${name}:`);
}

export function replaceCustomEmojiMessageTokens(
  content: string,
  resolveEmoji: (id: string) => CustomEmoji | null | undefined
): string {
  return content.replace(CUSTOM_EMOJI_TOKEN_PATTERN, (token, id: string, name: string) => {
    const emoji = resolveEmoji(id);

    if (!emoji) {
      return `![${CUSTOM_EMOJI_MARKDOWN_ALT_PREFIX}${id}:${name}](${CUSTOM_EMOJI_PENDING_DATA_URL})`;
    }

    return `![${CUSTOM_EMOJI_MARKDOWN_ALT_PREFIX}${id}:${name}](${emoji.dataUrl})`;
  });
}

export function extractCustomEmojiIds(content: string): string[] {
  return [...content.matchAll(CUSTOM_EMOJI_TOKEN_PATTERN)].reduce<string[]>((ids, match) => {
    const id = match[1];

    if (id && !ids.includes(id)) {
      ids.push(id);
    }

    return ids;
  }, []);
}

export function getCustomEmojiIdFromMarkdownAlt(alt?: string): string | null {
  if (!alt?.startsWith(CUSTOM_EMOJI_MARKDOWN_ALT_PREFIX)) {
    return null;
  }

  const id = alt.slice(CUSTOM_EMOJI_MARKDOWN_ALT_PREFIX.length).split(':', 1)[0]?.trim();

  return id || null;
}

export function isCustomEmojiMarkdownAlt(alt?: string): boolean {
  return !!getCustomEmojiIdFromMarkdownAlt(alt);
}

export const CUSTOM_EMOJI_CONTEXT_MENU_TARGET = 'data-custom-emoji';
export const CUSTOM_EMOJI_LIBRARY_CONTEXT_MENU_TARGET = 'data-custom-emoji-library';
export const CUSTOM_EMOJI_ID_ATTRIBUTE = 'data-custom-emoji-id';

export type CustomEmojiContextMenuAction = 'add' | 'remove';

export interface CustomEmojiContextMenuTarget {
  action: CustomEmojiContextMenuAction;
  emojiId: string;
}

export function resolveCustomEmojiContextMenuTarget(target: EventTarget | null): CustomEmojiContextMenuTarget | null {
  const element = getElementFromEventTarget(target);

  if (!element) {
    return null;
  }

  const libraryHost = element.closest(`[${CUSTOM_EMOJI_LIBRARY_CONTEXT_MENU_TARGET}]`);

  if (libraryHost) {
    const emojiId = libraryHost.getAttribute(CUSTOM_EMOJI_ID_ATTRIBUTE)?.trim();

    return emojiId ? { action: 'remove',
      emojiId } : null;
  }

  const addHost = element.closest(`[${CUSTOM_EMOJI_CONTEXT_MENU_TARGET}]`);

  if (addHost) {
    const emojiId = addHost.getAttribute(CUSTOM_EMOJI_ID_ATTRIBUTE)?.trim();

    return emojiId ? { action: 'add',
      emojiId } : null;
  }

  return null;
}

function getElementFromEventTarget(target: EventTarget | null): Element | null {
  if (target && typeof (target as Element).closest === 'function') {
    return target as Element;
  }

  const parentElement = (target as Node | null)?.parentElement;

  return parentElement && typeof parentElement.closest === 'function'
    ? parentElement
    : null;
}

export function getCustomEmojiLabelFromMarkdownAlt(alt?: string): string {
  if (!alt?.startsWith(CUSTOM_EMOJI_MARKDOWN_ALT_PREFIX)) {
    return alt || 'Custom emoji';
  }

  return alt.slice(CUSTOM_EMOJI_MARKDOWN_ALT_PREFIX.length).split(':')
    .slice(1)
    .join(':') || 'Custom emoji';
}

export function isCustomEmojiOnlyMessage(content: string): boolean {
  const withoutTokens = content.replace(CUSTOM_EMOJI_TOKEN_PATTERN, '').trim();

  return withoutTokens.length === 0 && CUSTOM_EMOJI_TOKEN_PRESENT_PATTERN.test(content);
}

export function isSingleUnicodeEmojiOnlyMessage(content: string): boolean {
  if (CUSTOM_EMOJI_TOKEN_PRESENT_PATTERN.test(content)) {
    return false;
  }

  const segments = splitTextIntoEmojiSegments(content.trim());

  return segments.length === 1 && segments[0]?.kind === 'emoji';
}

export function buildCustomEmojiSummary(emoji: CustomEmoji): CustomEmojiSummaryItem {
  return {
    id: emoji.id,
    hash: emoji.hash,
    updatedAt: emoji.updatedAt
  };
}

export function splitCustomEmojiDataUrl(dataUrl: string, chunkSize = CUSTOM_EMOJI_DATA_CHANNEL_CHUNK_SIZE_BYTES): CustomEmojiDataUrlTransfer {
  const match = dataUrl.match(CUSTOM_EMOJI_DATA_URL_PATTERN);

  if (!match || chunkSize < 1) {
    return {
      chunks: [],
      mime: 'image/webp',
      total: 0
    };
  }

  const mime = match[1] || 'image/webp';
  const base64 = match[2] || '';
  const chunks: string[] = [];

  for (let offset = 0; offset < base64.length; offset += chunkSize) {
    chunks.push(base64.slice(offset, offset + chunkSize));
  }

  return {
    chunks,
    mime,
    total: chunks.length
  };
}

export function assembleCustomEmojiDataUrl(mime: string, chunks: readonly string[]): string {
  return `data:${mime};base64,${chunks.join('')}`;
}

export function getCustomEmojiDataUrlByteSize(dataUrl: string): number | null {
  const match = dataUrl.match(CUSTOM_EMOJI_DATA_URL_PATTERN);

  if (!match) {
    return null;
  }

  const base64 = match[2] || '';
  const padding = base64.endsWith('==') ? 2 : (base64.endsWith('=') ? 1 : 0);

  return Math.floor(base64.length * 3 / 4) - padding;
}

export function canInlineCustomEmojiTransfer(emoji: CustomEmoji): boolean {
  const envelope = JSON.stringify({
    type: 'custom-emoji-full',
    customEmoji: emoji
  });

  return new TextEncoder().encode(envelope).byteLength <= CUSTOM_EMOJI_INLINE_MAX_JSON_BYTES;
}

export function shouldRequestCustomEmoji(local: CustomEmoji | undefined, remote: CustomEmojiSummaryItem): boolean {
  if (!local) {
    return true;
  }

  return local.hash !== remote.hash || local.updatedAt < remote.updatedAt;
}

export function unicodeEmojiKey(emoji: string): string {
  return `unicode:${emoji}`;
}

export function customEmojiKey(id: string): string {
  return `custom:${id}`;
}

export function normalizeEmojiPickerSearchQuery(query: string): string {
  return query.trim().toLowerCase();
}

export function matchesEmojiPickerSearch(
  searchTerms: readonly string[],
  emoji: string,
  query: string
): boolean {
  if (emoji.includes(query)) {
    return true;
  }

  return searchTerms.some((term) => term.includes(query));
}

export function filterUnicodeEmojiPickerEntries(
  entries: readonly UnicodeEmojiPickerEntry[],
  query: string
): UnicodeEmojiPickerEntry[] {
  const normalizedQuery = normalizeEmojiPickerSearchQuery(query);

  if (!normalizedQuery) {
    return [...entries];
  }

  return entries.filter((entry) => matchesEmojiPickerSearch(entry.searchTerms, entry.emoji, normalizedQuery));
}

export function filterCustomEmojisForPicker(
  emojis: readonly CustomEmoji[],
  query: string
): CustomEmoji[] {
  const normalizedQuery = normalizeEmojiPickerSearchQuery(query);

  if (!normalizedQuery) {
    return [...emojis];
  }

  return emojis.filter((emoji) => emoji.name.toLowerCase().includes(normalizedQuery));
}

function entryFromKey(key: string, customByKey: ReadonlyMap<string, CustomEmoji>): EmojiShortcutEntry | null {
  if (key.startsWith('unicode:')) {
    const emoji = key.slice('unicode:'.length);

    return emoji ? { kind: 'unicode', key, emoji, label: emoji } : null;
  }

  const custom = customByKey.get(key);

  return custom ? { kind: 'custom', key, emoji: custom, label: custom.name } : null;
}
