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

import { applyTextStyles, applyShadowToken, applyOuterBorder } from '../ThemeProvider/utils/styles'
import { useTheme } from '../ThemeProvider/useTheme'
import { useViewport } from '../ViewportProvider/useViewport'
import { buttonPropTypes } from './propTypes'
import { a11yProps } from '../utils/props/a11yProps'
import { clickProps } from '../utils/props/clickProps'
import { focusHandlerProps } from '../utils/props/handlerProps'
import {
  getCursorStyle,
  resolvePressableState,
  resolvePressableTokens
} from '../utils/pressability'
import { linkProps } from '../utils/props/linkProps'
import { resolveResponsiveProp } from '../utils/useResponsiveProp'
import { selectSystemProps } from '../utils/props/selectSystemProps'
import { viewProps } from '../utils/props/viewProps'
import { wrapStringsInText } from '../utils/children'
import { withLinkRouter } from '../utils/withLinkRouter'
import { contentfulProps } from '../utils/props/contentfulProps'
import { createMediaQueryStyles } from '../utils/ssr-media-query/utils/create-media-query-styles'
import { StyleSheet as StyleSheetUtils } from '../utils/ssr-media-query/ssr-media-query'
import { IconText } from '../Icon/IconText'

const [selectProps, selectedSystemPropTypes] = selectSystemProps([
  a11yProps,
  focusHandlerProps,
  linkProps,
  viewProps,
  contentfulProps
])

const getOuterBorderOffset = ({ outerBorderGap = 0, outerBorderWidth = 0 }) =>
  outerBorderGap + outerBorderWidth

const BORDER_OFFSET_SIDES_PER_AXIS = 2

const selectFlexAndWidthStyles = ({ width, flex }) => {
  const styles = {}

  if (width !== undefined && width !== null) {
    styles.width = width
  }

  if (width === '100%') {
    styles.flex = 1
  } else if (flex !== undefined && flex !== null) {
    styles.flex = flex
  }

  return styles
}

const selectOuterContainerStyles = (
  {
    opacity,
    outerBorderColor,
    outerBorderWidth,
    outerBorderGap,
    borderRadius,
    outerBackgroundColor,
    alignSelf
  },
  heightFull
) => ({
  backgroundColor: outerBackgroundColor,
  opacity,
  ...Platform.select({
    native: {
      alignSelf: alignSelf !== undefined ? alignSelf : 'flex-start'
    },
    web: heightFull ? {} : { alignSelf: 'flex-start' }
  }),
  ...applyOuterBorder({
    outerBorderGap,
    outerBorderWidth,
    outerBorderColor,
    borderRadius
  })
})

/**
 * Resolves the outer container's width and height from their tokens. The inner container's
 * bounding box is the bounding box of the button overall, so `outerBorderOffset` device pixels of
 * focus ring sit outside it and each axis compensates for twice that offset.
 *
 * Number tokens are device pixels and take the offset by addition; string tokens carry their own
 * units (e.g. `%`) and take it through `calc()` on web. The two axes resolve independently,
 * because a numeric height must not pull a string width onto the numeric path, where the offset
 * would concatenate rather than add (`'100%' + 8 === '100%8'`) and produce a length the platform
 * discards. `ButtonGroupItem` is the token set that reaches this: every brand gives it a numeric
 * height alongside a percentage width for the `equal` and `responsive` width variants.
 *
 * With no width token the button wraps its content, so a flex parent's default
 * `align-items: stretch` cannot stretch the focus ring past the content. That uses
 * `display: inline-flex` rather than `width: fit-content`, which Firefox doesn't support and
 * which can't be cascaded the way CSS allows (`width: fit-content; width: -moz-fit-content;`).
 *
 * On web the `calc()` keeps the compensating negative margin from pushing the element off-centre.
 * Native has no `calc()`, but unlike web its `flexGrow` fills the container width, so a
 * full-width token resolves to `flexGrow` instead. A sub-100% percentage keeps its raw value on
 * native: the slight offset matters less while the button isn't centred — if centring support is
 * added (e.g. `alignSelf: center`), set `marginHorizontal` to 0 when centred.
 */
