// import { Line } from "@react-three/drei";
// import { useFrame, useThree } from "@react-three/fiber";
// import React, { useRef, useState } from "react";
// import * as THREE from "three";

// /** --- Types --- */
// type PointData = {
//   pointId: string;
//   position: [number, number, number];
//   isCurved: boolean;
//   handleA: [number, number, number] | null;
//   handleB: [number, number, number] | null;
// };

// interface PathDataInterface {
//   pathId: string;
//   pathPoints: [PointData, PointData]; // always two points
// }

// type PathData = PathDataInterface[];

// interface PointHandleProps {
//   point: PointData;
//   pointIndex: number;
//   points: PointData[];
//   setPoints: React.Dispatch<React.SetStateAction<PointData[]>>;
//   setPaths: React.Dispatch<React.SetStateAction<PathData>>;
//   paths: PathData;
//   selected: number[];
//   setSelected: React.Dispatch<React.SetStateAction<number[]>>;
//   setShortestPath: React.Dispatch<React.SetStateAction<PathData>>;
// }

// /** --- Math helpers --- */
// function dist(a: PointData, b: PointData): number {
//   return Math.sqrt(
//     (a.position[0] - b.position[0]) ** 2 +
//       (a.position[1] - b.position[1]) ** 2 +
//       (a.position[2] - b.position[2]) ** 2
//   );
// }

// /** --- A* Algorithm --- */
// type AStarResult = {
//   pointIds: string[];
//   distance: number;
// };

// function aStarShortestPath(
//   startId: string,
//   goalId: string,
//   points: PointData[],
//   paths: PathData
// ): AStarResult | null {
//   const pointById = new Map(points.map((p) => [p.pointId, p]));
//   const start = pointById.get(startId);
//   const goal = pointById.get(goalId);
//   if (!start || !goal) return null;

//   const openSet = new Set<string>([startId]);
//   const cameFrom: Record<string, string | null> = {};
//   const gScore: Record<string, number> = {};
//   const fScore: Record<string, number> = {};

//   for (const p of points) {
//     cameFrom[p.pointId] = null;
//     gScore[p.pointId] = Infinity;
//     fScore[p.pointId] = Infinity;
//   }

//   gScore[startId] = 0;
//   fScore[startId] = dist(start, goal);

//   const neighborsOf = (id: string): { id: string; cost: number }[] => {
//     const me = pointById.get(id)!;
//     const out: { id: string; cost: number }[] = [];
//     for (const edge of paths) {
//       const [a, b] = edge.pathPoints;
//       if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) });
//       else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) });
//     }
//     return out;
//   };

//   while (openSet.size > 0) {
//     let current: string = [...openSet].reduce((a, b) =>
//       fScore[a] < fScore[b] ? a : b
//     );

//     if (current === goalId) {
//       const ids: string[] = [];
//       let node: string | null = current;
//       while (node) {
//         ids.unshift(node);
//         node = cameFrom[node];
//       }
//       return { pointIds: ids, distance: gScore[goalId] };
//     }

//     openSet.delete(current);

//     for (const nb of neighborsOf(current)) {
//       const tentativeG = gScore[current] + nb.cost;
//       if (tentativeG < gScore[nb.id]) {
//         cameFrom[nb.id] = current;
//         gScore[nb.id] = tentativeG;
//         fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal);
//         openSet.add(nb.id);
//       }
//     }
//   }

//   return null;
// }

// /** --- Convert node path to edges --- */
// // function nodePathToEdges(
// //   pointIds: string[],
// //   points: PointData[],
// //   paths: PathData
// // ): PathData {
// //   const byId = new Map(points.map((p) => [p.pointId, p]));
// //   const edges: PathData = [];

// //   for (let i = 0; i < pointIds.length - 1; i++) {
// //     const a = pointIds[i];
// //     const b = pointIds[i + 1];
// //
// //

// //     const edge = paths.find(
// //       (p) =>
// //         (p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
// //         (p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
// //     );

