import React from 'react'
import PropTypes from 'prop-types'
import { Platform, Pressable, StyleSheet, View } from 'react-native'
import { useThemeTokensCallback } from '../ThemeProvider/useThemeTokens'
import { a11yProps } from '../utils/props/a11yProps'
import { copyPropTypes } from '../utils/props/copyPropTypes'
import { getTokensPropType } from '../utils/props/tokens'
import { selectSystemProps } from '../utils/props/selectSystemProps'
import { useCopy } from '../utils/useCopy'
import { useUniqueId } from '../utils/useUniqueId'
import { variantProp } from '../utils/props/variantProp'
import { viewProps } from '../utils/props/viewProps'
import { A11yText } from '../A11yText/A11yText'
import { ScrollViewEnd } from '../HorizontalScroll/ScrollViewEnd'
import { dictionary } from './dictionary'
import { ScrollProgress } from './ScrollProgress'
import { ScrollProvider } from './ScrollContext'

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

const SCROLL_DESCRIPTION_PREFIX = 'scroll-description'
const KEY_ARROW_RIGHT = 'ArrowRight'
const KEY_ARROW_LEFT = 'ArrowLeft'
const KEY_SPACE = ' '
const MAX_PERCENTAGE = 100
const MIN_BAR_PERCENTAGE = 1
const ROLE_REGION = 'region'
const ROLE_NONE = 'none'
const COMPONENT_NAME = 'Scroll'
const SCROLL_ITEM_SELECTOR = '[data-scroll-item]'
const TAB_INDEX = 0
const INITIAL_DIMENSION = 0
const HALF_DIVISOR = 2

const selectPressableStyles = ({ progressHitAreaHeight, progressSpacing }) => ({
  height: progressHitAreaHeight,
  marginTop: progressSpacing
})

const selectThumbWrapperStyles = ({ thumbWidthPercent, thumbPositionPercent }) => ({
  width: `${thumbWidthPercent}%`,
  marginLeft: `${thumbPositionPercent}%`
})

const selectContainerA11yProps = ({ accessibilityLabel, descriptionId, handleKeyDown }) =>
  Platform.OS === 'web'
    ? {
        role: ROLE_REGION,
        'aria-label': accessibilityLabel,
        'aria-describedby': descriptionId,
        tabIndex: TAB_INDEX,
        onKeyDown: handleKeyDown
      }
    : {
        accessibilityRole: ROLE_NONE,
        accessibilityLabel
      }

const selectScrollContentStyle = (itemGap) => ({
  columnGap: itemGap
})

const calculateThumbWidth = ({ containerWidth, contentWidth }) => {
  if (contentWidth <= INITIAL_DIMENSION) return MAX_PERCENTAGE
  return Math.max(
    MIN_BAR_PERCENTAGE,
    Math.min(MAX_PERCENTAGE, (containerWidth / contentWidth) * MAX_PERCENTAGE)
  )
}

const calculateThumbPosition = ({ scrollOffset, scrollMax, thumbWidthPercent }) => {
  if (scrollMax <= INITIAL_DIMENSION) return INITIAL_DIMENSION
  return (scrollOffset / scrollMax) * (MAX_PERCENTAGE - thumbWidthPercent)
}

const getScrollableNode = (ref) => ref.current?.getScrollableNode?.() ?? null

/**
 * The `Scroll` component provides a continuous horizontal scrollable container with a progress
 * bar indicator. Items are displayed in a flow layout where all items share the width of the
 * widest item and the height of the tallest item.
 *
 * ## Props
 *
 * - Use `children` to pass `ScrollItem` components
 * - Use `accessibilityLabel` to provide a meaningful label for the scrollable region
 * - Use `copy` to select English or French for accessible labels
 * - Use `dictionary` to override the default translations
 *
 * ## Usability and A11y guidelines
 *
 * - The scroll container is keyboard-focusable with `tabIndex="0"`
 * - Each `ScrollItem` is individually focusable via Tab navigation
 * - Use left and right arrow keys to scroll by one item width
 * - Press Space bar to scroll right
 * - When tabbing through items, the scroll follows focus to keep the active item visible
 * - Screen readers announce the component as a region with a descriptive label
 * - Touch and swipe gestures are supported on mobile and mobile web
 *
 */
