// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { useDraggable } from '@neodrag/react';
import { useReactFlow, type XYPosition } from '@xyflow/react';
import { type ReactNode, useCallback, useRef, useState } from 'react';
import useFlowStore from '../VisProgStores';
import styles from '../../VisProg.module.css';
import { NodeDefaults, type NodeTypes} from '../NodeRegistry'
import {Tooltip} from "./NodeComponents.tsx";

/**
 * Props for a draggable node within the drag-and-drop toolbar.
 *
 * @property className - Optional custom CSS classes for styling.
 * @property children - The visual content or label rendered inside the draggable node.
 * @property nodeType - The type of node represented (key from `NodeTypes`).
 * @property onDrop - Function called when the node is dropped on the flow pane.
 */
interface DraggableNodeProps {
  className?: string;
  children: ReactNode;
  nodeType: keyof typeof NodeTypes;
  onDrop: (nodeType: keyof typeof NodeTypes, position: XYPosition) => void;
}

/**
 * A draggable node element used in the drag-and-drop toolbar.
 *
 * Integrates with the NeoDrag library to handle drag events.
 * On drop, it calls the provided `onDrop` function with the node type and drop position.
 *
 * @param props - The draggable node configuration.
 * @returns A React element representing a draggable node.
 */
function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeProps) {
  const draggableRef = useRef<HTMLDivElement>(null);
  const [position, setPosition] = useState<XYPosition>({ x: 0, y: 0 });

  // The NeoDrag hook enables smooth drag functionality for this element.
  // @ts-expect-error: NeoDrag typing incompatibility — safe to ignore.
  useDraggable(draggableRef, {
    position,
    onDrag: ({ offsetX, offsetY }) => {
      setPosition({ x: offsetX, y: offsetY });
    },
    onDragEnd: ({ event }) => {
      setPosition({ x: 0, y: 0 });
      onDrop(nodeType, { x: event.clientX, y: event.clientY });
    },
  });

  return (
    <Tooltip nodeType={nodeType}>
      <div>
        <div className={className}
             ref={draggableRef}
             id={`draggable-${nodeType}`}
             data-testid={`draggable-${nodeType}`}
        >
          {children}
        </div>
      </div>
    </Tooltip>)
}

/**
 * Adds a new node to the flow graph.
 *
 * Handles:
 * - Automatic node ID generation based on existing nodes of the same type.
 * - Loading of default data from the `NodeDefaults` registry.
 * - Integration with the flow store to update global node state.
 *
 * @param nodeType - The type of node to create (from `NodeTypes`).
 * @param position - The XY position in the flow canvas where the node will appear.
 */
function addNodeToFlow(nodeType: keyof typeof NodeTypes, position: XYPosition) {
  const { addNode } = useFlowStore.getState();

  // Load any predefined data for this node type.
  const defaultData = NodeDefaults[nodeType] ?? {}
  const id = crypto.randomUUID();
  
  // Create new node
  const newNode = {
    id: id,
    type: nodeType,
    position,
    data: JSON.parse(JSON.stringify(defaultData))
  }
  addNode(newNode);
}

/**
 * The drag-and-drop toolbar component for the visual programming interface.
 *
 * Displays draggable node templates based on entries in `NodeDefaults`.
 * Each droppable node can be dragged into the flow pane to instantiate it.
 *
 * Automatically filters nodes whose `droppable` flag is set to `true`.
 *
 * @returns A React element representing the drag-and-drop toolbar.
 */
export function DndToolbar() {
  const { screenToFlowPosition } = useReactFlow();

  /**
   * Handles dropping a node onto the flow pane.
   * Translates screen coordinates into flow coordinates using React Flow utilities.
   */
  const handleNodeDrop = useCallback(
    (nodeType: keyof typeof NodeTypes, screenPosition: XYPosition) => {
      const flow = document.querySelector('.react-flow');
      const flowRect = flow?.getBoundingClientRect();
      
      // Only add the node if it is inside the flow canvas area.
      const isInFlow =
        flowRect &&
        screenPosition.x >= flowRect.left &&
        screenPosition.x <= flowRect.right &&
        screenPosition.y >= flowRect.top &&
        screenPosition.y <= flowRect.bottom;

      if (isInFlow) {
        const position = screenToFlowPosition(screenPosition);
        addNodeToFlow(nodeType, position);
      }
    },
    [screenToFlowPosition],
  );


  // Map over the default nodes to get all nodes that can be dropped from the toolbar.
  const droppableNodes = Object.entries(NodeDefaults)
  .filter(([, data]) => data.droppable)
  .map(([type, data]) => ({ 
    type: type as DraggableNodeProps['nodeType'], 
    data 
  }));

  return (
    <div className={`flex-col gap-lg padding-md ${styles.innerDndPanel}`} id={"draggable-sidebar"}>
      <div className="description">
        You can drag these nodes to the pane to create new nodes.
      </div>
      <div className={`flex-row gap-lg ${styles.dndNodeContainer}`}>
        {/* Maps over all the nodes that are droppable, and puts them in the panel */}
        {droppableNodes.map(({type, data}) => (
          <DraggableNode
            key={type}
            className={styles[`draggable-node-${type}`]} // Our current style signature for nodes
            nodeType={type}
            onDrop={handleNodeDrop}
          >
            {data.label}
          </DraggableNode>
        ))}
      </div>
    </div>
  );
}
