import React from 'react'
import PropTypes from 'prop-types'
import { Text, View, Platform } from 'react-native'
import { viewports } from '@telus-uds/system-constants'

import { useResponsiveThemeTokens } from '../ThemeProvider/useResponsiveThemeTokens'
import { useTheme } from '../ThemeProvider/useTheme'
import { useThemeTokens } from '../ThemeProvider/useThemeTokens'
import { applyTextStyles } from '../ThemeProvider/utils/styles'
import { a11yProps } from '../utils/props/a11yProps'
import { variantProp } from '../utils/props/variantProp'
import { getTokensPropType } from '../utils/props/tokens'
import { getMaxFontMultiplier } from '../utils/a11y/textSize'
import { headingTags, textTags, getA11yPropsFromHtmlTag } from '../utils/a11y/semantics'
import { selectSystemProps } from '../utils/props/selectSystemProps'
import { textProps } from '../utils/props/textProps'
import { viewProps } from '../utils/props/viewProps'
import { StyleSheet, createMediaQueryStyles } from '../utils/ssr-media-query/ssr-media-query'
import { useViewport } from '../ViewportProvider/useViewport'
/**
 * @typedef {import('../utils/a11y/semantics').TextTag} TextTag
 */

const [selectContainerProps, selectedContainerPropTypes] = selectSystemProps([a11yProps, viewProps])
const [selectTextProps, selectedTextPropTypes] = selectSystemProps([textProps])
const selectTextStyles = (
  {
    fontWeight,
    fontSize,
    color,
    lineHeight,
    fontName,
    textAlign,
    textTransform,
    letterSpacing,
    textDecorationLine,
    fontStyle,
    gradient
  },
  themeOptions
) =>
  applyTextStyles({
    fontWeight,
    fontSize,
    color,
    lineHeight,
    fontName,
    fontStyle,
    themeOptions,
    textAlign,
    textTransform,
    letterSpacing,
    textDecorationLine,
    gradient
  })

const HALF_FONT_SIZE = 2
const QUARTER_FONT_SIZE = 4

/**
 * Derives the native `sub`/`sup` offset and sizing from the surrounding font size.
 *
 * Expects the tokens for a single viewport, not the viewport map `useResponsiveThemeTokens`
 * returns — passing the map makes every value here `NaN`.
 *
 * @param {{ fontSize: number }} tokens - resolved tokens for the current viewport
 * @param {'sub'|'sup'} type - which tag is being rendered
 * @returns {object} React Native text styles
 *
 * @example
 * selectMobileSubSupStyles({ fontSize: 16 }, 'sup')
 * // { fontSize: 8, lineHeight: 16, position: 'relative', top: -4 }
 */
const selectMobileSubSupStyles = ({ fontSize }, type) => ({
  fontSize: fontSize / HALF_FONT_SIZE,
  lineHeight: fontSize,
  position: 'relative',
  top: type === 'sub' ? fontSize / QUARTER_FONT_SIZE : -fontSize / QUARTER_FONT_SIZE
})

