import React from 'react'
import { Dimensions, Platform, Pressable, StyleSheet, Text, View } from 'react-native'
import { propTypes } from './shared'

import { applyShadowToken, applyTextStyles } from '../ThemeProvider/utils/styles'
import { useThemeTokens } from '../ThemeProvider/useThemeTokens'
import { a11yProps } from '../utils/props/a11yProps'
import { selectSystemProps } from '../utils/props/selectSystemProps'
import { selectTokens } from '../utils/props/tokens'
import { viewProps } from '../utils/props/viewProps'
import { Backdrop } from './Backdrop'
import { getTooltipPosition } from './getTooltipPosition'
import { TooltipButton } from '../TooltipButton/TooltipButton'
import { useCopy } from '../utils/useCopy'
import { dictionary } from './dictionary'

const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])

const selectTooltipStyles = ({
  backgroundColor,
  paddingTop,
  paddingBottom,
  paddingLeft,
  paddingRight,
  borderRadius
}) => ({
  backgroundColor,
  paddingTop,
  paddingBottom,
  paddingLeft,
  paddingRight,
  borderRadius
})
const selectTooltipShadowStyles = ({ shadow, borderRadius }) => ({
  borderRadius,
  ...applyShadowToken(shadow)
})

const selectToolTipSizeStyles = ({ maxWidth, minWidth }) => ({
  maxWidth,
  minWidth
})

const selectTooltipPositionStyles = ({ top, left, width }) => {
  return { top, left, width }
}
const selectArrowStyles = (
  { backgroundColor, arrowWidth, arrowBorderRadius, shadow },
  { position, width: tooltipWidth, height: tooltipHeight }
) => {
  // the arrow width is actually a diagonal of the rectangle that we'll use as a tip
  const rectangleSide = Math.sqrt((arrowWidth * arrowWidth) / 2)

  // position the arrow at the side and center of the tooltip - this happens before rotation
  // so we use the rectangle size as basis
  const verticalOffset = (-1 * rectangleSide) / 2
  const horizontalOffset = rectangleSide / 2

  // percentage-based absolute positioning doesn't act well on native, so we have to
  // calculate the pixel values
  const directionalStyles = {
    above: {
      bottom: verticalOffset,
      left: tooltipWidth / 2 - horizontalOffset,
      transform: [{ rotateZ: '45deg' }]
    },
    below: {
      top: verticalOffset,
      left: tooltipWidth / 2 - horizontalOffset,
      transform: [{ rotateZ: '-135deg' }]
    },
    left: {
      right: verticalOffset,
      top:
        tooltipHeight <= 45
          ? tooltipHeight / 2 - horizontalOffset
          : tooltipHeight - (tooltipHeight - 10),
      transform: [{ rotateZ: '-45deg' }]
    },
    right: {
      left: verticalOffset,
      top:
        tooltipHeight <= 45
          ? tooltipHeight / 2 - horizontalOffset
          : tooltipHeight - (tooltipHeight - 10),
      transform: [{ rotateZ: '135deg' }]
    }
  }

  return {
    backgroundColor,
    width: rectangleSide,
    height: rectangleSide,
    borderBottomRightRadius: arrowBorderRadius, // this corner will be the arrow tip after rotation
    ...applyShadowToken(shadow),
    ...directionalStyles[position]
  }
}

const selectTextStyles = (tokens) => applyTextStyles(selectTokens('Typography', tokens))
const defaultControl = (pressableState, variant, tokens) => (
  <TooltipButton pressableState={pressableState} variant={variant} tokens={tokens} />
)

/**
 * Tooltip provides a descriptive and detailed explanation or instructions. It can be used next to an input label
 * to help a user fill it in, or as a standalone component.
 *
 * By default the TooltipButton component will be used as a control for triggering the tooltip, but you may attach
 * a tooltip to any other component. A render function can be used to adjust the control's styling on state changes (hover, focus, etc.).
 *
 * ### Positioning
 * By default a Tooltip will be automatically positioned in a way that ensures it fits within the viewport.
 * You may suggest a position with a prop - it will be used, unless the tooltip would end up outside the viewport.
 *
 * ### Usage criteria
 * - You may use one when the information is useful only to a small percentage of users (ie. tech savvy people wouldn't need this info).
 * - Tooltips may also be useful when vertical space is an issue.
 */
