import {
  cloneElement,
  Fragment,
  isValidElement,
  useCallback,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
} from 'react'
import {
  BackHandler,
  Keyboard,
  type LayoutChangeEvent,
  Pressable,
  type StyleProp,
  useWindowDimensions,
  View,
  type ViewStyle,
} from 'react-native'
import {
  Gesture,
  GestureDetector,
  type GestureStateChangeEvent,
  type GestureUpdateEvent,
  type PanGestureHandlerEventPayload,
} from 'react-native-gesture-handler'
import {KeyboardEvents} from 'react-native-keyboard-controller'
import Animated, {
  clamp,
  interpolate,
  runOnJS,
  type SharedValue,
  useAnimatedReaction,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  type WithSpringConfig,
} from 'react-native-reanimated'
import {
  type EdgeInsets,
  useSafeAreaFrame,
  useSafeAreaInsets,
} from 'react-native-safe-area-context'
import {captureRef} from 'react-native-view-shot'
import {Image, type ImageErrorEventData} from 'expo-image'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useIsFocused} from '@react-navigation/native'
import flattenReactChildren from 'react-keyed-flatten-children'

import {HITSLOP_10} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {logger} from '#/logger'
import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
import {atoms as a, flatten, platform, tokens, useTheme} from '#/alf'
import {
  Context,
  ItemContext,
  MenuContext,
  useContextMenuContext,
  useContextMenuItemContext,
  useContextMenuMenuContext,
} from '#/components/ContextMenu/context'
import {
  type AuxiliaryViewProps,
  type ContextType,
  type ItemIconProps,
  type ItemProps,
  type ItemTextProps,
  type Measurement,
  type TriggerProps,
} from '#/components/ContextMenu/types'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {createPortalGroup} from '#/components/Portal'
import {Text} from '#/components/Typography'
import {IS_ANDROID, IS_IOS} from '#/env'
import {Backdrop} from './Backdrop'

export {
  type DialogControlProps as ContextMenuControlProps,
  useDialogControl as useContextMenuControl,
} from '#/components/Dialog'

const {Provider: PortalProvider, Outlet, Portal} = createPortalGroup()

const SPRING_IN: WithSpringConfig = {
  mass: 0.75,
  damping: 300,
  stiffness: 1200,
  restDisplacementThreshold: 0.01,
}

const SPRING_OUT: WithSpringConfig = {
  mass: IS_IOS ? 1.25 : 0.75,
  damping: 150,
  stiffness: 1000,
  restDisplacementThreshold: 0.01,
}

/**
 * Needs placing near the top of the provider stack, but BELOW the theme provider.
 */
export function Provider({children}: {children: React.ReactNode}) {
  return (
    <PortalProvider>
      {children}
      <Outlet />
    </PortalProvider>
  )
}