// General-purpose flexible theme-neutral base component for text
export const Typography = React.forwardRef(
  (
    {
      children,
      variant,
      heading,
      tag = typeof heading === 'string' ? heading : undefined,
      accessibilityRole = heading === true ? 'header' : undefined,
      block = false,
      align = Platform.OS === 'web' ? 'inherit' : undefined,
      tokens,
      dataSet,
      strikeThrough = false,
      ...rest
    },
    ref
  ) => {
    const viewport = useViewport()
    const { themeOptions } = useTheme()

    const { enableMediaQueryStyleSheet } = themeOptions

    const useTokens = enableMediaQueryStyleSheet ? useResponsiveThemeTokens : useThemeTokens
    const themeTokens = useTokens(
      'Typography',
      tokens,
      { ...variant, color: variant?.color || variant?.colour },
      !enableMediaQueryStyleSheet && { viewport }
    )
    const currentTokens = enableMediaQueryStyleSheet
      ? themeTokens[viewport] ?? themeTokens[viewports.keys[0]]
      : themeTokens
    const maxFontSizeMultiplier = getMaxFontMultiplier(currentTokens)
    const textDecorationLine = strikeThrough ? 'line-through' : 'none'
    const fontStyle = tag === 'em' ? 'italic' : undefined

    const generateGradientString = (gradientObj) => {
      let gradientStr = ''

      const {
        type,
        angle,
        stops: [stopOne, stopTwo],
        specification
      } = gradientObj

      if (type === 'radial') {
        gradientStr = `radial-gradient(${specification}`
        gradientStr += `, ${stopOne.color} ${stopOne.stop * 100}%, ${stopTwo.color} ${
          stopTwo.stop * 100
        }%`
      } else if (type === 'linear') {
        gradientStr = `linear-gradient(${angle}deg`
        if (angle === 135) {
          gradientStr += `, ${stopOne.color}, 75%, ${stopTwo.color}`
        } else {
          gradientStr += `, ${stopOne.color}, ${stopTwo.color}`
        }
      }

      gradientStr += ')'
      return gradientStr
    }

    const getStyleWithGradient = (styles) => {
      return styles.gradient
        ? {
            ...styles,
            color: 'transparent',
            backgroundImage: generateGradientString(styles.gradient),
            backgroundClip: 'text'
          }
        : styles
    }

    /**
     * Added to maintain previous behavior, as the 'auto' value for the 'align' prop now defaults to 'inherit' on the web,
     * maintaining the previous behavior that was changed in the Text component of react-native.
     */
    const resolvedAlign = Platform.OS === 'web' && align === 'auto' ? 'inherit' : align

    let textStyles
    let mediaIds

    if (enableMediaQueryStyleSheet) {
      const transformedThemeTokens = Object.entries(themeTokens).reduce(
        (acc, [vp, viewportTokens]) => {
          acc[vp] = selectTextStyles(
            {
              textAlign: resolvedAlign,
              textDecorationLine,
              ...viewportTokens,
              ...(fontStyle && { fontStyle })
            },
            themeOptions
          )
          if (themeTokens[vp].gradient) {
            const tokensWithGradient = {
              ...acc[vp],
              color: 'transparent',
              backgroundImage: generateGradientString(themeTokens[vp].gradient),
              backgroundClip: 'text'
            }
            return {
              ...acc,
              [vp]: tokensWithGradient
            }
          }
          return acc
        },
        {}
      )
      const mediaQueryStyles = createMediaQueryStyles(transformedThemeTokens)
      const { ids, styles } = StyleSheet.create({
        text: mediaQueryStyles
      })
      textStyles = styles.text
      mediaIds = ids.text
    } else {
      textStyles = selectTextStyles(
        {
          textAlign: resolvedAlign,
          textDecorationLine,
          ...themeTokens,
          ...(fontStyle && { fontStyle })
        },
        themeOptions
      )
    }

    const resolvedTextProps = {
      ...selectTextProps(rest),
      maxFontSizeMultiplier
    }

    const containerProps = {
      accessibilityRole,
      ...getA11yPropsFromHtmlTag(tag, accessibilityRole),
      ...selectContainerProps(rest)
    }

    const resetTagStyling = (child) => {
      if (typeof child !== 'object' || !(child?.type === 'sub' || child?.type === 'sup')) {
        return child
      }

      const childStyles = child?.props?.style || {}
      const supFontSize = childStyles.fontSize ?? currentTokens.superScriptFontSize
      const isMobile = Platform.OS === 'ios' || Platform.OS === 'android'
      const isSubSup = child?.type === 'sub' || child?.type === 'sup'

      const mobileStyles =
        isMobile && isSubSup ? selectMobileSubSupStyles(currentTokens, child?.type) : {}
      const defaultStyles = !isMobile && isSubSup ? { fontSize: supFontSize, lineHeight: 0 } : {}

      const sanitizedChild = React.cloneElement(
        isMobile && isSubSup ? (
          <View>
            <Text style={[childStyles, mobileStyles]}>{child.props.children}</Text>
          </View>
        ) : (
          child
        ),
        {
          style: { ...childStyles, ...defaultStyles }
        }
      )

      return sanitizedChild
    }

    const sanitizeChildren = () => {
      if (Array.isArray(children)) {
        // `React.Children.map` and not a plain `.map`: `resetTagStyling` clones `sub`/`sup`, and a
        // clone is unvalidated, so a plain map would make React demand a key the caller cannot give (#7254)
        return React.Children.map(children, resetTagStyling)
      }

      return resetTagStyling(children)
    }

    // `data-media` matches with `~=`, so append a caller's ids rather than letting `dataSet.media` replace the rules this component injects for itself (#7217)
    const mergedMediaIds = [mediaIds, dataSet?.media].filter(Boolean).join(' ')
    const dataSetValue = mergedMediaIds ? { ...dataSet, media: mergedMediaIds } : dataSet

    return block ? (
      <View ref={ref} {...containerProps}>
        <Text
          {...resolvedTextProps}
          style={getStyleWithGradient(textStyles)}
          dataSet={dataSetValue}
        >
          {sanitizeChildren(children)}
        </Text>
      </View>
    ) : (
      <Text
        ref={ref}
        {...containerProps}
        {...resolvedTextProps}
        style={getStyleWithGradient(textStyles)}
        dataSet={dataSetValue}
      >
        {sanitizeChildren(children)}
      </Text>
    )
  }
)
Typography.displayName = 'Typography'

Typography.propTypes = {
  ...selectedContainerPropTypes,
  ...selectedTextPropTypes,
  tokens: getTokensPropType('Typography'),
  variant: variantProp.propType,
  /**
   * Renders the text with "text-decoration: 'line-through'" applied.
   */
  strikeThrough: PropTypes.bool,
  /**
   * Renders the text as a semantic heading. If a html heading tag is provided, uses that tag on Web.
   *
   * Does not affect styling: <Typography heading="h2"> will render a <h2> that looks like plain text.
   * Use both `heading` and `variant` props to render semantic headings that look like headings.
   *
   * In native apps, if this is `true` or any html heading tag string and accessibilityRole prop
   * is not defined, the accessibilityRole will default to "heading".
   */
  heading: PropTypes.oneOf([...headingTags, true]),
  /**
   * Optional semantic HTML tag, to apply to the text on web. Does not change styling.
   *
   * `tag` and `heading` props set the same thing, so shouldn't be used together (`tag` overrides `heading`).
   *
   * 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(textTags),
  /**
   * If true, forces the element to behave as a View block even if nested in other text
   */
  block: PropTypes.bool,
  /**
   * Works the same as css text-align, note that RN doesn't support "start" or "end" (see https://reactnative.dev/docs/text-style-props#textalign)
   * Justify is known to make text harder to read, especially for people with dyslexia (see https://developer.mozilla.org/en-US/docs/Web/CSS/text-align#accessibility_concerns)
   */
  align: PropTypes.oneOf(['auto', 'left', 'right', 'center', 'justify']),
  /**
   * Same children allowed as a React Native <Text> node
   */
  children: PropTypes.node,
  /**
   * Data attributes to be added to the element
   */
  dataSet: PropTypes.object
}
