import { useMappingHelper } from '@shopify/flash-list'
import { Image } from 'expo-image'
import { memo, useMemo } from 'react'
import type { CustomEmoji } from 'src/lib/api-types'
import { openBrowserAsync } from 'src/utils'
import { Paragraph, Text } from 'tamagui'

const linkRegex =
  /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g
const mentionRegex = /@[.\w]+(?:@\w+\.\w+(?:\.\w+)*)?/g
const hashtagRegex = /#[\p{L}\p{N}]+(?:[\p{L}\p{N}_-]*[\p{L}\p{N}])?/gu
const emojiRegex = /:([a-zA-Z0-9_]{2,}):/g

export interface Part {
  type: 'text' | 'mention' | 'hashtag' | 'link' | 'emoji'
  value: string
  emoji?: CustomEmoji
}

export interface Match extends Part {
  type: 'mention' | 'hashtag' | 'link' | 'emoji'
  index: number
}

// Every captioned post pays for four regex scans + a sort + a node walk.
// With FlashList v2 recycling, the same caption can run this pipeline
// multiple times as cells bind/unbind. Memoize by (text, emojis-ref) so the
// second and later renders are a map lookup. The emojis array identity is
// stable per post (it comes from the Status object), so `(text, emojis)`
// works as a composite key.
const partsCache = new WeakMap<object, Map<string, Part[]>>()
// For callers without emojis we key the inner map on a sentinel object so we
// can still reuse the outer WeakMap structure.
const NO_EMOJIS: CustomEmoji[] = []

function getCachedParts(text: string, emojis: CustomEmoji[] | undefined): Part[] {
  const key = emojis && emojis.length > 0 ? emojis : NO_EMOJIS
  let byText = partsCache.get(key)
  if (!byText) {
    byText = new Map()
    partsCache.set(key, byText)
  }
  const hit = byText.get(text)
  if (hit) return hit
  const matches = parseText(text, emojis)
  const parts = getTextParts(text, matches)
  byText.set(text, parts)
  return parts
}

export function parseText(text: string, emojis?: Array<CustomEmoji>) {
  const matches: Match[] = []
  let match: RegExpExecArray | null
  const linkRangeIndices: { start: number; end: number }[] = []

  while ((match = linkRegex.exec(text)) !== null) {
    matches.push({ type: 'link', value: match[0], index: match.index })
    linkRangeIndices.push({ start: match.index, end: match.index + match[0].length })
  }

  while ((match = mentionRegex.exec(text)) !== null) {
    // make sure it does not parse parts of links
    // for we check that the previous character needs to be a whitespace or the beginning of the text
    if (match.index == 0 || [' ', '\n'].includes(text[match.index - 1])) {
      matches.push({ type: 'mention', value: match[0], index: match.index })
    }
  }

  while ((match = hashtagRegex.exec(text)) !== null) {
    const matchIndex = (match as RegExpExecArray).index
    // make sure it does not parse parts of links
    if (
      linkRangeIndices.findLastIndex(
        ({ start, end }) => matchIndex > start && matchIndex < end
      ) === -1
    ) {
      matches.push({ type: 'hashtag', value: match[0], index: match.index })
    }
  }

  if (emojis && emojis.length > 0) {
    const emojiMap = new Map<string, CustomEmoji>()
    for (const e of emojis) {
      if (e?.shortcode) emojiMap.set(e.shortcode, e)
    }
    if (emojiMap.size > 0) {
      while ((match = emojiRegex.exec(text)) !== null) {
        const matchIndex = match.index
        if (
          linkRangeIndices.findLastIndex(
            ({ start, end }) => matchIndex > start && matchIndex < end
          ) !== -1
        ) {
          continue
        }
        const emoji = emojiMap.get(match[1])
        if (emoji) {
          matches.push({
            type: 'emoji',
            value: match[0],
            index: matchIndex,
            emoji,
          })
        }
      }
    }
  }

  matches.sort((a, b) => a.index - b.index)

  return matches
}

export function getTextParts(text: string, matches: Match[]) {
  let lastIndex = 0
  const parts: Part[] = []

  matches.forEach((match) => {
    if (lastIndex < match.index) {
      parts.push({ type: 'text', value: text.slice(lastIndex, match.index) })
    }
    const part: Part = { type: match.type, value: match.value }
    if (match.emoji) part.emoji = match.emoji
    parts.push(part)
    lastIndex = match.index + match.value.length
  })

  if (lastIndex < text?.length) {
    parts.push({ type: 'text', value: text.slice(lastIndex) })
  }

  return parts
}