export const Tooltip = React.forwardRef(
  (
    {
      children,
      content,
      position = 'auto',
      copy = 'en',
      onPress = () => {},
      tokens,
      variant,
      inline = false,
      nativeID,
      activateOnHover = false,
      tooltipButtonTokens,
      testID,
      ...rest
    },
    ref
  ) => {
    const [isOpen, setIsOpen] = React.useState(false)
    const controlRef = React.useRef()
    const [controlLayout, setControlLayout] = React.useState(null)
    const [tooltipDimensions, setTooltipDimensions] = React.useState(null)
    const [windowDimensions, setWindowDimensions] = React.useState(Dimensions.get('window'))
    const [tooltipPosition, setTooltipPosition] = React.useState(null)

    const getCopy = useCopy({ dictionary, copy })
    const themeTokens = useThemeTokens('Tooltip', tokens, variant)

    const { arrowWidth, arrowOffset } = themeTokens

    React.useEffect(() => {
      const subscription = Dimensions.addEventListener('change', ({ window }) => {
        setWindowDimensions(window)
      })

      return () => subscription?.remove()
    })

    const toggleIsOpen = () => {
      onPress()
      setIsOpen(!isOpen)
    }
    const close = () => setIsOpen(false)

    const getPressableState = ({ pressed, hovered, focused }) => ({
      pressed,
      hover: hovered,
      focus: focused
    })

    const onTooltipLayout = ({
      nativeEvent: {
        layout: { width, height }
      }
    }) => {
      if (
        tooltipDimensions === null ||
        tooltipDimensions.width !== width ||
        tooltipDimensions.height !== height
      ) {
        setTooltipDimensions({
          width: Platform.select({
            web: width + 0.3, // avoids often unnecessary line breaks due to subpixel rendering of fonts
            native: width
          }),
          height
        })
      }
    }

    React.useEffect(() => {
      if (isOpen) {
        controlRef.current.measureInWindow((x, y, width, height) => {
          setControlLayout({ x, y, width, height })
        })
      } else {
        setControlLayout(null)
        setTooltipDimensions(null)
        setTooltipPosition(null)
      }
    }, [isOpen])

    React.useEffect(() => {
      setIsOpen(false)
    }, [windowDimensions])

    React.useEffect(() => {
      if (
        (tooltipPosition !== null && !tooltipPosition?.isNormalized) ||
        !isOpen ||
        controlLayout === null ||
        tooltipDimensions == null
      ) {
        return
      }

      const updatedPosition = getTooltipPosition(position, {
        controlLayout,
        tooltipDimensions,
        windowDimensions,
        arrowWidth,
        arrowOffset
      })

      // avoid ending up in an infinite normalization loop
      if (tooltipPosition?.isNormalized && updatedPosition.isNormalized) {
        return
      }

      setTooltipPosition(updatedPosition)
    }, [
      isOpen,
      position,
      tooltipDimensions,
      controlLayout,
      windowDimensions,
      arrowWidth,
      arrowOffset,
      tooltipPosition
    ])

    React.useEffect(() => {
      if (Platform.OS !== 'web') {
        return undefined
      }

      const handleHover = (event) => {
        if (activateOnHover) {
          const isInsideControl = controlRef.current && controlRef.current.contains(event.target)
          if (isInsideControl) {
            if (!isOpen) {
              setIsOpen(!isOpen)
            }
          } else {
            close()
          }
        }
      }

      document.addEventListener('mouseover', handleHover, { capture: true })

      return () => {
        document.removeEventListener('mouseover', handleHover, { capture: true })
      }
    }, [controlLayout, activateOnHover, isOpen, onPress])

    const control = children !== undefined ? children : defaultControl
    const pressableStyles =
      control === defaultControl ? Platform.select({ web: { outline: 'none' } }) : undefined
    const pressableHitSlop =
      control === defaultControl ? { top: 10, bottom: 10, left: 10, right: 10 } : undefined

    return (
      <View
        style={[
          staticStyles.container,
          Platform.select({
            web: {
              display: inline ? 'inline-block' : 'flex'
            }
          })
        ]}
        testID={testID}
        {...selectProps(rest)}
      >
        <Pressable
          onPress={toggleIsOpen}
          ref={controlRef}
          onBlur={close}
          style={pressableStyles}
          hitSlop={pressableHitSlop}
          accessibilityLabel={getCopy('a11yText')}
          accessibilityRole="button"
          nativeID={nativeID}
        >
          {typeof control === 'function'
            ? (pressableState) =>
                control(getPressableState(pressableState), variant, tooltipButtonTokens)
            : control}
        </Pressable>
        {isOpen && (
          <Backdrop onPress={close}>
            <View
              ref={ref}
              style={[
                staticStyles.tooltip,
                selectTooltipShadowStyles(themeTokens), // applied separately so that it doesn't cover the arrow
                selectToolTipSizeStyles(themeTokens),
                tooltipPosition && selectTooltipPositionStyles(tooltipPosition),
                (tooltipPosition === null || tooltipPosition?.isNormalized) &&
                  staticStyles.tooltipHidden // visually hide the tooltip until we have a final measurement
              ]}
              onLayout={onTooltipLayout}
              accessibilityRole="alert"
            >
              <View
                style={[
                  staticStyles.arrow,
                  tooltipPosition && selectArrowStyles(themeTokens, tooltipPosition)
                ]}
              />
              <View style={selectTooltipStyles(themeTokens)}>
                <Text style={selectTextStyles(themeTokens)}>{content}</Text>
              </View>
            </View>
          </Backdrop>
        )}
      </View>
    )
  }
)
Tooltip.displayName = 'NativeTooltip'

Tooltip.propTypes = {
  ...selectedSystemPropTypes,
  ...propTypes
}

const staticStyles = StyleSheet.create({
  container: {
    alignItems: 'flex-start'
  },
  tooltip: {
    position: 'absolute',
    top: 0,
    left: 0
  },
  tooltipHidden: {
    opacity: 0
  },
  arrow: {
    position: 'absolute'
  }
})