const selectOuterSizeStyles = ({ outerBorderGap, outerBorderWidth, width, height }) => {
  const outerBorderOffset = getOuterBorderOffset({ outerBorderGap, outerBorderWidth })
  const axisOffset = outerBorderOffset * BORDER_OFFSET_SIDES_PER_AXIS
  const isNumericHeight = typeof height === 'number' && height > 0
  const heightWithOffset = isNumericHeight ? height + axisOffset : height

  if (typeof width === 'number' && width > 0) {
    return { width: width + axisOffset, height: heightWithOffset }
  }

  if (!width) {
    return isNumericHeight
      ? { width, height: heightWithOffset }
      : { ...Platform.select({ web: { display: 'inline-flex' } }) }
  }

  if (Platform.OS === 'web') {
    return {
      width: `calc(${width} + ${axisOffset}px)`,
      height: isNumericHeight ? heightWithOffset : `calc(${height} + ${axisOffset}px)`
    }
  }

  if (width === '100%') {
    return { flexGrow: 1, ...(isNumericHeight ? { height: heightWithOffset } : {}) }
  }

  return { width, height: heightWithOffset }
}

const selectInnerContainerStyles = ({
  backgroundColor,
  paddingLeft,
  paddingRight,
  paddingTop,
  paddingBottom,
  shadow,
  borderWidth,
  borderLeftWidth,
  borderRightWidth,
  borderTopWidth,
  borderBottomWidth,
  minWidth
}) => {
  // Subtract border width from padding so overall button width/height doesn't
  // jump around if the border width changes (avoiding NaN and negative padding)
  const offsetBorder = ({ value, borderSize = borderWidth }) =>
    typeof value === 'number' && typeof borderSize === 'number'
      ? Math.max(0, value - borderSize)
      : value

  return {
    paddingLeft: offsetBorder({ value: paddingLeft, borderSize: borderLeftWidth }),
    paddingRight: offsetBorder({ value: paddingRight, borderSize: borderRightWidth }),
    paddingTop: offsetBorder({ value: paddingTop, borderSize: borderTopWidth }),
    paddingBottom: offsetBorder({ value: paddingBottom, borderSize: borderBottomWidth }),
    backgroundColor,
    minWidth,
    ...applyShadowToken(shadow)
  }
}

const selectBorderStyles = ({
  borderColor,
  borderWidth,
  borderRadius,
  borderLeftWidth,
  borderRightWidth,
  borderTopWidth,
  borderBottomWidth
}) => ({
  borderColor,
  borderWidth,
  borderRadius,
  borderLeftWidth,
  borderRightWidth,
  borderTopWidth,
  borderBottomWidth
})

const selectTextStyles = (
  { fontSize, color, lineHeight, fontName, fontWeight, textAlign, textLine, textLineStyle },
  themeOptions
) =>
  applyTextStyles({
    fontSize,
    color,
    lineHeight,
    fontName,
    fontWeight,
    themeOptions,
    textAlign,
    textDecorationLine: textLine,
    textDecorationStyle: textLineStyle
  })

const selectWebOnlyStyles = (inactive, themeTokens, { accessibilityRole }) => {
  return Platform.select({
    web: {
      // if it would overflow the container, wraps instead
      maxWidth: `calc(100% + ${getOuterBorderOffset(themeTokens) * 2}px)`,
      width: 'fit-content',
      outline: 'none', // removes the default browser :focus outline
      ...getCursorStyle(inactive, accessibilityRole)
    },
    default: {}
  })
}

const selectButtonStyles = ({ textAlign }) => {
  return {
    flexDirection: 'row',
    justifyContent: textAlign
  }
}

const selectItemIconTokens = ({ color, iconColor, iconSize }) => ({
  size: iconSize,
  color: iconColor || color
})

const defaultPressableState = { pressed: false, hovered: false, focused: false }