interface AutolinkTextProps {
  text: string
  emojis?: Array<CustomEmoji>
  onMentionPress: (mention: string, is_local_mention: boolean) => void
  onHashtagPress: (hashtag: string) => void
}

interface AutolinkTextPropsWithUsername extends AutolinkTextProps {
  username: string
  onUsernamePress: () => void
}

// Emoji images render inline inside `<Text>` via RN's native text-composition.
// Size is fixed slightly above the $body font size (14) so they cap-align with
// the surrounding glyphs; the negative marginBottom nudges them back onto the
// baseline rather than floating above it.
const EMOJI_SIZE = 19

function AutolinkTextInner(props: AutolinkTextProps | AutolinkTextPropsWithUsername) {
  const { text, emojis, onMentionPress, onHashtagPress } = props

  const { username, onUsernamePress } = props as AutolinkTextPropsWithUsername

  const parts = useMemo(() => getCachedParts(text, emojis), [text, emojis])
  const { getMappingKey } = useMappingHelper()

  return (
    <Paragraph size="$body" color="$color">
      {username ? (
        <Text
          fontWeight="600"
          color="$color"
          suppressHighlighting
          onPress={() => onUsernamePress()}
        >
          {username}{' '}
        </Text>
      ) : null}
      {parts?.map((part, index) => {
        // getMappingKey provides stable keys across recycled cells so FlashList
        // v2 can preserve text node identity when a post rebinds.
        const key = getMappingKey(`${part.type}:${part.value}`, index)
        if (part.type === 'mention') {
          return (
            <Text
              key={key}
              color="$interactiveActive"
              suppressHighlighting
              onPress={() =>
                onMentionPress(part.value, part.value.lastIndexOf('@') === 0)
              }
            >
              {part.value}
            </Text>
          )
        }

        if (part.type === 'hashtag') {
          return (
            <Text
              key={key}
              color="$interactiveActive"
              suppressHighlighting
              onPress={() => onHashtagPress(part.value.slice(1))}
            >
              {part.value}
            </Text>
          )
        }

        if (part.type === 'link') {
          return (
            <Text
              key={key}
              color="$interactiveActive"
              suppressHighlighting
              onPress={() => openBrowserAsync(part.value)}
            >
              {part.value}
            </Text>
          )
        }

        if (part.type === 'emoji' && part.emoji) {
          return (
            <Text key={key}>
              <Image
                source={{ uri: part.emoji.url }}
                style={{
                  width: EMOJI_SIZE,
                  height: EMOJI_SIZE,
                  marginBottom: -Math.round(EMOJI_SIZE * 0.15),
                }}
                contentFit="contain"
                cachePolicy="memory-disk"
                accessibilityLabel={part.value}
              />
            </Text>
          )
        }

        return <Text key={key}>{part.value}</Text>
      })}
    </Paragraph>
  )
}

// Parts derive from (text, emojis). Recycling in FlashList swaps `text` and
// `emojis` in place; unless a consumer also changes one of the callbacks,
// memoizing here skips the whole reconcile.
const AutolinkText = memo(AutolinkTextInner, (prev, next) => {
  return (
    prev.text === next.text &&
    prev.emojis === next.emojis &&
    prev.onMentionPress === next.onMentionPress &&
    prev.onHashtagPress === next.onHashtagPress &&
    (prev as AutolinkTextPropsWithUsername).username ===
      (next as AutolinkTextPropsWithUsername).username &&
    (prev as AutolinkTextPropsWithUsername).onUsernamePress ===
      (next as AutolinkTextPropsWithUsername).onUsernamePress
  )
})

export default AutolinkText

export function onMentionPressMethod(
  gotoUsernameProfile: (username: string) => void,
  authorAccountUrl: string
) {
  return (mention: string, is_username_local: boolean) => {
    let username = mention
    if (is_username_local) {
      const authors_homeserver = new URL(authorAccountUrl).hostname
      username = `${mention}@${authors_homeserver}`
    }
    gotoUsernameProfile(username)
  }
}