// //     if (edge) edges.push(edge);
// //     else {
// //       const pa = byId.get(a)!;
// //       const pb = byId.get(b)!;
// //       edges.push({
// //         pathId: `synthetic-${a}-${b}`,
// //         pathPoints: [pa, pb],
// //       });
// //     }
// //   }
// //

// //   return edges;
// // }
// function nodePathToEdges(
//   pointIds: string[],
//   points: PointData[],
//   paths: PathData
// ): PathData {
//   const byId = new Map(points.map((p) => [p.pointId, p]));
//   const edges: PathData = [];

//   for (let i = 0; i < pointIds.length - 1; i++) {
//     const a = pointIds[i];
//     const b = pointIds[i + 1];

//     const edge = paths.find(
//       (p) =>
//         (p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
//         (p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
//     );

//     if (edge) {
//       // Ensure correct order in edge
//       const [p1, p2] = edge.pathPoints;
//       edges.push({
//         pathId: edge.pathId,
//         pathPoints:
//           p1.pointId === a
//             ? ([p1, p2] as [PointData, PointData])
//             : ([p2, p1] as [PointData, PointData]),
//       });
//     } else {
//       const pa = byId.get(a)!;
//       const pb = byId.get(b)!;
//       edges.push({
//         pathId: `synthetic-${a}-${b}`,
//         pathPoints: [pa, pb],
//       });
//     }
//   }

//   return edges;
// }

// /** --- React Component --- */
// export default function PointHandlers({
//   point,
//   pointIndex,
//   points,
//   setPoints,
//   setPaths,
//   paths,
//   selected,
//   setSelected,
//   setShortestPath,
// }: PointHandleProps) {
//   const meshRef = useRef<THREE.Mesh>(null);
//   const handleARef = useRef<THREE.Mesh>(null);
//   const handleBRef = useRef<THREE.Mesh>(null);
//   const lineRef = useRef<any>(null!);
//   const { camera, gl, controls } = useThree();

//   const [dragging, setDragging] = useState<
//     null | "main" | "handleA" | "handleB"
//   >(null);
//   const dragOffset = useRef(new THREE.Vector3());
//   const [shortestEdges, setShortestEdges] = useState<PathData>([]);

//   /** Click handling */
//   const onPointClick = (e: any) => {
//     e.stopPropagation();
//     if (e.shiftKey) {
//       setSelected((prev) => {
//         if (prev.length === 0) return [pointIndex];
//         else if (prev.length === 1) {
//           const p1 = points[prev[0]];
//           const p2 = points[pointIndex];
//           const result = aStarShortestPath(
//             p1.pointId,
//             p2.pointId,
//             points,
//             paths
//           );
//           if (result) {
//             const edges = nodePathToEdges(result.pointIds, points, paths);
//             setShortestEdges(edges);
//             setShortestPath(edges);
//           } else {
//             setShortestEdges([]);
//           }
//           return [prev[0], pointIndex];
//         } else {
//           setShortestEdges([]);
//           return [pointIndex];
//         }
//       });
//     } else if (e.ctrlKey) {
//       setPoints((prev) => {
//         const updated = [...prev];
//         const p = { ...updated[pointIndex] };

//         if (!p.handleA && !p.handleB) {
//           p.handleA = [p.position[0] + 1, p.position[1], p.position[2]];
//           p.handleB = [p.position[0] - 1, p.position[1], p.position[2]];
//           p.isCurved = true;
//         } else {
//           p.handleA = null;
//           p.handleB = null;
//           p.isCurved = false;
//         }

//         updated[pointIndex] = p;
//         return updated;
//       });
//     }
//   };

//   /** Dragging logic */
//   const startDrag = (target: "main" | "handleA" | "handleB", e: any) => {
//     e.stopPropagation();
//     setDragging(target);
//     const targetRef =
//       target === "main"
//         ? meshRef.current
//         : target === "handleA"
//         ? handleARef.current
//         : handleBRef.current;
//     if (targetRef) dragOffset.current.copy(targetRef.position).sub(e.point);
//     if (controls) (controls as any).enabled = false;
//     gl.domElement.style.cursor = "grabbing";
//   };

