import { useRef, useState, useEffect } from 'react';
import { Root, createRoot } from 'react-dom/client';

import { useTranslation } from 'react-i18next';

// import Calcite components
import '@esri/calcite-components/dist/calcite/calcite.css';

import { setAssetPath } from '@esri/calcite-components/dist/components';
setAssetPath(window.location.href);

import '@esri/calcite-components/dist/components/calcite-accordion';
import '@esri/calcite-components/dist/components/calcite-accordion-item';
import '@esri/calcite-components/dist/components/calcite-link';
import '@esri/calcite-components/dist/components/calcite-list';
import '@esri/calcite-components/dist/components/calcite-list-item';
import '@esri/calcite-components/dist/components/calcite-action';
import {
  CalciteAccordion,
  CalciteAccordionItem,
  CalciteLink,
  CalciteList,
  CalciteListItem,
  CalciteAction,
} from '@esri/calcite-components-react';

import MapView from '@arcgis/core/views/MapView';
import Search from '@arcgis/core/widgets/Search';
import FeatureLayer from '@arcgis/core/layers/FeatureLayer';
import Graphic from '@arcgis/core/Graphic';
import { eachAlways } from '@arcgis/core/core/promiseUtils.js';
import SearchSource from '@arcgis/core/widgets/Search/SearchSource';
import Point from '@arcgis/core/geometry/Point';
import Polyline from '@arcgis/core/geometry/Polyline';
import Polygon from '@arcgis/core/geometry/Polygon';
import { geodesicBuffer } from '@arcgis/core/geometry/geometryEngine';
import MapImageLayer from '@arcgis/core/layers/MapImageLayer';
import SimpleFillSymbol from '@arcgis/core/symbols/SimpleFillSymbol';
import SimpleLineSymbol from '@arcgis/core/symbols/SimpleLineSymbol';
import SimpleMarkerSymbol from '@arcgis/core/symbols/SimpleMarkerSymbol';
import Sublayer from '@arcgis/core/layers/support/Sublayer';
import PopupTemplate from '@arcgis/core/PopupTemplate';

// create feaure layer from URL of data index layer
const datenIndexURL = 'https://gis.geosphere.at/maps/rest/services/datenindex/raster_5000/MapServer/0';
const indexLayer = new FeatureLayer({
  url: datenIndexURL,
  title: 'Datenindex 1:5.000',
  opacity: 0,
  legendEnabled: false,
});

// custom type definitions
interface CustomLayer {
  id: string;
  type: string;
  title: string;
  visible: boolean;
}

interface CustomGroupLayer {
  id: string;
  type: string;
  title: string;
  visible: boolean;
  layers: (CustomGroupLayer | CustomLayer)[];
}

interface LayerToFeaturesMap {
  [key: string]: string[];
}

