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'

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


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

  const iframeRef = useRef<HTMLIFrameElement> (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 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')
	{
	  onLoadComplete?.(data.data.videoInfo)
	  return
	}

      if (data.eventName === 'playerMetadataChange')
	{
	  onMetadataChange?.(data.data)
	  return
	}

      if (data.eventName === 'error')
        console.error ('niconico player error:', data)
    }

    addEventListener ('message', onMessage)

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

  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}
	  allowFullScreen
	  allow="autoplay"/>)
})