//   const stopDrag = () => {
//     setDragging(null);
//     gl.domElement.style.cursor = "auto";
//     if (controls) (controls as any).enabled = true;
//   };

//   useFrame(({ raycaster, mouse }) => {
//     if (!dragging) return;

//     const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
//     raycaster.setFromCamera(mouse, camera);

//     const intersection = new THREE.Vector3();
//     if (!raycaster.ray.intersectPlane(plane, intersection)) return;

//     const newPos = intersection.add(dragOffset.current);

//     setPoints((prevPoints) => {
//       const updatedPoints = [...prevPoints];
//       const point = { ...updatedPoints[pointIndex] };

//       if (dragging === "main") {
//         // Calculate delta movement
//         const delta = newPos
//           .clone()
//           .sub(new THREE.Vector3().fromArray(point.position));

//         // Move main point
//         point.position = newPos.toArray() as [number, number, number];

//         // Move handles with main point
//         if (point.handleA)
//           point.handleA = new THREE.Vector3()
//             .fromArray(point.handleA)
//             .add(delta)
//             .toArray() as [number, number, number];
//         if (point.handleB)
//           point.handleB = new THREE.Vector3()
//             .fromArray(point.handleB)
//             .add(delta)
//             .toArray() as [number, number, number];
//       } else {
//         // Dragging a handle
//         point[dragging] = newPos.toArray() as [number, number, number];

//         if (point.isCurved) {
//           // Mirror the opposite handle
//           const mainPos = new THREE.Vector3().fromArray(point.position);
//           const thisHandle = new THREE.Vector3().fromArray(point[dragging]!);
//           const mirrorHandle = mainPos
//             .clone()
//             .sub(thisHandle.clone().sub(mainPos));

//           if (dragging === "handleA")
//             point.handleB = mirrorHandle.toArray() as [number, number, number];
//           if (dragging === "handleB")
//             point.handleA = mirrorHandle.toArray() as [number, number, number];
//         }
//       }

//       updatedPoints[pointIndex] = point;

//       // Update all paths that include this point
//       setPaths((prevPaths: any) =>
//         prevPaths.map((path: any) => {
//           const updatedPathPoints = path.pathPoints.map((p: any) =>
//             p.pointId === point.pointId ? point : p
//           );
//           return { ...path, pathPoints: updatedPathPoints };
//         })
//       );

//       return updatedPoints;
//     });
//   });

//   /** Update line between handles */
//   useFrame(() => {
//     if (lineRef.current && point.handleA && point.handleB) {
//       const positions = lineRef.current.geometry.attributes.position
//         .array as Float32Array;
//       positions[0] = point.handleA[0];
//       positions[1] = point.handleA[1];
//       positions[2] = point.handleA[2];
//       positions[3] = point.handleB[0];
//       positions[4] = point.handleB[1];
//       positions[5] = point.handleB[2];
//       lineRef.current.geometry.attributes.position.needsUpdate = true;
//     }
//   });

//   return (
//     <>
//       {/* Main point */}
//       <mesh
//         ref={meshRef}
//         position={point.position}
//         onClick={onPointClick}
//         onPointerDown={(e) => startDrag("main", e)}
//         onPointerUp={stopDrag}
//       >
//         <sphereGeometry args={[0.3, 16, 16]} />
//         <meshStandardMaterial
//           color={selected.includes(pointIndex) ? "red" : "pink"}
//         />
//       </mesh>

