import React, { useEffect, useState, useRef } from 'react'
import PropTypes from 'prop-types'
import useIntersectionObserver from './hooks/useIntersectionObserver'
import { createGenerateClassName, StylesProvider } from '@mui/styles'
import { SheetsRegistry } from 'jss'
import Router from 'next/router'
import isBrowser from './utils/isBrowser'
import minifyStyles from './utils/minifyStyles'
import { useAmp } from 'next/amp'

const fuiEvents = ['mouseover', 'touchstart', 'scroll']
const touchEvents = ['touchstart', 'mouseover']
const eventOptions = { once: true, capture: true, passive: true }

let registries = []

// Only used for testing
export function getRegistryCount() {
  return registries.length
}

if (isBrowser()) {
  window.__lazyHydrateNavigated = false
}

/*
  This component renders the server side rendered stylesheets for the
  lazy hydrated components. Once they become hydrated, these stylesheets
  will be removed.
*/
export function LazyStyles() {
  /* istanbul ignore next */
  if (useAmp()) return null

  let styles = null
  try {
    styles = (
      <>
        {registries.map(registry => {
          function applyScope(sheet) {
            for (let rule of sheet.rules.index) {
              if (rule.type === 'conditional') {
                applyScope(rule)
              } else {
                rule.selectorText = `#${registry.id} ${rule.selectorText}`
              }
            }
          }

          // Apply these styles only to the wrapped component
          for (let sheet of registry.registry) {
            applyScope(sheet)
          }

          return (
            <style
              key={registry.id}
              id={registry.id}
              dangerouslySetInnerHTML={{ __html: minifyStyles(registry.toString()) }}
            />
          )
        })}
      </>
    )
  } finally {
    // Clear registeries so we do not leak memory
    registries = []
  }
  return styles
}

function LazyStylesProvider({ id, children }) {
  const generateClassName = createGenerateClassName({
    seed: id,
  })
  const registry = new SheetsRegistry()
  registry.id = id
  registries.push(registry)
  return (
    <StylesProvider
      sheetsManager={new Map()}
      serverGenerateClassName={generateClassName}
      sheetsRegistry={registry}
    >
      {children}
    </StylesProvider>
  )
}

Router.events.on('routeChangeStart', () => {
  window.__lazyHydrateNavigated = true
})

function LazyHydrateInstance({ id, className, ssrOnly, children, on, ...props }) {
  function isHydrated() {
    if (isBrowser()) {
      // If rendering after client side navigation
      if (window.__lazyHydrateNavigated) return true
      // return true
      if (ssrOnly) return false
      return !!props.hydrated
    } else {
      return true
    }
  }

  const childRef = useRef(null)
  const [hydrated, setHydrated] = useState(isHydrated())

  function hydrate() {
    if (!hydrated) {
      setHydrated(true)
      removeSSRStyles()
    }
  }

  function removeSSRStyles() {
    // Remove the server side generated stylesheet
    const stylesheet = window.document.getElementById(id)

    if (stylesheet) {
      stylesheet.remove()
    }
  }

  // hydrate if the hydrated prop is changed to true
  useEffect(() => {
    if (props.hydrated) {
      hydrate()
    }
  }, [props.hydrated, ssrOnly])

  if (on === 'visible') {
    useIntersectionObserver(
      // As root node does not have any box model, it cannot intersect.
      () => childRef.current.children[0],
      (visible, disconnect) => {
        if (visible) {
          hydrate()
          disconnect()
        }
      },
      [],
      // Fallback to eager hydration
      /* istanbul ignore next */
      () => hydrate(),
    )
  }

  useEffect(() => {
    if (hydrated) return

    const handler = () => {
      hydrate()
      clearEventListeners()
    }

    const clearEventListeners = () => {
      if (on === 'touch') {
        touchEvents.forEach(type => {
          childRef.current.removeEventListener(type, handler, eventOptions)
        })
      } else if (on === 'fui') {
        fuiEvents.forEach(type => {
          window.removeEventListener(type, handler, eventOptions)
        })
      }
    }

    const onUnmount = () => {
      // remove the SSR styles since the next time this component renders it will already be hydrated
      removeSSRStyles()
      clearEventListeners()
    }

    if (on === 'fui') {
      fuiEvents.forEach(type => window.addEventListener(type, handler, eventOptions))
    } else if (on === 'touch') {
      touchEvents.forEach(type => childRef.current.addEventListener(type, handler, eventOptions))
    }

    return onUnmount
  }, [on])

  if (hydrated) {
    return (
      <div ref={childRef} id={id} className={className} style={{ display: 'contents' }}>
        {children}
      </div>
    )
  } else {
    return (
      <div
        ref={childRef}
        id={id}
        className={className}
        style={{ display: 'contents' }}
        suppressHydrationWarning
        dangerouslySetInnerHTML={{ __html: '' }}
      />
    )
  }
}

/**
 * LazyHydrate a component based on a specified trigger
 *
 * Example usage:
 *
 *  <LazyHydrate id="foo">
 *    <div>some expensive component</div>
 *  </LazyHydrate>
 *
 */

function LazyHydrate({ children, ...props }) {
  /* istanbul ignore next */
  if (useAmp()) return children

  return (
    <LazyHydrateInstance {...props}>
      {/* LazyStylesProvider should not be used in the browser. Once components 
      are hydrated, their styles will automatically be managed by the app's main 
      StyleProvider. Using LazyStylesProvider in the browser will result in duplicated
      and conflicting styles in lazy components once they are hydrated. */}
      {isBrowser() ? children : <LazyStylesProvider {...props}>{children}</LazyStylesProvider>}
    </LazyHydrateInstance>
  )
}

LazyHydrate.defaultProps = {
  on: 'visible',
}

LazyHydrate.propTypes = {
  // Identification of component
  id: PropTypes.string.isRequired,
  // Control the hydration of the component externally with this prop
  hydrated: PropTypes.bool,
  // Force component to never hydrate
  ssrOnly: PropTypes.bool,
  /* 
    Event to trigger hydration
    eventOptions
      - `visible` triggers hydration when component comes into the viewport
      - `touch` triggers hydration on touchstart or mouseover
      - `fui` (default) triggers hydration when user first interacts with page
  */
  on: PropTypes.oneOf(['visible', 'touch', 'fui']),
}

export default LazyHydrate
