import { useRef, useEffect, useState, Dispatch, SetStateAction } from 'react';
import { useMediaQuery } from 'react-responsive';

import ArcGISMap from '@arcgis/core/Map';
import Extent from '@arcgis/core/geometry/Extent';
import MapView from '@arcgis/core/views/MapView';
import Basemap from '@arcgis/core/Basemap';
import MapImageLayer from '@arcgis/core/layers/MapImageLayer';
import WMTSLayer from '@arcgis/core/layers/WMTSLayer';
import Search from '@arcgis/core/widgets/Search';
import ScaleBar from '@arcgis/core/widgets/ScaleBar';
import Sketch from '@arcgis/core/widgets/Sketch';
import GraphicsLayer from '@arcgis/core/layers/GraphicsLayer';
import Graphic from '@arcgis/core/Graphic';
import * as geometryEngine from '@arcgis/core/geometry/geometryEngine';
import * as reactiveUtils from '@arcgis/core/core/reactiveUtils';
import Zoom from '@arcgis/core/widgets/Zoom';
import FeatureLayer from '@arcgis/core/layers/FeatureLayer';
import Polygon from '@arcgis/core/geometry/Polygon';
import SimpleFillSymbol from '@arcgis/core/symbols/SimpleFillSymbol';
import SimpleMarkerSymbol from '@arcgis/core/symbols/SimpleMarkerSymbol';
import esriConfig from '@arcgis/core/config';
import Point from '@arcgis/core/geometry/Point';

import { updateComputationResultEWS } from '@/redux/computationsEWSSlice';
import { updateCadastralData } from '@/redux/cadastreSlice';

import identifyAllLayers from '../../utils/identify';
import queryCadastre from '../../utils/queryCadastre';
import getAddress from '../../utils/getAddress';

import './esri-ui-potenzialkarte.css';
import { Vienna, Austria } from '@/public/borders-vienna-austria';

import ReduxProvider from '@/redux/provider';
import { useAppDispatch } from '@/redux/hooks';

import PanelPotenzialkarte from './panel-potenzialkarte';
import CalculationsMenu from './calculations-menu';

// import layer URLs and spatial reference system wkid
import { BASEMAP_AT_URL, AMPEL_EWS_URL, RESOURCES_EWS_URL, SRS } from '@/app/config/config';

// set path for local assets
esriConfig.assetsPath = '/assets';

const scaleLimit = 1000;
const gridSpacing = 10;

// initialize info panel callback functions
let setAddress: Dispatch<SetStateAction<string[]>>,
  setClosenessWarning: Dispatch<SetStateAction<boolean>>,
  setOutsideWarning: Dispatch<SetStateAction<boolean>>,
  setScaleWarning: Dispatch<SetStateAction<boolean>>;
export function initializeInfoPanelHandlers(
  setAddressCallback: Dispatch<SetStateAction<string[]>>,
  setClosenessWarningCallback: Dispatch<SetStateAction<boolean>>,
  setOutsideWarningCallback: Dispatch<SetStateAction<boolean>>,
  setScaleWarningCallback: Dispatch<SetStateAction<boolean>>
) {
  setAddress = setAddressCallback;
  setClosenessWarning = setClosenessWarningCallback;
  setOutsideWarning = setOutsideWarningCallback;
  setScaleWarning = setScaleWarningCallback;
}

// initialize calculations menu callback functions
let setPoints: Dispatch<SetStateAction<number[][]>>, setPolygon: Dispatch<SetStateAction<Polygon | null>>;
export function initializeCalculationsMenuHandlers(
  setPointsCallback: Dispatch<SetStateAction<number[][]>>,
  setPolygonCallback: Dispatch<SetStateAction<Polygon | null>>
) {
  setPoints = setPointsCallback;
  setPolygon = setPolygonCallback;
}

