<script lang="ts">
  import { onMount } from "svelte";
  import * as d3 from "d3";
  import type { NDKEvent } from "@nostr-dev-kit/ndk";

  export let events: NDKEvent[] = [];

  let svg: SVGSVGElement;
  let isDarkMode = false;
  const nodeRadius = 20;
  const linkDistance = 10;
  const arrowDistance = 10;
  const warmupClickEnergy = 0.9; // Energy to restart simulation on drag
  let container: HTMLDivElement;

  let width: number = 1000;
  let height: number = 600;
  let windowHeight: number;

  $: graphHeight = windowHeight ? Math.max(windowHeight * 0.2, 400) : 400;

  $: if (container) {
    width = container.clientWidth || width;
    height = container.clientHeight || height;
  }

  interface NetworkNode extends d3.SimulationNodeDatum {
    id: string;
    event?: NDKEvent;
    index?: number;
    isContainer: boolean;
    title: string;
    content: string;
    author: string;
    type: "Index" | "Content";
    x?: number;
    y?: number;
    fx?: number | null;
    fy?: number | null;
    vx?: number;
    vy?: number;
  }

  interface NetworkLink extends d3.SimulationLinkDatum<NetworkNode> {
    source: NetworkNode;
    target: NetworkNode;
    isSequential: boolean;
  }

  function createEventMap(events: NDKEvent[]): Map<string, NDKEvent> {
    return new Map(events.map((event) => [event.id, event]));
  }
  function updateNodeVelocity(
    node: NetworkNode,
    deltaVx: number,
    deltaVy: number,
  ) {
    if (typeof node.vx === "number" && typeof node.vy === "number") {
      node.vx = node.vx - deltaVx;
      node.vy = node.vy - deltaVy;
    }
  }

  function applyGlobalLogGravity(
    node: NetworkNode,
    centerX: number,
    centerY: number,
    alpha: number,
  ) {
    const dx = (node.x ?? 0) - centerX;
    const dy = (node.y ?? 0) - centerY;
    const distance = Math.sqrt(dx * dx + dy * dy);

    if (distance === 0) return;

    const force = Math.log(distance + 1) * 0.05 * alpha;
    updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force);
  }

  function applyConnectedGravity(
    node: NetworkNode,
    links: NetworkLink[],
    alpha: number,
  ) {
    const connectedNodes = links
      .filter(
        (link) => link.source.id === node.id || link.target.id === node.id,
      )
      .map((link) => (link.source.id === node.id ? link.target : link.source));

    if (connectedNodes.length === 0) return;

    const cogX = d3.mean(connectedNodes, (n) => n.x);
    const cogY = d3.mean(connectedNodes, (n) => n.y);

    if (cogX === undefined || cogY === undefined) return;

    const dx = (node.x ?? 0) - cogX;
    const dy = (node.y ?? 0) - cogY;
    const distance = Math.sqrt(dx * dx + dy * dy);

    if (distance === 0) return;

    const force = distance * 0.3 * alpha;
    updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force);
  }
  function getNode(
    id: string,
    nodeMap: Map<string, NetworkNode>,
    event?: NDKEvent,
    index?: number,
  ): NetworkNode | null {
    if (!id) return null;

    if (!nodeMap.has(id)) {
      const node: NetworkNode = {
        id,
        event,
        index,
        isContainer: event?.kind === 30040,
        title: event?.getMatchingTags("title")?.[0]?.[1] || "Untitled",
        content: event?.content || "",
        author: event?.pubkey || "",
        type: event?.kind === 30040 ? "Index" : "Content",
        x: width / 2 + (Math.random() - 0.5) * 100,
        y: height / 2 + (Math.random() - 0.5) * 100,
      };
      nodeMap.set(id, node);
    }
    return nodeMap.get(id) || null;
  }

  function getEventColor(eventId: string): string {
    const num = parseInt(eventId.slice(0, 4), 16);
    const hue = num % 360;
    const saturation = 70;
    const lightness = 75;
    return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
  }

  function generateGraph(events: NDKEvent[]): {
    nodes: NetworkNode[];
    links: NetworkLink[];
  } {
    const nodes: NetworkNode[] = [];
    const links: NetworkLink[] = [];
    const nodeMap = new Map<string, NetworkNode>();

    // Create event lookup map - O(n) operation done once
    const eventMap = createEventMap(events);

    const indexEvents = events.filter((e) => e.kind === 30040);

    indexEvents.forEach((index) => {
      if (!index.id) return;

      const contentRefs = index.getMatchingTags("e");
      const sourceNode = getNode(index.id, nodeMap, index);
      if (!sourceNode) return;
      nodes.push(sourceNode);

      contentRefs.forEach((tag, idx) => {
        if (!tag[1]) return;

        // O(1) lookup instead of O(n) search
        const targetEvent = eventMap.get(tag[1]);
        if (!targetEvent) return;

        const targetNode = getNode(tag[1], nodeMap, targetEvent, idx);
        if (!targetNode) return;
        nodes.push(targetNode);

        const prevNodeId =
          idx === 0 ? sourceNode.id : contentRefs[idx - 1]?.[1];
        const prevNode = nodeMap.get(prevNodeId);

        if (prevNode && targetNode) {
          links.push({
            source: prevNode,
            target: targetNode,
            isSequential: true,
          });
        }
      });
    });

    return { nodes, links };
  }
  function setupDragHandlers(
    simulation: d3.Simulation<NetworkNode, NetworkLink>,
  ) {
    // Create drag behavior with proper typing
    const dragBehavior = d3
      .drag<SVGGElement, NetworkNode>()
      .on(
        "start",
        (
          event: d3.D3DragEvent<SVGGElement, NetworkNode, NetworkNode>,
          d: NetworkNode,
        ) => {
          // Warm up simulation when drag starts
          if (!event.active)
            simulation.alphaTarget(warmupClickEnergy).restart();
          // Fix node position during drag
          d.fx = d.x;
          d.fy = d.y;
        },
      )
      .on(
        "drag",
        (
          event: d3.D3DragEvent<SVGGElement, NetworkNode, NetworkNode>,
          d: NetworkNode,
        ) => {
          // Update fixed position to drag position
          d.fx = event.x;
          d.fy = event.y;
        },
      )
      .on(
        "end",
        (
          event: d3.D3DragEvent<SVGGElement, NetworkNode, NetworkNode>,
          d: NetworkNode,
        ) => {
          // Cool down simulation when drag ends
          if (!event.active) simulation.alphaTarget(0);
          // Release fixed position, allowing forces to take over
          d.fx = null;
          d.fy = null;
        },
      );
    return dragBehavior;
  }
  function drawNetwork() {
    if (!svg || !events?.length) return;

    const { nodes, links } = generateGraph(events);
    if (!nodes.length) return;

    const svgElement = d3.select(svg).attr("viewBox", `0 0 ${width} ${height}`);
    // Set up zoom behavior
    let g = svgElement.append("g");

    const zoom = d3
      .zoom<SVGSVGElement, unknown>()
      .scaleExtent([0.1, 9])
      .on("zoom", (event) => {
        g.attr("transform", event.transform);
      });

    svgElement.call(zoom);
    if (g.empty()) {
      g = svgElement.append("g");

      // Define arrow marker with black fill
    }
    svgElement.select("defs").remove();
    const defs = svgElement.append("defs");
    defs
      .append("marker")
      .attr("id", "arrowhead")
      .attr("markerUnits", "strokeWidth") // Added this
      .attr("viewBox", "-10 -5 10 10")
      .attr("refX", 0)
      .attr("refY", 0)
      .attr("markerWidth", 5)
      .attr("markerHeight", 5)
      .attr("orient", "auto")
      .append("path")
      .attr("d", "M -10 -5 L 0 0 L -10 5 z")
      .attr("class", "network-link-leather")
      .attr("fill", "none")
      .attr("stroke-width", 1); // Added stroke

    // Force simulation setup
    const simulation = d3
      .forceSimulation<NetworkNode>(nodes)
      .force(
        "link",
        d3
          .forceLink<NetworkNode, NetworkLink>(links)
          .id((d) => d.id)
          .distance(linkDistance * 0.1),
      )
      .force("collide", d3.forceCollide<NetworkNode>().radius(nodeRadius * 4));
    simulation.on("end", () => {
      // Get the bounds of the graph
      const bounds = g.node()?.getBBox();
      if (bounds) {
        const dx = bounds.width;
        const dy = bounds.height;
        const x = bounds.x;
        const y = bounds.y;

        // Calculate scale to fit
        const scale = 1.25 / Math.max(dx / width, dy / height);
        const translate = [
          width / 2 - scale * (x + dx / 2),
          height / 2 - scale * (y + dy / 2),
        ];

        // Apply the initial transform
        svgElement
          .transition()
          .duration(750)
          .call(
            zoom.transform,
            d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale),
          );
      }
    });
    const dragHandler = setupDragHandlers(simulation);

    // Create links
    // First, make sure we're selecting and creating links correctly
    const link = g
      .selectAll("path") // Changed from "path.link" to just "path"
      .data(links)
      .join(
        (enter) =>
          enter
            .append("path")
            .attr("stroke-width", 2)
            .attr("marker-end", "url(#arrowhead)") // This should now be applied
            .attr("class", "network-link-leather"), // Add class if needed
        (update) => update,
        (exit) => exit.remove(),
      );

    // Create nodes
    const node = g
      .selectAll<SVGGElement, NetworkNode>("g.node")
      .data(nodes, (d: NetworkNode) => d.id)
      .join(
        (enter) => {
          const nodeEnter = enter
            .append("g")
            .attr("class", "node network-node-leather")
            .call(dragHandler);

          // add drag circle
          nodeEnter
            .append("circle")
            .attr("r", nodeRadius * 2.5)
            .attr("fill", "transparent")
            .attr("stroke", "transparent")
            .style("cursor", "move");

          // add visual circle, stroke based on current theme
          nodeEnter
            .append("circle")
            .attr("r", nodeRadius)
            .attr("class", (d: NetworkNode) =>
              !d.isContainer
                ? "network-node-leather network-node-content"
                : "network-node-leather",
            )
            .attr("stroke-width", 2);

          // add text labels
          nodeEnter
            .append("text")
            .attr("dy", "0.35em")
            .attr("text-anchor", "middle")
            .attr("fill", "black")
            .attr("font-size", "12px");
          // .attr("font-weight", "bold");

          return nodeEnter;
        },
        (update) => update,
        (exit) => exit.remove(),
      );

    // Add text labels
    node
      .select("circle:nth-child(2)")
      .attr("fill", (d: NetworkNode) =>
        !d.isContainer
          ? isDarkMode
            ? "#FFFFFF"
            : "network-link-leather"
          : getEventColor(d.id),
      );

    node.select("text").text((d: NetworkNode) => (d.isContainer ? "I" : "C"));
    // Add tooltips
    const tooltip = d3
      .select("body")
      .append("div")
      .attr(
        "class",
        "tooltip-leather fixed hidden p-4 rounded shadow-lg " +
          "bg-primary-0 dark:bg-primary-800 " +
          "border border-gray-200 dark:border-gray-800 " +
          "p-4 rounded shadow-lg border border-gray-200 dark:border-gray-800 " +
          "transition-colors duration-200",
      )
      .style("z-index", 1000);

    node
      .on("mouseover", function (event, d) {
        tooltip
          .style("display", "block")
          .html(
            `
            <div class="space-y-2">
              <div class="font-bold text-base">${d.title}</div>
              <div class="text-gray-600 dark:text-gray-400 text-sm">
                ${d.type} (${d.isContainer ? "30040" : "30041"})
              </div>
              <div class="text-gray-600 dark:text-gray-400 text-sm overflow-hidden text-ellipsis">
                ID: ${d.id}
              </div>
              ${
                d.content
                  ? `
                <div class="mt-2 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-auto max-h-40">
                  ${d.content}
                </div>
              `
                  : ""
              }
            </div>
          `,
          )
          .style("left", event.pageX - 10 + "px")
          .style("top", event.pageY + 10 + "px");
      })
      .on("mousemove", function (event) {
        tooltip
          .style("left", event.pageX + 10 + "px")
          .style("top", event.pageY - 10 + "px");
      })
      .on("mouseout", () => {
        tooltip.style("display", "none");
      });

    // Handle simulation ticks
    simulation.on("tick", () => {
      nodes.forEach((node) => {
        applyGlobalLogGravity(node, width / 2, height / 2, simulation.alpha());
        applyConnectedGravity(node, links, simulation.alpha());
      });
      link.attr("d", (d) => {
        const dx = d.target.x! - d.source.x!;
        const dy = d.target.y! - d.source.y!;
        const angle = Math.atan2(dy, dx);

        const sourceGap = nodeRadius;
        const targetGap = nodeRadius + arrowDistance; // Increased gap for arrowhead

        const startX = d.source.x! + sourceGap * Math.cos(angle);
        const startY = d.source.y! + sourceGap * Math.sin(angle);
        const endX = d.target.x! - targetGap * Math.cos(angle);
        const endY = d.target.y! - targetGap * Math.sin(angle);

        return `M${startX},${startY}L${endX},${endY}`;
      });
      node.attr("transform", (d) => `translate(${d.x},${d.y})`);
    });
  }

  onMount(() => {
    isDarkMode = document.body.classList.contains("dark");
    // Add window resize listener
    const handleResize = () => {
      windowHeight = window.innerHeight;
    };

    // Initial resize
    windowHeight = window.innerHeight;
    window.addEventListener("resize", handleResize);

    // Watch for theme changes
    const themeObserver = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.attributeName === "class") {
          const newIsDarkMode = document.body.classList.contains("dark");
          if (newIsDarkMode !== isDarkMode) {
            isDarkMode = newIsDarkMode;
            // drawNetwork();
          }
        }
      });
    });

    const resizeObserver = new ResizeObserver((entries) => {
      for (const entry of entries) {
        width = entry.contentRect.width;
        height = graphHeight;
      }

      // first remove all nodes and links
      d3.select(svg).selectAll("*").remove();
      drawNetwork();
    });

    // Start observers
    themeObserver.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"],
    });
    resizeObserver.observe(container);
    // Clean up
    return () => {
      themeObserver.disconnect();
      resizeObserver.disconnect();
    };
  });
  // Reactive redaw
  $: {
    if (svg && events?.length) {
      drawNetwork();
    }
  }
