import { Icon } from '@ef2/react';
import { Autocomplete, GoogleMap, Marker, useJsApiLoader } from '@react-google-maps/api';
import React, { ReactNode } from 'react';
import { ChangeEvent, useCallback, useRef, useState } from 'react';
import { classNamesTailwind } from '../../utils/theme';
import LocationFinderLocation from '../LocationFinderLocation';
import LocationFinderSelectedLocation from '../LocationFinderSelectedLocation';

export interface LocationFinderProps {
    googleMapsApiKey: string;
    locations: Location[];
    listInputPlaceholder?: string;
    numberOfLocationsLabel?: string;
    maxItemsInList?: number;
    styles?: google.maps.MapTypeStyle[];
    cluster?: boolean;
    markerIcon?: { url: string; width: number; height: number };
    selectedItemBackLabel?: string;
    language?: string;
    region?: string;
    // Defaults.
    defaultZoom?: number;
    defaultCenter?: google.maps.LatLngLiteral;
    defaultBounds?: google.maps.LatLngBoundsLiteral;
    listInputDefaultValue?: string;
    listInputLocationButtonIcon?: string;
    // ClassNames.
    classNameContainer: string;
    classNameCard?: string;
    classNameCardWrapper?: string;
    classNameList?: string;
    classNameListHeader?: string;
    classNameListInput?: string;
    classNameListInputLocationButton?: string;
    classNameListNumberOfLocations?: string;
    classNameListItem?: string;
    classNameListItemWrapper?: string;
    classNameListItemDistance?: string;
    classNameListItemDistanceKm?: string;
    classNameListItemDistanceKmLabel?: string;
    classNameListItemTitle?: string;
    classNameListItemStreet?: string;
    classNameSelectedItem?: string;
    classNameSelectedItemBack?: string;
    // Renders.
    renderListHeaderTop?: ({
        numberOfLocations,
        numberOfLocationsLabel
    }: {
        numberOfLocations: number;
        numberOfLocationsLabel: string;
    }) => ReactNode;
    renderListHeaderBottom?: ({
        numberOfLocations,
        numberOfLocationsLabel
    }: {
        numberOfLocations: number;
        numberOfLocationsLabel: string;
    }) => ReactNode;
    renderSelectedItemBack?: ({ onClick }: { onClick: () => void }) => ReactNode;
    renderSelectedItemImage?: ({ image }: { image?: string }) => ReactNode;
}

export interface Location {
    id: string;
    title: string;
    position: google.maps.LatLngLiteral;
    // Attribute 'distance' is only used in the front-end.
    distance?: number;
    street: string;
    city: string;
    openingHours?: OpeningHours;
    image?: string[];
}

export interface OpeningHours {
    days: OpeningHoursDays;
}

export interface OpeningHoursDays {
    [key: number]: OpeningHoursDaysDay;
}

export interface OpeningHoursDaysDay {
    closed?: boolean;
    slots: OpeningHoursDaysDaySlot[];
}

export interface OpeningHoursDaysDaySlot {
    from: OpeningHoursDaysDaySlotTime;
    to: OpeningHoursDaysDaySlotTime;
}

export interface OpeningHoursDaysDaySlotTime {
    hours: number;
    minutes: number;
}

const calculateDistance = (
    from: google.maps.LatLng | google.maps.LatLngLiteral,
    to: google.maps.LatLng | google.maps.LatLngLiteral
): number => {
    return google.maps.geometry.spherical.computeDistanceBetween(from, to) / 1000;
};

const LIBRARIES: ('geometry' | 'places')[] = ['places', 'geometry'];
const DEFAULT_ZOOM = 8;

const BOUNDS_NETHERLANDS: google.maps.LatLngBoundsLiteral = {
    east: 7.5,
    north: 53.7,
    south: 50.6,
    west: 3.1
};

const DEFAULT_CENTER: google.maps.LatLngLiteral = {
    lat: 52.370216,
    lng: 4.895168
};

