import { forwardRef,
	 useCallback,
	 useEffect,
	 useImperativeHandle,
	 useLayoutEffect,
	 useMemo,
	 useRef,
	 useState } from 'react'

import type { CSSProperties, ForwardedRef } from 'react'

import type { NiconicoMetadata, NiconicoVideoInfo, NiconicoViewerHandle } from '@/types'

type NiconicoPlayerMessage =
    | { eventName: 'enterProgrammaticFullScreen' }
    | { eventName: 'exitProgrammaticFullScreen' }
    | { eventName: 'loadComplete'
	playerId?:  string
	data:       { videoInfo: NiconicoVideoInfo } }
    | { eventName: 'playerMetadataChange'
	playerId?:  string
	data:       NiconicoMetadata }
    | { eventName: 'playerStatusChange' | 'statusChange'
	playerId?:  string
	data?:      unknown }
    | { eventName: 'error'
	playerId?:  string
	data?:      unknown
	code?:      string
	message?:   string }

type NiconicoCommand =
    | { eventName: 'play'; sourceConnectorType: 1; playerId: string }
    | { eventName: 'pause'; sourceConnectorType: 1; playerId: string }
    | { eventName: 'seek'; sourceConnectorType: 1; playerId: string; data: { time: number } }
    | { eventName: 'mute'; sourceConnectorType: 1; playerId: string; data: { mute: boolean } }
    | { eventName: 'volumeChange'; sourceConnectorType: 1; playerId: string;
	data: { volume: number } }
    | { eventName: 'commentVisibilityChange'; sourceConnectorType: 1; playerId: string;
	data: { commentVisibility: boolean } }

const EMBED_ORIGIN = 'https://embed.nicovideo.jp'
const LOAD_COMPLETE_TIMEOUT_MS = 8_000

type Props = {
    id:                 string
    width:              number
    height:             number
    style?:             CSSProperties
    onLoadComplete?:    (info: NiconicoVideoInfo) => void
    onMetadataChange?:  (meta: NiconicoMetadata) => void
    onError?:           (data: unknown) => void }


