import React, {
  useRef,
  useEffect,
  useState,
  useMemo,
  useCallback,
} from "react";
import * as THREE from "three";
import {
  findClosestWaypoint,
  findWaypointPath,
} from "@/pathfinding/WaypointAStar";
import type { Waypoint } from "@/pathfinding/types";
import type { Vector3Tuple } from "@/types/three/three";
function computeImageSource(
  img: HTMLImageElement | HTMLCanvasElement,
  baseBounds: { minX: number; maxX: number; minZ: number; maxZ: number },
  bounds: { minX: number; maxX: number; minZ: number; maxZ: number },
) {
  const imgW = img.width;
  const imgH = img.height;

  const baseW = baseBounds.maxX - baseBounds.minX;
  const baseH = baseBounds.maxZ - baseBounds.minZ;

  if (baseW === 0 || baseH === 0) {
    return { sx: 0, sy: 0, sW: imgW, sH: imgH };
  }

  const sx = ((bounds.minX - baseBounds.minX) / baseW) * imgW;
  const sy = ((bounds.minZ - baseBounds.minZ) / baseH) * imgH;
  const sW = ((bounds.maxX - bounds.minX) / baseW) * imgW;
  const sH = ((bounds.maxZ - bounds.minZ) / baseH) * imgH;

  return { sx, sy, sW, sH };
}

export interface EbikeGPSMapProps {
  /**
   * 3D world position of the player/bike (GPS start point)
   * If omitted, snaps to [0,0,0]
   */
  startPos?: { x: number; y: number; z: number } | undefined;
  destPos?: { x: number; y: number; z: number } | undefined;

  /**
   * Optional custom URL to the map background texture.
   * If not provided, renders a high-tech minimalist neon blueprint map dynamically.
   */
  mapImageUrl?: string;

  /**
   * Optional explicit bounds for mapping coordinates.
   * If omitted, bounds are calculated automatically to perfectly fit the road network!
   */
  worldBounds?: {
    minX: number;
    maxX: number;
    minZ: number;
    maxZ: number;
  };

  /**
   * Width of the 3D plane mesh (default: 1)
   */
  width?: number;

  /**
   * Height of the 3D plane mesh (default: 1)
   */
  height?: number;

  /**
   * Optional world position for the GPS screen (defaults to origin)
   */
  position?: Vector3Tuple;

  /**
   * Resolution of the offscreen canvas used for the map texture.
   * Higher values yield sharper rendering at the cost of GPU memory.
   * Default: 1024 (1024×1024 px)
   */
  canvasSize?: number;

  /**
   * Zoom level applied to the map view.
   * 1 = full world bounds, 2 = 2× zoom-in centred on the player, etc.
   * Values < 1 zoom out beyond the calculated world bounds.
   * Default: 1
   */
  zoom?: number;
}

/**
 * EbikeGPSMap
 * A premium, state-of-the-art 3D GPS navigation screen for the Ebike.
 * Loads the road network, runs A* pathfinding, and renders a glowing, animated
 * orange path over a sleek high-tech map background.
 */