// initialize the map view container
export let view: MapView;
export let pointGraphicsLayer: GraphicsLayer;
export let boundaryGraphicsLayer: GraphicsLayer;
export default function MapComponent() {
  const dispatch = useAppDispatch();
  const isMobile = useMediaQuery({ maxWidth: 640 });

  const mapDiv = useRef<HTMLDivElement | null>(null);
  const calculationsMenuRef = useRef<HTMLDivElement | null>(null);
  const loaded = useRef<boolean>(false);

  const [loading, setLoading] = useState<boolean>(false);

  useEffect(() => {
    let handle: IHandle;
    if (mapDiv.current && calculationsMenuRef.current) {
      view = new MapView({
        container: mapDiv.current,
        extent: new Extent({
          xmin: -19000,
          ymin: 325000,
          xmax: 29000,
          ymax: 360000,
          spatialReference: { wkid: SRS },
        }),
        spatialReference: { wkid: SRS },
        popupEnabled: false,
        popup: {
          dockEnabled: false,
          dockOptions: {
            buttonEnabled: false,
          },
          collapseEnabled: false,
          viewModel: {
            includeDefaultActions: false,
          },
        },
      });

      // listen to scale changes
      handle = reactiveUtils.watch(
        () => view.scale,
        () => {
          if (view.scale < scaleLimit) {
            setScaleWarning(false);
          } else {
            setScaleWarning(true);
          }
        }
      );

      // graphic layers
      let viennaGraphic = new Graphic({
        geometry: new Polygon({ rings: [Austria, Vienna], spatialReference: { wkid: SRS } }),
        symbol: {
          type: 'simple-fill',
          color: [209, 213, 219, 0.65],
          style: 'solid',
          outline: {
            color: 'white',
            width: 0,
          },
        } as unknown as SimpleFillSymbol,
      });
      const viennaGraphicsLayer = new GraphicsLayer({ title: 'Wien', listMode: 'hide' });
      viennaGraphicsLayer.add(viennaGraphic);

      pointGraphicsLayer = new GraphicsLayer({
        title: 'Planungslayer - Punkte',
        listMode: 'hide',
      });

      boundaryGraphicsLayer = new GraphicsLayer({
        title: 'EWS - innere Grenze',
        listMode: 'hide',
      });

      const polygonGraphicsLayer = new GraphicsLayer({
        title: 'Grundstücksgrenze',
        listMode: 'hide',
      });

      const highlightGraphicsLayer = new GraphicsLayer({
        title: 'Grundstücksgrenze',
        listMode: 'hide',
      });

      // thematic layers
      const ampelkarte_ews = new FeatureLayer({
        url: AMPEL_EWS_URL + '/0',
        title: 'Mögliche Einschränkungen',
        visible: false,
        listMode: 'show',
        opacity: 0.5,
      });

      const ews = new MapImageLayer({
        title: 'Ressourcen',
        url: RESOURCES_EWS_URL,
        visible: false,
        listMode: 'show',
      });

      // basemap in Viennese coordinate system due to tranformation inaccuracies from MGI to WGS84
      // default transformation in ArcGIS API from MGI to WGS84 is 1306
      // transformation 1618 is recommended
      const basemap_at = new WMTSLayer({
        url: BASEMAP_AT_URL,
        activeLayer: {
          id: 'geolandbasemap',
        },
        listMode: 'hide',
        serviceMode: 'KVP',
      });

      let basemap = new Basemap({
        baseLayers: [basemap_at],
        title: 'basemap',
        id: 'basemap',
        spatialReference: { wkid: SRS },
      });

      let arcgisMap = new ArcGISMap({
        basemap: basemap,
        layers: [
          ews,
          ampelkarte_ews,
          pointGraphicsLayer,
          boundaryGraphicsLayer,
          polygonGraphicsLayer,
          highlightGraphicsLayer,
          viennaGraphicsLayer,
        ],
      });

      const search = new Search({
        view,
        popupEnabled: true,
      });

      const scaleBar = new ScaleBar({
        view: view,
        unit: 'metric',
      });

      const zoom = new Zoom({
        view,
        layout: 'horizontal',
      });

      let sketch = new Sketch({
        // container: calculationsMenuRef.current,
        layer: pointGraphicsLayer,
        view: view,
        availableCreateTools: ['point'],
        visibleElements: {
          selectionTools: {
            'lasso-selection': true,
            'rectangle-selection': true,
          },
          settingsMenu: false,
          undoRedoMenu: false,
        },
        creationMode: 'single',
      });

      if (loaded.current) {
        sketch.container = calculationsMenuRef.current;
      }
      loaded.current = true;

      sketch.on('create', (event) => {
        if (event.tool === 'point' && event.state === 'complete') {
          // point symbol
          const symbol = {
            type: 'simple-marker',
            color: [255, 255, 255, 0.25],
          };

          // delete default point symbol
          pointGraphicsLayer.remove(event.graphic);

          let point = event.graphic.geometry as Point;
          let points;

          // add point to current list of points
          setPoints((currentPoints) => {
            points = [...currentPoints, [point.x, point.y]];
            return points;
          });

          // check if point is too close to any other point
          pointGraphicsLayer.graphics.forEach((graphic) => {
            if (geometryEngine.distance(graphic.geometry, point) < 5) {
              setClosenessWarning(true);
            }
          });

          // check if point is outside the parcel
          if (!geometryEngine.intersects(point, boundaryGraphicsLayer.graphics.at(0).geometry)) {
            setOutsideWarning(true);
          }

          // draw point graphic
          pointGraphicsLayer.add(
            new Graphic({
              geometry: point,
              symbol,
            })
          );
        }
      });

      sketch.on('update', (event) => {
        if (event.state === 'start') {
          // on first load update starts with two identical points
          // cancel second update
          if (sketch.updateGraphics.length === 2 && sketch.updateGraphics.at(0) === sketch.updateGraphics.at(1)) {
            sketch.cancel();
          }
        }

        if (event.state === 'complete') {
          // list of updated points
          let allPoints = pointGraphicsLayer.graphics.map((graphic) => graphic.geometry as Point);

          // update state of calculations menu
          setPoints(allPoints.map((point) => [point.x, point.y]).toArray());

          // check if all points are inside the boundary
          let allPointsInside = true;
          allPoints.forEach((point) => {
            if (!geometryEngine.intersects(point, boundaryGraphicsLayer.graphics.at(0).geometry)) {
              allPointsInside = false;
              return;
            }
          });

          setOutsideWarning(!allPointsInside);

          // check if points are too close
          let tooClose = false;
          allPoints.forEach((firstPoint) => {
            allPoints.forEach((secondPoint) => {
              if (secondPoint !== firstPoint) {
                if (geometryEngine.distance(firstPoint, secondPoint, 'meters') <= 4.9) {
                  tooClose = true;
                  return;
                }
              }
            });
          });

          setClosenessWarning(tooClose);
        }
      });

      // register event handler for mouse clicks
      view.on('click', ({ mapPoint }) => {
        // at application start if no polygon is drawn yet
        if (polygonGraphicsLayer.graphics.length === 0) {
          startNewQuery(mapPoint);
        } else {
          // check if point is selected
          view
            .hitTest(view.toScreen(mapPoint), {
              include: pointGraphicsLayer,
            })
            .then(({ results }) => {
              if (results && results.length > 0) {
                // if point was selected then start update
                const pointGraphic = (results.at(0) as any)?.graphic;
                sketch.update(pointGraphic);
              } else {
                if (
                  !geometryEngine.intersects(mapPoint, polygonGraphicsLayer.graphics.at(0).geometry) &&
                  view.scale < scaleLimit
                ) {
                  // if click was outside parcel and no point was selected then start new query
                  view.openPopup({
                    title: 'Wollen Sie ein neues Grundstück auswählen?',
                    location: mapPoint,
                    content: getPopupContent(mapPoint),
                  });
                }
              }
            });
        }
      });

      // listen to move event
      view.on('pointer-move', (event) => {
        let mapPoint = view.toMap({ x: event.x, y: event.y });
        view
          .hitTest(view.toScreen(mapPoint), {
            include: pointGraphicsLayer,
          })
          .then(({ results }) => {
            // if user hovers over a point
            if (results.length > 0 && event.buttons === 0 && !sketch.createGraphic) {
              const graphicHits = results?.filter((hitResult) => hitResult.type === 'graphic');

              let graphic = (graphicHits.at(0) as any)?.graphic;

              sketch.update(graphic);

              let pointGraphic = new Graphic({
                geometry: graphic.geometry,
                symbol: {
                  type: 'simple-marker',
                  size: '30px',
                  color: null,
                  outline: { color: '#00ffff' },
                } as unknown as SimpleMarkerSymbol,
              });
              highlightGraphicsLayer.add(pointGraphic);
              document.body.style.cursor = 'pointer';
            } else {
              highlightGraphicsLayer.removeAll();
              document.body.style.cursor = 'default';
            }
          });
      });

      const getPopupContent = (mapPoint: Point) => {
        const popupContent = document.createElement('div');
        popupContent.className = 'flex justify-center';

        const popupButton = document.createElement('button');
        popupButton.textContent = 'Ja';
        popupButton.className = 'text-white bg-gray-700 p-2 border-none outline-none cursor-pointer w-2/3';

        popupButton.addEventListener('mouseover', handleMouseOver);
        function handleMouseOver(this: HTMLButtonElement) {
          this.className = 'text-white bg-gray-300 p-2 border-none outline-none cursor-pointer w-2/3';
        }

        popupButton.addEventListener('mouseout', handleMouseOut);
        function handleMouseOut(this: HTMLButtonElement) {
          this.className = 'text-white bg-gray-700 p-2 border-none outline-none cursor-pointer w-2/3';
        }

        popupContent.append(popupButton);

        popupButton.onclick = () => {
          startNewQuery(mapPoint);
          view.popup.close();
        };

        return popupContent;
      };

      const startNewQuery = (mapPoint: Point) => {
        // remove existing graphics
        polygonGraphicsLayer.removeAll();
        boundaryGraphicsLayer.removeAll();
        pointGraphicsLayer.removeAll();

        // clear state
        setClosenessWarning(false);
        setOutsideWarning(false);
        setPolygon(null);
        setPoints([]);

        // initialize store in case there was a previous computation
        dispatch(updateCadastralData({}));
        dispatch(updateComputationResultEWS({}));

        // start new layer queries
        identifyAllLayers(view, mapPoint, dispatch, 'EWS');
        getAddress(mapPoint, setAddress);

        if (view.scale < scaleLimit) {
          // query cadastral data
          queryCadastre(view, polygonGraphicsLayer, mapPoint, dispatch, setPolygon, setPoints, gridSpacing);
        }
      };

      // add map to view
      view.map = arcgisMap;

      // add UI components
      view.ui.components = [];
      if (!isMobile) {
        view.ui.add([zoom, search], 'top-left');
        view.ui.add(scaleBar, 'bottom-left');
      }
    }

    return () => {
      view?.destroy();
      handle?.remove();
    };
  }, [dispatch, isMobile]);

  return (
    <div ref={mapDiv} className="absolute top-16 bottom-0 w-full">
      <PanelPotenzialkarte></PanelPotenzialkarte>
      <ReduxProvider>
        <CalculationsMenu isLoading={setLoading} ref={calculationsMenuRef}></CalculationsMenu>
      </ReduxProvider>
      {loading ? (
        <div
          className="absolute left-1/2 top-1/2 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-gray-950 border-r-transparent"
          role="status"
        ></div>
      ) : null}
    </div>
  );
}
