import { useEffect, useRef, useState } from 'react';
import L from 'leaflet';
import { useLodGrid, type MapBounds } from '@/hooks/useLodGrid';

const RISK_COLORS: [number, number, string][] = [
  [0.0, 0.2, '#22c55e'],
  [0.2, 0.4, '#3b82f6'],
  [0.4, 0.6, '#eab308'],
  [0.6, 0.8, '#f97316'],
  [0.8, 1.0, '#ef4444'],
];

// Pre-computed color buckets for fillStyle caching
const COLOR_BUCKETS: Record<string, { full: string; dim: string }> = {};
for (const [, , color] of RISK_COLORS) {
  COLOR_BUCKETS[color] = { full: color, dim: color + '14' };
}

function getRiskColor(value: number): string {
  for (const [min, max, color] of RISK_COLORS) {
    if (value >= min && value <= max) return color;
  }
  return '#22c55e';
}

// 100m grid step in degrees
const LAT_STEP = 0.0009;
const LON_STEP = 0.001046;

// Mercator helpers (avoid per-cell latLngToContainerPoint)
function latToMercY(lat: number): number {
  return 128 - (256 * Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360))) / (2 * Math.PI);
}

function lonToMercX(lon: number): number {
  return ((lon + 180) / 360) * 256;
}

interface LodGridLayerProps {
  map: L.Map | null;
  forecastDay: 1 | 3 | 7;
  visible?: boolean;
  riskRange?: [number, number];
  onCellClick?: (lat: number, lon: number, risk: number) => void;
}

