import TWEEN from '@tweenjs/tween.js'
import React, { useRef } from 'react'
import { GLView, ExpoWebGLRenderingContext } from 'expo-gl'
import { Renderer } from 'expo-three'
import OrbitControlsView from 'expo-three-orbit-controls'

// scene elements
import { getParticles } from './particles'
import { getEarth } from './earth'
import { getBackground } from './background'
import { getRingLight } from './ringLight'
import { getSphereHaze } from './sphereHaze'

// coordinates & utils
import { CountryCard } from '../types/CountryCard'
import { lngLatToVector3 } from './globe-util'
import { getLabels } from './labels'
import { getCoordinates } from './getCoordinates'
import { getPointLights } from './pointLights'
import { getSpotlight } from './spotlight'
import { useEffect } from 'react'
import { Camera, FogExp2, PerspectiveCamera, Scene } from 'three'
import { LogBox } from 'react-native'

LogBox.ignoreLogs(['.inverse() has been renamed to invert()'])

const radius = 100

interface GlobeProps {
  streamCoordinates: number[][]
  targetCountry: CountryCard
  allCountries: CountryCard[]
}

type TweenableVector3 = Record<'x' | 'y' | 'z', number>

function fract(x: number) {
  return x - Math.floor(x)
}

export const Globe = (props: GlobeProps): React.ReactElement => {
  const { streamCoordinates, targetCountry, allCountries } = props
  const [country, setCountry] = React.useState<CountryCard | null>(
    targetCountry
  )
  const [camera, setCamera] = React.useState<Camera | null>(null)
  const [selectLabel, setSelectLabel] = React.useState<
    ((targetCountry: CountryCard) => void) | null
  >(null)
  const orbitControlsRef = useRef<typeof OrbitControlsView>(null)

  let timeout: number
  useEffect(() => () => clearTimeout(timeout), [])
  useEffect(() => setCountry(props.targetCountry), [props])

  useEffect(() => {
    if (!country) return
    focusCountry?.(country)
    selectLabel?.(country)
  }, [country])

  function focusCountry(country: CountryCard) {
    if (!camera) return

    const controls = orbitControlsRef?.current?.getControls()
    if (controls) controls.autoRotate = false
    TWEEN.removeAll()

    const coords = getCoordinates(country)
    if (!coords) return

    const countryPosition = lngLatToVector3(coords.x, coords.y, radius)
    new TWEEN.Tween(camera.position as TweenableVector3)
      .to(countryPosition as TweenableVector3, 2000)
      .onStart(() => (controls.enabled = false))
      .onComplete(() => (controls.enabled = true))
      .easing(TWEEN.Easing.Exponential.Out)
      ?.start(undefined)
  }

  const onContextCreate = async (
    gl: ExpoWebGLRenderingContext
  ): Promise<void> => {
    // renderer
    const { drawingBufferWidth: width, drawingBufferHeight: height } = gl
    const renderer = new Renderer({ gl })
    renderer.setSize(width, height)

    // camera
    const camera = new PerspectiveCamera(90, width / height, 0.1, 900)
    camera.add(getSpotlight())
    camera.add(getRingLight(camera))
    camera.add(getPointLights(radius))
    setCamera(camera)

    const scene = new Scene()
    scene.fog = new FogExp2(0xffffff, 0.001)

    const { labels, selectLabel } = getLabels(allCountries, radius)
    setSelectLabel(selectLabel)
    scene.add(...labels)

    const earth = await getEarth(radius)
    scene.add(earth)

    const background = await getBackground(renderer)
    scene.background = background

    const { particles, updateParticles } = await getParticles(
      streamCoordinates,
      radius + 3
    )
    scene.add(particles)

    const { sphereHaze, updateSphereHaze } = await getSphereHaze(camera)
    camera.add(sphereHaze)

    scene.add(camera)

    // Setup an animation loop
    const render = (time: number) => {
      timeout = requestAnimationFrame(render)
      renderer.render(scene, camera)

      const controls = orbitControlsRef?.current?.getControls()
      if (controls && !controls.setup) {
        controls.autoRotate = true
        controls.autoRotateSpeed = 1.1
        controls.dampingFactor = 0.04
        controls.enableDamping = true
        controls.enablePan = false
        controls.maxDistance = 100 + radius
        controls.maxPolarAngle = Math.PI * 0.6
        controls.minDistance = 100 + radius
        controls.minPolarAngle = Math.PI * 0.2
        controls.rotateSpeed = 2
        controls.setup = true
      }

      updateParticles(fract(time / 10000))
      TWEEN.update(undefined)
      controls?.update()
      updateSphereHaze()
      gl.endFrameEXP()
    }

    render(0)
  }

  return (
    <OrbitControlsView
      style={{ flex: 1, justifyContent: 'center' }}
      camera={camera}
      ref={orbitControlsRef}
    >
      <GLView style={{ flex: 1 }} onContextCreate={onContextCreate} />
    </OrbitControlsView>
  )
}
