import { Node, ServoNode, VariableNode, CurveNode, MathNode, MapNode, NoiseNode } from './Node.js';
import { TextInputControl } from './Control.js';
import { encodeNodeGraph, loadFromBinary } from './NodeSerializer.js';

// === Grid Settings ===
const GRID_SPACING = 20;         // Distance between dots in pixels
const CANVAS_COLOR = "#ffffffff";
const GRID_COLOR = "#ff0000ff";    // Dot color
const GRID_RADIUS = 1;         // Dot radius in pixels


export class NodeEditor {
  constructor(canvas) {
    this.canvas = canvas;
    canvas.width = canvas.offsetWidth;
    canvas.height = canvas.offsetHeight;
    this.ctx = canvas.getContext('2d');
    this.nodes = [
      //new Node(100, 100),
      //new Node(300, 200),
      // new CurveNode(600, 400, 22),
      // new ServoNode(900, 400, 22),
      // new VariableNode(300, 800),
      // new MathNode(600, 800),
      // new MapNode(900, 800),
      // new NoiseNode(900, 1200),
    ];
    this.connections = []
    this.draggingConnection = null; // { fromNode: Node, x: number, y: number }
    this.hoveredConnection = null;

    //this.connections.push({ from: this.nodes[0], to: this.nodes[1] });
    //this.connections.push({ from: this.nodes[2], to: this.nodes[3] });


    this.draggingNode = null;
    this.offsetX = 0; // pan X
    this.offsetY = 0; // pan Y
    this.zoom = 1;    // zoom scale
    this.isPanning = false;
    this.panStart = { x: 0, y: 0 };

    this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
    this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
    this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
    this.canvas.addEventListener("dblclick", this.onDoubleClick.bind(this));
    this.canvas.addEventListener('wheel', this.onWheel.bind(this), { passive: false });
    this.canvas.addEventListener("contextmenu", this.onContextMenu.bind(this));



    this.modifiers = {
      ctrl: false,
      shift: false,
      alt: false
    };

    window.addEventListener("keydown", (e) => {
      if (e.key === "Control") this.modifiers.ctrl = true;
      if (e.key === "Shift") this.modifiers.shift = true;
      if (e.key === "Alt") this.modifiers.alt = true;

      for (const node of this.nodes) {
        for (const control of node.controls) {
          if (control.isFocused) {
            let clipboardHandled = false;

            if (control.onClipboard) {
              control.onClipboard(e);
              clipboardHandled = e.defaultPrevented;
            }

            if (!clipboardHandled && control.onKeyDown) {
              control.onKeyDown(e.key);
            }

            e.preventDefault();
            return;
          }
        }
      }
    });




    window.addEventListener("keyup", (e) => {
      if (e.key === "Control") this.modifiers.ctrl = false;
      if (e.key === "Shift") this.modifiers.shift = false;
      if (e.key === "Alt") this.modifiers.alt = false;
    });


    // let encoded = encodeNodeGraph(this.nodes, this.connections);
    // console.log(encoded);
    //loadFromBinary(this, encoded);
    //this.createGridCanvas();
  }

  start() {
    requestAnimationFrame(this.draw.bind(this));
  }

  clear() {
    this.nodes = [];
    this.connections = [];

  }

  addNode(node) {
    this.nodes.push(node);
    this._redraw?.(); // or trigger draw manually
  }


  generateDefaultNodes(curveSets, motorIDs) {
    //console.log("Generating Default Nodes");
    //console.log(curveSets, motorIDs);

    for (var i = 0; i < motorIDs.length; i++) {
      let inputNode = new CurveNode(200, 20 + i * 140, motorIDs[i]);
      let outputNode = new ServoNode(500, 40 + i * 140, motorIDs[i]);
      this.nodes.push(inputNode);
      this.nodes.push(outputNode);
      this.connections.push({ from: inputNode, to: outputNode });
      console.log(inputNode);
    }
    this.draw();
    //this.addVariableNode(50, 120, "Var");
  }