export function Root({children}: {children: React.ReactNode}) {
  const playHaptic = useHaptics()
  const [mode, setMode] = useState<'full' | 'auxiliary-only'>('full')
  const [measurement, setMeasurement] = useState<Measurement | null>(null)
  const returnLocationSV = useSharedValue<{x: number; y: number} | null>(null)
  const animationSV = useSharedValue(0)
  const translationSV = useSharedValue(0)
  const isFocused = useIsFocused()
  const hoverables = useRef<
    Map<string, {id: string; rect: Measurement; onTouchUp: () => void}>
  >(new Map())
  const hoverablesSV = useSharedValue<
    Record<string, {id: string; rect: Measurement}>
  >({})
  const syncHoverablesThrottleRef =
    useRef<ReturnType<typeof setTimeout>>(undefined)
  const [hoveredMenuItem, setHoveredMenuItem] = useState<string | null>(null)

  const onHoverableTouchUp = useCallback((id: string) => {
    const hoverable = hoverables.current.get(id)
    if (!hoverable) {
      logger.warn(`No such hoverable with id ${id}`)
      return
    }
    hoverable.onTouchUp()
  }, [])

  const onCompletedClose = useCallback(() => {
    hoverables.current.clear()
    setMeasurement(null)
  }, [])

  const context = useMemo(
    () =>
      ({
        isOpen: !!measurement && isFocused,
        measurement,
        returnLocationSV,
        animationSV,
        translationSV,
        mode,
        open: (evt: Measurement, mode: 'full' | 'auxiliary-only') => {
          setMeasurement(evt)
          setMode(mode)
          animationSV.set(withSpring(1, SPRING_IN))
          // reset return location
          returnLocationSV.set(null)
        },
        close: () => {
          animationSV.set(
            withSpring(0, SPRING_OUT, finished => {
              if (finished) {
                hoverablesSV.set({})
                translationSV.set(0)
                // note: return location has to be reset on open,
                // rather than on close, otherwise there's a flicker
                // where the reanimated update is faster than the react render
                runOnJS(onCompletedClose)()
              }
            }),
          )
        },
        registerHoverable: (
          id: string,
          rect: Measurement,
          onTouchUp: () => void,
        ) => {
          hoverables.current.set(id, {id, rect, onTouchUp})
          // we need this data on the UI thread, but we want to limit cross-thread communication
          // and this function will be called in quick succession, so we need to throttle it
          if (syncHoverablesThrottleRef.current)
            clearTimeout(syncHoverablesThrottleRef.current)
          syncHoverablesThrottleRef.current = setTimeout(() => {
            syncHoverablesThrottleRef.current = undefined
            hoverablesSV.set(
              Object.fromEntries(
                // eslint-ignore
                [...hoverables.current.entries()].map(([id, {rect}]) => [
                  id,
                  {id, rect},
                ]),
              ),
            )
          }, 1)
        },
        hoverablesSV,
        onTouchUpMenuItem: onHoverableTouchUp,
        hoveredMenuItem,
        setHoveredMenuItem: item => {
          if (item) playHaptic('Light')
          setHoveredMenuItem(item)
        },
      }) satisfies ContextType,
    [
      measurement,
      returnLocationSV,
      setMeasurement,
      onCompletedClose,
      isFocused,
      animationSV,
      translationSV,
      hoverablesSV,
      onHoverableTouchUp,
      hoveredMenuItem,
      setHoveredMenuItem,
      playHaptic,
      mode,
    ],
  )

  useEffect(() => {
    if (IS_ANDROID && context.isOpen) {
      const listener = BackHandler.addEventListener('hardwareBackPress', () => {
        context.close()
        return true
      })

      return () => listener.remove()
    }
  }, [context])

  return <Context.Provider value={context}>{children}</Context.Provider>
}