//       {/* Curve handles */}
//       {point.isCurved && point.handleA && point.handleB && (
//         <>
//           <Line
//             ref={lineRef}
//             points={[point.handleA, point.handleB]}
//             color="gray"
//             lineWidth={1}
//           />
//           <mesh
//             ref={handleARef}
//             position={point.handleA}
//             onPointerDown={(e) => startDrag("handleA", e)}
//             onPointerUp={stopDrag}
//           >
//             <sphereGeometry args={[0.15, 8, 8]} />
//             <meshStandardMaterial color="orange" />
//           </mesh>
//           <mesh
//             ref={handleBRef}
//             position={point.handleB}
//             onPointerDown={(e) => startDrag("handleB", e)}
//             onPointerUp={stopDrag}
//           >
//             <sphereGeometry args={[0.15, 8, 8]} />
//             <meshStandardMaterial color="green" />
//           </mesh>
//         </>
//       )}

//       {/* Draw connected paths */}

//       {/* Highlight shortest path */}
//       {shortestEdges.map((edge) => (
//         <Line
//           key={`sp-${edge.pathId}`}
//           points={edge.pathPoints.map((p) => p.position)}
//           color="yellow"
//           lineWidth={3}
//         />
//       ))}
//     </>
//   );
// }

/** --- Types --- */
import { Line } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber";
import React, { useRef, useState } from "react";
import * as THREE from "three";

/** --- Types --- */
type PointData = {
  pointId: string;
  position: [number, number, number];
  isCurved: boolean;
  handleA: [number, number, number] | null;
  handleB: [number, number, number] | null;
};

interface PathDataInterface {
  pathId: string;
  pathPoints: [PointData, PointData]; // always two points
}

type PathData = PathDataInterface[];

interface PointHandleProps {
  point: PointData;
  pointIndex: number;
  points: PointData[];
  setPoints: React.Dispatch<React.SetStateAction<PointData[]>>;
  setPaths: React.Dispatch<React.SetStateAction<PathData>>;
  paths: PathData;
  selected: number[];
  setSelected: React.Dispatch<React.SetStateAction<number[]>>;
  setShortestPath: React.Dispatch<React.SetStateAction<PathData>>;
}

/** --- Math helpers --- */
function dist(a: PointData, b: PointData): number {
  return Math.sqrt(
    (a.position[0] - b.position[0]) ** 2 +
      (a.position[1] - b.position[1]) ** 2 +
      (a.position[2] - b.position[2]) ** 2
  );
}

/** --- A* Algorithm --- */
type AStarResult = {
  pointIds: string[];
  distance: number;
};

function aStarShortestPath(
  startId: string,
  goalId: string,
  points: PointData[],
  paths: PathData
): AStarResult | null {
  const pointById = new Map(points.map((p) => [p.pointId, p]));
  const start = pointById.get(startId);
  const goal = pointById.get(goalId);
  if (!start || !goal) return null;

  const openSet = new Set<string>([startId]);
  const cameFrom: Record<string, string | null> = {};
  const gScore: Record<string, number> = {};
  const fScore: Record<string, number> = {};

  for (const p of points) {
    cameFrom[p.pointId] = null;
    gScore[p.pointId] = Infinity;
    fScore[p.pointId] = Infinity;
  }

  gScore[startId] = 0;
  fScore[startId] = dist(start, goal);

  const neighborsOf = (id: string): { id: string; cost: number }[] => {
    const me = pointById.get(id)!;
    const out: { id: string; cost: number }[] = [];
    for (const edge of paths) {
      const [a, b] = edge.pathPoints;
      if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) });
      else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) });
    }
    return out;
  };

  while (openSet.size > 0) {
    let current: string = [...openSet].reduce((a, b) =>
      fScore[a] < fScore[b] ? a : b
    );

    if (current === goalId) {
      const ids: string[] = [];
      let node: string | null = current;
      while (node) {
        ids.unshift(node);
        node = cameFrom[node];
      }
      return { pointIds: ids, distance: gScore[goalId] };
    }

    openSet.delete(current);

    for (const nb of neighborsOf(current)) {
      const tentativeG = gScore[current] + nb.cost;
      if (tentativeG < gScore[nb.id]) {
        cameFrom[nb.id] = current;
        gScore[nb.id] = tentativeG;
        fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal);
        openSet.add(nb.id);
      }
    }
  }

  return null;
}