export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
  startPos = { x: 0, y: 0, z: 0 },
  destPos,
  mapImageUrl,
  worldBounds,
  width = 1,
  height = 1,
  position = [0, 0, 0],
  canvasSize = 1024,
  zoom = 1,
}) => {
  const [waypoints, setWaypoints] = useState<Waypoint[]>([]);
  const [mapImage, setMapImage] = useState<
    HTMLImageElement | HTMLCanvasElement | null
  >(null);

  // Offscreen high-res canvas for crystal clear rendering
  // Use useMemo to create canvas once - this is a stable reference that won't change
  const offscreenCanvas = useMemo(() => {
    const canvas = document.createElement("canvas");
    canvas.width = canvasSize;
    canvas.height = canvasSize;
    return canvas;
    // eslint-disable-next-line react-hooks/exhaustive-deps -- Canvas should only be created once
  }, []);

  // Resize the canvas whenever canvasSize changes
  // Note: Modifying canvas dimensions is intentional and necessary for rendering
  useEffect(() => {
    // Use Object.assign to resize canvas - this is a necessary mutation for canvas rendering
    Object.assign(offscreenCanvas, { width: canvasSize, height: canvasSize });
    if (textureRef.current) {
      textureRef.current.needsUpdate = true;
    }
  }, [canvasSize, offscreenCanvas]);

  const textureRef = useRef<THREE.CanvasTexture | null>(null);
  const animTimeRef = useRef<number>(0);

  // Load waypoints (localStorage with /roadNetwork.json fallback)
  useEffect(() => {
    let cancelled = false;
    const saved = localStorage.getItem("la-fabrik-waypoints");
    if (saved) {
      try {
        const parsed = JSON.parse(saved);
        if (Array.isArray(parsed) && parsed.length > 0) {
          // Use queueMicrotask to avoid synchronous setState in effect
          queueMicrotask(() => {
            if (!cancelled) setWaypoints(parsed);
          });
          return;
        }
      } catch (e) {
        console.error(
          "[GPS Component] Error loading local storage waypoints",
          e,
        );
      }
    }

    // Fallback to static roadNetwork.json
    fetch("/roadNetwork.json")
      .then((res) => {
        if (res.ok) return res.json();
        throw new Error("Not found");
      })
      .then((data) => {
        if (!cancelled && Array.isArray(data)) {
          setWaypoints(data);
        }
      })
      .catch((err) => {
        console.log("[GPS Component] No default road network found.", err);
      });

    return () => {
      cancelled = true;
    };
  }, []);

  // Pre-load background map image (standard HTML5 Image loader)
  // Since the user's PNG is already transparent, we don't need fetch or pixel manipulation!
  useEffect(() => {
    if (!mapImageUrl) {
      // Use queueMicrotask to avoid synchronous setState in effect
      queueMicrotask(() => setMapImage(null));
      return;
    }

    const img = new Image();
    img.onload = () => {
      setMapImage(img);
    };
    img.onerror = () => {
      console.warn(
        `[GPS Component] Failed to load map background image from ${mapImageUrl}. Falling back to dynamic vector map.`,
      );
      setMapImage(null);
    };
    img.src = mapImageUrl;
  }, [mapImageUrl]);

  // Determine grid boundaries (before zoom)
  const baseBounds = useMemo(() => {
    if (worldBounds) return worldBounds;

    if (waypoints.length === 0) {
      return { minX: -200, maxX: 200, minZ: -200, maxZ: 200 };
    }

    const xs = waypoints.map((w) => w.x);
    const zs = waypoints.map((w) => w.z);
    const minX = Math.min(...xs);
    const maxX = Math.max(...xs);
    const minZ = Math.min(...zs);
    const maxZ = Math.max(...zs);

    // Padding (15% to ensure full view breathing room)
    const padX = (maxX - minX) * 0.15 || 40;
    const padZ = (maxZ - minZ) * 0.15 || 40;

    return {
      minX: minX - padX,
      maxX: maxX + padX,
      minZ: minZ - padZ,
      maxZ: maxZ + padZ,
    };
  }, [waypoints, worldBounds]);

  // Apply zoom: shrink the view window around the player position
  const bounds = useMemo(() => {
    const clampedZoom = Math.max(0.1, zoom);
    if (clampedZoom === 1) return baseBounds;

    const centerX = startPos.x;
    const centerZ = startPos.z;
    const halfW = (baseBounds.maxX - baseBounds.minX) / 2 / clampedZoom;
    const halfH = (baseBounds.maxZ - baseBounds.minZ) / 2 / clampedZoom;

    return {
      minX: centerX - halfW,
      maxX: centerX + halfW,
      minZ: centerZ - halfH,
      maxZ: centerZ + halfH,
    };
  }, [baseBounds, zoom, startPos]);

  // Snapped positions
  const startPosSnapped = useMemo(() => {
    if (waypoints.length === 0) return null;
    return findClosestWaypoint(waypoints, startPos);
  }, [waypoints, startPos]);

  const destPosSnapped = useMemo(() => {
    if (!destPos || waypoints.length === 0) return null;
    return findClosestWaypoint(waypoints, destPos);
  }, [waypoints, destPos]);

  // Calculated active A* route
  const activePath = useMemo(() => {
    if (!startPosSnapped || !destPosSnapped || waypoints.length === 0)
      return [];
    return findWaypointPath(waypoints, startPosSnapped, destPosSnapped);
  }, [waypoints, startPosSnapped, destPosSnapped]);

  // Translation helper: 3D world to Canvas pixels
  const worldToCanvas = useCallback(
    (wx: number, wz: number, size: number) => {
      const { minX, maxX, minZ, maxZ } = bounds;
      const px = ((wx - minX) / (maxX - minX)) * size;
      const py = ((wz - minZ) / (maxZ - minZ)) * size;
      return { x: px, y: py };
    },
    [bounds],
  );

  // Draw loop - returns true if texture needs update
  const draw = useCallback(() => {
    const canvas = offscreenCanvas;
    if (!canvas) return;
    const ctx = canvas.getContext("2d", {
      willReadFrequently: true,
      alpha: true,
    });
    if (!ctx) return;

    const size = canvas.width;

    ctx.clearRect(0, 0, size, size);

    // 1. Draw Map Background (Image or premium blueprint vectors)
    if (mapImage) {
      const src = computeImageSource(mapImage, baseBounds, bounds);
      const sx = Math.max(0, Math.min(mapImage.width, src.sx));
      const sy = Math.max(0, Math.min(mapImage.height, src.sy));
      const sW = Math.max(1, Math.min(mapImage.width - sx, src.sW));
      const sH = Math.max(1, Math.min(mapImage.height - sy, src.sH));

      ctx.drawImage(mapImage, sx, sy, sW, sH, 0, 0, size, size);
      ctx.globalAlpha = 1.0;
    } else {
      // Dynamic Sci-fi background grid (Background is transparent!)

      // Sci-fi subgrid
      ctx.strokeStyle = "rgba(30, 41, 59, 0.4)";
      ctx.lineWidth = 1;
      const step = size / 32;
      for (let x = 0; x < size; x += step) {
        ctx.beginPath();
        ctx.moveTo(x, 0);
        ctx.lineTo(x, size);
        ctx.stroke();
      }
      for (let y = 0; y < size; y += step) {
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(size, y);
        ctx.stroke();
      }

      // Aesthetic concentric radar topo-rings
      ctx.strokeStyle = "rgba(71, 85, 105, 0.06)";
      ctx.lineWidth = 2;
      for (let r = size / 6; r < size; r += size / 6) {
        ctx.beginPath();
        ctx.arc(size / 2, size / 2, r, 0, 2 * Math.PI);
        ctx.stroke();
      }

      // Faint diagonal technical accents
      ctx.strokeStyle = "rgba(56, 189, 248, 0.03)";
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.lineTo(size, size);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(size, 0);
      ctx.lineTo(0, size);
      ctx.stroke();
    }

    // 2. Draw Active Orange Glowing Path (Neon Highway effect)
    if (activePath.length > 1) {
      // Pass 1: Wide transparent orange bloom
      ctx.beginPath();
      let pt = worldToCanvas(activePath[0]!.x, activePath[0]!.z, size);
      ctx.moveTo(pt.x, pt.y);
      for (let i = 1; i < activePath.length; i++) {
        pt = worldToCanvas(activePath[i]!.x, activePath[i]!.z, size);
        ctx.lineTo(pt.x, pt.y);
      }
      ctx.strokeStyle = "rgba(249, 115, 22, 0.2)"; // Faint bright orange
      ctx.lineWidth = 20;
      ctx.lineCap = "round";
      ctx.lineJoin = "round";
      ctx.shadowBlur = 30;
      ctx.shadowColor = "#f97316"; // Neon Orange
      ctx.stroke();

      // Pass 2: Saturated glow core
      ctx.beginPath();
      pt = worldToCanvas(activePath[0]!.x, activePath[0]!.z, size);
      ctx.moveTo(pt.x, pt.y);
      for (let i = 1; i < activePath.length; i++) {
        pt = worldToCanvas(activePath[i]!.x, activePath[i]!.z, size);
        ctx.lineTo(pt.x, pt.y);
      }
      ctx.strokeStyle = "#f97316"; // Vibrant orange
      ctx.lineWidth = 8;
      ctx.shadowBlur = 12;
      ctx.shadowColor = "#ea580c";
      ctx.stroke();

      // Pass 3: High-intensity white core
      ctx.beginPath();
      pt = worldToCanvas(activePath[0]!.x, activePath[0]!.z, size);
      ctx.moveTo(pt.x, pt.y);
      for (let i = 1; i < activePath.length; i++) {
        pt = worldToCanvas(activePath[i]!.x, activePath[i]!.z, size);
        ctx.lineTo(pt.x, pt.y);
      }
      ctx.strokeStyle = "#fff7ed"; // Cream white
      ctx.lineWidth = 3;
      ctx.shadowBlur = 0; // Turn off shadows for the core
      ctx.stroke();

      // 3. Energy Particle Pulse animation tracing the road
      const segments: {
        start: { x: number; y: number };
        end: { x: number; y: number };
        len: number;
      }[] = [];
      let totalLen = 0;
      for (let i = 0; i < activePath.length - 1; i++) {
        const p1 = worldToCanvas(activePath[i]!.x, activePath[i]!.z, size);
        const p2 = worldToCanvas(
          activePath[i + 1]!.x,
          activePath[i + 1]!.z,
          size,
        );
        const len = Math.sqrt(
          Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2),
        );
        segments.push({ start: p1, end: p2, len });
        totalLen += len;
      }

      if (totalLen > 0) {
        const targetLen = totalLen * animTimeRef.current;
        let currentLen = 0;
        let dotPt = segments[0]!.start;

        for (const seg of segments) {
          if (currentLen + seg.len >= targetLen) {
            const ratio = (targetLen - currentLen) / seg.len;
            dotPt = {
              x: seg.start.x + (seg.end.x - seg.start.x) * ratio,
              y: seg.start.y + (seg.end.y - seg.start.y) * ratio,
            };
            break;
          }
          currentLen += seg.len;
        }

        // Draw multiple glowing pulses along the path
        ctx.beginPath();
        ctx.arc(dotPt.x, dotPt.y, 8, 0, 2 * Math.PI);
        ctx.fillStyle = "#ffffff";
        ctx.shadowBlur = 15;
        ctx.shadowColor = "#f97316";
        ctx.fill();
        ctx.shadowBlur = 0;
      }
    }

    // 4. Draw Snap Markers (Start and End)
    if (destPosSnapped) {
      const pt = worldToCanvas(destPosSnapped.x, destPosSnapped.z, size);
      const pulseSize = 12 + Math.sin(Date.now() * 0.007) * 4;

      // Pulse ring
      ctx.beginPath();
      ctx.arc(pt.x, pt.y, pulseSize, 0, 2 * Math.PI);
      ctx.strokeStyle = "rgba(249, 115, 22, 0.4)";
      ctx.lineWidth = 3;
      ctx.stroke();

      // Solid target core
      ctx.beginPath();
      ctx.arc(pt.x, pt.y, 6, 0, 2 * Math.PI);
      ctx.fillStyle = "#ea580c"; // Deep target orange
      ctx.strokeStyle = "#ffffff";
      ctx.lineWidth = 2;
      ctx.fill();
      ctx.stroke();
    }

    if (startPosSnapped) {
      const pt = worldToCanvas(startPosSnapped.x, startPosSnapped.z, size);

      // Start Marker (Player Arrow/Dot)
      ctx.beginPath();
      ctx.arc(pt.x, pt.y, 8, 0, 2 * Math.PI);
      ctx.fillStyle = "#0ea5e9"; // Cool cyberpunk sky blue
      ctx.strokeStyle = "#ffffff";
      ctx.lineWidth = 2.5;
      ctx.fill();
      ctx.stroke();

      // Tech details
      ctx.beginPath();
      ctx.arc(pt.x, pt.y, 3, 0, 2 * Math.PI);
      ctx.fillStyle = "#ffffff";
      ctx.fill();
    }
  }, [
    offscreenCanvas,
    mapImage,
    baseBounds,
    bounds,
    activePath,
    worldToCanvas,
    destPosSnapped,
    startPosSnapped,
  ]);

  // 60 FPS animation ticker
  useEffect(() => {
    let animId: number;
    const tick = () => {
      animTimeRef.current += 0.004; // Slow, premium sweep speed
      if (animTimeRef.current > 1) animTimeRef.current = 0;

      draw();

      // Update texture after draw
      if (textureRef.current) {
        textureRef.current.needsUpdate = true;
      }

      animId = requestAnimationFrame(tick);
    };
    animId = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(animId);
  }, [draw]);

  return (
    <mesh castShadow receiveShadow position={position}>
      <planeGeometry args={[width, height]} />
      <meshBasicMaterial
        toneMapped={false}
        transparent={true}
        opacity={1}
        depthWrite={false}
        side={THREE.DoubleSide}
      >
        <canvasTexture
          ref={textureRef}
          attach="map"
          image={offscreenCanvas}
          format={THREE.RGBAFormat}
          minFilter={THREE.LinearFilter}
          magFilter={THREE.LinearFilter}
        />
      </meshBasicMaterial>
    </mesh>
  );
};
