import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { drawBaseMap } from "./canvas/drawBaseMap";
import { drawPlanWorks } from "./canvas/drawPlanWorks";
import { drawRequests } from "./canvas/drawRequests";
import { drawStations } from "./canvas/drawStations";
import { drawSwath } from "./canvas/drawSwath";
import { drawTiles } from "./canvas/drawTiles";
import { drawTracks } from "./canvas/drawTracks";
import { createMapProjection } from "./geometry/mapProjection";
import { createPolygonSpatialIndex, hitTestPolygons, queryPolygonsByBBox } from "./geometry/spatialIndex";
import type { Tgu2DMapLayersState } from "./model/mapLayerTypes";
import type { MapPolygon, MapSize, MapViewState, ScreenPoint, Tgu2DMapScene } from "./model/mapTypes";
import type { Tgu2DMapSelection } from "./model/mapSelectionTypes";

type Tgu2DMapViewProps = {
  scene: Tgu2DMapScene;
  layers: Tgu2DMapLayersState;
  redrawToken: number;
  onObjectSelect: (selection?: Tgu2DMapSelection) => void;
  pickMode?: boolean;
  onPickPoint?: (point: { lat: number; lon: number }) => void;
  targets?: Tgu2DMapTarget[];
  pickTarget?: Tgu2DMapTarget;
  selectedTargetId?: string;
  onTargetSelect?: (id: string) => void;
};

export type Tgu2DMapTarget = {
  id: string;
  name: string;
  lat: number;
  lon: number;
};

type DragState = {
  startX: number;
  startY: number;
  centerAtStart: {
    lon: number;
    lat: number;
  };
};

type TooltipState = {
  x: number;
  y: number;
  title: string;
  subtitle: string;
};

const INITIAL_VIEW: MapViewState = {
  centerLon: 45,
  centerLat: 30,
  zoom: 2
};