export function Trigger({
  children,
  label,
  contentLabel,
  style,
  onTap,
}: TriggerProps) {
  const context = useContextMenuContext()
  const playHaptic = useHaptics()
  const insets = useSafeAreaInsets()
  const ref = useRef<View>(null)
  const isFocused = useIsFocused()
  const [image, setImage] = useState<string | null>(null)
  const [pendingMeasurement, setPendingMeasurement] = useState<{
    measurement: Measurement
    mode: 'full' | 'auxiliary-only'
  } | null>(null)

  const open = useNonReactiveCallback(
    async (mode: 'full' | 'auxiliary-only') => {
      playHaptic()
      const [measurement, capture] = await Promise.all([
        measureView(ref.current, insets),
        captureRef(ref, {result: 'data-uri'}).catch(err => {
          logger.error(err instanceof Error ? err : String(err), {
            message: 'Failed to capture image of context menu trigger',
          })
          // will cause the image to fail to load, but it will get handled gracefully
          return '<failed capture>'
        }),
      ])
      Keyboard.dismiss()
      setImage(capture)
      if (measurement) {
        setPendingMeasurement({measurement, mode})
      }
    },
  )

  // after keyboard hides, the position might change - set a return location
  useEffect(() => {
    if (context.isOpen && context.measurement) {
      const hide = KeyboardEvents.addListener('keyboardDidHide', () => {
        measureView(ref.current, insets)
          .then(newMeasurement => {
            if (!newMeasurement || !context.measurement) return
            if (
              newMeasurement.x !== context.measurement.x ||
              newMeasurement.y !== context.measurement.y
            ) {
              context.returnLocationSV.set({
                x: newMeasurement.x,
                y: newMeasurement.y,
              })
            }
          })
          .catch(() => {})
      })

      return () => {
        hide.remove()
      }
    }
  }, [context, insets])

  const tapGesture = useMemo(() => {
    const gesture = Gesture.Tap()
      .numberOfTaps(1)
      .cancelsTouchesInView(false)
      .runOnJS(true)
    if (onTap) {
      gesture.onEnd(() => void onTap())
    }
    return gesture
  }, [onTap])

  const doubleTapGesture = useMemo(() => {
    return Gesture.Tap()
      .numberOfTaps(2)
      .hitSlop(HITSLOP_10)
      .onEnd(() => void open('auxiliary-only'))
      .runOnJS(true)
  }, [open])

  const {
    hoverablesSV,
    setHoveredMenuItem,
    onTouchUpMenuItem,
    translationSV,
    animationSV,
  } = context
  const hoveredItemSV = useSharedValue<string | null>(null)

  useAnimatedReaction(
    () => hoveredItemSV.get(),
    (hovered, prev) => {
      if (hovered !== prev) {
        runOnJS(setHoveredMenuItem)(hovered)
      }
    },
  )

  const pressAndHoldGesture = useMemo(() => {
    return Gesture.Pan()
      .activateAfterLongPress(500)
      .cancelsTouchesInView(false)
      .averageTouches(true)
      .onStart(() => {
        'worklet'
        runOnJS(open)('full')
      })
      .onUpdate(evt => {
        'worklet'
        const item = getHoveredHoverable(evt, hoverablesSV, translationSV)
        hoveredItemSV.set(item)
      })
      .onEnd(() => {
        'worklet'
        // don't recalculate hovered item - if they haven't moved their finger from
        // the initial press, it's jarring to then select the item underneath
        // as the menu may have slid into place beneath their finger
        const item = hoveredItemSV.get()
        if (item) {
          runOnJS(onTouchUpMenuItem)(item)
        }
      })
  }, [open, hoverablesSV, onTouchUpMenuItem, hoveredItemSV, translationSV])

  // Order matters here: doubleTapGesture must come before tapGesture.
  const composedGestures = Gesture.Exclusive(
    doubleTapGesture,
    tapGesture,
    pressAndHoldGesture,
  )

  const measurement = context.measurement || pendingMeasurement?.measurement

  return (
    <>
      <GestureDetector gesture={composedGestures}>
        <View ref={ref} style={[{opacity: context.isOpen ? 0 : 1}, style]}>
          {children({
            IS_NATIVE: true,
            control: {
              isOpen: context.isOpen,
              open: mode => void open(mode),
            },
            state: {
              pressed: false,
              hovered: false,
              focused: false,
            },
            props: {
              ref: null,
              onPress: null,
              onFocus: null,
              onBlur: null,
              onPressIn: null,
              onPressOut: null,
              accessibilityHint: null,
              accessibilityLabel: label,
              accessibilityRole: null,
            },
          })}
        </View>
      </GestureDetector>
      {isFocused && image && measurement && (
        <Portal>
          <TriggerClone
            label={contentLabel}
            translation={translationSV}
            animation={animationSV}
            image={image}
            measurement={measurement}
            returnLocation={context.returnLocationSV}
            onDisplay={() => {
              if (pendingMeasurement) {
                context.open(
                  pendingMeasurement.measurement,
                  pendingMeasurement.mode,
                )
                setPendingMeasurement(null)
              }
            }}
          />
        </Portal>
      )}
    </>
  )
}