const LocationFinder = ({
    googleMapsApiKey,
    listInputPlaceholder = 'Zoek op plaats of postcode',
    locations,
    region = 'NL',
    numberOfLocationsLabel = 'vestigingen',
    defaultZoom = DEFAULT_ZOOM,
    defaultCenter = DEFAULT_CENTER,
    defaultBounds = BOUNDS_NETHERLANDS,
    styles,
    classNameContainer,
    classNameCard,
    classNameCardWrapper,
    classNameListInput,
    classNameListNumberOfLocations,
    classNameListItem,
    classNameListInputLocationButton,
    classNameListItemWrapper,
    classNameListItemDistance,
    classNameListItemDistanceKm,
    classNameListItemDistanceKmLabel,
    classNameListItemTitle,
    classNameListItemStreet,
    classNameList,
    classNameListHeader,
    classNameSelectedItem,
    classNameSelectedItemBack,
    renderListHeaderTop,
    renderListHeaderBottom,
    renderSelectedItemBack,
    renderSelectedItemImage,
    markerIcon,
    listInputDefaultValue,
    listInputLocationButtonIcon,
    selectedItemBackLabel,
    language = 'nl'
}: LocationFinderProps) => {
    const [autocomplete, setAutocomplete] = useState<google.maps.places.Autocomplete>();
    const [map, setMap] = useState<google.maps.Map | undefined>(undefined);
    const [listLocations, setListLocations] = useState<Location[]>(locations);
    const [showDistance, setShowDistance] = useState<boolean>(false);
    const [pendingRefine, setPendingRefine] = useState<boolean>(false);
    const [selectedLocation, setSelectedLocation] = useState<Location | undefined>(undefined);
    const [previousZoom, setPreviousZoom] = useState<number>(defaultZoom);
    const inputRef = useRef<HTMLInputElement>(null);

    // Methods.
    const reset = useCallback(() => {
        if (!map) {
            return;
        }

        if (inputRef.current) {
            inputRef.current.value = '';
        }

        // map.fitBounds(bounds);
        map.setZoom(defaultZoom);

        setShowDistance(false);
        setListLocations(locations);
    }, [map, defaultBounds, inputRef]);

    const refine = useCallback(() => {
        if (!map) {
            return;
        }

        const zoom = map.getZoom();
        const bounds = map.getBounds();
        const center = map.getCenter();

        if (!bounds || !center) {
            return;
        }

        const showDistance = zoom ? zoom > 8 : false;
        setShowDistance(showDistance);

        const listLocations = showDistance
            ? locations
                  .filter((location) => bounds.contains(location.position))
                  .map((location) => ({ ...location, distance: calculateDistance(center, location.position) }))
                  .sort((a, b) => a.distance - b.distance)
            : locations.filter((location) => bounds.contains(location.position));

        setListLocations(listLocations);
    }, [map]);

    const handleOnChange = useCallback(() => {
        if (!map) {
            return;
        }

        setPendingRefine(true);
    }, [map]);

    const handleOnLoad = useCallback(
        (map: google.maps.Map) => {
            // map.fitBounds(bounds);
            setMap(map);
        },
        [setMap]
    );

    const handeOnAutocompleteLoad = useCallback(
        (autocomplete: google.maps.places.Autocomplete) => {
            setAutocomplete(autocomplete);
        },
        [setAutocomplete]
    );

    const handleOnPlaceChanged = useCallback(() => {
        if (!autocomplete || !map) {
            return;
        }

        const place = autocomplete.getPlace();
        const geometry = place.geometry;

        if (geometry?.viewport) {
            map.fitBounds(geometry.viewport);
        }

        if (geometry?.location) {
            map.setCenter(geometry.location);
        }

        setPendingRefine(true);
    }, [autocomplete, map]);

    const handleOnInputChange = useCallback(
        (event: ChangeEvent<HTMLInputElement>) => {
            const value = event.target.value;

            if (value === '') {
                reset();
            }
        },
        [map]
    );

    const handleOnIdle = useCallback(() => {
        if (pendingRefine) {
            setPendingRefine(false);
            refine();
        }
    }, [pendingRefine, setPendingRefine]);

    const handleOnItemClick = useCallback(
        (location: Location) => {
            if (!map) {
                return;
            }

            const previousZoom = map.getZoom();

            if (previousZoom) {
                setPreviousZoom(previousZoom);
            }

            map.setZoom(12);
            map.setCenter(location.position);

            setPendingRefine(true);
            setSelectedLocation(location);
        },
        [map]
    );

    const handleOnBackClick = useCallback(() => {
        map?.setZoom(previousZoom);
        setSelectedLocation(undefined);
    }, [map, previousZoom]);

    const handleOnMarkerClick = useCallback(
        (location: Location) => {
            handleOnItemClick(location);
        },
        [map]
    );

    const handleOInputLocationButtonClick = useCallback(() => {
        navigator.geolocation.getCurrentPosition((position) => {
            const pos: google.maps.LatLngLiteral = {
                lat: position.coords.latitude,
                lng: position.coords.longitude
            };

            // const previousZoom = map.getZoom();

            // if (previousZoom) {
            //     setPreviousZoom(previousZoom);
            // }

            map.setZoom(12);
            map.setCenter(pos);
        });
    }, [map]);

    // const handleOnDragEnd = useCallback(() => {
    //     // Update input when the user drags the map.
    //     if (selectedLocation && listLocations.length > 0 && listLocations[0].id !== selectedLocation.id && inputRef.current) {
    //         const location = listLocations[0];
    //         setSelectedLocation(location);
    //         inputRef.current.value = location.city;
    //     }

    //     if (!selectedLocation) {

    //     }

    //     handleOnChange();
    // }, [map]);

    const { isLoaded } = useJsApiLoader({
        googleMapsApiKey,
        libraries: LIBRARIES,
        language: language,
        region: region
    });

    // TODO loading.
    if (!isLoaded) {
        return null;
    }

    return (
        <>
            <div className="w-full relative">
                <GoogleMap
                    onLoad={handleOnLoad}
                    mapContainerClassName={classNamesTailwind('w-full relative z-0 h-[60vh] min-h-[25rem] md:h-screen', classNameContainer)}
                    zoom={defaultZoom}
                    center={defaultCenter}
                    onIdle={handleOnIdle}
                    onZoomChanged={handleOnChange}
                    onDragEnd={handleOnChange}
                    options={{
                        maxZoom: 16,
                        minZoom: 5,
                        restriction: { latLngBounds: defaultBounds },
                        mapTypeControl: false,
                        styles: styles
                    }}
                >
                    {locations.map((location) => (
                        <Marker
                            key={location.id}
                            icon={
                                markerIcon
                                    ? { url: markerIcon.url, scaledSize: new google.maps.Size(markerIcon.width, markerIcon.height) }
                                    : undefined
                            }
                            position={location.position}
                            onClick={() => handleOnMarkerClick(location)}
                        />
                    ))}
                </GoogleMap>
                <div className={classNamesTailwind('md:absolute -mt-12 md:mt-0 relative top-0 w-full left-0 h-full invisible z-20')}>
                    <div className={classNamesTailwind('container mx-auto invisible h-full')}>
                        <div className={classNamesTailwind('grid grid-cols-4 gap-x-4 md:grid-cols-12 invisible h-full')}>
                            <div
                                className={classNamesTailwind(
                                    'md:my-12 col-span-4 md:col-span-7 lg:col-span-5 xl:col-span-4',
                                    classNameCardWrapper
                                )}
                            >
                                <div
                                    className={classNamesTailwind(
                                        'bg-white py-4 visible h-full flex flex-col max-h-[40rem] overflow-auto z-20 relative shadow-lg',
                                        classNameCard
                                    )}
                                >
                                    {!selectedLocation ? (
                                        <>
                                            <div className={classNamesTailwind('px-4', classNameListHeader)}>
                                                {renderListHeaderTop &&
                                                    renderListHeaderTop({
                                                        numberOfLocations: listLocations.length,
                                                        numberOfLocationsLabel
                                                    })}
                                                <div className="flex items-stretch focus-within:z-10">
                                                    <Autocomplete
                                                        onLoad={handeOnAutocompleteLoad}
                                                        className="flex-grow"
                                                        onPlaceChanged={handleOnPlaceChanged}
                                                    >
                                                        <input
                                                            ref={inputRef}
                                                            onChange={handleOnInputChange}
                                                            defaultValue={listInputDefaultValue}
                                                            className={classNamesTailwind(
                                                                'block w-full border-gray-300 placeholder-gray-400 border-r-0 focus:border-indigo-500 focus:ring-indigo-500',
                                                                classNameListInput
                                                            )}
                                                            placeholder={listInputPlaceholder}
                                                            type="text"
                                                        />
                                                    </Autocomplete>
                                                    <button
                                                        className={classNamesTailwind(
                                                            'flex-none border border-gray-300 border-l-0 pr-3 pl-2',
                                                            classNameListInputLocationButton
                                                        )}
                                                        onClick={handleOInputLocationButtonClick}
                                                    >
                                                        <Icon name={listInputLocationButtonIcon} />
                                                    </button>
                                                </div>
                                                {renderListHeaderBottom ? (
                                                    renderListHeaderBottom({
                                                        numberOfLocations: listLocations.length,
                                                        numberOfLocationsLabel
                                                    })
                                                ) : (
                                                    <span
                                                        className={classNamesTailwind(
                                                            'text-gray-500 text-sm mt-2 block',
                                                            classNameListNumberOfLocations
                                                        )}
                                                    >
                                                        {listLocations.length} {numberOfLocationsLabel}
                                                    </span>
                                                )}
                                            </div>
                                            <ul className={classNamesTailwind('overflow-y-auto divide-y', classNameList)}>
                                                {listLocations.map((location) => (
                                                    <LocationFinderLocation
                                                        onClick={handleOnItemClick}
                                                        showDistance={showDistance}
                                                        key={location.id}
                                                        location={location}
                                                        classNameListItem={classNameListItem}
                                                        classNameListItemWrapper={classNameListItemWrapper}
                                                        classNameListItemDistance={classNameListItemDistance}
                                                        classNameListItemDistanceKm={classNameListItemDistanceKm}
                                                        classNameListItemDistanceKmLabel={classNameListItemDistanceKmLabel}
                                                        classNameListItemTitle={classNameListItemTitle}
                                                        classNameListItemStreet={classNameListItemStreet}
                                                    />
                                                ))}
                                            </ul>
                                        </>
                                    ) : (
                                        <LocationFinderSelectedLocation
                                            className={classNameSelectedItem}
                                            classNameBack={classNameSelectedItemBack}
                                            onBackClick={handleOnBackClick}
                                            location={selectedLocation}
                                            renderBack={renderSelectedItemBack}
                                            renderImage={renderSelectedItemImage}
                                            backLabel={selectedItemBackLabel}
                                        />
                                    )}
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </>
    );
};

export default LocationFinder;
