import { useCallback, useMemo, useRef, useState } from 'react';
import type { PickingInfo } from '@deck.gl/core';
import { IconLayer, ScatterplotLayer, TextLayer } from '@deck.gl/layers';
import Supercluster from 'supercluster';

import type { POI, SchoolMetadata } from '../types';
import {
  POI_GROUP_COLORS,
  MINOR_POI_CATEGORIES,
  MINOR_POI_ZOOM_THRESHOLD,
  POI_CLUSTER_RADIUS,
  POI_CLUSTER_MAX_ZOOM,
} from '../lib/consts';
import { getPoiIconUrl } from '../lib/map-utils';

export interface PopupInfo {
  x: number;
  y: number;
  name: string;
  category: string;
  icon_category?: string;
  group: string;
  emoji: string;
  id: string;
  isCluster?: boolean;
  clusterCount?: number;
  school?: SchoolMetadata;
}

interface ClusterPoint {
  lng: number;
  lat: number;
  count: number;
  clusterId: number;
}

interface UsePoiLayersProps {
  pois: POI[];
  zoom: number;
  isDark: boolean;
}

function getPoiIconUrlForPoi(poi: POI): string {
  return getPoiIconUrl(poi.category, poi.emoji, poi.icon_category, poi.name);
}

function isBundledPoiIcon(url: string): boolean {
  return url.startsWith('/assets/poi-icons/') || url.startsWith('data:image/svg+xml');
}

function hasBundledPoiLogo(poi: POI): boolean {
  return isBundledPoiIcon(getPoiIconUrlForPoi(poi));
}

function getPoiGroupColor(group: string): [number, number, number] {
  const color = POI_GROUP_COLORS[group];
  if (!color) {
    throw new Error(`Missing POI group color for '${group}'`);
  }
  return color;
}

function getPoiIconSize(poi: POI): number {
  return hasBundledPoiLogo(poi) ? 14 : 11;
}

