import React, { useCallback, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Platform, View, StyleSheet } from 'react-native'

import { ButtonBase } from './ButtonBase'
import { Fieldset } from '../Fieldset/Fieldset'
import { useViewport } from '../ViewportProvider/useViewport'
import { useThemeTokens, useThemeTokensCallback } from '../ThemeProvider/useThemeTokens'
import { useSpacingScale } from '../utils/useSpacingScale'
import { a11yProps } from '../utils/props/a11yProps'
import { containUniqueFields } from '../utils/containUniqueFields'
import { focusHandlerProps } from '../utils/props/handlerProps'
import { pressProps } from '../utils/props/pressProps'
import { getTokensPropType } from '../utils/props/tokens'
import { selectSystemProps } from '../utils/props/selectSystemProps'
import { useMultipleInputValues } from '../utils/input'
import { variantProp } from '../utils/props/variantProp'
import { viewProps } from '../utils/props/viewProps'
import { getPressHandlersWithArgs } from '../utils/pressability'

const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
const [selectItemProps, selectedItemPropTypes] = selectSystemProps([
  a11yProps,
  focusHandlerProps,
  pressProps,
  viewProps
])
const EQUAL_WIDTH = 'equal'
const RESPONSIVE_WIDTH = 'responsive'
/**
 * A `responsive` group only fills its container in the stacked layout the brand themes opt into at
 * the xs viewport; above xs they keep `direction: row` and the group sizes to its content like the
 * default width. Deciding that from the resolved `direction` token keeps the breakpoint in
 * theme.json, where all three brands already declare it. Dropping the gate makes the group full
 * width at every viewport, which is the regression from #6850.
 */
const DIRECTION_COLUMN = 'column'

