import React, { useEffect, useMemo, useCallback, useState } from 'react';
import DeckGL from '@deck.gl/react';
import { FlyToInterpolator, WebMercatorViewport } from '@deck.gl/core';
import { SelectionLayer } from '@deck.gl-community/editable-layers';
import { Coordinate, Layer } from './mapvis.types';
import { VISComponentType, VisualizationPropTypes } from '../../common';
import { layerTypes, createBaseMap } from './layers';
import { MapSettings } from './settings';
import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
import { HighlightAlt, SearchOutlined } from '@mui/icons-material';
import SearchBar from './search';

export type MapProps = { layer: string };

const settings: MapProps = { layer: 'nodelink' };

const INITIAL_VIEW_STATE = {
  latitude: 52.1006,
  longitude: 5.6464,
  zoom: 6,
  bearing: 0,
  pitch: 0,
};

const FLY_SPEED = 1000;

const baseLayer = createBaseMap();

export const MapVis = ({ data, settings, handleSelect, graphMetadata }: VisualizationPropTypes<MapProps>) => {
  const [layer, setLayer] = useState<Layer | undefined>(undefined);
  const [viewport, setViewport] = useState<Record<string, any>>(INITIAL_VIEW_STATE);
  const [hoverObject, setHoverObject] = useState<Node | null>(null);
  const [selected, setSelected] = useState<any[]>([]);
  const [selectingRectangle, setSelectingRectangle] = useState<boolean>(false);
  const [layerIds, setLayerIds] = useState<string[]>([]);
  const [isSearching, setIsSearching] = useState<boolean>(false);

  const getFittedViewport = useCallback(
    (minLat: number, maxLat: number, minLon: number, maxLon: number) => {
      const viewportWebMercator = new WebMercatorViewport(viewport).fitBounds(
        [
          [minLon, minLat],
          [maxLon, maxLat],
        ],
        { padding: 20 },
      );
      const { zoom, longitude, latitude } = viewportWebMercator;
      return { zoom, longitude, latitude };
    },
    [viewport],
  );

  const flyToBoundingBox = useCallback(
    (minLat: number, maxLat: number, minLon: number, maxLon: number, options = {}) => {
      const fittedViewport = getFittedViewport(minLat, maxLat, minLon, maxLon);
      setViewport((prevViewport) => ({
        ...prevViewport,
        ...options,
        ...fittedViewport,
        transitionDuration: FLY_SPEED,
        transitionInterpolator: new FlyToInterpolator(),
      }));
    },
    [getFittedViewport],
  );

  useEffect(() => {
    setLayer({
      type: settings.layer ? layerTypes?.[settings.layer] : layerTypes.nodelink,
      config: settings,
    });
  }, [settings.layer]);

  const dataLayer = useMemo(() => {
    if (!layer || !settings.layer) return null;

    const coordinateLookup: { [id: string]: Coordinate } = data.nodes.reduce(
      (acc, node) => {
        const latitude = settings?.[node.label]?.lat ? (node?.attributes?.[settings[node.label].lat] as string) : undefined;
        const longitude = settings?.[node.label]?.lon ? (node?.attributes?.[settings[node.label].lon] as string) : undefined;

        if (latitude !== undefined && longitude !== undefined) {
          acc[node._id] = [parseFloat(longitude), parseFloat(latitude)];
        }

        return acc;
      },
      {} as { [id: string]: Coordinate },
    );

    return new layer.type({
      graph: data,
      metaData: graphMetadata,
      config: settings,
      selected: selected,
      hoverObject: hoverObject,
      getNodeLocation: (d: string) => coordinateLookup[d],
      flyToBoundingBox: flyToBoundingBox,
      setLayerIds: (val: string[]) => setLayerIds(val),
    });
  }, [layer, data, selected, hoverObject, settings]);

  const selectionLayer = useMemo(
    () =>
      selectingRectangle &&
      new (SelectionLayer as any)({
        id: 'selection',
        selectionType: 'rectangle',
        onSelect: ({ pickingInfos }: { pickingInfos: any[] }) => {
          if (pickingInfos.length > 0) {
            const nodes = [];
            const edges = [];

            for (const selectedItem of pickingInfos) {
              const { object } = selectedItem;
              if (object._id) {
                if (object.from & object.to) {
                  edges.push(object);
                } else {
                  nodes.push(object);
                }
              }
            }
            setSelected(nodes.map((node) => node._id));
            handleSelect({ nodes, edges });
          } else {
            handleSelect();
          }
          setSelectingRectangle(false);
        },
        layerIds: layerIds,
        getTentativeFillColor: () => [22, 37, 67, 100],
      }),
    [selectingRectangle, layer],
  );

  return (
    <div className="w-full h-full flex-grow relative overflow-hidden">
      <div className="absolute left-0 top-0 z-50 m-1">
        <div className="cursor-pointer p-1 bg-white shadow-md rounded mb-1" onClick={() => setSelectingRectangle(true)}>
          <HighlightAlt />
        </div>
        <div className="cursor-pointer p-1 bg-white shadow-md rounded" onClick={() => setIsSearching(!isSearching)}>
          <SearchOutlined />
        </div>
      </div>
      {isSearching && (
        <SearchBar
          onSearch={(boundingbox: [number, number, number, number]) => {
            flyToBoundingBox(...boundingbox);
            setIsSearching(false);
          }}
        />
      )}
      <DeckGL
        layers={[baseLayer, dataLayer, selectionLayer]}
        controller={true}
        initialViewState={viewport}
        onViewStateChange={({ viewState }) => setViewport(viewState)}
        onClick={({ object }) => {
          if (data) {
            if (!object) {
              handleSelect();
              setSelected([]);
              return;
            }
            if (object.hasOwnProperty('attributes') && object.hasOwnProperty('id') && object.hasOwnProperty('label')) {
              handleSelect({ nodes: [object] });
              setSelected([object.id]);
            }
            if (object.type === 'Feature') {
              const ids = object.properties.nodes;
              if (ids.length > 0) {
                const nodes = data.nodes.filter((node) => ids.includes((node as unknown as { id: string }).id));
                handleSelect({ nodes: [...nodes] });
              } else {
                handleSelect();
                setSelected([]);
                return;
              }
            }
          }
        }}
        onHover={({ object }) => {
          setHoverObject(object !== undefined ? object : null);
        }}
      />
      <div className="absolute right-0 top-0 p-1 z-50 bg-white bg-opacity-75 text-xs">
        {'© '}
        <a className="underline" href="http://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer">
          OpenStreetMap
        </a>
      </div>
    </div>
  );
};

export const MapComponent: VISComponentType<MapProps> = {
  displayName: 'MapVis',
  component: MapVis,
  settingsComponent: MapSettings,
  settings: settings,
};

export default MapComponent;