export const Scroll = React.forwardRef(
  (
    {
      children,
      tokens,
      variant,
      copy,
      dictionary: customDictionary = dictionary,
      accessibilityLabel,
      ...rest
    },
    ref
  ) => {
    const getCopy = useCopy({ dictionary: customDictionary, copy })
    const resolvedAccessibilityLabel = accessibilityLabel || getCopy('accessibilityLabel')
    const getTokens = useThemeTokensCallback(COMPONENT_NAME, tokens, variant)
    const defaultTokens = React.useMemo(() => getTokens(), [getTokens])
    const descriptionId = useUniqueId(SCROLL_DESCRIPTION_PREFIX)
    const scrollRef = React.useRef(null)
    const pressableRef = React.useRef(null)
    const scrollContainerRef = React.useRef(null)
    const scrollMaxRef = React.useRef(INITIAL_DIMENSION)
    const scrollOffsetRef = React.useRef(INITIAL_DIMENSION)
    const thumbWidthPercentRef = React.useRef(MAX_PERCENTAGE)
    const thumbRef = React.useRef(null)
    const isDraggingRef = React.useRef(false)
    const dragStartRef = React.useRef({
      mouseX: INITIAL_DIMENSION,
      scrollOffset: INITIAL_DIMENSION
    })

    const [scrollOffset, setScrollOffset] = React.useState(INITIAL_DIMENSION)
    const [containerWidth, setContainerWidth] = React.useState(INITIAL_DIMENSION)
    const [contentWidth, setContentWidth] = React.useState(INITIAL_DIMENSION)
    const [itemWidth, setItemWidth] = React.useState(INITIAL_DIMENSION)
    const [itemHeight, setItemHeight] = React.useState(INITIAL_DIMENSION)

    const { itemGap } = defaultTokens
    const scrollMax = Math.max(INITIAL_DIMENSION, contentWidth - containerWidth)

    React.useEffect(() => {
      scrollMaxRef.current = scrollMax
    }, [scrollMax])

    const thumbWidthPercent = calculateThumbWidth({ containerWidth, contentWidth })
    thumbWidthPercentRef.current = thumbWidthPercent
    const thumbPositionPercent = calculateThumbPosition({
      scrollOffset,
      scrollMax,
      thumbWidthPercent
    })

    const thumbWrapperStyle = React.useMemo(
      () => [
        staticStyles.thumbWrapper,
        selectThumbWrapperStyles({ thumbWidthPercent, thumbPositionPercent }),
        Platform.OS === 'web' ? staticStyles.thumbCursor : null
      ],
      [thumbWidthPercent, thumbPositionPercent]
    )

    const handleScroll = React.useCallback(
      ({
        nativeEvent: {
          contentOffset: { x }
        }
      }) => {
        scrollOffsetRef.current = x
        setScrollOffset(x)
      },
      []
    )

    const handleContainerLayout = React.useCallback(
      ({
        nativeEvent: {
          layout: { width }
        }
      }) => {
        setContainerWidth(width)
      },
      []
    )

    const handleContentWidth = React.useCallback((width) => {
      setContentWidth(width)
    }, [])

    const handleItemLayout = React.useCallback((width, height) => {
      setItemWidth((prev) => Math.max(prev, width))
      setItemHeight((prev) => Math.max(prev, height))
    }, [])

    React.useEffect(() => {
      if (Platform.OS !== 'web') return undefined
      const element = scrollContainerRef.current
      if (!element) return undefined
      const handleWheel = (event) => {
        event.preventDefault()
        event.stopPropagation()
        const isHorizontal = Math.abs(event.deltaX) > Math.abs(event.deltaY)
        const delta = isHorizontal ? event.deltaX : event.deltaY
        const scrollableNode = getScrollableNode(scrollRef)
        if (scrollableNode) {
          const maxScroll = scrollMaxRef.current
          const newScrollLeft = Math.max(
            INITIAL_DIMENSION,
            Math.min(maxScroll, scrollableNode.scrollLeft + delta)
          )
          scrollableNode.scrollLeft = newScrollLeft
          scrollOffsetRef.current = newScrollLeft
          setScrollOffset(newScrollLeft)
        }
      }
      element.addEventListener('wheel', handleWheel, { passive: false })
      return () => element.removeEventListener('wheel', handleWheel)
    }, [])

    React.useEffect(() => {
      if (Platform.OS !== 'web') return undefined
      const element = scrollContainerRef.current
      if (!element) return undefined

      const handleFocusIn = (event) => {
        const scrollableNode = getScrollableNode(scrollRef)
        if (!scrollableNode) return
        const focusedItem = event.target.closest(SCROLL_ITEM_SELECTOR)
        if (!focusedItem) return
        const itemLeft = focusedItem.offsetLeft
        const itemRight = itemLeft + focusedItem.offsetWidth
        const visibleLeft = scrollableNode.scrollLeft
        const currentContainerWidth = containerWidth
        const visibleRight = visibleLeft + currentContainerWidth
        if (itemRight > visibleRight) {
          scrollRef.current.scrollTo({ x: itemRight - currentContainerWidth, animated: true })
        } else if (itemLeft < visibleLeft) {
          scrollRef.current.scrollTo({ x: itemLeft, animated: true })
        }
      }

      element.addEventListener('focusin', handleFocusIn)
      return () => element.removeEventListener('focusin', handleFocusIn)
    }, [containerWidth])

    const hasScrollbar = scrollMax > INITIAL_DIMENSION

    React.useEffect(() => {
      if (Platform.OS !== 'web' || !hasScrollbar) return undefined
      const thumbElement = thumbRef.current
      if (!thumbElement) return undefined

      const handleMouseDown = (event) => {
        event.preventDefault()
        event.stopPropagation()
        isDraggingRef.current = true
        dragStartRef.current = {
          mouseX: event.clientX,
          scrollOffset: getScrollableNode(scrollRef)?.scrollLeft ?? INITIAL_DIMENSION
        }
        thumbElement.style.cursor = 'grabbing'
        document.body.style.cursor = 'grabbing'
        document.body.style.userSelect = 'none'
      }

      const handleMouseMove = (event) => {
        if (!isDraggingRef.current) return
        const trackElement = pressableRef.current
        if (!trackElement) return
        const trackWidth = trackElement.offsetWidth
        const thumbPixelWidth = (thumbWidthPercentRef.current / MAX_PERCENTAGE) * trackWidth
        const availableTrackPixels = trackWidth - thumbPixelWidth
        if (availableTrackPixels <= INITIAL_DIMENSION) return
        const mouseDelta = event.clientX - dragStartRef.current.mouseX
        const scrollDelta = (mouseDelta / availableTrackPixels) * scrollMaxRef.current
        const targetScroll = Math.max(
          INITIAL_DIMENSION,
          Math.min(scrollMaxRef.current, dragStartRef.current.scrollOffset + scrollDelta)
        )
        const scrollableNode = getScrollableNode(scrollRef)
        if (scrollableNode) {
          scrollableNode.scrollLeft = targetScroll
          scrollOffsetRef.current = targetScroll
          setScrollOffset(targetScroll)
        }
      }

      const handleMouseUp = () => {
        if (!isDraggingRef.current) return
        isDraggingRef.current = false
        thumbElement.style.cursor = ''
        document.body.style.cursor = ''
        document.body.style.userSelect = ''
      }

      thumbElement.addEventListener('mousedown', handleMouseDown)
      document.addEventListener('mousemove', handleMouseMove)
      document.addEventListener('mouseup', handleMouseUp)

      return () => {
        thumbElement.removeEventListener('mousedown', handleMouseDown)
        document.removeEventListener('mousemove', handleMouseMove)
        document.removeEventListener('mouseup', handleMouseUp)
      }
    }, [hasScrollbar])

    const handleKeyDown = React.useCallback(
      (event) => {
        if (event.key === KEY_ARROW_RIGHT || event.key === KEY_SPACE) {
          event.preventDefault()
          if (scrollRef.current) {
            scrollRef.current.scrollTo({
              x: scrollOffsetRef.current + itemWidth,
              animated: true
            })
          }
        } else if (event.key === KEY_ARROW_LEFT) {
          event.preventDefault()
          if (scrollRef.current) {
            scrollRef.current.scrollTo({
              x: Math.max(INITIAL_DIMENSION, scrollOffsetRef.current - itemWidth),
              animated: true
            })
          }
        }
      },
      [itemWidth]
    )

    const handleProgressPress = React.useCallback(
      (event) => {
        if (scrollMax <= INITIAL_DIMENSION || isDraggingRef.current) return
        if (Platform.OS === 'web' && pressableRef.current) {
          const { offsetX } = event.nativeEvent
          const { offsetWidth } = pressableRef.current
          const thumbPixelWidth = (thumbWidthPercent / MAX_PERCENTAGE) * offsetWidth
          const availableTrack = offsetWidth - thumbPixelWidth
          if (availableTrack <= INITIAL_DIMENSION) return
          const clickPosition = Math.max(
            INITIAL_DIMENSION,
            Math.min(availableTrack, offsetX - thumbPixelWidth / HALF_DIVISOR)
          )
          const targetX = (clickPosition / availableTrack) * scrollMax
          if (scrollRef.current) {
            scrollRef.current.scrollTo({ x: targetX, animated: true })
          }
        } else if (pressableRef.current) {
          const { locationX } = event.nativeEvent
          pressableRef.current.measure((_x, _y, width) => {
            const thumbPixelWidth = (thumbWidthPercent / MAX_PERCENTAGE) * width
            const availableTrack = width - thumbPixelWidth
            if (availableTrack <= INITIAL_DIMENSION) return
            const clickPosition = Math.max(
              INITIAL_DIMENSION,
              Math.min(availableTrack, locationX - thumbPixelWidth / HALF_DIVISOR)
            )
            const targetX = (clickPosition / availableTrack) * scrollMax
            if (scrollRef.current) {
              scrollRef.current.scrollTo({ x: targetX, animated: true })
            }
          })
        }
      },
      [scrollMax, thumbWidthPercent]
    )

    const handleContainerRef = React.useCallback(
      (node) => {
        scrollContainerRef.current = node
        if (typeof ref === 'function') ref(node)
        else if (ref) Object.assign(ref, { current: node })
      },
      [ref]
    )

    return (
      <View
        ref={handleContainerRef}
        style={staticStyles.container}
        {...selectContainerA11yProps({
          accessibilityLabel: resolvedAccessibilityLabel,
          descriptionId,
          handleKeyDown
        })}
        {...selectProps(rest)}
      >
        <A11yText text={getCopy('instructionText')} nativeID={descriptionId} />
        <View style={staticStyles.scrollContainer}>
          <ScrollViewEnd
            ref={scrollRef}
            horizontal
            onScrollEnd={handleScroll}
            onScroll={handleScroll}
            onContentSizeChange={handleContentWidth}
            onLayout={handleContainerLayout}
            showsHorizontalScrollIndicator={false}
            contentContainerStyle={[staticStyles.scrollContent, selectScrollContentStyle(itemGap)]}
          >
            <ScrollProvider
              itemWidth={itemWidth}
              itemHeight={itemHeight}
              onItemLayout={handleItemLayout}
            >
              {children}
            </ScrollProvider>
          </ScrollViewEnd>
        </View>
        {scrollMax > INITIAL_DIMENSION && (
          <Pressable
            ref={pressableRef}
            style={[staticStyles.pressable, selectPressableStyles(defaultTokens)]}
            onPress={handleProgressPress}
          >
            {(pressableState) => (
              <ScrollProgress
                ref={thumbRef}
                pressableState={pressableState}
                getTokens={getTokens}
                thumbWrapperStyle={thumbWrapperStyle}
                getCopy={getCopy}
              />
            )}
          </Pressable>
        )}
      </View>
    )
  }
)

Scroll.displayName = 'Scroll'

Scroll.propTypes = {
  ...selectedSystemPropTypes,
  /**
   * Scroll items to display. Should be `ScrollItem` components.
   */
  children: PropTypes.node.isRequired,
  /**
   * Theme tokens for customizing the Scroll appearance
   */
  tokens: getTokensPropType('Scroll'),
  /**
   * Variant for the Scroll component
   */
  variant: variantProp.propType,
  /**
   * Select English or French copy for accessible labels.
   */
  copy: copyPropTypes,
  /**
   * Override the default dictionary by passing the complete dictionary object for `en` and `fr`.
   */
  dictionary: PropTypes.shape({
    en: PropTypes.object,
    fr: PropTypes.object
  }),
  /**
   * Accessibility label for the scrollable region
   */
  accessibilityLabel: PropTypes.string
}

const staticStyles = StyleSheet.create({
  container: {
    width: '100%'
  },
  scrollContainer: {
    width: '100%'
  },
  pressable: {
    justifyContent: 'center',
    width: '100%'
  },
  scrollContent: {
    alignSelf: 'flex-start',
    flexGrow: 1
  },
  thumbWrapper: {
    height: '100%'
  },
  thumbCursor: {
    cursor: 'grab'
  }
})