export function usePoiLayers({ pois, zoom, isDark }: UsePoiLayersProps) {
  const [popupInfo, setPopupInfo] = useState<PopupInfo | null>(null);

  const handlePoiHover = useCallback((info: PickingInfo<POI>) => {
    if (info.object && info.x !== undefined && info.y !== undefined) {
      setPopupInfo({
        x: info.x,
        y: info.y,
        name: info.object.name,
        category: info.object.category,
        icon_category: info.object.icon_category,
        group: info.object.group,
        emoji: info.object.emoji,
        id: info.object.id,
        school: info.object.school,
      });
    } else {
      setPopupInfo(null);
    }
  }, []);

  const handlePoiHoverRef = useRef(handlePoiHover);
  handlePoiHoverRef.current = handlePoiHover;
  const stablePoiHover = useCallback((info: PickingInfo<POI>) => {
    handlePoiHoverRef.current(info);
  }, []);

  const handleClusterHover = useCallback((info: PickingInfo<ClusterPoint>) => {
    if (info.object && info.x !== undefined && info.y !== undefined) {
      setPopupInfo({
        x: info.x,
        y: info.y,
        name: `${info.object.count} places`,
        category: 'Zoom in to see details',
        group: '',
        emoji: '',
        id: '',
        isCluster: true,
        clusterCount: info.object.count,
      });
    } else {
      setPopupInfo(null);
    }
  }, []);

  const handleClusterHoverRef = useRef(handleClusterHover);
  handleClusterHoverRef.current = handleClusterHover;
  const stableClusterHover = useCallback((info: PickingInfo<ClusterPoint>) => {
    handleClusterHoverRef.current(info);
  }, []);

  const clusterIndex = useMemo(() => {
    if (pois.length === 0) return null;
    const index = new Supercluster<POI>({
      radius: POI_CLUSTER_RADIUS,
      maxZoom: POI_CLUSTER_MAX_ZOOM,
    });
    const features: Supercluster.PointFeature<POI>[] = pois.map((poi) => ({
      type: 'Feature',
      geometry: { type: 'Point', coordinates: [poi.lng, poi.lat] },
      properties: poi,
    }));
    index.load(features);
    return index;
  }, [pois]);

  const clusterZoom = Math.floor(zoom);
  const showMinorPois = zoom >= MINOR_POI_ZOOM_THRESHOLD;

  const { visiblePois, clusters } = useMemo(() => {
    if (!clusterIndex || pois.length === 0) {
      return { visiblePois: [] as POI[], clusters: [] as ClusterPoint[] };
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const allFeatures = clusterIndex.getClusters([-180, -85, 180, 85], clusterZoom) as any[];
    const individual: POI[] = [];
    const clusterPoints: ClusterPoint[] = [];
    for (const feature of allFeatures) {
      if (feature.properties.cluster) {
        clusterPoints.push({
          lng: feature.geometry.coordinates[0],
          lat: feature.geometry.coordinates[1],
          count: feature.properties.point_count,
          clusterId: feature.properties.cluster_id,
        });
      } else {
        const poi = feature.properties as POI;
        if (!showMinorPois && MINOR_POI_CATEGORIES.has(poi.category)) continue;
        individual.push(poi);
      }
    }
    return { visiblePois: individual, clusters: clusterPoints };
  }, [clusterIndex, clusterZoom, showMinorPois, pois]);

  const poiShadowLayer = useMemo(
    () =>
      new ScatterplotLayer<POI>({
        id: 'poi-shadow',
        data: visiblePois,
        getPosition: (d) => [d.lng, d.lat],
        getRadius: (d) => (hasBundledPoiLogo(d) ? 0 : 10),
        radiusUnits: 'pixels',
        getFillColor: isDark ? [0, 0, 0, 50] : [0, 0, 0, 25],
        pickable: false,
        transitions: { getRadius: { duration: 300, enter: () => [0] } },
      }),
    [visiblePois, isDark]
  );

  const poiBackgroundLayer = useMemo(
    () =>
      new ScatterplotLayer<POI>({
        id: 'poi-background',
        data: visiblePois,
        getPosition: (d) => [d.lng, d.lat],
        getRadius: (d) => (hasBundledPoiLogo(d) ? 14 : 8),
        radiusUnits: 'pixels',
        getFillColor: (d) =>
          hasBundledPoiLogo(d)
            ? ([0, 0, 0, 0] as [number, number, number, number])
            : isDark
              ? ([41, 37, 36, 255] as [number, number, number, number])
              : ([255, 255, 255, 255] as [number, number, number, number]),
        getLineColor: (d) => {
          if (hasBundledPoiLogo(d)) return [0, 0, 0, 0] as [number, number, number, number];
          const c = getPoiGroupColor(d.group);
          return [c[0], c[1], c[2], 255] as [number, number, number, number];
        },
        getLineWidth: 2.5,
        lineWidthUnits: 'pixels',
        stroked: true,
        pickable: true,
        onHover: stablePoiHover,
        transitions: { getRadius: { duration: 300, enter: () => [0] } },
      }),
    [visiblePois, isDark, stablePoiHover]
  );

  const poiIconLayer = useMemo(
    () =>
      new IconLayer<POI>({
        id: 'poi-icons',
        data: visiblePois,
        getPosition: (d) => [d.lng, d.lat],
        getIcon: (d) => {
          const url = getPoiIconUrlForPoi(d);
          const isLogo = isBundledPoiIcon(url);
          return {
            url,
            width: isLogo ? 96 : 72,
            height: isLogo ? 48 : 72,
          };
        },
        getSize: getPoiIconSize,
        sizeUnits: 'pixels',
        pickable: false,
        transitions: { getSize: { duration: 300, enter: () => [0] } },
      }),
    [visiblePois]
  );

  const clusterCircleLayer = useMemo(
    () =>
      new ScatterplotLayer<ClusterPoint>({
        id: 'poi-clusters',
        data: clusters,
        getPosition: (d) => [d.lng, d.lat],
        getRadius: (d) => Math.min(30, 14 + Math.sqrt(d.count) * 2),
        radiusUnits: 'pixels',
        getFillColor: isDark ? [5, 129, 114, 220] : [20, 184, 166, 220],
        getLineColor: [255, 255, 255, isDark ? 60 : 120],
        getLineWidth: 2,
        lineWidthUnits: 'pixels',
        stroked: true,
        pickable: true,
        onHover: stableClusterHover,
        transitions: { getRadius: { duration: 300, enter: () => [0] } },
      }),
    [clusters, isDark, stableClusterHover]
  );

  const clusterTextLayer = useMemo(
    () =>
      new TextLayer<ClusterPoint>({
        id: 'poi-cluster-text',
        data: clusters,
        getPosition: (d) => [d.lng, d.lat],
        getText: (d) => (d.count >= 1000 ? `${(d.count / 1000).toFixed(1)}k` : String(d.count)),
        getSize: 12,
        getColor: [255, 255, 255, 255],
        fontWeight: 700,
        fontFamily: 'Inter, system-ui, sans-serif',
        getTextAnchor: 'middle',
        getAlignmentBaseline: 'center',
        sizeUnits: 'pixels',
        pickable: false,
      }),
    [clusters]
  );

  const poiLayers = useMemo(
    () => [poiShadowLayer, poiBackgroundLayer, poiIconLayer, clusterCircleLayer, clusterTextLayer],
    [poiShadowLayer, poiBackgroundLayer, poiIconLayer, clusterCircleLayer, clusterTextLayer]
  );

  const clearPopupInfo = useCallback(() => setPopupInfo(null), []);

  return { poiLayers, visiblePois, popupInfo, clearPopupInfo };
}
