import React from 'react'
import PropTypes from 'prop-types'
import { View } from 'react-native'
import { a11yProps } from '../utils/props/a11yProps'
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 { useMediaQuerySpacing } from '../utils/useMediaQuerySpacing'
import { useSpacingScale } from '../utils/useSpacingScale'
import { useResponsiveProp } from '../utils/useResponsiveProp'
import { viewProps } from '../utils/props/viewProps'
import {
  StyleSheet as StyleSheetUtils,
  createMediaQueryStyles
} from '../utils/ssr-media-query/ssr-media-query'
import { useTheme } from '../ThemeProvider/useTheme'
import { useThemeTokens } from '../ThemeProvider/useThemeTokens'
import { useViewport } from '../ViewportProvider/useViewport'
import { getStackedContent } from './getStackedContent'
import { staticStyles, selectFlexStyles } from './common'

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

const OFFSET_MULTIPLIER = -1

const offsetSides = {
  row: 'marginBottom',
  column: 'marginRight'
}

const gapSides = {
  row: 'bottom',
  column: 'right'
}

const spaceSides = {
  row: 'right',
  column: 'bottom'
}

/**
 * An alternative fallback implementation of `StackWrap` that doesn't use CSS `gap`.
 *
 * This is not intended to be used directly. It will automatically be used instead of StackWrap in:
 *
 * - Native apps (no `gap` style prop in React Native).
 * - Older (~<=2020) browsers which don't support CSS `gap`.
 * - Cases where the `space` between items should be different to the `gap` between wrapped rows .
 *
 * Unlike the cleaner and more side-effect-free CSS gap StackWrap implementation, this renders a Box (View)
 * between the container and the stacked children, and requires a negative margin on the container.
 *
 * Under the `enableMediaQueryStyleSheet` theme option that negative margin is emitted as media rules.
 * The per-item Boxes take their padding from `Box`'s own media rules, so a compensating offset that
 * was not itself a media rule would only match them at the one viewport it was resolved at.
 *
 * `offsetSides` has no entry for the reversed directions, so for those there is no side to offset and
 * no rule worth emitting; that gap is left as it renders today rather than fixed here.
 */
export const StackWrapBox = React.forwardRef(
  (
    {
      space = 1,
      gap = space,
      direction: directionProp = 'row',
      children,
      tokens,
      variant,
      tag,
      accessibilityRole,
      itemAccessibilityRole,
      dataSet,
      ...rest
    },
    ref
  ) => {
    const viewport = useViewport()
    const {
      themeOptions: { enableMediaQueryStyleSheet }
    } = useTheme()
    const direction = useResponsiveProp(directionProp, 'row')
    const themeTokens = useThemeTokens('StackView', tokens, variant, { viewport })
    const flexStyles = selectFlexStyles(themeTokens)
    const selectedProps = selectProps({
      accessibilityRole,
      ...getA11yPropsFromHtmlTag(tag, accessibilityRole),
      ...rest
    })

    // Mimic CSS `gap` using box spacing on the side after a wrapped row then offsetting it on the last row.
    const { sizeByViewport } = useMediaQuerySpacing(gap)
    const gapSize = useSpacingScale(gap)
    const offsetSide = offsetSides[direction]
    const offsetStyle = { [offsetSide]: OFFSET_MULTIPLIER * gapSize }
    const boxProps = { [gapSides[direction]]: gap, [spaceSides[direction]]: space }
    const content = getStackedContent(children, {
      direction,
      space: 0,
      box: boxProps,
      itemAccessibilityRole
    })

    const baseStyles = [flexStyles, staticStyles.wrap, staticStyles[direction]]

    let wrapStyles = [...baseStyles, offsetStyle]
    let dataSetValue = dataSet

    if (enableMediaQueryStyleSheet && offsetSide) {
      const mediaQueryStyles = createMediaQueryStyles({
        xs: { [offsetSide]: OFFSET_MULTIPLIER * sizeByViewport.xs },
        sm: { [offsetSide]: OFFSET_MULTIPLIER * sizeByViewport.sm },
        md: { [offsetSide]: OFFSET_MULTIPLIER * sizeByViewport.md },
        lg: { [offsetSide]: OFFSET_MULTIPLIER * sizeByViewport.lg },
        xl: { [offsetSide]: OFFSET_MULTIPLIER * sizeByViewport.xl }
      })

      const { ids, styles } = StyleSheetUtils.create({
        stackWrap: {
          ...mediaQueryStyles
        }
      })

      wrapStyles = [...baseStyles, styles.stackWrap]
      dataSetValue = { media: ids.stackWrap, ...dataSet }
    }

    return (
      <View ref={ref} {...selectedProps} style={wrapStyles} dataSet={dataSetValue}>
        {content}
      </View>
    )
  }
)
StackWrapBox.displayName = 'StackWrapBox'

StackWrapBox.propTypes = {
  ...selectedSystemPropTypes,
  tokens: getTokensPropType('StackView'),
  /**
   * Sets the `flexDirection` of the container
   */
  direction: responsiveProps.getTypeOptionallyByViewport(
    PropTypes.oneOf(['column', 'row', 'row-reverse', 'column-reverse'])
  ),
  /**
   * The size of the space between items according to the theme's spacing scale.
   * Either a number corresponding to a position on the theme's spacing scale (1 is smallest, 2 is second smallest, etc),
   * or, a SpacingObject with viewport keys and options (see `useSpacingScale` for details).
   */
  space: spacingProps.types.spacingValue,
  /**
   * The size of the space between lines of content, according to the theme's spacing scale. By default, matches `space`.
   * Either a number corresponding to a position on the theme's spacing scale (1 is smallest, 2 is second smallest, etc),
   * or, a SpacingObject with viewport keys and options (see `useSpacingScale` for details).
   */
  gap: spacingProps.types.spacingValue,
  /**
   * 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),
  /**
   * Optional accessibility role to apply to each item in the stack.
   * On web, items are wrapped (or cloned) with this role, enabling correct list semantics
   * when the container tag is "ul" or "ol".
   */
  itemAccessibilityRole: PropTypes.string,
  /**
   * A StackWrap may take any children, but will have no effect if it is only passed one child or is passed children
   * wrapped in a component. If necessary, children may be wrapped in one React Fragment.
   */
  children: PropTypes.node,
  /**
   * Data attributes to be applied to the element. When media query stylesheet is enabled,
   * this will include media query IDs for responsive styling.
   */
  dataSet: PropTypes.object
}
