import React from 'react'
import PropTypes from 'prop-types'

import { useViewport } from '../ViewportProvider/useViewport'
import { useThemeTokens } from '../ThemeProvider/useThemeTokens'
import { a11yProps } from '../utils/props/a11yProps'
import { containUniqueFields } from '../utils/containUniqueFields'
import { focusHandlerProps } from '../utils/props/handlerProps'
import { getTokensPropType } from '../utils/props/tokens'
import { selectSystemProps } from '../utils/props/selectSystemProps'
import { useInputValue } from '../utils/input'
import { variantProp } from '../utils/props/variantProp'
import { viewProps } from '../utils/props/viewProps'
import { getStackedContent } from '../StackView/getStackedContent'
import { Radio } from './Radio'
import { Fieldset } from '../Fieldset/Fieldset'

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

/**
 * A group of Radios that behave as a radio group. Use when users select a single choice from mutually
 * exclusive options.
 *
 * ## Component API
 *
 * Use the `items` prop to pass an array of objects describing each Radio in the group:
 *
 *  - `label`: main text passed to Radio's `label` prop
 *  - `id`: identifier used to store which Radio is selected (uses `label` if undefinded)
 *  - `onChange`: optional function called on selection, in addition to updating the group's selection state
 *
 * ### Controlled version
 *
 * If the selection state is controlled from outside, it needs to receive `checkedId` and `onChange` props.
 *
 * ### Uncontrolled version
 *
 * If the RadioGroup manages its own state, you can use `initialCheckedId` prop to provide the initial value.
 * Whenever the radio gets toggled, it calls the `onChange` callback with the new value (string).
 *
 * ### Use in forms
 *
 * For web forms, the `name` prop may be used to define the name of the group's `fieldset` and input elements.
 *
 * ### Validation
 *
 * Validation state may be set by passing 'error' or 'success' to the `validation` prop.
 *
 * ## A11y guidelines
 *
 * RadioGroup accepts all the common accessibility props, but also sets accessibility role `'radiogroup'`
 * and controls the accessibility state of children like Radio and Feedback based on current state.
 *
 * @example
 * ```jsx
 * <RadioGroup
 *   initialCheckedId="radio1"
 *   items={[
 *     { label: 'Radio 1', id: 'radio1' },
 *     { label: 'Radio 2', id: 'radio2' },
 *     { label: 'Radio 3', id: 'radio3' }
 *   ]}
 *   legend="Radios"
 *   hint="Choose one option"
 * />
 * ```
 */
export const RadioGroup = React.forwardRef(
  (
    {
      copy = 'en',
      tokens,
      radioTokens,
      variant,
      items = [],
      legend,
      tooltip,
      hint,
      hintPosition = 'inline',
      validation,
      feedback,
      initialCheckedId,
      checkedId,
      onChange,
      readOnly,
      name: inputGroupName,
      inactive,
      ...rest
    },
    ref
  ) => {
    const viewport = useViewport()

    /* showIcon passes a boolean to displayIcon
     */
    const {
      space,
      fieldSpace,
      showIcon,
      outlineWidth,
      borderTopLeftRadius,
      borderTopRightRadius,
      borderBottomLeftRadius,
      borderBottomRightRadius,
      outlineOffset
    } = useThemeTokens('RadioGroup', tokens, variant, {
      viewport
    })

    const borderTokens = {
      outlineWidth,
      borderTopLeftRadius,
      borderTopRightRadius,
      borderBottomLeftRadius,
      borderBottomRightRadius,
      outlineOffset
    }

    const { currentValue, setValue } = useInputValue({
      value: checkedId,
      initialValue: initialCheckedId,
      onChange,
      readOnly: readOnly || inactive
    })

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

    const radios = items.map(
      ({ label, id, onChange: itemOnChange, ref: itemRef, description, ...itemRest }, index) => {
        const radioId = id || `Radio[${index}]`
        const isChecked = currentValue === radioId
        const handleChange = (newCheckedState, event) => {
          if (typeof itemOnChange === 'function') itemOnChange(newCheckedState, event)
          if (newCheckedState) setValue(radioId, event)
        }
        return (
          <Radio
            ref={itemRef}
            key={radioId}
            id={radioId}
            checked={isChecked}
            onChange={handleChange}
            inactive={inactive}
            label={label}
            name={inputGroupName}
            tokens={radioTokens}
            variant={variant}
            description={description}
            {...selectItemProps(itemRest)}
          />
        )
      }
    )

    return (
      <Fieldset
        copy={copy}
        ref={ref}
        name={inputGroupName}
        legend={legend}
        tooltip={tooltip}
        hint={hint}
        hintPosition={hintPosition}
        space={fieldSpace}
        feedback={feedback}
        inactive={inactive}
        validation={validation}
        showErrorBorder
        tokens={borderTokens}
        showIcon={showIcon}
        accessibilityRole="radiogroup"
        {...selectProps(rest)}
      >
        {getStackedContent(radios, { space, direction: 'column' })}
      </Fieldset>
    )
  }
)
RadioGroup.displayName = 'RadioGroup'

RadioGroup.propTypes = {
  ...selectedSystemPropTypes,
  /**
   * Whether the English or French copy will be used (e.g. for accessibility labels).
   */
  copy: PropTypes.oneOf(['en', 'fr']),
  /**
   * Optional theme token overrides for the outer RadioGroup component
   */
  tokens: getTokensPropType('RadioGroup'),
  /**
   * Optional theme token overrides for each inner Radio component
   */
  radioTokens: getTokensPropType('Radio'),
  /**
   * Theme variants, shared between both RadioGroup and Radio
   */
  variant: variantProp.propType,
  /**
   * Array of objects containing specifics for each Radio to be rendered in the group.
   */
  items: PropTypes.arrayOf(
    PropTypes.exact({
      ...selectedItemPropTypes,
      label: PropTypes.string,
      id: PropTypes.string,
      onChange: PropTypes.func,
      description: PropTypes.string,
      ref: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any })])
    })
  ),
  /**
   * 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 provided, the radio with this id is selected on first render.
   */
  initialCheckedId: PropTypes.string,
  /**
   * If not undefined, the radio with this id is selected (or none is selected if `null`), and the
   * element's selection state will be controlled by its parent using the `onChange` function.
   */
  checkedId: PropTypes.string,
  /**
   * Function to call on change in selection state. Is required if the selection state is controlled
   * by a parent using checkedId and the input is not readOnly.
   */
  onChange: PropTypes.func,
  /**
   * If true, the radios cannot be selected by the user and simply show their current state.
   */
  readOnly: PropTypes.bool,
  /**
   * If true, the radios 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 and each radio input.
   */
  name: PropTypes.string
}
