import {
  APIProvider,
  Map,
  useMap,
  type MapCameraChangedEvent,
} from '@vis.gl/react-google-maps'
import type { DeckGLComponentProps, InstanceRef } from '../types'
import { useCallback, useImperativeHandle, useMemo } from 'react'
import { DeckGlOverlay } from './deck-overlay'
import { useInstantiatedLayers } from './use-instantiated-layers'
import type { MapViewState, ViewStateChangeParameters } from '@deck.gl/core'
import type { GoogleMapsOverlay } from '@deck.gl/google-maps'
import type { ControllerOptions } from 'node_modules/@deck.gl/core/dist/controllers/controller'

export function GoogleMaps({
  id,
  instanceRef,
  basemap,
  layers,
  gmapsProps: { apiKey, APIProviderProps, ..._gmapsProps } = { apiKey: '' },
  overlayRef,
  parameters,
  controller,
  onLoad,
  onIdle,
  onViewStateChange,
  ...rest
}: Omit<DeckGLComponentProps, 'onLoad'> & {
  onLoad: (event: { type: 'deckgl' | 'load'; value: boolean }) => void
}) {
  const _controller: ControllerOptions = useMemo(() => {
    if (typeof controller === 'boolean') {
      return {
        dragPan: controller,
        doubleClickZoom: controller,
        scrollZoom: controller,
      }
    }

    const _controller = controller as ControllerOptions

    return {
      dragPan: _controller?.dragPan ?? true,
      doubleClickZoom: _controller?.doubleClickZoom ?? true,
      scrollZoom: _controller?.scrollZoom ?? true,
    }
  }, [controller])

  const _layers = useInstantiatedLayers(layers)

  const mappingGmapsToDeck = useCallback(
    ({ detail: { center, zoom, heading, tilt } }: MapCameraChangedEvent) => {
      return {
        maxZoom: rest.initialViewState?.maxZoom,
        minZoom: rest.initialViewState?.minZoom,
        latitude: center.lat,
        longitude: center.lng,
        zoom: zoom - 1,
        bearing: heading,
        pitch: tilt,
      }
    },
    [rest.initialViewState?.maxZoom, rest.initialViewState?.minZoom],
  )

  const handleViewStateChange = useCallback(
    (newViewState: MapViewState) => {
      const _instanceRef = instanceRef?.current as GoogleMapsOverlay
      // @ts-expect-error accessing private property
      const deckInstance = _instanceRef?._deck as InstanceRef['deck']

      const viewState = {
        // @ts-expect-error accessing protected props
        ...deckInstance?.viewState,
        ...newViewState,
      }

      const _viewState: MapViewState = {
        ...viewState,
        latitude: viewState.latitude,
        longitude: viewState.longitude,
        zoom: viewState.zoom,
        pitch: viewState.pitch,
        bearing: viewState.bearing,
        // @ts-expect-error - `width` and `height` is not in the types
        width: deckInstance?.width,
        height: deckInstance?.height,
      }

      onViewStateChange?.({
        viewState: _viewState,
        interactionState: {
          isZooming: true,
        },
      } as ViewStateChangeParameters<MapViewState>)
    },
    [instanceRef, onViewStateChange],
  )

  const handleGmapsChange = useCallback(
    (props: MapCameraChangedEvent) => {
      const newViewState = mappingGmapsToDeck(props)
      handleViewStateChange(newViewState)
    },
    [mappingGmapsToDeck, handleViewStateChange],
  )

  return (
    <APIProvider apiKey={apiKey} {...APIProviderProps}>
      <div className='deckgl-map'>
        <Map
          id={id}
          mapTypeId={basemap}
          disableDefaultUI={true}
          {..._gmapsProps}
          center={{
            lat: rest.initialViewState?.latitude ?? 0,
            lng: rest.initialViewState?.longitude ?? 0,
          }}
          zoom={(rest.initialViewState?.zoom ?? 0) + 1}
          maxZoom={(rest.initialViewState?.maxZoom ?? 23) + 1}
          minZoom={(rest.initialViewState?.minZoom ?? 0) + 1}
          tilt={rest.initialViewState?.pitch}
          heading={rest.initialViewState?.bearing}
          onIdle={onIdle}
          onTilesLoaded={() => {
            onLoad({
              type: 'load',
              value: true,
            })
            handleViewStateChange(rest.initialViewState!)
          }}
          onBoundsChanged={handleGmapsChange}
          onZoomChanged={handleGmapsChange}
          scrollwheel={
            typeof _controller.scrollZoom === 'boolean'
              ? _controller.scrollZoom
              : !!_controller.scrollZoom
          }
          gestureHandling={!_controller.dragPan ? 'none' : 'greedy'}
          disableDoubleClickZoom={!_controller.doubleClickZoom}
        >
          <MapReferences id={id} overlayRef={overlayRef} />
          <DeckGlOverlay
            id={id}
            instanceRef={instanceRef}
            layers={_layers}
            onLoad={() => {
              onLoad({
                type: 'deckgl',
                value: true,
              })
            }}
            parameters={{
              // @ts-expect-error - `depthTest` is not in the types
              depthTest: false,
              ...parameters,
            }}
            {...rest}
          />
        </Map>
      </div>
    </APIProvider>
  )
}

function MapReferences({
  id,
  overlayRef,
}: {
  id: DeckGLComponentProps['id']
  overlayRef: DeckGLComponentProps['overlayRef']
}) {
  const gmaps = useMap(id)!

  useImperativeHandle(overlayRef, () => {
    return gmaps
  }, [gmaps])

  return null
}
