import React from 'react'

import { Box } from '../Box/Box'
import { Divider } from '../Divider/Divider'
import { Spacer } from '../Spacer/Spacer'

/**
 * @typedef {import('react').ReactChildren} ReactChildren
 * @typedef {import('react').ReactElement} ReactElement
 * @typedef {import('../utils/props/spacingProps.js').SpacingValue} SpacingValue
 */

/**
 * Whether a separator between two stacked items must be vertical, which is when the container lays
 * them out along the horizontal axis. Takes the direction the separators were given, either a plain
 * `'row' | 'column'` or one already resolved per viewport, and returns the same shape of booleans, so
 * a `Divider` can follow a responsive container direction the same way a `Spacer` does.
 *
 * @param {"row" | "column" | Record<string, "row" | "column">} separatorDirection
 * @returns {boolean | Record<string, boolean>}
 */
const isVerticalSeparator = (separatorDirection) =>
  typeof separatorDirection === 'object'
    ? Object.keys(separatorDirection).reduce(
        (byViewport, viewport) => ({
          ...byViewport,
          [viewport]: separatorDirection[viewport] === 'row'
        }),
        {}
      )
    : separatorDirection === 'row'

/**
 * Takes valid React Children and inserts a specified amount of space between each valid (not nullish) top
 * level element, according to the provided `options`.
 *
 * Returns an array of the original elements plus inserted spacing elements.
 * This array of elements is keyed and may be used as a component's `children` without mapping.
 *
 * @param {ReactChildren} children
 * @param {object} [options] -
 *  - `space`: amount of space to insert using the theme spacing scale (see `useSpacingScale`)
 *  - `direction`: if 'row' or 'row-reverse', applies space horizontally, if 'column' or 'column-reverse', applies space vertically
 *  - `spacerDirection`: overrides the direction given to inserted Spacers and the orientation of inserted
 *    Dividers, and may be an object keyed by viewports so the separator follows a container direction
 *    that is itself responsive
 *  - `box`: if truthy, wraps each valid child in a Box component; if an object, passes it to Box as props
 *  - `divider`: if truthy, inserts Divider components; if an object, passes it to Divider as props
 *  - `preserveFragments`: if true, adds no space between top-level elements that were passed inside a nested fragment
 * @param {SpacingValue} [options.space]
 * @param {boolean | object} [options.divider]
 * @param {"row" | "column" | "row-reverse" | "column-reverse"} [options.direction]
 * @param {"row" | "column" | object} [options.spacerDirection]
 * @param {boolean} [options.preserveFragments]
 * @returns {ReactElement[]}
 */
export const getStackedContent = (
  children,
  {
    divider,
    space,
    direction = 'column',
    spacerDirection,
    box,
    preserveFragments = false,
    itemAccessibilityRole
  }
) => {
  const boxProps = box && typeof box === 'object' ? box : { space }
  const dividerProps = divider && typeof divider === 'object' ? divider : {}
  const separatorDirection = spacerDirection ?? (direction.startsWith('row') ? 'row' : 'column')

  // Avoid surprises if children contains comments, nulls, or is a variable wrapped as a fragment
  const topLevelChildren = preserveFragments ? children : unpackFragment(children)
  const validChildren = React.Children.toArray(topLevelChildren).filter(Boolean)
  const content = validChildren.reduce((newChildren, child, index) => {
    const boxID = `Stack-Box-${index}`

    let item
    if (box) {
      // If wrapped in Box, that Box needs a key.
      // If possible, use an existing content key; use an index-based key only if necessary.
      item = (
        <Box
          {...boxProps}
          accessibilityRole={itemAccessibilityRole}
          key={child.key || boxID}
          testID={boxID}
        >
          {child}
        </Box>
      )
    } else if (itemAccessibilityRole) {
      item = React.cloneElement(child, {
        accessibilityRole: itemAccessibilityRole,
        key: child.key || boxID
      })
    } else {
      item = child
    }
    if (!index || (!space && !divider)) return [...newChildren, item]

    const testID = `Stack-${divider ? 'Divider' : 'Spacer'}-${index}`
    const commonProps = { testID, space }
    const separator = divider ? (
      <Divider
        vertical={isVerticalSeparator(separatorDirection)}
        key={testID}
        {...dividerProps}
        {...commonProps}
      />
    ) : (
      <Spacer direction={separatorDirection} key={testID} {...commonProps} />
    )
    return [...newChildren, separator, item]
  }, [])

  return content
}

/**
 * Unpacks top-level fragments, so that common compositional patterns such as the following examples
 * can be iterated as flat siblings (as if they were `<Child1 /><Child2 /><Child3 />`):
 *
 * - Setting `children` as a property wrapped in a fragment:
 *
 * ```jsx
 * args.children = <><Child1 /><Child2 /><Child3 /></>
 * ```
 *
 * - Defining `children` as a variable wrapped in a fragment:
 *
 * ```jsx
 * const content = <><Child1 /><Child2 /><Child3 /></>
 * if (someCondition) return <SomeWrapper>{content}</SomeWrapper>
 * ```
 *
 * - Using fragments at the top level of a JSX block for conditional rendering:
 *
 * ```jsx
 * <Child1 />
 * {someCondition && (
 *   <>
 *     <Child2 />
 *     <Child3 />
 *   </>
 * )}
 * ```
 *
 * @param {ReactChildren} child
 * @returns {ReactChildren}
 */
const unpackFragment = (child) => {
  // If this level is a set of top-level siblings rather than one child, check each in turn
  if (React.Children.count(child) > 1) return React.Children.map(child, unpackFragment)

  // When a fragment is found, unpack its children to the top level and check them
  if (child?.type === React.Fragment) return unpackFragment(child.props?.children)

  // Stop unpacking as soon as any non-fragment child is found
  return child
}