</script>

<div
  class="flex flex-col w-full h-[calc(100vh-120px)] min-h-[400px] max-h-[900px] p-4 gap-4"
>
  <div class="h-[calc(100%-130px)] min-h-[300px]" bind:this={container}>
    <svg
      bind:this={svg}
      class="w-full h-full border border-gray-300 dark:border-gray-700 rounded"
    />
  </div>
  <!-- Legend -->
  <div class="leather-legend">
    <h3 class="text-lg font-bold mb-2 h-leather">Legend</h3>
    <ul class="legend-list">
      <li class="legend-item">
        <div class="legend-icon">
          <span
            class="legend-circle"
            style="background-color: hsl(200, 70%, 75%)"
          >
          </span>
          <span class="legend-letter">I</span>
        </div>
        <span>Index events (kind 30040) - Each with a unique pastel color</span>
      </li>

      <li class="legend-item">
        <div class="legend-icon">
          <span class="legend-circle content"></span>
          <span class="legend-letter">C</span>
        </div>
        <span>Content events (kind 30041) - Publication sections</span>
      </li>

      <li class="legend-item">
        <svg class="w-6 h-6 mr-2" viewBox="0 0 24 24">
          <path d="M4 12h16M16 6l6 6-6 6" class="network-link-leather" />
        </svg>
        <span>Arrows indicate reading/sequence order</span>
      </li>
    </ul>
  </div>
</div>

<style>
  .legend-list {
    @apply list-disc pl-5 space-y-2 text-gray-800 dark:text-gray-300;
  }

  .legend-item {
    @apply flex items-center;
  }

  .legend-icon {
    @apply relative w-6 h-6 mr-2;
  }

  .legend-circle {
    @apply absolute inset-0 rounded-full border-2 border-black;
  }

  .legend-circle.content {
    @apply bg-gray-700 dark:bg-gray-300;
    background-color: #d6c1a8;
  }

  .legend-letter {
    @apply absolute inset-0 flex items-center justify-center text-black text-xs;
  }
</style>