/**
 * an image of the underlying trigger with a grow animation
 */
function TriggerClone({
  translation,
  animation,
  image,
  measurement,
  returnLocation,
  onDisplay,
  label,
}: {
  translation: SharedValue<number>
  animation: SharedValue<number>
  image: string
  measurement: Measurement
  returnLocation: SharedValue<{x: number; y: number} | null>
  onDisplay: () => void
  label: string
}) {
  const {_} = useLingui()

  const animatedStyles = useAnimatedStyle(() => {
    const anim = animation.get()
    const ret = returnLocation.get()
    const returnOffsetX = ret
      ? interpolate(anim, [0, 1], [ret.x - measurement.x, 0])
      : 0
    const returnOffsetY = ret
      ? interpolate(anim, [0, 1], [ret.y - measurement.y, 0])
      : 0

    return {
      transform: [
        {translateX: returnOffsetX},
        {translateY: translation.get() * anim + returnOffsetY},
      ],
    }
  })

  const handleError = useCallback(
    (evt: ImageErrorEventData) => {
      logger.error('Context menu image load error', {message: evt.error})
      onDisplay()
    },
    [onDisplay],
  )

  return (
    <Animated.View
      style={[
        a.absolute,
        {
          top: measurement.y,
          left: measurement.x,
          width: measurement.width,
          height: measurement.height,
        },
        a.z_10,
        a.pointer_events_none,
        animatedStyles,
      ]}>
      <Image
        onDisplay={onDisplay}
        onError={handleError}
        source={image}
        style={{
          width: measurement.width,
          height: measurement.height,
        }}
        accessibilityLabel={label}
        accessibilityHint={_(msg`The subject of the context menu`)}
        accessibilityIgnoresInvertColors={false}
        cachePolicy="none"
      />
    </Animated.View>
  )
}

export function AuxiliaryView({
  children,
  align = 'left',
  style,
}: AuxiliaryViewProps) {
  const context = useContextMenuContext()
  const {width: screenWidth} = useWindowDimensions()
  const {top: topInset} = useSafeAreaInsets()
  const ensureOnScreenTranslationSV = useSharedValue(0)

  const {isOpen, mode, measurement, translationSV, animationSV} = context

  const animatedStyle = useAnimatedStyle(() => {
    return {
      opacity: clamp(animationSV.get(), 0, 1),
      transform: [
        {
          translateY:
            (ensureOnScreenTranslationSV.get() || translationSV.get()) *
            animationSV.get(),
        },
        {scale: interpolate(animationSV.get(), [0, 1], [0.2, 1])},
      ],
    }
  })

  const xOffset = (flatten(style)?.marginLeft as number) ?? 0
  const menuContext = useMemo(() => ({align, xOffset}), [align, xOffset])

  const onLayout = useCallback(() => {
    if (!measurement) return

    let translation = 0

    // vibes based, just assuming it'll fit within this space. revisit if we use
    // AuxiliaryView for something tall
    const TOP_INSET = topInset + 80

    const distanceMessageFromTop = measurement.y - TOP_INSET
    if (distanceMessageFromTop < 0) {
      translation = -distanceMessageFromTop
    }

    // normally, the context menu is responsible for measuring itself and moving everything into the right place
    // however, in auxiliary-only mode, that doesn't happen, so we need to do it ourselves here
    if (mode === 'auxiliary-only') {
      translationSV.set(translation)
      ensureOnScreenTranslationSV.set(0)
    }
    // however, we also need to make sure that for super tall triggers, we don't go off the screen
    // so we have an additional cap on the standard transform every other element has
    // note: this breaks the press-and-hold gesture for the reaction items. unfortunately I think
    // we'll just have to live with it for now, fixing it would be possible but be a large complexity
    // increase for an edge case
    else {
      ensureOnScreenTranslationSV.set(translation)
    }
  }, [mode, measurement, translationSV, topInset, ensureOnScreenTranslationSV])

  if (!isOpen || !measurement) return null

  return (
    <Portal>
      <Context.Provider value={context}>
        <MenuContext.Provider value={menuContext}>
          <Animated.View
            onLayout={onLayout}
            style={[
              a.absolute,
              {
                top: measurement.y,
                transformOrigin:
                  align === 'left' ? 'bottom left' : 'bottom right',
              },
              align === 'left'
                ? {left: measurement.x}
                : {right: screenWidth - measurement.x - measurement.width},
              animatedStyle,
              a.z_20,
              style,
            ]}>
            {children}
          </Animated.View>
        </MenuContext.Provider>
      </Context.Provider>
    </Portal>
  )
}