/**
 * Checks whether resolving the `tokens` prop produced a viewport-keyed map of token sets, as
 * returned by `useResponsiveThemeTokensCallback`, rather than the flat set returned by
 * `useThemeTokensCallback`. The media query stylesheet can only build rules from the map, so this
 * is what decides whether the media query path can run.
 *
 * @param {object} [resolvedTokens] - the `tokens` prop resolved for a single pressable state
 * @returns {boolean}
 *
 * @example
 * isViewportTokenMap({ xs: {}, sm: {}, md: {}, lg: {}, xl: {} }) // true
 * isViewportTokenMap({ backgroundColor: '#ffffff' }) // false
 */
const isViewportTokenMap = (resolvedTokens) =>
  Boolean(resolvedTokens) && viewports.keys.every((key) => key in resolvedTokens)

/**
 * Renders the pressable, themed shell shared by every button-like component. Not exported from the
 * package: `Button`, `ButtonLink`, `ButtonGroup`, `ButtonDropdown`, `Tags`, `ToggleSwitch`,
 * `Search`, `QuickLinksItem` and the `Pagination` buttons all render it with their own theme.
 *
 * The `tokens` prop is usually a getter that resolves tokens from the button's pressable state.
 * When the `enableMediaQueryStyleSheet` theme option is on and that getter returns a viewport-keyed
 * map, from `useResponsiveThemeTokensCallback`, the outer layout box is expressed as CSS media
 * rules so it does not re-lay out on hydration. A getter returning a flat token set keeps the
 * JS-resolved styles for every viewport.
 */
