import { useCallback, useEffect, useRef, useState } from 'react';

import type { Geolocation } from '~/lib/songwhipLookup/types';
import type { FC } from 'react';
import type { AddressInputProps, AddressTypes } from './types';

import { assertEnvVar } from '~/lib/utils/assert';
import Text from '~/src/components/Text';
import TextInput from '~/src/components/TextInput';
import useLoadScript from '~/src/hooks/useLoadScript';
import Box from '../Box';
import Loading from '../Loading';

const GOOGLE_API_KEY = assertEnvVar(
  process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY,
  'NEXT_PUBLIC_GOOGLE_MAPS_API_KEY'
);

const GOOGLE_MAPS_SCRIPT_SRC = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_API_KEY}&callback=clearTimeout`;

let geocoder: google.maps.Geocoder | undefined;

const AddressInput: FC<AddressInputProps> = ({
  addressType,
  defaultValue,
  onChange,
  ...inputProps
}) => {
  const [defaultAddress, setDefaultAddress] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [showMap, setShowMap] = useState(false);
  const [error, setError] = useState(false);
  const mapRef = useRef(null);

  const { interface: googleApi } = useLoadScript({
    src: GOOGLE_MAPS_SCRIPT_SRC,
    getInterface: () => window['google'],
  });

  useEffect(() => {
    if (!googleApi) return;

    if (!geocoder) {
      geocoder = new googleApi.maps.Geocoder();
    }

    if (geocoder && defaultValue) {
      setIsLoading(true);
      setError(false);
      setShowMap(false);

      reverseGeocode({ geocoder, geolocation: defaultValue, addressType })
        .then((address) => {
          setDefaultAddress(address);

          if (mapRef?.current) {
            drawMap({
              googleApi,
              element: mapRef.current,
              geolocation: defaultValue,
            });

            setIsLoading(false);
            setShowMap(true);
          }
        })
        .catch(() => {
          setIsLoading(false);
          setError(true);
        });
    }
  }, [googleApi]);

  const handleChange = useCallback(
    (event) => {
      setIsLoading(true);
      setError(false);
      setShowMap(false);

      geocode({ geocoder, address: event.value })
        .then((result) => {
          if (result && mapRef?.current) {
            drawMap({
              googleApi,
              element: mapRef.current,
              geolocation: result,
            });

            setIsLoading(false);
            setShowMap(true);

            if (onChange) {
              onChange(result);
            }
          }
        })
        .catch(() => {
          setIsLoading(false);

          if (event.value) {
            setError(true);
          }
        });
    },
    [googleApi]
  );

  return (
    <>
      <TextInput
        {...inputProps}
        testId="addressInput"
        defaultValue={defaultAddress}
        onInputEnd={handleChange}
      />
      <Box flexBox centerContent margin="2rem 0 0" height="300px">
        {isLoading && <Loading />}
        {error && (
          <Text size="1.5rem" centered>
            Invalid address
          </Text>
        )}
        <div
          ref={mapRef}
          style={{
            height: showMap ? '100%' : '0',
            flexGrow: showMap ? 1 : 0,
            borderRadius: '5px',
          }}
        />
        {!showMap && !error && !isLoading && (
          <Text size="1.5rem" centered>
            No address
          </Text>
        )}
      </Box>
    </>
  );
};

const geocode = async ({
  geocoder,
  address,
}: {
  geocoder?: google.maps.Geocoder;
  address: string;
}) => {
  const result = await geocoder?.geocode({ address });

  if (!result) return;

  const latitude = result.results[0]?.geometry.location.lat();
  const longitude = result.results[0]?.geometry.location.lng();

  if (!latitude || !longitude) return;

  return { latitude, longitude };
};

const reverseGeocode = ({
  geocoder,
  geolocation,
  addressType,
}: {
  geocoder: google.maps.Geocoder;
  geolocation: Geolocation;
  addressType?: AddressTypes;
}) => {
  return geocoder
    .geocode({
      location: { lat: geolocation.latitude, lng: geolocation.longitude },
    })
    .then((result) => {
      // Reverse geocoding can return multiple addresses
      // ordered from more specific to more generic.
      // We can improve our chances to find the address we're looking for
      // by filtering the results by their "address type".
      if (addressType) {
        const bestMatch = result?.results?.find((item) => {
          return item.types.indexOf(addressType) >= 0;
        });

        if (bestMatch) {
          return bestMatch.formatted_address;
        }
      }

      return result?.results?.[0]?.formatted_address;
    });
};

const drawMap = ({
  googleApi,
  element,
  geolocation,
}: {
  googleApi: any;
  element: HTMLElement;
  geolocation: Geolocation;
}) => {
  const map = new googleApi.maps.Map(element, {
    center: { lat: geolocation.latitude, lng: geolocation.longitude },
    zoom: 16,
  });

  new googleApi.maps.Marker({
    map,
    position: { lat: geolocation.latitude, lng: geolocation.longitude },
  });
};

export default AddressInput;