const MENU_WIDTH = 240

export function Outer({
  children,
  label,
  style,
  align = 'left',
}: {
  children: React.ReactNode
  label?: string
  style?: StyleProp<ViewStyle>
  align?: 'left' | 'right'
}) {
  const t = useTheme()
  const context = useContextMenuContext()
  const insets = useSafeAreaInsets()
  const frame = useSafeAreaFrame()
  const {width: screenWidth} = useWindowDimensions()

  const {animationSV, translationSV} = context

  const animatedContainerStyle = useAnimatedStyle(() => ({
    transform: [{translateY: translationSV.get() * animationSV.get()}],
  }))

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: clamp(animationSV.get(), 0, 1),
    transform: [{scale: interpolate(animationSV.get(), [0, 1], [0.2, 1])}],
  }))

  const onLayout = useCallback(
    (evt: LayoutChangeEvent) => {
      if (!context.measurement) return // should not happen
      let translation = 0

      // pure vibes based
      const TOP_INSET = insets.top + 80
      const BOTTOM_INSET_IOS = insets.bottom + 20
      const BOTTOM_INSET_ANDROID = insets.bottom + 12

      const {height} = evt.nativeEvent.layout
      const topPosition =
        context.measurement.y + context.measurement.height + tokens.space.xs
      const bottomPosition = topPosition + height
      const safeAreaBottomLimit =
        frame.height -
        platform({
          ios: BOTTOM_INSET_IOS,
          android: BOTTOM_INSET_ANDROID,
          default: 0,
        })
      const diff = bottomPosition - safeAreaBottomLimit
      if (diff > 0) {
        translation = -diff
      } else {
        const distanceMessageFromTop = context.measurement.y - TOP_INSET
        if (distanceMessageFromTop < 0) {
          translation = -Math.max(distanceMessageFromTop, diff)
        }
      }

      if (translation !== 0) {
        translationSV.set(translation)
      }
    },
    [context.measurement, frame.height, insets, translationSV],
  )

  const xOffset = (flatten(style)?.marginLeft as number) ?? 0
  const menuContext = useMemo(() => ({align, xOffset}), [align, xOffset])

  if (!context.isOpen || !context.measurement) return null

  return (
    <Portal>
      <Context.Provider value={context}>
        <MenuContext.Provider value={menuContext}>
          <Backdrop animation={animationSV} onPress={context.close} />
          {context.mode === 'full' && (
            /* containing element - stays the same size, so we measure it
           to determine if a translation is necessary. also has the positioning */
            <Animated.View
              onLayout={onLayout}
              style={[
                a.absolute,
                a.z_10,
                a.mt_xs,
                {
                  width: MENU_WIDTH,
                  top: context.measurement.y + context.measurement.height,
                },
                align === 'left'
                  ? {left: context.measurement.x}
                  : {
                      right:
                        screenWidth -
                        context.measurement.x -
                        context.measurement.width,
                    },
                animatedContainerStyle,
              ]}>
              {/* scaling element - has the scale/fade animation on it */}
              <Animated.View
                style={[
                  a.rounded_md,
                  a.shadow_md,
                  t.atoms.bg_contrast_25,
                  a.w_full,
                  // @ts-ignore react-native-web expects string, and this file is platform-split -sfn
                  // note: above @ts-ignore cannot be a @ts-expect-error because this does not cause an error
                  // in the typecheck CI - presumably because of RNW overriding the types
                  {
                    transformOrigin:
                      // "top right" doesn't seem to work on android, so set explicitly in pixels
                      align === 'left' ? [0, 0, 0] : [MENU_WIDTH, 0, 0],
                  },
                  animatedStyle,
                  style,
                ]}>
                {/* innermost element - needs an overflow: hidden for children, but we also need a shadow,
                so put the shadow on the scaling element and the overflow on the innermost element */}
                <View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}>
                  {label ? (
                    <Text
                      numberOfLines={1}
                      style={[
                        a.pl_md,
                        a.pt_md,
                        a.pr_lg,
                        a.pb_md,
                        a.text_xs,
                        t.atoms.text_contrast_medium,
                      ]}>
                      {label}
                    </Text>
                  ) : null}
                  {flattenReactChildren(children).map((child, i) => {
                    return isValidElement(child) &&
                      (child.type === Item || child.type === Divider) ? (
                      <Fragment key={i}>
                        {cloneElement(child, {
                          // @ts-expect-error not typed
                          style: {
                            borderRadius: 0,
                            borderWidth: 0,
                          },
                        })}
                      </Fragment>
                    ) : null
                  })}
                  {label ? <View style={[a.pb_md]} /> : null}
                </View>
              </Animated.View>
            </Animated.View>
          )}
        </MenuContext.Provider>
      </Context.Provider>
    </Portal>
  )
}