export default forwardRef ((props: Props, ref: ForwardedRef<NiconicoViewerHandle>) => {
  const { id, width, height, style = { }, onLoadComplete, onMetadataChange, onError } = props

  const iframeRef = useRef<HTMLIFrameElement> (null)
  const loadCompleteTimerRef = useRef<ReturnType<typeof setTimeout> | null> (null)
  const playerId = useMemo (
      () => `nico-${ id }-${ Math.random ().toString (36).slice (2) }`,
      [id])

  const [screenWidth, setScreenWidth] = useState<CSSProperties['width']> ()
  const [screenHeight, setScreenHeight] = useState<CSSProperties['height']> ()
  const [landscape, setLandscape] = useState<boolean> (false)
  const [fullScreen, setFullScreen] = useState<boolean> (false)

  const src =
      `${ EMBED_ORIGIN }/watch/${ id }`
      + '?jsapi=1'
      + `&playerId=${ encodeURIComponent (playerId) }`
      + '&persistence=1'
      + '&oldScript=1'
      + '&referer='
      + '&from=0'
      + '&allowProgrammaticFullScreen=1'

  const styleFullScreen: CSSProperties =
      fullScreen
      ? { top: 0,
	  left: landscape ? 0 : '100%',
	  position: 'fixed',
	  width: screenWidth,
	  height: screenHeight,
	  zIndex: 2_147_483_647,
	  maxWidth: 'none',
	  transformOrigin: '0% 0%',
	  transform: landscape ? 'none' : 'rotate(90deg)',
	  WebkitTransformOrigin: '0% 0%',
	  WebkitTransform: landscape ? 'none' : 'rotate(90deg)' }
      : { }

  const margedStyle: CSSProperties =
      { border: 'none', maxWidth: '100%', ...style, ...styleFullScreen }

  const clearLoadCompleteTimer = useCallback (() => {
    if (!(loadCompleteTimerRef.current))
      return

    clearTimeout (loadCompleteTimerRef.current)
    loadCompleteTimerRef.current = null
  }, [])

  const startLoadCompleteTimer = useCallback (() => {
    clearLoadCompleteTimer ()
    loadCompleteTimerRef.current = setTimeout (() => {
      onError?.({
	  eventName: 'loadCompleteTimeout',
	  reason:    'niconico video length was not reported by embed'})
    }, LOAD_COMPLETE_TIMEOUT_MS)
  }, [clearLoadCompleteTimer, onError])

  const postToPlayer = useCallback ((message: NiconicoCommand) => {
    const win = iframeRef.current?.contentWindow
    if (!(win))
      return

    win.postMessage (message, EMBED_ORIGIN)
  }, [])

  const play = useCallback (() => {
    postToPlayer ({ eventName: 'play', sourceConnectorType: 1, playerId })
  }, [playerId, postToPlayer])

  const pause = useCallback (() => {
    postToPlayer ({ eventName: 'pause', sourceConnectorType: 1, playerId })
  }, [playerId, postToPlayer])

  const seek = useCallback ((time: number) => {
    postToPlayer (
	{ eventName: 'seek', sourceConnectorType: 1, playerId,
	  data: { time } })
  }, [playerId, postToPlayer])

  const mute = useCallback (() => {
    postToPlayer ({ eventName: 'mute', sourceConnectorType: 1, playerId, data: { mute: true } })
  }, [playerId, postToPlayer])

  const unmute = useCallback (() => {
    postToPlayer ({ eventName: 'mute', sourceConnectorType: 1, playerId, data: { mute: false } })
  }, [playerId, postToPlayer])

  const setVolume = useCallback ((volume: number) => {
    postToPlayer (
	{ eventName: 'volumeChange', sourceConnectorType: 1, playerId, data: { volume } })
  }, [playerId, postToPlayer])

  const showComments = useCallback (() => {
    postToPlayer (
	{ eventName: 'commentVisibilityChange', sourceConnectorType: 1, playerId,
	  data: { commentVisibility: true } })
  }, [playerId, postToPlayer])

  const hideComments = useCallback (() => {
    postToPlayer (
	{ eventName: 'commentVisibilityChange', sourceConnectorType: 1, playerId,
	  data: { commentVisibility: false } })
  }, [playerId, postToPlayer])

  useImperativeHandle (
      ref,
      () => ({ play, pause, seek, mute, unmute, setVolume, showComments, hideComments }),
      [play, pause, seek, mute, unmute, setVolume, showComments, hideComments])

  useEffect (() => {
    const onMessage = (event: MessageEvent<NiconicoPlayerMessage>) => {
      if (!(iframeRef.current)
	  || (event.source !== iframeRef.current.contentWindow)
	  || (event.origin !== EMBED_ORIGIN))
	return

      const data = event.data

      if (!(data)
	  || typeof data !== 'object'
	  || !('eventName' in data))
	return

      if (('playerId' in data)
	  && data.playerId
	  && data.playerId !== playerId)
	return

      if (data.eventName === 'enterProgrammaticFullScreen')
	{
	  setFullScreen (true)
	  return
	}

      if (data.eventName === 'exitProgrammaticFullScreen')
	{
	  setFullScreen (false)
	  return
	}

      if (data.eventName === 'loadComplete')
	{
	  clearLoadCompleteTimer ()
	  onLoadComplete?.(data.data.videoInfo)
	  return
	}

      if (data.eventName === 'playerMetadataChange')
	{
	  if (Number.isFinite (data.data.duration) && data.data.duration > 0)
	    clearLoadCompleteTimer ()

	  onMetadataChange?.(data.data)
	  return
	}

      if (data.eventName === 'error')
	{
	  clearLoadCompleteTimer ()
	  console.error ('niconico player error:', data)
	  onError?.(data)
	}
    }

    addEventListener ('message', onMessage)

    return () => removeEventListener ('message', onMessage)
  }, [clearLoadCompleteTimer, onError, onLoadComplete, onMetadataChange, playerId])

  useEffect (() => clearLoadCompleteTimer, [clearLoadCompleteTimer])

  useLayoutEffect (() => {
    if (!(fullScreen))
      return

    const initialScrollX = scrollX
    const initialScrollY = scrollY
    let timer: ReturnType<typeof setTimeout>
    let ended = false

    const pollingResize = () => {
      if (ended)
	return

      const isLandscape = innerWidth >= innerHeight
      const windowWidth = `${ isLandscape ? innerWidth : innerHeight }px`
      const windowHeight = `${ isLandscape ? innerHeight : innerWidth }px`

      setLandscape (isLandscape)
      setScreenWidth (windowWidth)
      setScreenHeight (windowHeight)
      timer = setTimeout (startPollingResize, 200)
    }

    const startPollingResize = () => {
      if (requestAnimationFrame)
	requestAnimationFrame (pollingResize)
      else
	pollingResize ()
    }

    startPollingResize ()

    return () => {
      clearTimeout (timer)
      ended = true
      scrollTo (initialScrollX, initialScrollY)
    }
  }, [fullScreen])

  useEffect (() => {
    if (!(fullScreen))
      return

    scrollTo (0, 0)
  }, [screenWidth, screenHeight, fullScreen])

  return (
      <iframe
	  ref={iframeRef}
	  src={src}
		  width={width}
		  height={height}
		  style={margedStyle}
		  onLoad={startLoadCompleteTimer}
		  allowFullScreen
		  allow="autoplay"/>)
})
