import React from 'react'
import { Text } from 'react-native'
import { A11yText } from '../A11yText/A11yText'

/**
 * 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}
 */
export 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
}

const isStringOrNumber = (child) => typeof child === 'string' || typeof child === 'number'
// Wrap an A11yText with neighouring text strings so it doesn't split them into multiple <Text>s
// Check displayName first as it is explicitly set and reliable regardless of export style (named vs default)
const isWrapable = (child) =>
  isStringOrNumber(child) ||
  child.type === A11yText ||
  child.type?.__UDS_COMPONENT_NAME__ === 'FootnoteLink'
const combineKeys = (childrenArray) =>
  childrenArray.reduce((newKey, child) => `${newKey}${child.key || ''}`, '')

// Group wrappable children for one `<Text>` parent, merging adjacent text nodes
const wrapChild = (child, wrappedText) => {
  const lastIndex = wrappedText.length - 1
  // If possible, simplify content by combining text nodes into one string.
  // Historically the deprecated jest-native `.toHaveTextContent` mishandled arrays
  // of text nodes in <Text> elements; the merge also keeps rendered output stable.
  if (lastIndex >= 0 && isStringOrNumber(child) && isStringOrNumber(wrappedText[lastIndex])) {
    /* eslint-disable-next-line no-param-reassign */
    wrappedText[lastIndex] = `${wrappedText[lastIndex]}${child}`
  } else {
    wrappedText.push(child)
  }
}

/**
 * React Native on Native crashes if text content is rendered outside `<Text>`, and on web,
 * text style inheritance behaves differently with text compared to regular HTML.
 *
 * Call this function on children that may contain text (strings or numbers) at the top level,
 * and any that are found will be wrapped in a React Native `<Text>` element with supplied props.
 *
 * Note that this does not wrap strings that are nested children of rendered top level children:
 * `wrapStringsInText(<View>Some text</View>)` will not wrap the inner text and will still crash,
 * but `wrapStringsInText(<>{someString}{anotherString}</>)` wraps the strings inside the fragment.
 *
 * @param {ReactChildren} children
 * @param {TextProps} props
 * @returns {ReactChildren}
 */
export const wrapStringsInText = (children, textProps = {}) => {
  const childrenArray = unpackFragment(React.Children.toArray(children))

  // Group adjacent wrapable children together in one Text element to create as few Text elements
  // as possible, e.g. give <X>Text {someString}</X> one Text, same as <X>{`Text ${someString}`}</X>
  const wrapables = [[]]
  let wrapablesIndex = 0
  childrenArray.forEach((child) => {
    if (isWrapable(child)) {
      // Make this child a child of the current `Text`
      wrapChild(child, wrapables[wrapablesIndex])
    } else {
      // Close current `Text` and start a new one after this child
      wrapables.push(child, [])
      wrapablesIndex += 2
    }
  })

  const items = wrapables.reduce((flatChildren, group, index) => {
    // Skip nullish children and empty arrays
    if (!group || (Array.isArray(group) && !group.length)) return flatChildren

    return [
      ...flatChildren,
      Array.isArray(group) && group.some((child) => isStringOrNumber(child)) ? (
        // Wrap text nodes and their wrappable neighbours in Text with as stable a key as possible.
        // Avoid one-item arrays: the deprecated jest-native `.toHaveTextContent`
        // sometimes failed to match text content in arrays, and unwrapping keeps
        // the rendered output stable.
        <Text key={combineKeys(group) || index} {...textProps}>
          {group.length === 1 ? group[0] : group}
        </Text>
      ) : (
        group
      )
    ]
  }, [])
  return items.length === 1 ? items[0] : items
}