// custom React component
export default function SearchComponent({ view }: { view: MapView }) {
  const [currentTarget, setCurrentTarget] = useState<Graphic | null>(null);

  const rendered = useRef<boolean>(false);
  const currentGraphicRef = useRef<Graphic | null>(null);
  currentGraphicRef.current = currentTarget;

  const { t } = useTranslation();

  // get map image layer from sublayer
  const getMapImageLayer = (layerURL: string): MapImageLayer | undefined => {
    if (view) {
      const filteredLayerViews = view.allLayerViews.filter((layerView) => {
        const regex = /^https:\/\/.+\/MapServer/g;
        const matches = layerURL.match(regex);
        let mapImageLayerURL;
        if (matches && matches.length > 0) mapImageLayerURL = matches[0];

        if (layerView.layer.type === 'map-image') {
          return (layerView.layer as MapImageLayer).url === mapImageLayerURL;
        } else {
          return false;
        }
      });

      let mapImageLayer;
      if (filteredLayerViews.length > 0) {
        mapImageLayer = filteredLayerViews.at(0).layer as MapImageLayer;
      }

      return mapImageLayer;
    }
  };

  // handle toggle layer visibility
  const handleToggleVisibility = (event: any) => {
    const layerItem = event.target;

    const layerId = layerItem.getAttribute('text');
    const icon = layerItem.getAttribute('icon');

    if (layerId) {
      const layer = view.map?.findLayerById(layerId);
      if (icon === 'view-hide') {
        layerItem.setAttribute('icon', 'view-visible');
        if (layer) {
          layer.visible = true;
        }
      } else {
        layerItem.setAttribute('icon', 'view-hide');
        if (layer) {
          layer.visible = false;
        }
      }
    }
  };

  // handle zoom to feature
  const handleZoomTo = async (event: any) => {
    const layerURL = event.target.getAttribute('text');
    const res = await fetch(layerURL + '?f=json');
    const json = await res.json();

    const mapImageLayer = getMapImageLayer(layerURL);
    const sr = mapImageLayer?.spatialReference;
    const geometry = json.feature?.geometry;

    view.graphics.removeAll();
    const higlightOptions = view.highlightOptions;
    if (geometry.x && geometry.y && higlightOptions && higlightOptions.color && higlightOptions.fillOpacity) {
      const point = new Point({
        x: geometry.x,
        y: geometry.y,
        spatialReference: sr,
      });
      const graphic = new Graphic({
        geometry: point,
        symbol: {
          type: 'simple-marker',
          style: 'circle',
          size: '8px',
          color: [
            higlightOptions.color.r,
            higlightOptions.color.g,
            higlightOptions.color.b,
            higlightOptions.fillOpacity,
          ],
          outline: {
            color: [higlightOptions.color.r, higlightOptions.color.g, higlightOptions.color.b, higlightOptions.color.a],
            width: 1,
          },
        } as unknown as SimpleMarkerSymbol,
      });
      view.graphics.add(graphic);
      view.goTo(geodesicBuffer(point, 1000, 'meters'));
      return;
    }

    if (geometry.paths && higlightOptions && higlightOptions.color && higlightOptions.fillOpacity) {
      const polyline = new Polyline({
        paths: geometry.paths,
        spatialReference: sr,
      });
      const graphic = new Graphic({
        geometry: polyline,
        symbol: {
          type: 'simple-line',
          color: [higlightOptions.color.r, higlightOptions.color.g, higlightOptions.color.b],
          width: '2px',
        } as unknown as SimpleLineSymbol,
      });
      view.graphics.add(graphic);
      view.goTo(geodesicBuffer(polyline.extent.center, 5000, 'meters'));
      return;
    }

    if (geometry.rings && higlightOptions && higlightOptions.color && higlightOptions.fillOpacity) {
      const polygon = new Polygon({
        rings: geometry.rings,
        spatialReference: sr,
      });
      const graphic = new Graphic({
        geometry: polygon,
        symbol: {
          type: 'simple-fill',
          color: [
            higlightOptions.color.r,
            higlightOptions.color.g,
            higlightOptions.color.b,
            higlightOptions.fillOpacity,
          ],
          outline: {
            color: [higlightOptions.color.r, higlightOptions.color.g, higlightOptions.color.b, higlightOptions.color.a],
            width: 1,
          },
        } as unknown as SimpleFillSymbol,
      });
      view.graphics.add(graphic);
      view.goTo(polygon);
      return;
    }
  };

  // remove layers from layer tree by given filter
  const removeLayers = (layers: (CustomGroupLayer | CustomLayer)[], keepLayer: any): any => {
    return layers
      .filter((layer) => keepLayer(layer))
      .map((layer) => {
        if (layer.type === 'group' && (layer as CustomGroupLayer).layers) {
          return { ...layer, layers: removeLayers((layer as CustomGroupLayer).layers, keepLayer) };
        } else {
          return layer;
        }
      });
  };

  // get custom layer objects from layer tree
  const getLayerObjects = (layers: any) => {
    return layers.map((layer: any) => {
      if (layer.layers) {
        return {
          id: layer.id,
          type: layer.type,
          title: layer.title,
          visible: layer.visible,
          layers: getLayerObjects(layer.layers.toArray()),
        };
      } else {
        return { id: layer.id, type: layer.type, title: layer.title, visible: layer.visible };
      }
    });
  };

  // build query string for custom search source (BEV geocoding service)
  const buildQueryString = (searchTerm: string) => `?term=${encodeURI(searchTerm)}`;

  const url = 'https://kataster.bev.gv.at/api/all4map';
  const customSearchSource = new SearchSource({
    placeholder: t('search.placeholder'),

    // provide suggestions to the Search widget
    getSuggestions: async (params) => {
      const res = await fetch(url + buildQueryString(params.suggestTerm));
      const json = await res.json();

      if (json.data && json.data.features?.length > 0) {
        return json.data.features.map((feature: any) => {
          return {
            key: 'bev',
            text: feature.properties.name,
            sourceIndex: params.sourceIndex,
          };
        });
      }
    },

    // find results from suggestions
    getResults: async (params) => {
      const res = await fetch(url + buildQueryString(params.suggestResult.text));
      const json = await res.json();

      if (json.data && json.data.features?.length > 0) {
        const searchResults = json.data.features.map((feature: any) => {
          // create a graphic for the search widget
          if (feature.geometry.type === 'Point') {
            const graphic = new Graphic({
              geometry: new Point({
                x: feature.geometry.coordinates[0],
                y: feature.geometry.coordinates[1],
                spatialReference: {
                  wkid: 4326,
                },
              }),
              symbol: {
                type: 'simple-marker',
                style: 'circle',
                color: [51, 51, 204, 0.5],
                size: '8px',
                outline: {
                  color: 'white',
                  width: 1,
                },
              } as unknown as SimpleMarkerSymbol,
            });

            return {
              extent: null,
              feature: graphic,
              target: new Graphic({
                // create buffer for point geometries to allow zoom to
                geometry: geodesicBuffer(graphic.geometry, 1000, 'meters') as Polygon,
              }),
              name: feature.properties.name,
            };
          } else {
            const coords = feature.geometry.coordinates;
            const graphic = new Graphic({
              geometry: new Polygon({
                rings: coords,
                spatialReference: {
                  wkid: 4326,
                },
              }),
              symbol: {
                type: 'simple-fill',
                color: [51, 51, 204, 0.5],
                style: 'solid',
                outline: {
                  color: 'white',
                  width: 1,
                },
              } as unknown as SimpleFillSymbol,
            });

            return {
              extent: graphic.geometry.extent,
              feature: graphic,
              target: graphic,
              name: feature.properties.name,
            };
          }
        });

        // return search results
        return searchResults;
      }
    },
  });

  // create query from cellcode for 3x3 neighbourhood
  const createQueryFromCellcode = (cellcode: string) => {
    const { north, east }: any = cellcode.match(/N(?<north>\d+)E(?<east>\d+)/)?.groups;
    const northNumber = parseInt(north);
    const eastNumber = parseInt(east);
    const operations = [
      [1, -1],
      [1, 0],
      [1, 1],
      [0, -1],
      [0, 1],
      [-1, -1],
      [-1, 0],
      [-1, 1],
    ];

    const cellcodeQueries = operations.map(
      (operation) => `cellcode = '10kmN${northNumber + operation[0]}E${eastNumber + operation[1]}'`
    );
    return `cellcode = '${cellcode}' OR ` + cellcodeQueries.join(' OR ');
  };

  useEffect(() => {
    if (rendered.current) return;
    rendered.current = true;

    // add data index layer
    view.map.layers.push(indexLayer);

    // add search widget with custom search source
    const search = new Search({
      view: view,
      popupEnabled: false,
      sources: [customSearchSource],
      includeDefaultSources: false,
    });

    view.ui.add(search, 'top-left');

    // add event handler for select-result events
    search.on('select-result', (event) => {
      view.closePopup();

      // get selected feature and display it on map
      const graphic = event.result.feature;
      if (graphic.geometry.type === 'point') {
        setCurrentTarget(
          new Graphic({
            // create buffer for point geometries to allow zoom to
            geometry: geodesicBuffer(graphic.geometry, 1000, 'meters') as Polygon,
          })
        );
      } else {
        setCurrentTarget(graphic);
      }

      view.graphics.removeAll();
      view.graphics.add(graphic);

      // query for intersecting features
      indexLayer
        .queryFeatures({
          geometry: graphic.geometry,
          spatialRelationship: 'intersects',
          returnGeometry: false,
          outFields: ['fid', 'cellcode'],
        })
        .then((featureSet) => {
          if (featureSet.features.length === 0) {
            return;
          }

          // if feature has an extent just return intersecting raster cells
          // otherwise create a query including a 3x3 neighbourhood
          if (event.result.extent) {
            const objectIds = featureSet.features.map((feature) => feature.attributes['fid'] as number);
            queryRelatedFeaturesFromOIds(objectIds, false);
          } else {
            const cellcode = featureSet.features[0].attributes['cellcode'];
            const query = createQueryFromCellcode(cellcode);

            indexLayer
              .queryFeatures({
                where: query,
                returnGeometry: false,
                outFields: ['fid'],
              })
              .then((resultSet) => {
                const objectIds = resultSet.features.map((feature) => feature.attributes['fid']);
                queryRelatedFeaturesFromOIds(objectIds, false);
              });
          }
        });
    });

    // for each raster cell given by its OID and it each relationship class
    // of the index layer - query related features
    const queryRelatedFeaturesFromOIds = (objectIds: number[], isPopupContent: boolean) => {
      const relatedFeaturesByLayer: LayerToFeaturesMap = {};
      const promises = indexLayer.relationships.map((relationship) => {
        return indexLayer
          .queryRelatedFeatures({
            outFields: ['url'],
            relationshipId: relationship.id,
            objectIds: objectIds,
          })
          .then((relatedFeatureSets) => {
            Object.keys(relatedFeatureSets).forEach((objectId) => {
              if (!relatedFeatureSets[objectId]) {
                return;
              }

              const regex = /^https:\/\/.+\/MapServer\/\d+/g;
              const relatedFeatures = relatedFeatureSets[objectId].features;

              // get url of related feature layer
              let layerURL: string = '';
              if (relatedFeatures.length > 0) {
                const matches = relatedFeatures[0].attributes.url.match(regex);
                if (matches.length > 0) {
                  layerURL = matches[0];
                }
              }
              const urls = relatedFeatures.map((feature: any) => feature.attributes.url);

              if (relatedFeaturesByLayer[layerURL]) {
                relatedFeaturesByLayer[layerURL] = relatedFeaturesByLayer[layerURL].concat(urls);
              } else {
                relatedFeaturesByLayer[layerURL] = urls;
              }
            });
          });
      });

      // wait until all promises are fulfilled
      return eachAlways(promises).then(() => {
        const relatedFeatures: { [key: string]: JSX.Element[] } = {};
        const mapImageLayers: MapImageLayer[] = [];
        Object.keys(relatedFeaturesByLayer).forEach((layerURL) => {
          const relatedFeatureURLs = [...new Set(relatedFeaturesByLayer[layerURL])];
          const mapImageLayer = getMapImageLayer(layerURL);

          let mapImageLayerId: string = '';
          let sublayer: Sublayer | undefined;
          if (mapImageLayer) {
            mapImageLayerId = mapImageLayer.id;
            mapImageLayers.push(mapImageLayer);

            mapImageLayer.sublayers.forEach((layer) => {
              if (layer.url === layerURL) {
                sublayer = layer;
              }
            });
          }

          const text = sublayer?.id ? sublayer?.id.toString() : '';
          // create UI item for sublayer
          const accordionItem = (
            <CalciteAccordionItem heading={sublayer?.title} key={sublayer?.id}>
              <CalciteAction
                slot="actions-end"
                icon={sublayer?.visible ? 'view-visible' : 'view-hide'}
                text={text}
                appearance="transparent"
                onClick={() => {
                  if (sublayer) {
                    sublayer.visible = !sublayer.visible;
                  }
                }}
              ></CalciteAction>

              <CalciteList>
                {relatedFeatureURLs.map((relatedFeatureURL) => {
                  const regex = /\d+$/g;
                  const matches = relatedFeatureURL.match(regex);
                  let featureId;
                  if (matches && matches.length > 0) featureId = matches[0];
                  return (
                    <CalciteListItem key={featureId}>
                      <CalciteAction
                        slot="actions-start"
                        icon="magnifying-glass"
                        text={relatedFeatureURL}
                        appearance="transparent"
                        onClick={handleZoomTo}
                      ></CalciteAction>
                      <CalciteLink
                        href={relatedFeatureURL}
                        target="_blank"
                        slot="content"
                      >{`Feature ${featureId}`}</CalciteLink>
                    </CalciteListItem>
                  );
                })}
              </CalciteList>
            </CalciteAccordionItem>
          );

          if (!relatedFeatures[mapImageLayerId]) {
            relatedFeatures[mapImageLayerId] = [accordionItem];
          } else {
            relatedFeatures[mapImageLayerId].push(accordionItem);
          }
        });

        // create cutom layer objects tree
        const layerObjects = getLayerObjects(view.map.layers.toArray());

        // remove layers that are not in the seach result
        let remainingLayers = removeLayers(layerObjects, (layer: CustomGroupLayer | CustomLayer) => {
          if (
            layer.type === 'map-image' &&
            mapImageLayers.findIndex((mapImageLayer) => mapImageLayer.id === layer.id) === -1
          ) {
            return false;
          } else {
            return true;
          }
        });

        remainingLayers = removeLayers(remainingLayers, (layer: CustomGroupLayer | CustomLayer) => {
          if (
            (layer.type === 'group' &&
              ((layer as CustomGroupLayer).layers.length === 0 ||
                (layer as CustomGroupLayer).layers.every(
                  (child) => child.type === 'imagery' || child.type === 'imagery-tile'
                ))) ||
            layer.type === 'feature' ||
            layer.type === 'imagery' ||
            layer.type === 'imagery-tile'
          ) {
            return false;
          } else {
            return true;
          }
        });

        // recursively build UI from remaining layers
        const buildLayerTree = (layer: CustomGroupLayer | CustomLayer) => {
          if (layer.type === 'group') {
            return (
              <CalciteAccordionItem heading={layer.title} key={layer.id}>
                <CalciteAction
                  slot="actions-end"
                  icon={layer.visible ? 'view-visible' : 'view-hide'}
                  text={layer.id}
                  appearance="transparent"
                  onClick={handleToggleVisibility}
                ></CalciteAction>
                {(layer as CustomGroupLayer).layers &&
                  (layer as CustomGroupLayer).layers.map((sublayer) => buildLayerTree(sublayer))}
              </CalciteAccordionItem>
            );
          } else if (layer.type === 'map-image') {
            return (
              <CalciteAccordionItem heading={layer.title} key={layer.id}>
                <CalciteAction
                  slot="actions-end"
                  icon={layer.visible ? 'view-visible' : 'view-hide'}
                  text={layer.id}
                  appearance="transparent"
                  onClick={handleToggleVisibility}
                ></CalciteAction>
                {relatedFeatures[layer.id]}
              </CalciteAccordionItem>
            );
          }
        };

        const layertree = (
          <CalciteAccordion appearance="transparent" iconPosition="start" scale="s">
            {remainingLayers.map((layer: CustomGroupLayer | CustomLayer) => buildLayerTree(layer))}
          </CalciteAccordion>
        );

        // render react components into div element
        const contentDiv = document.createElement('div');
        const root: Root = createRoot(contentDiv);
        root.render(layertree);

        if (isPopupContent) {
          return contentDiv;
        } else {
          let location;
          if (currentGraphicRef.current?.geometry?.type === 'polygon') {
            location = (currentGraphicRef.current?.geometry as Polygon).centroid;
          } else {
            location = currentGraphicRef.current?.geometry;
          }

          view.openPopup({
            title: t('search.popup-title'),
            content: contentDiv,
            location: location,
          });
        }
      });
    };

    // create popup content for popup template of index layer
    const createPopupContent = async (target: any) => {
      const cellcode = target.graphic.attributes['cellcode'];
      const query = createQueryFromCellcode(cellcode);

      const objectIds = await indexLayer
        .queryFeatures({
          where: query,
          returnGeometry: false,
          outFields: ['fid'],
        })
        .then((resultSet) => {
          // return Ids of neighbouring features (3x3 neighbourhood)
          return resultSet.features.map((feature) => feature.attributes['fid']);
        });

      return queryRelatedFeaturesFromOIds(objectIds, true);
    };

    const popupTemplate = {
      title: t('search.popup-title'),
      content: createPopupContent,
      returnGeometry: false,
      outFields: [],
    };

    indexLayer.popupTemplate = popupTemplate as unknown as PopupTemplate;
  });

  return null;
}
