import React, { useEffect, useMemo, ReactElement, memo } from 'react'
import { Platform, StyleProp, View, ViewStyle } from 'react-native'
import {
  responsivePositioningOptions,
  GROUP,
  SECTION,
  LOCATION_INPUT,
} from '@adalo/constants'
import { normalizeAttributes } from 'utils/styles'

import {
  usePushNode,
  useResponsiveObject,
  useHandleLayoutChange,
  useRenderedChildren,
  useVerticallyExpandedFixedComponent,
  getLayoutCacheKey,
} from 'hooks/responsive'

import {
  RunnerObject,
  Attributes,
  Screen,
  ComponentFixedPosition,
  Layout,
  DeviceType,
  LayoutMetadata,
} from 'types'
import { IApp } from 'interfaces'
import { getDeviceType } from 'utils/device'
import { getScreenSize } from 'utils/responsive'

const { FIXED_ON_SCROLL } = responsivePositioningOptions

export interface ResponsiveComponentProps {
  branding: unknown
  ObjectClass: React.FC
  object: RunnerObject
  component: Screen

  getBaseProps: (
    obj: RunnerObject,
    types: Record<string, any>
  ) => React.Props<React.FC>
  renderChildren: (children: RunnerObject[]) => ReactElement[]

  pushId: string
  active: boolean
  visible: boolean
  visibleOnDevice: boolean
  setHeight: (pushId: string, obj: RunnerObject, height: number) => void
  setVisible: (
    pushId: string,
    obj: RunnerObject,
    visible: boolean,
    visibleOnDevice: boolean
  ) => void
  bindingData: unknown[]
  adjustedNodeHeight: number
  parentLayoutMetadata: LayoutMetadata
  isReusableComponent: boolean
  previewType: string
  layoutType: 'body' | 'fixed'
  app: IApp
  getFlags: () => Record<string, boolean>
}

const permittedExpandableTypes = new Set<string>([SECTION, GROUP])
const determineLayoutAdjustments = ({
  component,
  object,
  renderObject,
  layoutType,
  positioningLayout,
}: {
  component: Screen
  object: RunnerObject
  renderObject: RunnerObject
  layoutType: ResponsiveComponentProps['layoutType']
  positioningLayout: Layout
}): {
  componentFixedPosition: ComponentFixedPosition
  shouldApplyRepositioning: boolean
} => {
  const widthPercentage = ((positioningLayout: Layout): number => {
    if (typeof positioningLayout.width === 'string') {
      return Math.ceil(parseFloat(positioningLayout.width))
    }

    if (!positioningLayout.width) {
      return 0
    }

    return Math.ceil(positioningLayout.width)
  })(positioningLayout)

  const hasRectangleChildFixedOnTop = renderObject.children?.some(
    child => child.type === SECTION && child.layout.top === 0
  )
  const isGroupWithRectangleFixedOnTop = object.type === GROUP && hasRectangleChildFixedOnTop // prettier-ignore
  const isFixedBottom = component.height === object.attributes?.y + object.attributes?.height // prettier-ignore

  // we only want to expand components that are either a rectangle (section) or
  // a group that contain rectangles (sections), and only if the width is 100%
  if (
    positioningLayout.top === 20 &&
    widthPercentage >= 100 &&
    typeof positioningLayout.height === 'number' &&
    (object.type === SECTION || isGroupWithRectangleFixedOnTop)
  ) {
    return {
      componentFixedPosition: ComponentFixedPosition.TOP,
      shouldApplyRepositioning: true,
    }
  }

  // Rectangles or groups containing rectangles are expanded if we detect they
  // were placed on the very bottom of the screen in the builder
  if (
    typeof positioningLayout.height === 'number' &&
    isFixedBottom &&
    permittedExpandableTypes.has(object.type) &&
    layoutType === 'fixed' &&
    widthPercentage >= 100
  ) {
    return {
      componentFixedPosition: ComponentFixedPosition.BOTTOM,
      shouldApplyRepositioning: true,
    }
  }

  // The `useVerticallyExpandedFixedComponent` hook will still run
  // however the data returned from the hook will not be acted upon
  return {
    componentFixedPosition: ComponentFixedPosition.BOTTOM,
    shouldApplyRepositioning: false,
  }
}