const ButtonBaseInner = React.forwardRef(
  (
    {
      id,
      href,
      hrefAttrs,
      children,
      tokens = {},
      disabled = false, // alias for inactive
      inactive = disabled,
      selected = false,
      icon,
      iconPosition = icon ? 'left' : undefined,
      iconProps,
      heightFull = true,
      ...rawRest
    },
    ref
  ) => {
    const { themeOptions } = useTheme()
    const viewport = useViewport()
    const { onPress, ...rest } = clickProps.toPressProps(rawRest)

    const extraButtonState = React.useMemo(
      () => ({ inactive, selected, iconPosition }),
      [inactive, selected, iconPosition]
    )

    const resolveButtonTokens = React.useCallback(
      (pressableState) => resolvePressableTokens(tokens, pressableState, extraButtonState),
      [tokens, extraButtonState]
    )

    const defaultStateTokens = React.useMemo(
      () =>
        themeOptions.enableMediaQueryStyleSheet
          ? resolveButtonTokens(defaultPressableState)
          : undefined,
      [themeOptions.enableMediaQueryStyleSheet, resolveButtonTokens]
    )

    const enableMediaQueryStyleSheet = isViewportTokenMap(defaultStateTokens)

    const systemProps = selectProps(rest)

    let layoutMediaQueryStyles
    let flexAndWidthStylesIds

    if (enableMediaQueryStyleSheet) {
      const layoutTokensByViewport = Object.entries(defaultStateTokens).reduce(
        (acc, [vp, viewportTokens]) => {
          const flexAndWidthStyles =
            viewportTokens.width === '100%' && viewportTokens.flex === 1
              ? selectFlexAndWidthStyles(viewportTokens)
              : {}

          acc[vp] = {
            ...staticStyles.row,
            ...selectWebOnlyStyles(inactive, viewportTokens, systemProps),
            ...(Object.keys(flexAndWidthStyles).length > 0 ? flexAndWidthStyles : {}),
            ...selectOuterSizeStyles(viewportTokens)
          }
          return acc
        },
        {}
      )

      const mediaQueryStyles = createMediaQueryStyles(layoutTokensByViewport)
      const { ids, styles } = StyleSheetUtils.create({
        layout: {
          ...mediaQueryStyles
        }
      })

      layoutMediaQueryStyles = styles.layout
      flexAndWidthStylesIds = ids.layout
    }

    const getPressableStyle = (pressableState) => {
      if (enableMediaQueryStyleSheet) {
        const themeTokens = resolveResponsiveProp(resolveButtonTokens(pressableState), viewport)
        return [layoutMediaQueryStyles, selectOuterContainerStyles(themeTokens, heightFull)]
      }

      const themeTokens = resolveButtonTokens(pressableState)
      const flexAndWidthStyles =
        themeTokens.width === '100%' && themeTokens.flex === 1
          ? selectFlexAndWidthStyles(themeTokens)
          : {}

      return [
        staticStyles.row,
        selectWebOnlyStyles(inactive, themeTokens, systemProps),
        selectOuterContainerStyles(themeTokens, heightFull),
        ...(Object.keys(flexAndWidthStyles).length > 0 ? [flexAndWidthStyles] : []),
        selectOuterSizeStyles(themeTokens)
      ]
    }

    const dataSetProp =
      flexAndWidthStylesIds || rawRest.dataSet
        ? {
            dataSet: {
              ...(flexAndWidthStylesIds ? { media: flexAndWidthStylesIds } : {}),
              ...rawRest.dataSet
            }
          }
        : {}

    return (
      <Pressable
        ref={ref}
        href={href}
        onPress={linkProps.handleHref({ href, onPress })}
        style={getPressableStyle}
        disabled={inactive}
        hrefAttrs={hrefAttrs}
        {...systemProps}
        {...dataSetProp}
      >
        {(pressableState) => {
          const themeTokens = enableMediaQueryStyleSheet
            ? resolveResponsiveProp(resolveButtonTokens(pressableState), viewport)
            : resolveButtonTokens(pressableState)
          const containerStyles = selectInnerContainerStyles(themeTokens)
          const borderStyles = selectBorderStyles(themeTokens)
          const textStyles = [
            selectTextStyles(themeTokens, themeOptions),
            staticStyles.text,
            Platform.select({
              web: {
                pointerEvents: 'none' // bubbles press event to parent `Pressable`
              }
            })
          ]
          const iconTokens = selectItemIconTokens(themeTokens)
          const { iconSpace } = themeTokens

          // If the container has a width set, fill it instead of sizing from content.
          // If in future we support text alignments other than center, add here.
          const stretchStyles = themeTokens.width ? staticStyles.stretch : staticStyles.align
          const IconComponent = icon || themeTokens.icon

          const rowStyles = selectButtonStyles(themeTokens)

          return (
            <View
              id={id}
              style={[
                containerStyles,
                borderStyles,
                stretchStyles,
                staticStyles.row,
                Platform.select({
                  web: {
                    maxWidth: '100%', // ensure overflowing content wraps
                    // TODO: https://github.com/telus/universal-design-system/issues/487
                    transition: 'background-color 200ms, border-color 200ms',
                    pointerEvents: 'none', // bubbles press event to parent `Pressable`
                    ...rowStyles
                  }
                })
              ]}
            >
              <IconText
                icon={IconComponent}
                iconPosition={iconPosition}
                space={icon ? iconSpace : 0}
                iconProps={{ ...iconProps, tokens: iconTokens }}
              >
                {wrapStringsInText(
                  typeof children === 'function'
                    ? children({
                        ...resolvePressableState(pressableState, extraButtonState),
                        textStyles
                      })
                    : children,
                  { style: textStyles }
                )}
              </IconText>
            </View>
          )
        }}
      </Pressable>
    )
  }
)
ButtonBaseInner.displayName = 'ButtonBase'

ButtonBaseInner.propTypes = {
  id: PropTypes.string,
  ...selectedSystemPropTypes,
  ...buttonPropTypes
}

const staticStyles = StyleSheet.create({
  row: {
    // Apply all button alignment horizontally; no vertical stacking within a button
    flexDirection: 'row',
    justifyContent: 'center'
  },
  text: {
    ...Platform.select({
      // TODO: https://github.com/telus/universal-design-system/issues/487
      web: { transition: 'color 200ms' }
    })
  },
  align: {
    alignItems: 'center'
  },
  stretch: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center'
  }
})

export const ButtonBase = withLinkRouter(ButtonBaseInner)
