import React from 'react'
import PropTypes from 'prop-types'
import { View, ScrollView, Platform, StyleSheet, ImageBackground, Image } from 'react-native'
import { useTheme } from '../ThemeProvider/useTheme'
import { useThemeTokens, useThemeTokensCallback } from '../ThemeProvider/useThemeTokens'
import { useResponsiveThemeTokens } from '../ThemeProvider/useResponsiveThemeTokens'
import { a11yProps } from '../utils/props/a11yProps'
import { createMediaQueryStyles } from '../utils/ssr-media-query/utils/create-media-query-styles'
import { getA11yPropsFromHtmlTag, layoutTags } from '../utils/a11y/semantics'
import { getTokensPropType } from '../utils/props/tokens'
import { responsiveProps } from '../utils/props/responsiveProps'
import { selectSystemProps } from '../utils/props/selectSystemProps'
import { spacingProps } from '../utils/props/spacingProps'
import { useResponsiveProp } from '../utils/useResponsiveProp'
import { useSpacingScale } from '../utils/useSpacingScale'
import { variantProp } from '../utils/props/variantProp'
import { viewProps } from '../utils/props/viewProps'
import { StyleSheet as RNMQStyleSheet } from '../utils/ssr-media-query/ssr-media-query'
import { getSpacingScale } from '../utils/getSpacingScale'
import { formatImageSource } from '../utils/formatImageSource'
import { backgroundImageStylesMap, backgroundPositions } from './backgroundImageStylesMap'
import { useViewport } from '../ViewportProvider/useViewport'

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

/**
 * @typedef {import('../utils/props/spacingProps.js').SpacingValue} SpacingValue
 * @typedef {import('../utils/props/spacingProps.js').SpacingIndex} SpacingIndex
 * @typedef {import('../utils/props/spacingProps.js').SpacingObject} SpacingObject
 * @typedef {import('../utils/props/spacingProps.js').SpacingOptions} SpacingOptions
 */

/**
 * Box's geometry styles: border width, corner radii, and any padding set directly by a token. Token
 * paddings are only copied across when present because this object is spread after the spacing scale
 * values and would otherwise blank them.
 */
const selectBoxGeometryStyles = ({
  borderWidth,
  borderTopLeftRadius,
  borderTopRightRadius,
  borderBottomLeftRadius,
  borderBottomRightRadius,
  ...rest
}) => {
  const styles = {
    borderWidth,
    borderTopLeftRadius,
    borderTopRightRadius,
    borderBottomLeftRadius,
    borderBottomRightRadius
  }

  const paddings = ['paddingLeft', 'paddingRight', 'paddingTop', 'paddingBottom']
  paddings.forEach((side) => {
    if (rest[side]) {
      styles[side] = rest[side]
    }
  })
  return styles
}

const selectBoxAppearanceStyles = ({ backgroundColor, borderColor, gradient }, customGradient) => {
  const styles = { backgroundColor, borderColor }

  if (gradient) {
    const {
      angle,
      stops: [stopOne, stopTwo]
    } = gradient

    if (Platform.OS === 'web') {
      styles.backgroundImage = `linear-gradient(${angle}deg, ${stopOne.color}, 75%, ${stopTwo.color})`
    } else if (customGradient && Platform.OS !== 'web') {
      styles.colors = [stopOne.color, stopTwo.color]
    }
  }
  return styles
}

const selectBoxStyles = (themeTokens, customGradient) => ({
  ...selectBoxGeometryStyles(themeTokens),
  ...selectBoxAppearanceStyles(themeTokens, customGradient)
})

/**
 * Box styles for the media query stylesheet path, the responsive mirror of `selectBoxStyles`. The
 * resolved media styles carry the padding and geometry that `RNMQStyleSheet` matched against
 * `Dimensions` on native, where no CSSOM exists for the media ids to reference, so they must be
 * spread here or the box renders with no padding at all.
 */
const selectResponsiveBoxStyles = (resolvedMediaStyles, viewportTokens, customGradient) => ({
  ...resolvedMediaStyles,
  ...selectBoxAppearanceStyles(viewportTokens, customGradient)
})