  encodeNodeGraph() {
    return encodeNodeGraph(this.nodes, this.connections);
  }

  loadFromBinary(data) {
    console.log(data);
    this.clear();
    loadFromBinary(this, data);
  }

  draw() {
    this.ctx.fillStyle = CANVAS_COLOR;
    this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
    //this.ctx.drawImage(this.gridCanvas, 0, 0);
    this.drawGrid();
    this.ctx.save();
    this.ctx.translate(this.offsetX, this.offsetY);
    this.ctx.scale(this.zoom, this.zoom);

    this.drawConnections();

    for (const node of this.nodes) {

      node.draw(this.ctx);
    }

    this.ctx.restore();

    requestAnimationFrame(this.draw.bind(this));
  }

  drawConnections() {
    this.ctx.strokeStyle = "#444";
    this.ctx.lineWidth = 2;
    for (const conn of this.connections) {
      const x1 = conn.from.output.x;
      const y1 = conn.from.output.y;
      const x2 = conn.to.input.x;
      const y2 = conn.to.input.y;
      const dx = Math.abs(x2 - x1) * 0.5;

      this.ctx.beginPath();
      this.ctx.moveTo(x1, y1);
      this.ctx.bezierCurveTo(x1 + dx, y1, x2 - dx, y2, x2, y2);

      if (conn === this.hoveredConnection) {
        this.ctx.strokeStyle = "#666"; // lighter on hover
        this.ctx.lineWidth = 6.5;
      } else {
        this.ctx.strokeStyle = "#444";
        this.ctx.lineWidth = 4;
      }

      this.ctx.stroke();
    }

    // Live dragging connection
    if (this.draggingConnection) {
      const x1 = this.draggingConnection.fromNode.output.x;
      const y1 = this.draggingConnection.fromNode.output.y;
      const x2 = this.draggingConnection.x;
      const y2 = this.draggingConnection.y;
      const dx = Math.abs(x2 - x1) * 0.5;

      this.ctx.beginPath();
      this.ctx.moveTo(x1, y1);
      this.ctx.bezierCurveTo(x1 + dx, y1, x2 - dx, y2, x2, y2);
      this.ctx.stroke();
    }
  }

  createGridCanvas() {
    const gridCanvas = document.createElement('canvas');
    gridCanvas.width = this.canvas.width;
    gridCanvas.height = this.canvas.height;
    const ctx = gridCanvas.getContext('2d');
    ctx.fillStyle = GRID_COLOR;

    for (let x = 0; x < gridCanvas.width; x += GRID_SPACING) {
      for (let y = 0; y < gridCanvas.height; y += GRID_SPACING) {
        ctx.fillRect(x, y, 1, 1);
      }
    }

    this.gridCanvas = gridCanvas;
  }


  drawGrid() {
    const { width, height } = this.canvas;
    const spacing = GRID_SPACING * this.zoom;
    const radius = GRID_RADIUS * this.zoom;

    // Skip grid rendering if spacing is too small
    if (spacing < 2) return;

    // Dynamically skip grid points when zoomed out
    const skip = spacing < 8 ? Math.ceil(8 / spacing) : 1;

    this.ctx.fillStyle = GRID_COLOR;
    this.ctx.beginPath();

    const startX = (this.offsetX % spacing + spacing) % spacing;
    const startY = (this.offsetY % spacing + spacing) % spacing;

    let i = 0;
    for (let x = startX; x < width; x += spacing, i++) {
      if (i % skip !== 0) continue;

      let j = 0;
      for (let y = startY; y < height; y += spacing, j++) {
        if (j % skip !== 0) continue;

        this.ctx.fillRect(x - radius / 2, y - radius / 2, radius, radius);

      }
    }

    this.ctx.fill();
  }