export const ButtonGroup = React.forwardRef(
  (
    {
      variant,
      buttonVariant,
      tokens,
      items = [],
      values,
      initialValues,
      maxValues = 1,
      onChange,
      readOnly = false,
      inactive = false,
      legend,
      tooltip,
      hint,
      hintPosition = 'inline',
      validation,
      feedback,
      name: inputGroupName,
      copy,
      iconPosition,

      accessibilityRole = maxValues === 1
        ? 'radiogroup' // radiogroup is cross-platform; only web aria has generic groups
        : Platform.select({ web: 'group', default: 'none' }),
      ...rest
    },
    ref
  ) => {
    const viewport = useViewport()
    const themeTokens = useThemeTokens('ButtonGroup', tokens, variant, { viewport })
    const {
      direction,
      space,
      fieldSpace,
      borderRadius,
      backgroundColor,
      padding,
      gap,
      alignItems
    } = themeTokens

    const isMobileNonContained =
      Platform.OS !== 'web' && (!variant || variant?.style !== 'contained')

    const themeButtonTokensCallback = useThemeTokensCallback('ButtonGroupItem', tokens, variant)
    const gapValue = useSpacingScale(gap || space)

    const isWeb = Platform.OS === 'web'
    const buttonWidthVariant = variant?.width
    const isStackedResponsive =
      buttonWidthVariant === RESPONSIVE_WIDTH && direction === DIRECTION_COLUMN

    /**
     * Resolves one button's tokens, overriding the width the `ButtonGroupItem` theme resolved.
     * The brands declare that item's `equal` and `responsive` widths as a full width with no
     * viewport condition, so the width each variant actually needs is decided here:
     *
     * - `equal` fills its share of the row at every viewport.
     * - `responsive` above xs cancels the theme's full width to a null width, so the button
     *   resolves exactly like the default width. `'auto'` would not: it is truthy, which puts the
     *   button on different `ButtonBase` branches for both size and alignment.
     * - `responsive` stacked on native overrides with `'auto'`. The value has to stay truthy,
     *   because only a set width gives the inner container — the element carrying the background
     *   and border, so the visible pill — the `flex` that lets it fill the button.
     * - `responsive` stacked on web overrides with nothing at all, leaving the theme's own full
     *   width to drive it. That absence is deliberate: on native the same token would resolve to
     *   `flexGrow`, which grows height inside a column.
     *
     * `alignSelf` follows the width the button ends up with rather than the one the theme
     * resolved. It also can't be left to the container: `ButtonBase` applies `alignSelf` on native
     * only, so a per-item value silently pre-empts the group's `alignItems` there.
     */
    const getButtonTokens = useCallback(
      (state) => {
        const themeButtonTokens = themeButtonTokensCallback(state)

        const shouldUseTransparentBackground =
          isMobileNonContained && !state.selected && !state.pressed && !state.hover && !state.focus

        let widthStyle

        switch (buttonWidthVariant) {
          case EQUAL_WIDTH:
            widthStyle = staticStyles.equalWidth
            break
          case RESPONSIVE_WIDTH:
            if (!isStackedResponsive) {
              widthStyle = staticStyles.contentWidth
            } else if (!isWeb) {
              widthStyle = staticStyles.stackedNativeWidth
            }
            break
          // no default
        }

        const buttonTokens = {
          ...themeButtonTokens,
          ...widthStyle,
          ...(shouldUseTransparentBackground && { backgroundColor: 'transparent' })
        }

        let alignSelf = buttonTokens.width ? 'flex-start' : 'center'
        if (isStackedResponsive) {
          alignSelf = alignItems
        }

        return { ...buttonTokens, alignSelf }
      },
      [
        themeButtonTokensCallback,
        isMobileNonContained,
        buttonWidthVariant,
        isStackedResponsive,
        isWeb,
        alignItems
      ]
    )

    const fieldsetStyles = useMemo(() => {
      let fieldsetBase
      switch (buttonWidthVariant) {
        case EQUAL_WIDTH:
          fieldsetBase = staticStyles.fieldsetFullWidth
          break
        case RESPONSIVE_WIDTH:
          fieldsetBase = isStackedResponsive
            ? staticStyles.fieldsetFullWidth
            : staticStyles.fieldsetBase
          break
        default:
          fieldsetBase = staticStyles.fieldsetBase
          break
      }

      return {
        ...fieldsetBase,
        borderRadius,
        backgroundColor: isMobileNonContained ? 'transparent' : backgroundColor || 'transparent',
        padding
      }
    }, [
      borderRadius,
      backgroundColor,
      padding,
      isMobileNonContained,
      buttonWidthVariant,
      isStackedResponsive
    ])

    const buttonsContainerStyles = useMemo(() => {
      let flexWrap
      if (isWeb) {
        flexWrap = 'wrap'
      } else {
        switch (buttonWidthVariant) {
          case EQUAL_WIDTH:
            flexWrap = 'nowrap'
            break
          case RESPONSIVE_WIDTH:
            flexWrap = isStackedResponsive ? 'nowrap' : 'wrap'
            break
          default:
            flexWrap = 'wrap'
            break
        }
      }

      let justifyContent
      switch (buttonWidthVariant) {
        case EQUAL_WIDTH:
          justifyContent = 'space-evenly'
          break
        default:
          justifyContent = 'flex-start'
          break
      }

      let viewBaseStyle
      switch (buttonWidthVariant) {
        case EQUAL_WIDTH:
          viewBaseStyle = {}
          break
        case RESPONSIVE_WIDTH:
          viewBaseStyle = isStackedResponsive ? {} : staticStyles.viewBase
          break
        default:
          viewBaseStyle = staticStyles.viewBase
          break
      }

      return {
        ...viewBaseStyle,
        flexWrap,
        flexDirection: direction === DIRECTION_COLUMN ? DIRECTION_COLUMN : 'row',
        gap: gapValue || 0,
        justifyContent
      }
    }, [direction, gapValue, buttonWidthVariant, isWeb, isStackedResponsive])

    const { currentValues, toggleOneValue } = useMultipleInputValues({
      initialValues,
      values,
      maxValues,
      onChange,
      readOnly
    })

    const systemProps = selectProps({
      accessibilityRole,
      ...rest
    })
    const itemA11yRole = systemProps.accessibilityRole === 'radiogroup' ? 'radio' : 'checkbox'

    const uniqueFields = ['id', 'label']
    if (!containUniqueFields(items, uniqueFields)) {
      throw new Error(`ButtonGroup items must have unique ${uniqueFields.join(', ')}`)
    }

    // Some web screenreaders e.g. MacOS Voiceover don't handle radiogroups properly unless radio is direct child of radiogroup
    const buttonsWrapperRole =
      Platform.OS === 'web' && accessibilityRole === 'radiogroup' ? accessibilityRole : undefined

    return (
      <Fieldset
        {...systemProps}
        ref={ref}
        name={inputGroupName}
        legend={legend}
        tooltip={tooltip}
        hint={hint}
        hintPosition={hintPosition}
        space={fieldSpace}
        feedback={feedback}
        readOnly={readOnly}
        inactive={inactive}
        validation={validation}
        accessibilityRole={accessibilityRole}
        style={fieldsetStyles}
        {...selectProps(rest)}
      >
        <View accessibilityRole={buttonsWrapperRole} style={buttonsContainerStyles}>
          {items.map(
            (
              {
                label,
                id = label,
                accessibilityLabel,
                ref: itemRef,
                inactive: itemInactive,
                ...itemRest
              },
              index
            ) => {
              const isSelected = currentValues.includes(id)

              // Pass an object of relevant component state as first argument for any passed-in press handlers
              const pressHandlers = getPressHandlersWithArgs(rest, [{ id, label, currentValues }])

              const handlePress = (event) => {
                if (pressHandlers.onPress) pressHandlers.onPress(event)
                toggleOneValue(id, event)
              }

              const iconProp = itemRest.icon || undefined

              const itemA11y = {
                accessibilityState: { checked: isSelected },
                accessibilityRole: itemA11yRole,
                accessibilityLabel,
                ...a11yProps.getPositionInSet(items.length, index)
              }

              const isInactive = itemInactive !== undefined ? itemInactive : inactive

              // Ensure button is direct child of group as MacOS voiceover only applies "X of Y" to
              // "radio" if it's a direct child of "radiogroup", even if aria-posinset etc exists
              return (
                <ButtonBase
                  ref={itemRef}
                  key={id}
                  variant={buttonVariant}
                  {...pressHandlers}
                  onPress={handlePress}
                  tokens={getButtonTokens}
                  selected={isSelected}
                  inactive={isInactive}
                  icon={iconProp}
                  {...selectItemProps({
                    ...itemRest,
                    ...itemA11y
                  })}
                  iconPosition={iconPosition}
                >
                  {label}
                </ButtonBase>
              )
            }
          )}
        </View>
      </Fieldset>
    )
  }
)
ButtonGroup.displayName = 'ButtonGroup'

