import React, { Children, useEffect, useLayoutEffect } from 'react'

type State = JSX.Element[] | undefined

export type SideEffectProps = {
  reduceComponentsToState: <T extends {}>(
    components: Array<React.ReactElement<any>>,
    props: T
  ) => State
  handleStateChange?: (state: State) => void
  headManager: any
  inAmpMode?: boolean
  children: React.ReactNode
}

const isServer = typeof window === 'undefined'
const useClientOnlyLayoutEffect = isServer ? () => {} : useLayoutEffect
const useClientOnlyEffect = isServer ? () => {} : useEffect

export default function SideEffect(props: SideEffectProps) {
  const { headManager, reduceComponentsToState } = props

  function emitChange() {
    if (headManager && headManager.mountedInstances) {
      const headElements = Children.toArray(
        Array.from(headManager.mountedInstances as Set<React.ReactNode>).filter(
          Boolean
        )
      ) as React.ReactElement[]
      headManager.updateHead(reduceComponentsToState(headElements, props))
    }
  }

  if (isServer) {
    headManager?.mountedInstances?.add(props.children)
    emitChange()
  }

  useClientOnlyLayoutEffect(() => {
    headManager?.mountedInstances?.add(props.children)
    return () => {
      headManager?.mountedInstances?.delete(props.children)
    }
  })

  // We need to call `updateHead` method whenever the `SideEffect` is trigger in all
  // life-cycles: mount, update, unmount. However, if there are multiple `SideEffect`s
  // being rendered, we only trigger the method from the last one.
  // This is ensured by keeping the last unflushed `updateHead` in the `_pendingUpdate`
  // singleton in the layout effect pass, and actually trigger it in the effect pass.
  useClientOnlyLayoutEffect(() => {
    if (headManager) {
      headManager._pendingUpdate = emitChange
    }
    return () => {
      if (headManager) {
        headManager._pendingUpdate = emitChange
      }
    }
  })

  useClientOnlyEffect(() => {
    if (headManager && headManager._pendingUpdate) {
      headManager._pendingUpdate()
      headManager._pendingUpdate = null
    }
    return () => {
      if (headManager && headManager._pendingUpdate) {
        headManager._pendingUpdate()
        headManager._pendingUpdate = null
      }
    }
  })

  return null
}