  snapToGrid(x, y) {
    const spacing = GRID_SPACING;
    const snappedX = Math.round(x / spacing) * spacing;
    const snappedY = Math.round(y / spacing) * spacing;
    return { x: snappedX, y: snappedY };
  }

  screenToWorld(mx, my) {
    const rect = this.canvas.getBoundingClientRect();
    const x = (mx - rect.left - this.offsetX) / this.zoom;
    const y = (my - rect.top - this.offsetY) / this.zoom;
    return { x, y };
  }


  onMouseDown(e) {
    if (e.button === 1) { // middle mouse
      this.isPanning = true;
      this.panStart.x = e.clientX;
      this.panStart.y = e.clientY;
      e.preventDefault();
      return;
    }

    const { x: wx, y: wy } = this.screenToWorld(e.clientX, e.clientY);

    for (const node of this.nodes) {
      if (node.hasOutput) {
        const { x, y } = node.output;
        const radius = 8;
        const dx = wx - x;
        const dy = wy - y;
        if (dx * dx + dy * dy < radius * radius) {
          this.draggingConnection = {
            fromNode: node,
            x: wx,
            y: wy
          };
          return;
        }
      }
      for (const control of node.controls) {
        control._ctx = this.ctx; // inject context if needed

        const handled = control.onEvent?.("mouseDown", {
          mx: wx,
          my: wy,
          parentX: node.x,
          parentY: node.y
        });

        if (handled) return;
      }
    }

    for (let i = this.nodes.length - 1; i >= 0; i--) {
      const node = this.nodes[i];
      if (node.contains(wx, wy)) {
        node.onSelect(true);
        node.onDrag(true);
        node.offsetX = wx - node.x;
        node.offsetY = wy - node.y;
        this.draggingNode = node;

        this.nodes.splice(i, 1);
        this.nodes.push(node);
        break;
      }
    }
  }




  onMouseMove(e) {
    this.hoveredConnection = null;
    if (this.isPanning) {
      const dx = e.clientX - this.panStart.x;
      const dy = e.clientY - this.panStart.y;
      this.offsetX += dx;
      this.offsetY += dy;
      this.panStart.x = e.clientX;
      this.panStart.y = e.clientY;
      return;
    }



    const { x: wx, y: wy } = this.screenToWorld(e.clientX, e.clientY);

    if (this.draggingConnection) {
      this.draggingConnection.x = wx;
      this.draggingConnection.y = wy;
      return;
    }

    for (const conn of this.connections) {
      const x1 = conn.from.output.x;
      const y1 = conn.from.output.y;
      const x2 = conn.to.input.x;
      const y2 = conn.to.input.y;
      const dx = Math.abs(x2 - x1) * 0.5;

      for (let t = 0; t <= 1; t += 0.01) {
        const px = this.bezierPoint(x1, x1 + dx, x2 - dx, x2, t);
        const py = this.bezierPoint(y1, y1, y2, y2, t);
        const dist = Math.hypot(wx - px, wy - py);
        if (dist < 8) {
          this.hoveredConnection = conn;
          break;
        }
      }

      if (this.hoveredConnection) break;
    }

    // First, check if any control is under the cursor
    let controlHovered = false;

    for (let i = this.nodes.length - 1; i >= 0; i--) {
      const node = this.nodes[i];
      for (const control of node.controls) {
        control._ctx = this.ctx; // inject context if needed

        if (control.contains?.(wx, wy, node.x, node.y)) {
          controlHovered = true;
        }

        control.onEvent?.("mouseMove", {
          mx: wx,
          my: wy,
          parentX: node.x,
          parentY: node.y,
          modifiers: { ...this.modifiers, zoom: this.zoom }
        });
      }

      if (!this.draggingNode && !controlHovered) {
        for (const node of this.nodes) {
          node.onMouseMove(wx, wy);
        }
      }

    }


    // If dragging, update position and skip hover logic
    if (this.draggingNode) {
      const rawX = wx - this.draggingNode.offsetX;
      const rawY = wy - this.draggingNode.offsetY;
      const { x, y } = this.snapToGrid(rawX, rawY);
      this.draggingNode.x = x;
      this.draggingNode.y = y;

      this.draggingNode.onHover(true);
      return;
    }

    // If a control is hovered, skip node hover
    if (controlHovered) {
      for (const node of this.nodes) {
        node.onHover(false);
      }
      return;
    }

    // Otherwise, handle node hover normally
    let hoveringHandled = false;
    for (let i = this.nodes.length - 1; i >= 0; i--) {
      const node = this.nodes[i];
      if (!hoveringHandled && node.contains(wx, wy)) {
        node.onHover(true);
        hoveringHandled = true;
      } else {
        node.onHover(false);
      }
    }
  }