export function Item({
  children,
  label,
  unstyled,
  style,
  onPress,
  position,
  destructive = false,
  ...rest
}: ItemProps) {
  const t = useTheme()
  const context = useContextMenuContext()
  const playHaptic = useHaptics()
  const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
  const {
    state: pressed,
    onIn: onPressIn,
    onOut: onPressOut,
  } = useInteractionState()
  const id = useId()
  const {align, xOffset: menuXOffset} = useContextMenuMenuContext()

  const {close, measurement, registerHoverable} = context

  const handleLayout = useCallback(
    (evt: LayoutChangeEvent) => {
      if (!measurement) return // should be impossible

      const layout = evt.nativeEvent.layout

      const yOffset = position
        ? position.y
        : measurement.y + measurement.height + tokens.space.xs
      const xOffset = position
        ? position.x
        : align === 'left'
          ? measurement.x + menuXOffset
          : measurement.x + measurement.width - layout.width - menuXOffset

      registerHoverable(
        id,
        {
          width: layout.width,
          height: layout.height,
          y: yOffset + layout.y,
          x: xOffset + layout.x,
        },
        () => {
          close()
          onPress()
        },
      )
    },
    [
      id,
      measurement,
      registerHoverable,
      close,
      onPress,
      align,
      menuXOffset,
      position,
    ],
  )

  const itemContext = useMemo(
    () => ({disabled: Boolean(rest.disabled), destructive}),
    [rest.disabled, destructive],
  )

  return (
    <Pressable
      {...rest}
      onLayout={handleLayout}
      accessibilityHint=""
      accessibilityLabel={label}
      onFocus={onFocus}
      onBlur={onBlur}
      onPress={e => {
        close()
        onPress?.(e)
      }}
      onPressIn={e => {
        onPressIn()
        rest.onPressIn?.(e)
        playHaptic('Light')
      }}
      onPressOut={e => {
        onPressOut()
        rest.onPressOut?.(e)
      }}
      style={[
        !unstyled && [
          a.flex_row,
          a.align_center,
          a.px_2xl,
          a.rounded_md,
          t.atoms.bg_contrast_25,
          {gap: 6, minHeight: 44, paddingVertical: 10},
          (focused || pressed || context.hoveredMenuItem === id) &&
            !rest.disabled &&
            t.atoms.bg_contrast_50,
        ],
        style,
      ]}>
      <ItemContext.Provider value={itemContext}>
        {typeof children === 'function'
          ? children(
              (focused || pressed || context.hoveredMenuItem === id) &&
                !rest.disabled,
            )
          : children}
      </ItemContext.Provider>
    </Pressable>
  )
}