const setBackgroundImage = ({
  src,
  alt,
  backgroundImageResizeMode,
  backgroundImagePosition,
  backgroundImageAlign,
  content,
  testID
}) => {
  const backgroundImageTestID = testID ? `${testID}-background-image` : undefined

  if (backgroundImageResizeMode === 'contain' && backgroundImagePosition && backgroundImageAlign) {
    const positionKey = `${backgroundImagePosition}-${backgroundImageAlign}`

    if (Platform.OS === 'web') {
      const backgroundPosition = backgroundPositions[positionKey] || 'center center'

      const backgroundImageStyle = {
        backgroundImage: `url(${src})`,
        backgroundSize: 'contain',
        backgroundRepeat: 'no-repeat',
        backgroundPosition
      }

      return (
        <View
          style={[staticStyles.imageBackground, backgroundImageStyle]}
          aria-label={alt}
          testID={backgroundImageTestID}
        >
          {content}
        </View>
      )
    }
    const positionStyles = backgroundImageStylesMap[positionKey] || {}

    return (
      <View style={staticStyles.containContainer}>
        <Image
          source={src}
          resizeMode={backgroundImageResizeMode}
          style={[staticStyles.containImage, positionStyles]}
          accessible={true}
          accessibilityLabel={alt}
          accessibilityIgnoresInvertColors={true}
          testID={backgroundImageTestID}
        />
        <View style={staticStyles.contentOverlay}>{content}</View>
      </View>
    )
  }

  return (
    <ImageBackground
      source={src}
      resizeMode={backgroundImageResizeMode}
      style={staticStyles.imageBackground}
      accessible={true}
      accessibilityLabel={alt}
      testID={backgroundImageTestID}
    >
      {content}
    </ImageBackground>
  )
}

/**
 * A layout utility component. Use Box to create space (padding) around content.
 *
 * ## Spacing
 *
 * For most simple uses, pass a number to Box's `space` prop referring to an index in the theme's
 * spacing scale. For example, for a box with the theme's smallest non-zero amount of padding on all sides:
 *
 * ```jsx
 * <Box space={1}>
 * ```
 *
 * #### Targetting specific sides
 *
 * Box allows spacing value props to be assigned to any side or all sides using the following props:
 *
 * - `space` sets the default for all sides, which is overridden by other props below
 * - `horizontal` sets defaults for `left` and `right` (overriding `space` there if it is set)
 * - `vertical` sets defaults for `top` and `bottom` (overriding `space` there if it is set)
 * - `left` sets the left side padding, inside the box's bounds
 * - `right` sets the right side padding, inside the box's bounds
 * - `top` sets the top side padding, inside the box's bounds
 * - `bottom` sets the bottom side padding, inside the box's bounds
 *
 * Box only controls spacing within its bounds. If space is needed around a box outside of its bounds,
 * use `Spacer`, `StackView` or wrap the `Box` inside another `Box`.
 *
 * #### Viewport-specific spacing
 *
 * Responsive behaviours may be set by passing any of the above props an object keyed by viewports.
 *
 * For example, if a theme's spacing scale is [0, 4, 8...], this below will have padding of 4px on top,
 * bottom, and right, and its left padding will be 0px on xs, sm and md viewports and 8px on lg and xl viewports:
 *
 * ```jsx
 * <Box space={1} left={{ xs: 0, lg: 2 }} />`
 * ```
 *
 * #### Subtracting from spacing
 *
 * A parent may sometimes need to reduce the spacing size on one or more sides of a `Box` by some variable.
 * For example, the parent may have a border on one side and want to reduce the spacing of a child box
 * by the width of that border on that side. This can be achieved using the `subtract` option of the
 * spacing object, for example:
 *
 * ```jsx
 * <Box space={2} left={{ space: 2, options: { subtract: themeTokens.borderWidthLeft } }} />`
 * ```
 *
 * See `useSpacingScale` hook for other spacing value options and documentation.
 *
 * ## Theming
 *
 * Box is intended for layout, so minimal theming is supported. Use components like `Card` for
 * more sophisticated theming.
 *
 * ## Scroll
 *
 * If passed, the box will be scrollable. If an object is passed, it will be passed to React Native's
 * `ScrollView` component as props.
 *
 * When building native iOS and Android apps, it is important to remember to ensure any screen containing
 * text content is inside a scrollable box, as screens are not scrollable by default and even very
 * short text will require scrolling on small devices at the highest accessibility text scaling settings.
 */