  onMouseUp(e) {
    const { x: wx, y: wy } = this.screenToWorld(e.clientX, e.clientY);
    if (this.isPanning) {
      this.isPanning = false;
      return;
    }

    if (this.draggingConnection) {
      for (const node of this.nodes) {
        if (node.hasInput) {
          const { x, y } = node.input;
          const radius = 8;
          const dx = wx - x;
          const dy = wy - y;

          if (dx * dx + dy * dy < radius * radius) {
            // Remove any existing connection to this node's input
            for (let i = this.connections.length - 1; i >= 0; i--) {
              if (this.connections[i].to === node) {
                this.connections.splice(i, 1);
              }
            }

            // Add the new connection
            this.connections.push({
              from: this.draggingConnection.fromNode,
              to: node
            });

            break;
          }
        }
      }

      this.draggingConnection = null;
    }



    for (const node of this.nodes) {
      for (const control of node.controls) {
        control._ctx = this.ctx; // inject context if needed
        const handled = control.onEvent?.("mouseUp", {
          mx: wx,
          my: wy,
          parentX: node.x,
          parentY: node.y
        });
        if (handled) return;
      }
    }


    if (this.draggingNode) {
      this.draggingNode.onDrag(false);
      this.draggingNode = null;
    }

    for (const node of this.nodes) {
      node.onSelect(false);
    }
  }

  onDoubleClick(e) {
    const { x: wx, y: wy } = this.screenToWorld(e.clientX, e.clientY);

    // First: check if a node was double-clicked
    for (let i = this.nodes.length - 1; i >= 0; i--) {
      const node = this.nodes[i];
      if (!node.canDelete){
        continue;
      }
      const nodeBounds = {
        x: node.x,
        y: node.y,
        width: node.width ?? 120, // fallback width
        height: node.height ?? 60, // fallback height
      };

      if (
        wx >= nodeBounds.x &&
        wx <= nodeBounds.x + nodeBounds.width &&
        wy >= nodeBounds.y &&
        wy <= nodeBounds.y + nodeBounds.height
      ) {
        console.log("Double-clicked node:", node);
        this.nodes.splice(i, 1);

        // Remove any connections involving this node
        this.connections = this.connections.filter(
          conn => conn.from !== node && conn.to !== node
        );

        return;
      }
    }

    // Second: check if a connection was double-clicked
    for (let i = this.connections.length - 1; i >= 0; i--) {
      const conn = this.connections[i];
      const x1 = conn.from.output.x;
      const y1 = conn.from.output.y;
      const x2 = conn.to.input.x;
      const y2 = conn.to.input.y;
      const dx = Math.abs(x2 - x1) * 0.5;

      for (let t = 0; t <= 1; t += 0.05) {
        const px = this.bezierPoint(x1, x1 + dx, x2 - dx, x2, t);
        const py = this.bezierPoint(y1, y1, y2, y2, t);
        const dist = Math.hypot(wx - px, wy - py);
        if (dist < 8) {
          console.log("Double-clicked connection");
          this.connections.splice(i, 1);
          return;
        }
      }
    }
  }