/** --- Convert node path to edges --- */
function nodePathToEdges(
  pointIds: string[],
  points: PointData[],
  paths: PathData
): PathData {
  const byId = new Map(points.map((p) => [p.pointId, p]));
  const edges: PathData = [];

  for (let i = 0; i < pointIds.length - 1; i++) {
    const a = pointIds[i];
    const b = pointIds[i + 1];

    const edge = paths.find(
      (p) =>
        (p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
        (p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
    );

    if (edge) {
      const [p1, p2] = edge.pathPoints;
      edges.push({
        pathId: edge.pathId,
        pathPoints:
          p1.pointId === a
            ? ([p1, p2] as [PointData, PointData])
            : ([p2, p1] as [PointData, PointData]),
      });
    } else {
      const pa = byId.get(a)!;
      const pb = byId.get(b)!;
      edges.push({
        pathId: `synthetic-${a}-${b}`,
        pathPoints: [pa, pb],
      });
    }
  }

  return edges;
}

/** --- React Component --- */
export default function PointHandlers({
  point,
  pointIndex,
  points,
  setPoints,
  setPaths,
  paths,
  selected,
  setSelected,
  setShortestPath,
}: PointHandleProps) {
  const meshRef = useRef<THREE.Mesh>(null);
  const handleARef = useRef<THREE.Mesh>(null);
  const handleBRef = useRef<THREE.Mesh>(null);
  const lineRef = useRef<any>(null);
  const { camera, gl, controls, raycaster } = useThree();

  const [dragging, setDragging] = useState<
    null | "main" | "handleA" | "handleB"
  >(null);
  const dragOffset = useRef(new THREE.Vector3());
  const [shortestEdges, setShortestEdges] = useState<PathData>([]);

  /** Click handling */
  const onPointClick = (e: any) => {
    e.stopPropagation();

    if (e.shiftKey) {
      setSelected((prev) => {
        if (prev.length === 0) return [pointIndex];
        if (prev.length === 1) {
          // defer shortest path calculation
          setTimeout(() => {
            const p1 = points[prev[0]];
            const p2 = points[pointIndex];
            const result = aStarShortestPath(
              p1.pointId,
              p2.pointId,
              points,
              paths
            );
            if (result) {
              const edges = nodePathToEdges(result.pointIds, points, paths);
              setShortestEdges(edges);
              setShortestPath(edges);
            } else {
              setShortestEdges([]);
            }
          }, 0);
          return [prev[0], pointIndex];
        }
        return [pointIndex];
      });
    } else if (e.ctrlKey) {
      setPoints((prev) => {
        const updated = [...prev];
        const p = { ...updated[pointIndex] };

        if (!p.handleA && !p.handleB) {
          p.handleA = [p.position[0] + 1, 0, p.position[2]];
          p.handleB = [p.position[0] - 1, 0, p.position[2]];
          p.isCurved = true;
        } else {
          p.handleA = null;
          p.handleB = null;
          p.isCurved = false;
        }

        updated[pointIndex] = p;
        return updated;
      });
    }
  };

  /** Dragging logic */
  const startDrag = (target: "main" | "handleA" | "handleB", e: any) => {
    e.stopPropagation();
    setDragging(target);
    const targetRef =
      target === "main"
        ? meshRef.current
        : target === "handleA"
        ? handleARef.current
        : handleBRef.current;
    if (targetRef && targetRef.position) {
      dragOffset.current
        .copy(new THREE.Vector3(targetRef.position.x, 0, targetRef.position.z))
        .sub(e.point);
    }

    if (controls) (controls as any).enabled = false;
    gl.domElement.style.cursor = "grabbing";
  };

  const stopDrag = () => {
    setDragging(null);
    gl.domElement.style.cursor = "auto";
    if (controls) (controls as any).enabled = true;
  };

  /** Update position in useFrame */
  useFrame(({ mouse }) => {
    if (!dragging) return;
    const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);

    const intersection = new THREE.Vector3();
    if (!raycaster.ray.intersectPlane(plane, intersection)) return;
    const newPos = intersection.add(dragOffset.current);

    setPoints((prevPoints) => {
      const updatedPoints = [...prevPoints];
      const p = { ...updatedPoints[pointIndex] };

      if (dragging === "main") {
        const delta = newPos
          .clone()
          .sub(new THREE.Vector3().fromArray(p.position));
        p.position = [newPos.x, 0, newPos.z];

        if (p.handleA) {
          p.handleA = new THREE.Vector3()
            .fromArray(p.handleA)
            .add(new THREE.Vector3(delta.x, 0, delta.z))
            .toArray() as [number, number, number];
        }

        if (p.handleB) {
          p.handleB = new THREE.Vector3()
            .fromArray(p.handleB)
            .add(new THREE.Vector3(delta.x, 0, delta.z))
            .toArray() as [number, number, number];
        }
      } else {
        p[dragging] = [newPos.x, 0, newPos.z];
        if (p.isCurved) {
          const mainPos = new THREE.Vector3().fromArray(p.position);
          const thisHandle = new THREE.Vector3().fromArray(p[dragging]!);
          const mirrorHandle = mainPos
            .clone()
            .sub(thisHandle.clone().sub(mainPos));
          console.log("mirrorHandle: ", mirrorHandle);
          if (dragging === "handleA")
            p.handleB = mirrorHandle.toArray() as [number, number, number];
          if (dragging === "handleB")
            p.handleA = mirrorHandle.toArray() as [number, number, number];
        }
      }

      updatedPoints[pointIndex] = p;

      setPaths((prevPaths: any) =>
        prevPaths.map((path: any) => ({
          ...path,
          pathPoints: path.pathPoints.map((pp: any) =>
            pp.pointId === p.pointId ? p : pp
          ),
        }))
      );

      return updatedPoints;
    });
  });

  /** Update line between handles */
  useFrame(() => {
    if (lineRef.current && point.handleA && point.handleB) {
      const positions = lineRef.current.geometry.attributes.position
        .array as Float32Array;
      positions[0] = point.handleA[0];
      positions[1] = point.handleA[1];
      positions[2] = point.handleA[2];
      positions[3] = point.handleB[0];
      positions[4] = point.handleB[1];
      positions[5] = point.handleB[2];
      lineRef.current.geometry.attributes.position.needsUpdate = true;
    }
  });

  return (
    <>
      {/* Main point */}
      <mesh
        ref={meshRef}
        position={point.position}
        onClick={onPointClick}
        onPointerDown={(e) => startDrag("main", e)}
        onPointerUp={stopDrag}
      >
        <sphereGeometry args={[0.1, 16, 16]} />
        <meshStandardMaterial
          color={selected.includes(pointIndex) ? "red" : "pink"}
        />
      </mesh>

      {/* Curve handles */}
      {point.isCurved && point.handleA && point.handleB && (
        <>
          <Line
            ref={lineRef}
            points={[point.handleA, point.handleB]}
            color="gray"
            lineWidth={1}
          />
          <mesh
            ref={handleARef}
            position={point.handleA}
            onPointerDown={(e) => startDrag("handleA", e)}
            onPointerUp={stopDrag}
          >
            <sphereGeometry args={[0.15, 8, 8]} />
            <meshStandardMaterial color="orange" />
          </mesh>
          <mesh
            ref={handleBRef}
            position={point.handleB}
            onPointerDown={(e) => startDrag("handleB", e)}
            onPointerUp={stopDrag}
          >
            <sphereGeometry args={[0.15, 8, 8]} />
            <meshStandardMaterial color="green" />
          </mesh>
        </>
      )}

      {/* Highlight shortest path */}
      {shortestEdges.map((edge) => (
        <Line
          key={`sp-${edge.pathId}`}
          points={edge.pathPoints.map((p) => p.position)}
          color="yellow"
          lineWidth={3}
        />
      ))}
    </>
  );
}