export const Box = React.forwardRef(
  (
    {
      space,
      horizontal = space,
      vertical = space,
      top = vertical,
      bottom = vertical,
      left = horizontal,
      right = horizontal,
      flex,
      children,
      variant,
      tokens,
      scroll,
      tag,
      accessibilityRole,
      testID,
      dataSet,
      customGradient,
      backgroundImage,
      ...rest
    },
    ref
  ) => {
    const props = {
      accessibilityRole,
      ...getA11yPropsFromHtmlTag(tag, accessibilityRole),
      ...selectProps(rest)
    }

    const viewport = useViewport()
    const { themeOptions } = useTheme()
    const { enableMediaQueryStyleSheet } = themeOptions

    const useTokens = enableMediaQueryStyleSheet ? useResponsiveThemeTokens : useThemeTokens
    const themeTokens = useTokens(
      'Box',
      tokens,
      variant,
      !enableMediaQueryStyleSheet && { viewport }
    )
    const getSpacingTokens = useThemeTokensCallback('spacingScale')

    let boxStyles = {
      flex,
      paddingLeft: useSpacingScale(left),
      paddingRight: useSpacingScale(right),
      paddingTop: useSpacingScale(top),
      paddingBottom: useSpacingScale(bottom),
      ...selectBoxStyles(themeTokens, customGradient)
    }
    let boxMediaIds

    if (enableMediaQueryStyleSheet) {
      const { transformedThemeTokens } = Object.entries(themeTokens).reduce(
        (acc, [vp, viewportTokens]) => {
          acc.transformedThemeTokens[vp] = {
            paddingLeft: getSpacingScale(left, vp, getSpacingTokens),
            paddingRight: getSpacingScale(right, vp, getSpacingTokens),
            paddingTop: getSpacingScale(top, vp, getSpacingTokens),
            paddingBottom: getSpacingScale(bottom, vp, getSpacingTokens),
            ...selectBoxGeometryStyles(viewportTokens)
          }
          return acc
        },
        {
          transformedThemeTokens: {}
        }
      )

      const mediaQueryStyles = createMediaQueryStyles(transformedThemeTokens)
      const { ids, styles } = RNMQStyleSheet.create({
        box: {
          ...mediaQueryStyles
        }
      })
      boxStyles = {
        flex,
        ...selectResponsiveBoxStyles(styles.box, themeTokens[viewport], customGradient)
      }
      boxMediaIds = ids.box
    }

    let content = children
    if (typeof customGradient === 'function')
      content = customGradient(boxStyles.colors, boxStyles)(children)

    const { src = '', alt = '', resizeMode = '', position = '', align = '' } = backgroundImage || {}
    const backgroundImageResizeMode = useResponsiveProp(resizeMode, 'cover')
    const backgroundImagePosition = useResponsiveProp(position)
    const backgroundImageAlign = useResponsiveProp(align)
    const imageSourceViewport = formatImageSource(useResponsiveProp(src))

    if (backgroundImage && src) {
      const { paddingTop, paddingBottom, paddingLeft, paddingRight, ...containerStyle } = boxStyles

      const hasPadding = paddingTop || paddingBottom || paddingLeft || paddingRight
      const paddedContent = hasPadding ? (
        <View style={{ paddingTop, paddingBottom, paddingLeft, paddingRight }}>{children}</View>
      ) : (
        children
      )

      content = setBackgroundImage({
        src: imageSourceViewport,
        alt,
        backgroundImageResizeMode,
        backgroundImagePosition,
        backgroundImageAlign,
        content: paddedContent,
        testID
      })

      const dataSetValue = boxMediaIds ? { media: boxMediaIds, ...dataSet } : dataSet

      if (scroll) {
        const scrollProps = typeof scroll === 'object' ? scroll : {}
        scrollProps.contentContainerStyle = [containerStyle, scrollProps.contentContainerStyle]
        return (
          <ScrollView {...scrollProps} {...props} testID={testID} dataSet={dataSetValue} ref={ref}>
            {content}
          </ScrollView>
        )
      }
      return (
        <View {...props} style={containerStyle} testID={testID} dataSet={dataSetValue} ref={ref}>
          {content}
        </View>
      )
    }

    const dataSetValue = boxMediaIds ? { media: boxMediaIds, ...dataSet } : dataSet

    if (scroll) {
      const scrollProps = typeof scroll === 'object' ? scroll : {}
      scrollProps.contentContainerStyle = [boxStyles, scrollProps.contentContainerStyle]
      return (
        <ScrollView {...scrollProps} {...props} testID={testID} dataSet={dataSetValue} ref={ref}>
          {content}
        </ScrollView>
      )
    }
    return (
      <View {...props} style={boxStyles} testID={testID} dataSet={dataSetValue} ref={ref}>
        {content}
      </View>
    )
  }
)
Box.displayName = 'Box'