  onWheel(e) {
    const { x: wx, y: wy } = this.screenToWorld(e.clientX, e.clientY);

    // Check if any control is under the cursor
    for (let i = this.nodes.length - 1; i >= 0; i--) {
      const node = this.nodes[i];
      for (const control of node.controls) {
        if (control.contains?.(wx, wy, node.x, node.y)) {
          const direction = e.deltaY < 0 ? 1 : -1;
          control.onScroll?.(direction);
          e.preventDefault(); // prevent zoom
          return;
        }
      }
    }

    // No control captured scroll — apply zoom
    e.preventDefault();
    const zoomFactor = 1.1;
    const mouseX = e.clientX;
    const mouseY = e.clientY;

    const canvasRect = this.canvas.getBoundingClientRect();
    const x = (mouseX - canvasRect.left - this.offsetX) / this.zoom;
    const y = (mouseY - canvasRect.top - this.offsetY) / this.zoom;
    const prevZoom = this.zoom;
    if (e.deltaY < 0) {
      this.zoom *= zoomFactor;
    } else {
      this.zoom /= zoomFactor;
    }

    // Adjust offset to keep world-space point under cursor fixed
    this.offsetX -= (wx * this.zoom - wx * prevZoom);
    this.offsetY -= (wy * this.zoom - wy * prevZoom);
  }

  onContextMenu(e) {
    e.preventDefault();

    const { x: wx, y: wy } = this.screenToWorld(e.clientX, e.clientY);

    const menu = document.createElement("div");
    menu.className = "context-menu";
    menu.style.position = "fixed";
    menu.style.left = `${e.clientX}px`;
    menu.style.top = `${e.clientY}px`; // drops down from cursor
    menu.style.background = "#222";
    menu.style.color = "#fff";
    menu.style.padding = "4px 0";
    menu.style.borderRadius = "6px";
    menu.style.boxShadow = "0 4px 12px rgba(0,0,0,0.4)";
    menu.style.zIndex = 1000;
    menu.style.fontFamily = "sans-serif";
    menu.style.minWidth = "160px";

    const options = [
      //{ label: "Add Servo", action: () => this.addNode(new ServoNode(wx, wy, 0)) },
      //{ label: "Add Curve", action: () => this.addNode(new CurveNode(wx, wy, 0)) },
      { label: "Add Noise", action: () => this.addNode(new NoiseNode(wx, wy)) },
      { label: "Add Variable", action: () => this.addNode(new VariableNode(wx, wy)) },
      { label: "Add Math", action: () => this.addNode(new MathNode(wx, wy)) },
      { label: "Add Map", action: () => this.addNode(new MapNode(wx, wy)) },
    ];

    options.forEach(opt => {
      const item = document.createElement("div");
      item.textContent = opt.label;
      item.style.padding = "6px 12px";
      item.style.cursor = "pointer";
      item.style.transition = "background 0.2s ease";
      item.addEventListener("mouseenter", () => {
        item.style.background = "#444";
      });
      item.addEventListener("mouseleave", () => {
        item.style.background = "transparent";
      });
      item.addEventListener("click", () => {
        opt.action();
        document.body.removeChild(menu);
      });
      menu.appendChild(item);
    });

    document.body.appendChild(menu);

    const removeMenu = () => {
      if (document.body.contains(menu)) {
        document.body.removeChild(menu);
      }
    };

    setTimeout(() => {
      document.addEventListener("click", removeMenu, { once: true });
    }, 0);
  }



  bezierPoint(p0, p1, p2, p3, t) {
    const c0 = (1 - t) ** 3;
    const c1 = 3 * (1 - t) ** 2 * t;
    const c2 = 3 * (1 - t) * t ** 2;
    const c3 = t ** 3;
    return c0 * p0 + c1 * p1 + c2 * p2 + c3 * p3;
  }


}