import { default as ForceGraph3D } from "3d-force-graph";
import { forceX, forceY, forceZ } from "d3-force-3d";
// biome-ignore lint/performance/noNamespaceImport: This is how it should be imported
import * as Three from "three";
import SpriteText from "three-spritetext";

/**
 * @typedef GalaxyConfig
 * @property {number} nodeId id of the current user node
 * @property {string} dataUrl url to fetch the galaxy data from
 **/

/**
 * Load the galaxy of an user
 * @param {GalaxyConfig} config
 **/
window.loadGalaxy = (config) => {
  window.getNodeFromId = (id) => {
    return Graph.graphData().nodes.find((n) => n.id === id);
  };

  window.getLinksFromNodeId = (id) => {
    return Graph.graphData().links.filter(
      (l) => l.source.id === id || l.target.id === id,
    );
  };

  window.focusNode = (node) => {
    highlightNodes.clear();
    highlightLinks.clear();

    hoverNode = node || null;
    if (node) {
      // collect neighbors and links for highlighting
      for (const link of window.getLinksFromNodeId(node.id)) {
        highlightLinks.add(link);
        highlightNodes.add(link.source);
        highlightNodes.add(link.target);
      }
    }

    // refresh node and link display
    Graph.nodeThreeObject(Graph.nodeThreeObject())
      .linkWidth(Graph.linkWidth())
      .linkDirectionalParticles(Graph.linkDirectionalParticles());

    // Aim at node from outside it
    const distance = 42;
    const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);

    const newPos =
      node.x || node.y || node.z
        ? { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }
        : { x: 0, y: 0, z: distance }; // special case if node is in (0,0,0)

    Graph.cameraPosition(
      newPos, // new position
      node, // lookAt ({ x, y, z })
      3000, // ms transition duration
    );
  };

  const highlightNodes = new Set();
  const highlightLinks = new Set();
  let hoverNode = null;

  const grpahDiv = document.getElementById("3d-graph");
  const Graph = ForceGraph3D();
  Graph(grpahDiv);
  Graph.jsonUrl(config.dataUrl)
    .width(
      grpahDiv.parentElement.clientWidth > 1200
        ? 1200
        : grpahDiv.parentElement.clientWidth,
    ) // Not perfect at all. JS-fu master from the future, please fix this :-)
    .height(1000)
    .enableNodeDrag(false) // allow easier navigation
    .onNodeClick((node) => {
      const camera = Graph.cameraPosition();
      const distance = Math.sqrt(
        (node.x - camera.x) ** 2 + (node.y - camera.y) ** 2 + (node.z - camera.z) ** 2,
      );
      if (distance < 120 || highlightNodes.has(node)) {
        window.focusNode(node);
      }
    })
    .linkWidth((link) => (highlightLinks.has(link) ? 0.4 : 0.0))
    .linkColor((link) =>
      highlightLinks.has(link) ? "rgba(255,160,0,1)" : "rgba(128,255,255,0.6)",
    )
    .linkVisibility((link) => highlightLinks.has(link))
    .nodeVisibility((node) => highlightNodes.has(node) || node.mass > 4)
    // .linkDirectionalParticles(link => highlightLinks.has(link) ? 3 : 1) // kinda buggy for now, and slows this a bit, but would be great to help visualize lanes
    .linkDirectionalParticleWidth(0.2)
    .linkDirectionalParticleSpeed(-0.006)
    .nodeThreeObject((node) => {
      const sprite = new SpriteText(node.name);
      sprite.material.depthWrite = false; // make sprite background transparent
      sprite.color = highlightNodes.has(node)
        ? node === hoverNode
          ? "rgba(200,0,0,1)"
          : "rgba(255,160,0,0.8)"
        : "rgba(0,255,255,0.2)";
      sprite.textHeight = 2;
      sprite.center = new Three.Vector2(1.2, 0.5);
      return sprite;
    })
    .onEngineStop(() => {
      window.focusNode(window.getNodeFromId(config.nodeId));
      Graph.onEngineStop(() => {
        /* nope */
      }); // don't call ourselves in a loop while moving the focus
    });

  // Set distance between stars
  Graph.d3Force("link").distance((link) => link.value);

  // Set high masses nearer the center of the galaxy
  // TODO: quick and dirty strength computation, this will need tuning.
  Graph.d3Force(
    "positionX",
    forceX().strength((node) => {
      return 1 - 1 / node.mass;
    }),
  );
  Graph.d3Force(
    "positionY",
    forceY().strength((node) => {
      return 1 - 1 / node.mass;
    }),
  );
  Graph.d3Force(
    "positionZ",
    forceZ().strength((node) => {
      return 1 - 1 / node.mass;
    }),
  );
};