Box.propTypes = {
  ...selectedSystemPropTypes,
  variant: variantProp.propType,
  tokens: getTokensPropType('Box'),
  /**
   * Sets default padding on all sides of the box using the theme's spacing scale.
   *
   * @see {@link SpacingValue}
   */
  space: spacingProps.types.spacingValue,
  /**
   * Sets top and bottom padding using the theme's spacing scale.
   *
   * @see {@link SpacingValue}
   */
  vertical: spacingProps.types.spacingValue,
  /**
   * Sets left and right padding using the theme's spacing scale.
   *
   * @see {@link SpacingValue}
   */
  horizontal: spacingProps.types.spacingValue,
  /**
   * Sets bottom padding using the theme's spacing scale.
   *
   * @see {@link SpacingValue}
   */
  bottom: spacingProps.types.spacingValue,
  /**
   * Sets left padding using the theme's spacing scale.
   *
   * @see {@link SpacingValue}
   */
  left: spacingProps.types.spacingValue,
  /**
   * Sets right padding using the theme's spacing scale.
   *
   * @see {@link SpacingValue}
   */
  right: spacingProps.types.spacingValue,
  /**
   * Sets top padding using the theme's spacing scale.
   *
   * @see {@link SpacingValue}
   */
  top: spacingProps.types.spacingValue,
  /**
   * Sets the `flex` style, which controls `flexGrow`, `flexShrink` and `flexBasis` styles.
   *
   * Set as 1 for the box to stretch and grow to fit in the available space, or another number to share
   * space disproportionately with siblings. See https://reactnative.dev/docs/flexbox#flex for details.
   *
   * With the default `0`, the box takes its minimum width from its content.
   */
  flex: PropTypes.number,
  /**
   * Renders a scrollable ScrollView instead of an unscrollable View.
   *
   * May take an object of ScrollView props which are passed to the rendered ScrollView.
   */
  scroll: PropTypes.oneOfType([
    PropTypes.bool,
    ScrollView.propTypes ? PropTypes.shape(ScrollView.propTypes) : PropTypes.object
  ]),
  /**
   * Optional semantic HTML tag, to apply to the container on web. Does not change styling.
   *
   * In native apps, if a header tag is provided ("h1", "h2" etc) and accessibilityRole prop
   * is not defined, the accessibilityRole will default to "heading".
   */
  tag: PropTypes.oneOf(layoutTags),
  /**
   * Use in tests if the box itself needs to be targetted and no other way of selecting the
   * box is available (e.g. it cannot be targetted using an appropriate `accessibilityRole`).
   */
  testID: PropTypes.string,
  /**
   * @ignore
   */
  dataSet: PropTypes.object,
  /**
   * Box accepts any content as children.
   */
  children: PropTypes.node.isRequired,
  /**
  Use this prop if need to add a custom gradient for mobile
  */
  customGradient: PropTypes.func,
  /**
   * Apply background image to the box.
   */
  backgroundImage: PropTypes.shape({
    // The image src is either a URI string or a number (when a local image src is bundled in IOS or Android app)
    // src is an object when used responsively to provide different image sources for different screen sizes
    src: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]).isRequired,
    alt: PropTypes.string,
    resizeMode: responsiveProps.getTypeOptionallyByViewport(
      PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat', 'center'])
    ),
    position: responsiveProps.getTypeOptionallyByViewport(
      PropTypes.oneOf(['none', 'bottom', 'left', 'right', 'top'])
    ),
    align: responsiveProps.getTypeOptionallyByViewport(
      PropTypes.oneOf(['start', 'end', 'center', 'stretch'])
    )
  })
}

const staticStyles = StyleSheet.create({
  imageBackground: { width: '100%', height: '100%' },
  contentOverlay: {
    position: 'relative',
    width: '100%',
    height: '100%',
    zIndex: 1
  },
  containContainer: {
    width: '100%',
    height: '100%',
    overflow: 'hidden',
    position: 'relative'
  },
  containImage: {
    position: 'absolute',
    width: '100%',
    height: '100%'
  }
})
