import * as React from 'react'
import { NativeSafeAreaProvider } from './NativeSafeAreaProvider'
import { getWindowFrame } from './getWindowFrame'
import { initialWindowMetrics } from './InitialWindow'
import { feedTamaguiSafeArea } from './tamaguiSafeArea'
import type { EdgeInsets, InsetChangedEvent, Metrics, Rect } from './SafeArea-types'

const isDev = process.env.NODE_ENV !== 'production'

export const SafeAreaInsetsContext = React.createContext<EdgeInsets | null>(null)
if (isDev) {
  SafeAreaInsetsContext.displayName = 'SafeAreaInsetsContext'
}

export const SafeAreaFrameContext = React.createContext<Rect | null>(null)
if (isDev) {
  SafeAreaFrameContext.displayName = 'SafeAreaFrameContext'
}

export interface SafeAreaProviderProps {
  children?: React.ReactNode
  initialMetrics?: Metrics | null
  /**
   * @deprecated
   */
  initialSafeAreaInsets?: EdgeInsets | null
}

const ZERO_INSETS: EdgeInsets = { top: 0, bottom: 0, left: 0, right: 0 }

export function SafeAreaProvider({
  children,
  initialMetrics,
  initialSafeAreaInsets,
  ...others
}: SafeAreaProviderProps) {
  const parentInsets = useParentSafeAreaInsets()
  const parentFrame = useParentSafeAreaFrame()
  // the outermost provider measures the window, so it alone feeds tamagui.
  const isRoot = parentInsets === null
  // default to zero insets so useSafeAreaInsets() doesn't throw on the first
  // render. Real insets are populated shortly after mount (on web via
  // NativeSafeAreaProvider's useEffect DOM measurement, on native via the
  // native insetChanged event).
  const [insets, setInsets] = React.useState<EdgeInsets>(
    initialMetrics?.insets ?? initialSafeAreaInsets ?? parentInsets ?? ZERO_INSETS
  )
  const [frame, setFrame] = React.useState<Rect>(
    initialMetrics?.frame ??
      parentFrame ??
      // backwards compat so we render anyway if we don't have frame.
      getWindowFrame()
  )
  const onInsetsChange = React.useCallback(
    (event: InsetChangedEvent) => {
      const {
        nativeEvent: { frame: nextFrame, insets: nextInsets },
      } = event

      if (isRoot && nextFrame) {
        feedTamaguiSafeArea({ insets: nextInsets, frame: nextFrame }, tamaguiHooks)
      }

      // urgent, as upstream: a transition here stalls behind the urgent renders a
      // rotation or fold keeps making, so the new window renders with the old insets.
      setFrame((curFrame) => {
        if (
          // Backwards compat with old native code that won't send frame.
          nextFrame &&
          (nextFrame.height !== curFrame.height ||
            nextFrame.width !== curFrame.width ||
            nextFrame.x !== curFrame.x ||
            nextFrame.y !== curFrame.y)
        ) {
          return nextFrame
        }
        return curFrame
      })

      setInsets((curInsets) => {
        if (
          !curInsets ||
          nextInsets.bottom !== curInsets.bottom ||
          nextInsets.left !== curInsets.left ||
          nextInsets.right !== curInsets.right ||
          nextInsets.top !== curInsets.top
        ) {
          return nextInsets
        }
        return curInsets
      })
    },
    [isRoot]
  )

  return (
    <NativeSafeAreaProvider onInsetsChange={onInsetsChange} {...others}>
      <SafeAreaFrameContext.Provider value={frame}>
        <SafeAreaInsetsContext.Provider value={insets}>
          {children}
        </SafeAreaInsetsContext.Provider>
      </SafeAreaFrameContext.Provider>
    </NativeSafeAreaProvider>
  )
}

function useParentSafeAreaInsets(): EdgeInsets | null {
  return React.useContext(SafeAreaInsetsContext)
}

function useParentSafeAreaFrame(): Rect | null {
  return React.useContext(SafeAreaFrameContext)
}

const NO_INSETS_ERROR =
  'No safe area value available. Make sure you are rendering `<SafeAreaProvider>` at the top of your app.'

export function useSafeAreaInsets(): EdgeInsets {
  const insets = React.useContext(SafeAreaInsetsContext)
  if (insets == null) {
    throw new Error(NO_INSETS_ERROR)
  }
  return insets
}

export function useSafeAreaFrame(): Rect {
  const frame = React.useContext(SafeAreaFrameContext)
  if (frame == null) {
    throw new Error(NO_INSETS_ERROR)
  }
  return frame
}

export type WithSafeAreaInsetsProps = {
  insets: EdgeInsets
}

export function withSafeAreaInsets<T>(
  WrappedComponent: React.ComponentType<T & WithSafeAreaInsetsProps>
): React.ForwardRefExoticComponent<
  React.PropsWithoutRef<T> & React.RefAttributes<unknown>
> {
  return React.forwardRef((props, ref) => {
    const insets = useSafeAreaInsets()
    // @ts-expect-error
    return <WrappedComponent {...props} insets={insets} ref={ref} />
  }) as any
}

const tamaguiHooks = { useSafeAreaInsets, useSafeAreaFrame }
feedTamaguiSafeArea(initialWindowMetrics, tamaguiHooks)

/**
 * @deprecated
 */
export function useSafeArea(): EdgeInsets {
  return useSafeAreaInsets()
}

/**
 * @deprecated
 */
export const SafeAreaConsumer = SafeAreaInsetsContext.Consumer

/**
 * @deprecated
 */
export const SafeAreaContext = SafeAreaInsetsContext