export function LodGridLayer({
  map,
  forecastDay,
  visible = true,
  riskRange,
  onCellClick,
}: LodGridLayerProps) {
  const [zoom, setZoom] = useState(map?.getZoom() ?? 10);
  const [mapBounds, setMapBounds] = useState<MapBounds | undefined>();
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const paneRef = useRef<HTMLElement | null>(null);
  const animFrameRef = useRef<number>(0);
  const clickCallbackRef = useRef(onCellClick);
  const gridsRef = useRef<number[][]>([]);
  const forecastDayRef = useRef(forecastDay);
  const riskRangeRef = useRef(riskRange);
  const visibleRef = useRef(visible);
  const drawnOriginRef = useRef<{ x: number; y: number } | null>(null);

  // Keep refs in sync
  useEffect(() => { clickCallbackRef.current = onCellClick; }, [onCellClick]);
  useEffect(() => { forecastDayRef.current = forecastDay; }, [forecastDay]);
  useEffect(() => { riskRangeRef.current = riskRange; }, [riskRange]);
  useEffect(() => { visibleRef.current = visible; }, [visible]);

  // Track map bounds and zoom
  useEffect(() => {
    if (!map) return;
    const update = () => {
      const b = map.getBounds();
      setMapBounds({
        min_lat: b.getSouth(),
        max_lat: b.getNorth(),
        min_lon: b.getWest(),
        max_lon: b.getEast(),
      });
      setZoom(map.getZoom());
    };
    update();
    map.on('moveend', update);
    map.on('zoomend', update);
    return () => {
      map.off('moveend', update);
      map.off('zoomend', update);
    };
  }, [map]);

  const { grids } = useLodGrid(zoom, forecastDay, mapBounds);

  // Update gridsRef only when we have actual data (preserve stale data during loading)
  useEffect(() => {
    if (grids.length > 0) {
      gridsRef.current = grids;
    }
  }, [grids]);

  // Create canvas overlay pane and attach to map
  useEffect(() => {
    if (!map) return;

    const pane = map.createPane('lod-grid-pane');
    pane.style.zIndex = '450';
    pane.style.pointerEvents = 'none';
    paneRef.current = pane;

    const canvas = document.createElement('canvas');
    canvas.style.position = 'absolute';
    canvas.style.top = '0';
    canvas.style.left = '0';
    canvas.style.width = '100%';
    canvas.style.height = '100%';
    canvas.style.pointerEvents = 'none';
    pane.appendChild(canvas);
    canvasRef.current = canvas;

    // Handle map clicks for grid cell selection
    const handleMapClick = (e: L.LeafletMouseEvent) => {
      if (!clickCallbackRef.current) return;
      const currentGrids = gridsRef.current;
      if (!currentGrids || currentGrids.length === 0) return;

      const { lat, lng } = e.latlng;
      const riskIdx = forecastDayRef.current === 1 ? 2 : forecastDayRef.current === 3 ? 3 : 4;
      let nearestDist = Infinity;
      let nearestRisk = 0;
      let nearestLat = 0;
      let nearestLon = 0;

      for (const g of currentGrids) {
        const d = Math.sqrt((g[0] - lat) ** 2 + (g[1] - lng) ** 2);
        if (d < nearestDist) {
          nearestDist = d;
          nearestRisk = g[riskIdx] ?? 0;
          nearestLat = g[0];
          nearestLon = g[1];
        }
      }

      if (nearestDist < 0.01) {
        clickCallbackRef.current(nearestLat, nearestLon, nearestRisk);
      }
    };

    map.on('click', handleMapClick);

    // Full redraw function
    const redraw = () => {
      if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
      animFrameRef.current = requestAnimationFrame(() => {
        const container = map.getContainer();
        const w = container.clientWidth;
        const h = container.clientHeight;
        const dpr = window.devicePixelRatio || 1;

        canvas.width = w * dpr;
        canvas.height = h * dpr;
        canvas.style.width = w + 'px';
        canvas.style.height = h + 'px';

        const ctx = canvas.getContext('2d');
        if (!ctx) return;

        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, w, h);

        // Reset drift transform after redraw
        canvas.style.transform = '';
        drawnOriginRef.current = null;

        if (!visibleRef.current) return;

        const currentGrids = gridsRef.current;
        if (!currentGrids || currentGrids.length === 0) return;

        const z = map.getZoom();
        const riskIdx = forecastDayRef.current === 1 ? 2 : forecastDayRef.current === 3 ? 3 : 4;
        const range = riskRangeRef.current;
        const mapBounds = map.getBounds();
        const south = mapBounds.getSouth();
        const north = mapBounds.getNorth();
        const west = mapBounds.getWest();
        const east = mapBounds.getEast();

        // Use Mercator math for pixel conversion (avoids per-cell latLngToContainerPoint)
        const scale = 2 ** z;
        const origin = map.getPixelOrigin();
        drawnOriginRef.current = { x: origin.x, y: origin.y };

        // Pre-compute Mercator Y steps for cell size at this zoom
        const halfLat = LAT_STEP / 2;
        const halfLon = LON_STEP / 2;

        // Group cells by color to minimize fillStyle changes
        const colorGroups: Record<string, { x: number; y: number; w: number; h: number }[]> = {};

        // Viewport culling margin in degrees
        const margin = 0.02;
        const isHighZoom = z >= 12;
        const isMedZoom = z >= 10;

        for (const g of currentGrids) {
          const lat = g[0];
          const lon = g[1];
          const risk = g[riskIdx] ?? 0;

          // Pre-filter: skip zero-risk cells (majority of cells at most zooms)
          if (risk === 0) continue;

          // Viewport culling
          if (lat < south - margin || lat > north + margin ||
              lon < west - margin || lon > east + margin) {
            continue;
          }

          // Risk range filter
          let alpha = 0.85;
          if (range) {
            if (risk < range[0]) {
              alpha = 0.08;
            } else if (risk > range[1]) {
              alpha = 0.3;
            }
          }

          const color = getRiskColor(risk);

          if (isHighZoom) {
            // Compute cell rectangle using Mercator math
            const lx = lonToMercX(lon - halfLon) * scale - origin.x;
            const rx = lonToMercX(lon + halfLon) * scale - origin.x;
            const ty = latToMercY(lat + halfLat) * scale - origin.y;
            const by = latToMercY(lat - halfLat) * scale - origin.y;
            const cellW = rx - lx;
            const cellH = by - ty;

            if (cellW < 0.5 || cellH < 0.5) continue;

            // Group by color+alpha for batch rendering
            const key = alpha < 1 ? `${color}_${alpha}` : color;
            if (!colorGroups[key]) colorGroups[key] = [];
            colorGroups[key].push({ x: lx, y: ty, w: cellW, h: cellH });
          } else {
            // Medium/low zoom: compute center pixel
            const cx = lonToMercX(lon) * scale - origin.x;
            const cy = latToMercY(lat) * scale - origin.y;

            const key = alpha < 1 ? `${color}_${alpha}` : color;
            if (!colorGroups[key]) colorGroups[key] = [];
            colorGroups[key].push({ x: cx, y: cy, w: 0, h: 0 });
          }
        }

        // Render grouped cells
        for (const [key, cells] of Object.entries(colorGroups)) {
          const parts = key.split('_');
          const color = parts[0];
          const alpha = parts.length > 1 ? parseFloat(parts[1]) : 1;

          ctx.globalAlpha = alpha;
          ctx.fillStyle = color;

          if (isHighZoom) {
            for (const c of cells) {
              ctx.fillRect(c.x, c.y, c.w, c.h);
            }
            // Stroke only at high enough cell sizes
            ctx.globalAlpha = 0.4;
            ctx.strokeStyle = '#ffffff';
            ctx.lineWidth = 0.5;
            for (const c of cells) {
              if (c.w > 2 && c.h > 2) {
                ctx.strokeRect(c.x, c.y, c.w, c.h);
              }
            }
          } else if (isMedZoom) {
            const size = Math.max(2, Math.min(6, z - 7));
            const halfSize = size / 2;
            for (const c of cells) {
              ctx.fillRect(c.x - halfSize, c.y - halfSize, size, size);
            }
          } else {
            const radius = Math.max(1, Math.min(3, z - 5));
            for (const c of cells) {
              ctx.beginPath();
              ctx.arc(c.x, c.y, radius, 0, Math.PI * 2);
              ctx.fill();
            }
          }
        }

        ctx.globalAlpha = 1;
      });
    };

    // Debounced full redraw (avoid thrashing during rapid pan/zoom)
    let redrawTimer: ReturnType<typeof setTimeout> | null = null;
    const debouncedRedraw = () => {
      if (redrawTimer) clearTimeout(redrawTimer);
      redrawTimer = setTimeout(redraw, 300);
    };

    // During pan: apply CSS transform to track tile movement (fixes drift)
    const onMove = () => {
      const drawn = drawnOriginRef.current;
      if (!drawn) {
        // No previous draw yet, just request a redraw
        redraw();
        return;
      }
      const current = map.getPixelOrigin();
      const dx = drawn.x - current.x;
      const dy = drawn.y - current.y;
      canvas.style.transform = `translate(${dx}px, ${dy}px)`;
    };

    // On moveend/zoomend: reset transform and do debounced full redraw
    const onMoveEnd = () => {
      canvas.style.transform = '';
      drawnOriginRef.current = null;
      debouncedRedraw();
    };

    const onResize = () => redraw();

    map.on('move', onMove);
    map.on('moveend', onMoveEnd);
    map.on('zoomend', onMoveEnd);
    map.on('resize', onResize);

    // Store redraw reference for external triggers
    (canvas as any).__lodRedraw = redraw;

    // Initial draw
    redraw();

    return () => {
      map.off('move', onMove);
      map.off('moveend', onMoveEnd);
      map.off('zoomend', onMoveEnd);
      map.off('resize', onResize);
      map.off('click', handleMapClick);
      if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
      if (redrawTimer) clearTimeout(redrawTimer);
      pane.removeChild(canvas);
      if (pane.parentNode) pane.parentNode.removeChild(pane);
      canvasRef.current = null;
      paneRef.current = null;
    };
  }, [map]);

  // Trigger redraw when data changes
  useEffect(() => {
    const canvas = canvasRef.current;
    if (canvas && (canvas as any).__lodRedraw) {
      (canvas as any).__lodRedraw();
    }
  }, [grids, forecastDay, riskRange, visible]);

  return null;
}