export function ItemText({children, style}: ItemTextProps) {
  const t = useTheme()
  const {disabled, destructive} = useContextMenuItemContext()
  return (
    <Text
      numberOfLines={2}
      ellipsizeMode="middle"
      style={[
        a.flex_1,
        a.text_md,
        a.font_semi_bold,
        t.atoms.text_contrast_high,
        style,
        destructive && {color: t.palette.negative_500},
        disabled && t.atoms.text_contrast_low,
      ]}>
      {children}
    </Text>
  )
}

export function ItemIcon({icon: Comp}: ItemIconProps) {
  const t = useTheme()
  const {disabled, destructive} = useContextMenuItemContext()
  return (
    <Comp
      size="lg"
      fill={
        disabled
          ? t.atoms.text_contrast_low.color
          : destructive
            ? t.palette.negative_500
            : t.atoms.text_contrast_medium.color
      }
    />
  )
}

export function ItemRadio({selected}: {selected: boolean}) {
  const t = useTheme()
  const enableSquareButtons = useEnableSquareButtons()
  return (
    <View
      style={[
        a.justify_center,
        a.align_center,
        enableSquareButtons ? a.rounded_sm : a.rounded_full,
        t.atoms.border_contrast_high,
        {
          borderWidth: 1,
          height: 20,
          width: 20,
        },
      ]}>
      {selected ? (
        <View
          style={[
            a.absolute,
            enableSquareButtons ? a.rounded_sm : a.rounded_full,
            {height: 14, width: 14},
            selected ? {backgroundColor: t.palette.primary_500} : {},
          ]}
        />
      ) : null}
    </View>
  )
}

export function LabelText({children}: {children: React.ReactNode}) {
  const t = useTheme()
  return (
    <Text
      style={[
        a.font_semi_bold,
        t.atoms.text_contrast_medium,
        {marginBottom: -8},
      ]}>
      {children}
    </Text>
  )
}

export function Divider() {
  const t = useTheme()
  return (
    <View
      style={[t.atoms.border_contrast_low, a.flex_1, {borderTopWidth: 3}]}
    />
  )
}

function measureView(view: View | null, insets: EdgeInsets) {
  if (!view) return Promise.resolve(null)
  return new Promise<Measurement>(resolve => {
    view?.measureInWindow((x, y, width, height) =>
      resolve({
        x,
        y:
          y +
          platform({
            default: 0,
            android: insets.top, // not included in measurement
          }),
        width,
        height,
      }),
    )
  })
}

function getHoveredHoverable(
  evt:
    | GestureStateChangeEvent<PanGestureHandlerEventPayload>
    | GestureUpdateEvent<PanGestureHandlerEventPayload>,
  hoverables: SharedValue<Record<string, {id: string; rect: Measurement}>>,
  translation: SharedValue<number>,
) {
  'worklet'

  const x = evt.absoluteX
  const y = evt.absoluteY
  const yOffset = translation.get()

  const rects = Object.values(hoverables.get())

  for (const {id, rect} of rects) {
    const isWithinLeftBound = x >= rect.x
    const isWithinRightBound = x <= rect.x + rect.width
    const isWithinTopBound = y >= rect.y + yOffset
    const isWithinBottomBound = y <= rect.y + rect.height + yOffset

    if (
      isWithinLeftBound &&
      isWithinRightBound &&
      isWithinTopBound &&
      isWithinBottomBound
    ) {
      return id
    }
  }

  return null
}