export function Tgu2DMapView({
  scene,
  layers,
  redrawToken,
  onObjectSelect,
  pickMode = false,
  onPickPoint,
  targets = [],
  pickTarget,
  selectedTargetId,
  onTargetSelect
}: Tgu2DMapViewProps) {
  const wrapperRef = useRef<HTMLDivElement | null>(null);
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const dragRef = useRef<DragState | undefined>(undefined);
  const hoverRef = useRef<MapPolygon | undefined>(undefined);
  const [size, setSize] = useState<MapSize>({ width: 1000, height: 560 });
  const [view, setView] = useState<MapViewState>(INITIAL_VIEW);
  const [tileVersion, setTileVersion] = useState(0);
  const handleTileLoad = useCallback(() => setTileVersion((v) => v + 1), []);
  const [tooltip, setTooltip] = useState<TooltipState>();
  const selectedPolygonRef = useRef<MapPolygon | undefined>(undefined);
  const activePolygons = useMemo(
    () => scene.polygons.filter((polygon) => layers[polygon.layer]),
    [scene.polygons, layers]
  );
  const polygonIndex = useMemo(() => createPolygonSpatialIndex(activePolygons), [activePolygons]);
  const projection = useMemo(() => createMapProjection(view, size), [view, size]);
  const visiblePolygons = useMemo(() => {
    // When the canvas is wider than the world tile, the entire globe is visible —
    // bbox filtering would produce a nonsensical range, so skip it.
    // When the viewport crosses the antimeridian, topLeft.lon ends up east of
    // bottomRight.lon — same situation, show everything.
    const topLeft = projection.unproject({ x: 0, y: 0 });
    const bottomRight = projection.unproject({ x: size.width, y: size.height });
    if (size.width >= projection.worldSize || topLeft.lon >= bottomRight.lon) {
      return activePolygons;
    }

    return queryPolygonsByBBox(polygonIndex, {
      minLon: topLeft.lon,
      maxLon: bottomRight.lon,
      minLat: Math.min(topLeft.lat, bottomRight.lat),
      maxLat: Math.max(topLeft.lat, bottomRight.lat)
    });
  }, [activePolygons, polygonIndex, projection, size.height, size.width]);

  useEffect(() => {
    const element = wrapperRef.current;
    if (!element) return;

    const resizeObserver = new ResizeObserver(() => {
      setSize({
        width: Math.max(320, element.clientWidth),
        height: Math.max(320, element.clientHeight)
      });
    });
    resizeObserver.observe(element);
    setSize({
      width: Math.max(320, element.clientWidth),
      height: Math.max(320, element.clientHeight)
    });

    return () => resizeObserver.disconnect();
  }, []);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const frameId = window.requestAnimationFrame(() => {
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      canvas.width = Math.floor(size.width * dpr);
      canvas.height = Math.floor(size.height * dpr);
      canvas.style.width = `${size.width}px`;
      canvas.style.height = `${size.height}px`;

      const context = canvas.getContext("2d");
      if (!context) return;

      context.setTransform(dpr, 0, 0, dpr, 0, 0);
      context.clearRect(0, 0, size.width, size.height);
      drawTiles(context, view, projection, size, handleTileLoad);
      drawBaseMap(context, projection);

      if (layers.requests) {
        drawRequests(context, visiblePolygons.filter((polygon) => polygon.layer === "requests"), projection, size);
      }
      if (layers.swath) {
        drawSwath(context, visiblePolygons.filter((polygon) => polygon.layer === "swath"), projection, size);
      }
      if (layers.planWorks) {
        drawPlanWorks(context, visiblePolygons.filter((polygon) => polygon.layer === "planWorks"), projection, size);
      }
      if (layers.tracks) {
        drawTracks(context, scene.lines.filter((line) => layers[line.layer]), projection);
      }
      if (layers.stations) {
        drawStations(context, scene.markers.filter((marker) => marker.layer === "stations"), projection);
      }
    });

    return () => window.cancelAnimationFrame(frameId);
  }, [handleTileLoad, layers, projection, redrawToken, scene.lines, scene.markers, size, tileVersion, view, visiblePolygons]);

  const screenPointFromEvent = useCallback((event: React.PointerEvent | React.MouseEvent): ScreenPoint => {
    const bounds = wrapperRef.current?.getBoundingClientRect();
    return {
      x: bounds ? event.clientX - bounds.left : 0,
      y: bounds ? event.clientY - bounds.top : 0
    };
  }, []);

  const updateHover = useCallback(
    (event: React.PointerEvent) => {
      const screenPoint = screenPointFromEvent(event);
      const polygon = hitTestPolygons(polygonIndex, projection.unproject(screenPoint));
      if (polygon?.id === hoverRef.current?.id) return;

      hoverRef.current = polygon;
      if (!polygon) {
        setTooltip(undefined);
        return;
      }

      setTooltip({
        x: screenPoint.x + 12,
        y: screenPoint.y + 12,
        title: polygon.name,
        subtitle: layerTitle(polygon.layer)
      });
    },
    [polygonIndex, projection, screenPointFromEvent]
  );

  const onPointerDown = (event: React.PointerEvent) => {
    const screenPoint = screenPointFromEvent(event);
    dragRef.current = {
      startX: screenPoint.x,
      startY: screenPoint.y,
      centerAtStart: { lon: view.centerLon, lat: view.centerLat }
    };
    wrapperRef.current?.setPointerCapture(event.pointerId);
  };

  const onPointerMove = (event: React.PointerEvent) => {
    const drag = dragRef.current;
    if (!drag) {
      updateHover(event);
      return;
    }

    const screenPoint = screenPointFromEvent(event);
    const dx = screenPoint.x - drag.startX;
    const dy = screenPoint.y - drag.startY;
    const dragProjection = createMapProjection(
      {
        centerLon: drag.centerAtStart.lon,
        centerLat: drag.centerAtStart.lat,
        zoom: view.zoom
      },
      size
    );
    const nextCenter = dragProjection.unproject({ x: size.width / 2 - dx, y: size.height / 2 - dy });
    setView((current) => ({
      ...current,
      centerLon: nextCenter.lon,
      centerLat: Math.max(-82, Math.min(82, nextCenter.lat))
    }));
  };

  const onPointerUp = (event: React.PointerEvent) => {
    dragRef.current = undefined;
    wrapperRef.current?.releasePointerCapture(event.pointerId);
  };

  const onClick = (event: React.MouseEvent) => {
    const screenPoint = screenPointFromEvent(event);
    if (pickMode) {
      onPickPoint?.(projection.unproject(screenPoint));
      onObjectSelect(undefined);
      return;
    }

    const polygon = hitTestPolygons(polygonIndex, projection.unproject(screenPoint));
    selectedPolygonRef.current = polygon;

    if (!polygon) {
      onObjectSelect(undefined);
      return;
    }

    onObjectSelect({
      type: "polygon",
      id: polygon.id,
      name: polygon.name,
      layer: polygon.layer,
      planId: polygon.planId,
      spacecraftId: polygon.spacecraftId,
      requestId: polygon.requestId,
      surveyType: polygon.surveyType,
      status: polygon.status
    });
  };

  const onWheel = (event: React.WheelEvent) => {
    event.preventDefault();
    const screenPoint = screenPointFromEvent(event);
    const before = projection.unproject(screenPoint);
    const nextZoom = Math.max(0.8, Math.min(8, view.zoom + (event.deltaY < 0 ? 0.35 : -0.35)));
    const nextProjection = createMapProjection({ ...view, zoom: nextZoom }, size);
    const after = nextProjection.unproject(screenPoint);

    setView((current) => ({
      ...current,
      zoom: nextZoom,
      centerLon: current.centerLon + before.lon - after.lon,
      centerLat: Math.max(-82, Math.min(82, current.centerLat + before.lat - after.lat))
    }));
  };

  const selectedPolygon = selectedPolygonRef.current;
  const overlayPolygons = uniquePolygons([selectedPolygon, hoverRef.current]);

  return (
    <div
      className={`tgu-2d-map ${pickMode ? "is-picking" : ""}`}
      ref={wrapperRef}
      onClick={onClick}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={onPointerUp}
      onPointerCancel={onPointerUp}
      onPointerLeave={() => {
        dragRef.current = undefined;
        hoverRef.current = undefined;
        setTooltip(undefined);
      }}
      onWheel={onWheel}
    >
      <canvas className="tgu-2d-map__canvas" ref={canvasRef} />
      <svg className="tgu-2d-map__overlay" width={size.width} height={size.height} aria-hidden="true">
        {overlayPolygons.flatMap((polygon) => {
          const raw = polygon.points.map((point) => projection.project(point));
          const pts = unwrapX(raw, projection.worldSize);
          const cls = polygon.id === selectedPolygon?.id ? "is-selected" : "is-hovered";
          return ([0, projection.worldSize, -projection.worldSize] as const).flatMap((dx) => {
            const visible = pts.some((p) => {
              const x = p.x + dx;
              return x >= -64 && x <= size.width + 64 && p.y >= -64 && p.y <= size.height + 64;
            });
            if (!visible) return [];
            return (
              <polygon
                key={`${polygon.id}:${dx}`}
                points={pts.map((p) => `${p.x + dx},${p.y}`).join(" ")}
                className={cls}
              />
            );
          });
        })}
        {targets.map((t) => {
          const screen = projection.project(t);
          const isSelected = t.id === selectedTargetId;
          return (
            <g
              className={`tgu-2d-map__target tgu-2d-map__target--confirmed${isSelected ? " is-selected" : ""}`}
              key={t.id}
              transform={`translate(${screen.x} ${screen.y})`}
              onClick={(e) => { e.stopPropagation(); onTargetSelect?.(t.id); }}
              style={{ cursor: "pointer" }}
            >
              <circle r="6" />
              <line x1="-10" x2="10" y1="0" y2="0" />
              <line x1="0" x2="0" y1="-10" y2="10" />
              <text x="10" y="-8">{t.name}</text>
            </g>
          );
        })}
        {pickTarget && (() => {
          const screen = projection.project(pickTarget);
          return (
            <g className="tgu-2d-map__target tgu-2d-map__target--pending" key="pick" transform={`translate(${screen.x} ${screen.y})`}>
              <circle r="6" />
              <line x1="-10" x2="10" y1="0" y2="0" />
              <line x1="0" x2="0" y1="-10" y2="10" />
              <text x="10" y="-8">{pickTarget.name}</text>
            </g>
          );
        })()}
      </svg>
      {tooltip && (
        <div className="tgu-2d-map__tooltip" style={{ left: tooltip.x, top: tooltip.y }}>
          <div>{tooltip.title}</div>
          <span>{tooltip.subtitle}</span>
        </div>
      )}
      <div className="tgu-2d-map__attribution">© <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer">OpenStreetMap</a> contributors · © <a href="https://carto.com/attributions" target="_blank" rel="noopener noreferrer">CARTO</a></div>
      <div className="tgu-2d-map__zoom">
        <button type="button" onClick={() => setView((current) => ({ ...current, zoom: Math.min(8, current.zoom + 0.5) }))}>+</button>
        <button type="button" onClick={() => setView((current) => ({ ...current, zoom: Math.max(0.8, current.zoom - 0.5) }))}>-</button>
        <button type="button" onClick={() => setView(INITIAL_VIEW)}>□</button>
      </div>
    </div>
  );
}

function layerTitle(layer: MapPolygon["layer"]): string {
  switch (layer) {
    case "planWorks":
      return "Работы плана";
    case "swath":
      return "Полоса обзора";
    case "tracks":
      return "Трасса КА";
    case "requests":
      return "Заявка на съёмку";
  }
}

function unwrapX(points: { x: number; y: number }[], worldSize: number): { x: number; y: number }[] {
  if (points.length === 0) return [];
  const out: { x: number; y: number }[] = [points[0]];
  let prevX = points[0].x;
  for (let i = 1; i < points.length; i++) {
    let x = points[i].x;
    while (x - prevX > worldSize / 2) x -= worldSize;
    while (prevX - x > worldSize / 2) x += worldSize;
    out.push({ x, y: points[i].y });
    prevX = x;
  }
  return out;
}

function uniquePolygons(polygons: Array<MapPolygon | undefined>): MapPolygon[] {
  const result: MapPolygon[] = [];
  const ids = new Set<string>();
  for (const polygon of polygons) {
    if (!polygon || ids.has(polygon.id)) continue;
    ids.add(polygon.id);
    result.push(polygon);
  }
  return result;
}