export const ResponsiveComponentNoMemo = ({
  branding,
  ObjectClass,
  object,
  component,

  getBaseProps,
  renderChildren,

  setHeight,
  setVisible,
  active,
  visible,
  visibleOnDevice,

  pushId,
  bindingData,
  adjustedNodeHeight,
  parentLayoutMetadata,

  previewType,
  layoutType,
  app,
}: ResponsiveComponentProps): ReactElement | null => {
  const isFixed = object?.responsivity?.verticalPositioning === FIXED_ON_SCROLL
  let { nodeHeight, newY } = usePushNode(component.id, pushId, isFixed)
  const layoutCacheKey = getLayoutCacheKey(previewType)
  const { renderObject, positioningLayout, layoutMetadata } =
    useResponsiveObject(object, newY, parentLayoutMetadata, app, layoutCacheKey)

  const deviceType = useMemo(() => {
    const { width } = getScreenSize()

    return getDeviceType(width, app) as DeviceType
  }, [renderObject])

  // apply offset for bottom fixed components
  if (positioningLayout.bottom === -20) {
    positioningLayout.bottom = 0
  }

  const { componentFixedPosition, shouldApplyRepositioning } =
    determineLayoutAdjustments({
      component,
      object,
      renderObject,
      layoutType,
      positioningLayout,
    })
  // this hook must always run
  const { newTop, newBottom, newHeight } = useVerticallyExpandedFixedComponent(
    componentFixedPosition,
    positioningLayout,
    renderObject,
    previewType,
    layoutCacheKey,
    deviceType
  )

  useEffect(() => {
    setVisible(pushId, renderObject, visible, visibleOnDevice)
  }, [visible, pushId, renderObject])

  // TODO(dyego) In native in some situations the layout
  // property gets frozen, there is a card to address this issue:
  // https://trello.com/c/l74BvoGf/96-top-rectangle-within-a-group-is-pulled-up-to-fill-in-the-inset-area-instead-of-getting-expanded
  if (Object.isFrozen(renderObject.layout)) {
    renderObject.layout = { ...renderObject.layout }
  }

  // If there are any rectangles with children we have to make sure
  // the nodeHeight is not smaller than the height of the
  // renderObject, otherwise the children will overflow the parent
  // This only applies to top fixed rectangles, since we have to
  // both decrease their "top" position and increase their base height
  if (
    renderObject.type === SECTION &&
    typeof renderObject?.layout?.height === 'number' &&
    renderObject?.layout?.height > nodeHeight &&
    renderObject.children?.length !== 0 &&
    renderObject.attributes.y === 20
  ) {
    nodeHeight = renderObject.layout.height
  }

  // we only want to expand components that are either a rectangle (section) or
  // a group that contain rectangles (sections), and only if the width is 100%
  if (
    componentFixedPosition === ComponentFixedPosition.TOP &&
    shouldApplyRepositioning
  ) {
    if (typeof newTop === 'number') {
      // we decrease the top by the amount we increased the height
      positioningLayout.top = newTop
    }

    if (typeof newHeight === 'number' && object.type === SECTION) {
      renderObject.layout.height = newHeight
    }
  }

  // Rectangles or groups containing rectangles are expanded if we detect they
  // were placed on the very bottom of the screen in the builder
  if (
    componentFixedPosition === ComponentFixedPosition.BOTTOM &&
    shouldApplyRepositioning
  ) {
    if (typeof newBottom === 'number') {
      if (newTop === 0) {
        // if  the component is sticky the bottom property is set
        // so we can use it to position the component at the bottom
        positioningLayout.bottom = newBottom * -1
      } else {
        // if the component is non-sticky the top property is set and we increase
        // the top by the amount we increased the height
        positioningLayout.top = newTop
      }
    }

    if (typeof newHeight === 'number') {
      renderObject.layout.height = newHeight
      nodeHeight = newHeight
    }
  }

  const handleLayoutChange = useHandleLayoutChange({
    renderObject,
    setHeight,
    nodeHeight: adjustedNodeHeight || nodeHeight,
    pushId,
    layoutCacheKey,
  })

  const baseProps = useMemo(
    () =>
      getBaseProps(renderObject, {
        newY,
        nodeHeight: adjustedNodeHeight || nodeHeight,
        bindingData,
        parentLayoutMetadata: layoutMetadata,
      }),
    [
      renderObject,
      newY,
      adjustedNodeHeight,
      nodeHeight,
      bindingData,
      active,
      layoutCacheKey,
      layoutMetadata,
    ]
  )

  const renderedChildren = useRenderedChildren(
    renderObject,
    renderChildren,
    visible,
    active,
    layoutMetadata,
    layoutCacheKey
  )

  if (!visible) {
    return null
  }

  if (branding) {
    renderObject.attributes = normalizeAttributes(
      renderObject.attributes,
      branding as Object
    ) as Attributes
  }

  // positioningLayout
  let child = <ObjectClass {...baseProps}>{renderedChildren}</ObjectClass>

  if (renderObject.attributes?.variableHeight) {
    child = (
      <View pointerEvents="box-none" style={{ position: 'relative' }}>
        {child}
      </View>
    )
  }

  const minHeightStyle: StyleProp<ViewStyle> = {}
  if (nodeHeight && Platform.OS === 'android') {
    // The wrapping View must completely wrap it's child for all the content within it to be tappable in android.
    // If nodeHeight isn't set as the height, it'll only have the object.layout height set, which could be less than
    // the inner child's height if it has variable height content, and not all the content within it would be tappable.
    minHeightStyle.minHeight = nodeHeight

    if (object.type === LOCATION_INPUT) {
      const { height, borderWidth = 0 } = object.attributes
      minHeightStyle.minHeight = height + 2 * borderWidth
    }
  }

  return (
    <View
      onLayout={handleLayoutChange}
      pointerEvents="box-none"
      style={[positioningLayout, minHeightStyle]}
    >
      {child}
    </View>
  )
}

export const ResponsiveComponent = memo(ResponsiveComponentNoMemo)