ButtonGroup.propTypes = {
  ...selectedSystemPropTypes,
  tokens: getTokensPropType('ButtonGroup'),
  /**
   * To allow Button specific variant
   */
  buttonVariant: variantProp.propType,
  /**
   * Default variant prop
   */
  variant: variantProp.propType,
  /**
   * The maximum number of items a user may select at once. Defaults to 1 and behaves
   * like radio buttons. To have no limit and allow any number of selections, pass `null`.
   */
  maxValues: PropTypes.number,
  /**
   * The options a user may select
   */
  items: PropTypes.arrayOf(
    PropTypes.shape({
      ...selectedItemPropTypes,
      /**
       * The text displayed to the user in the button, describing this option,
       * passed to the button as its child.
       */
      label: PropTypes.string.isRequired,
      /**
       * An optional accessibility label may be passed to ButtonGroup Buttons
       * which is applied as normal for a React Native accessibilityLabel prop.
       */
      accessibilityLabel: PropTypes.string,
      /**
       * An optional unique string may be provided to identify this option,
       * which will be used in code and passed to any onChange function.
       * If not provided, the label is used.
       */
      id: PropTypes.string,
      /**
       * An optional ref for one individual button in the ButtonGroup
       */
      ref: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any })]),
      /**
       * If true, this individual button cannot be interacted with. Takes precedence
       * over the group-level `inactive` prop. Useful for disabling specific options.
       */
      inactive: PropTypes.bool
    })
  ),
  /**
   * If provided, this function is called when the current selection is changed
   * and is passed an array of the `id`s of all currently selected `items`.
   */
  onChange: PropTypes.func,
  /**
   * If the selected item(s) in the button group are to be controlled externally by
   * a parent component, pass an array of strings as well as an `onChange` handler.
   * Passing an array for "values" makes the ButtonGroup a "controlled" component that
   * expects its state to be handled via `onChange` and so doesn't handle it itself.
   */
  values: PropTypes.arrayOf(PropTypes.string),
  /**
   * If `values` is not passed, making the ButtonGroup an "uncontrolled" component
   * managing its own selected state, a default set of selections may be provided.
   * Changing the `initialValues` does not change the user's selections.
   */
  initialValues: PropTypes.arrayOf(PropTypes.string),
  /**
   * Defines if the icon will be displayed on the right or left side of the label.
   */
  iconPosition: PropTypes.oneOf(['left', 'right']),
  /**
   * Main text used to describe this group, used in Fieldset's Legend element.
   */
  legend: PropTypes.string,
  /**
   * Optional additional text giving more detail to help a user make a choice.
   */
  hint: PropTypes.string,
  /**
   * Position of the hint relative to label. Use `below` to display a larger hint below the label.
   */
  hintPosition: PropTypes.oneOf(['inline', 'below']),
  /**
   * Optional tooltip text content to include alongside the legend and hint.
   */
  tooltip: PropTypes.string,
  /**
   * Current validation status of the group, passed to the feedback element if there is one.
   */
  validation: PropTypes.oneOf(['error', 'success']),
  /**
   * If provided, a Feedback element is rendered containing this text.
   */
  feedback: PropTypes.string,
  /**
   * If true, the buttons cannot be selected by the user and simply show their current state.
   */
  readOnly: PropTypes.bool,
  /**
   * If true, the buttons cannot be interacted with, elements are set as `disabled` and if the
   * theme supports `inactive` appearances rules, these are applied.
   */
  inactive: PropTypes.bool,
  /**
   * On Web, this is passed to the `name` attribute of the fieldset.
   */
  name: PropTypes.string,
  /**
   * Sets the language of microcopy in subcomponents (e.g. Tooltip's default accessibility label).
   */
  copy: PropTypes.oneOf(['en', 'fr'])
}

const staticStyles = StyleSheet.create({
  equalWidth: {
    width: '100%',
    flex: 1
  },
  contentWidth: {
    width: null
  },
  stackedNativeWidth: {
    width: 'auto'
  },
  fieldsetBase: {
    alignSelf: 'flex-start',
    display: 'inline',
    width: 'auto'
  },
  fieldsetFullWidth: {
    width: '100%'
  },
  viewBase: {
    flexWrap: 'wrap',
    alignContent: 'stretch',
    alignItems: 'center'
  }
})
