console.log("loaded pw.js")

function findPaths(source_idx, links) {
    const resultLinks = new Set();
    const visited = new Set();
    // Helper function for depth-first search
    function dfs(current) {

        if (visited.has(current)) {
            return;
        }

        if (links.filter(link => link.target.id === current).length === 0) {
            return;
        }

        visited.add(current);
        links.forEach(link => {
            if (
                link.target.id === current &&
                !visited.has(link.source.id) &&
                link.source.id !== link.target.id // Avoid self-loops
            ) {
                resultLinks.add(link); // Add link to result
                dfs(link.source.id);
            }
        });
        visited.delete(current);
    }
    // Start DFS
    dfs(source_idx);
    return Array.from(resultLinks);
}

function predictFromNode(url) {
    fetch("", {
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
            "X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
        },
        body: new URLSearchParams({node: url})
    })
    .then(response => response.json())
    .then(data => {
        console.log("Success:", data);
        window.location.href = data.success;
    })
    .catch(error => {
        console.error("Error:", error);
    });
}

// data = {{ pathway.d3_json | safe }};
// elem = 'vizdiv'
function draw(pathway, elem) {

    const initialzoom = 2.5
    const nodeRadius = 20;
    const linkDistance = 100;
    const chargeStrength = -200;
    const depthSpacing = 150;
    const width = document.getElementById(elem).offsetWidth, height = width * 0.75;

    function assignPositions(nodes) {
        const levelSpacing = 75; // vertical space between levels
        const horizontalSpacing = 75; // horizontal space between nodes
        const depthMap = new Map();

        // Avoid leaving unconnected Nodes leaving the viewport
        nodes.forEach(node => {if (node.depth < 0)  node.depth = 0;});

        // Sort nodes by depth first to minimize crossings
        const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);

        sortedNodes.forEach(node => {
            if (!depthMap.has(node.depth)) {
                depthMap.set(node.depth, 0);
            }

            const nodesInLevel = nodes.filter(n => n.depth === node.depth).length;

            // For pseudo nodes, try to position them to minimize crossings
            if (node.pseudo) {
                const parentLinks = links.filter(l => l.target.id === node.id);
                const childLinks = links.filter(l => l.source.id === node.id);

                if (parentLinks.length > 0 && childLinks.length > 0) {
                    const parentX = parentLinks[0].source.x || (width / 2);
                    const childrenX = childLinks.map(l => l.target.x || (width / 2));
                    const avgChildX = childrenX.reduce((sum, x) => sum + x, 0) / childrenX.length;

                    // Position pseudo node between parent and average child position
                    node.fx = (parentX + avgChildX) / 2;
                } else {
                    node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
                }
            } else {
                node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
            }

            node.fy = (node.depth + initialzoom + 0.5) * levelSpacing + 50;
            depthMap.set(node.depth, depthMap.get(node.depth) + 1);
        });
    }

    // Function to update pseudo node positions based on connected nodes
    function updatePseudoNodePositions() {
        nodes.forEach(node => {
            if (node.pseudo && !node.isDragging) { // Don't auto-update if being dragged
                const parentLinks = links.filter(l => l.target.id === node.id);
                const childLinks = links.filter(l => l.source.id === node.id);

                if (parentLinks.length > 0 && childLinks.length > 0) {
                    const parent = parentLinks[0].source;
                    const children = childLinks.map(l => l.target);

                    // Calculate optimal position to minimize crossing
                    const parentX = parent.x;
                    const parentY = parent.y;
                    const childrenX = children.map(c => c.x);
                    const childrenY = children.map(c => c.y);
                    const avgChildX = d3.mean(childrenX);
                    const avgChildY = d3.mean(childrenY);

                    // Position pseudo node between parent and average child position
                    node.fx = (parentX + avgChildX) / 2;
                    node.fy = (parentY + avgChildY) / 2; // Allow vertical movement too
                }
            }
        });
    }


    // Enhanced ticked function
    function ticked() {
        // Update pseudo node positions first
        updatePseudoNodePositions();

        link.attr("d", d => {
            // Check if it's a self-loop (source equals target)
            if (d.source.id === d.target.id) {
                // Create a bezier curve for self-loops
                const x = d.source.x;
                const y = d.source.y;
                const loopRadius = nodeRadius * 2; // Adjust size of the loop

                // Create a circular path to the left of the node
                return `M ${x},${y - nodeRadius}
                        C ${x - loopRadius},${y - nodeRadius - loopRadius}
                          ${x - loopRadius},${y + nodeRadius + loopRadius}
                          ${x},${y + nodeRadius}`;
            } else {
                // Regular straight line for normal edges
                return `M ${d.source.x},${d.source.y} L ${d.target.x},${d.target.y}`;
            }
        });

        node.attr("transform", d => `translate(${d.x},${d.y})`);

        linkText
         .attr("x", d => (d.source.x + d.target.x) / 2)
         .attr("y", d => (d.source.y + d.target.y) / 2);

    }

    function dragstarted(event, d) {
        // Prevent zoom pan when dragging nodes
        event.sourceEvent.stopPropagation();

        if (!event.active) simulation.alphaTarget(0.3).restart();
        d.fx = d.x;
        d.fy = d.y;

        // Mark if this node is being dragged
        d.isDragging = true;

        // If dragging a non-pseudo node, mark connected pseudo nodes for update
        if (!d.pseudo) {
            markConnectedPseudoNodes(d);
        }
    }

    function dragged(event, d) {
        // Prevent zoom pan when dragging nodes
        event.sourceEvent.stopPropagation();

        d.fx = event.x;
        d.fy = event.y;

        // Update connected pseudo nodes in real-time
        if (!d.pseudo) {
            updateConnectedPseudoNodes(d);
        }
    }

    function dragended(event, d) {
        // Prevent zoom pan when dragging nodes
        event.sourceEvent.stopPropagation();

        if (!event.active) simulation.alphaTarget(0);

        // Mark that dragging has ended
        d.isDragging = false;

        // Final update of connected pseudo nodes
        if (!d.pseudo) {
            updateConnectedPseudoNodes(d);
        }
    }

    // Helper function to mark connected pseudo nodes
    function markConnectedPseudoNodes(draggedNode) {
        // Find pseudo nodes connected to this node
        const connectedPseudos = new Set();

        // Check as parent of pseudo nodes
        links.filter(l => l.source.id === draggedNode.id && l.target.pseudo)
            .forEach(l => connectedPseudos.add(l.target));

        // Check as child of pseudo nodes
        links.filter(l => l.target.id === draggedNode.id && l.source.pseudo)
            .forEach(l => connectedPseudos.add(l.source));

        return connectedPseudos;
    }

    // Helper function to update connected pseudo nodes
    function updateConnectedPseudoNodes(draggedNode) {
        const connectedPseudos = markConnectedPseudoNodes(draggedNode);

        connectedPseudos.forEach(pseudoNode => {
            if (!pseudoNode.isDragging) { // Don't update if pseudo node is being dragged
                const parentLinks = links.filter(l => l.target.id === pseudoNode.id);
                const childLinks = links.filter(l => l.source.id === pseudoNode.id);

                if (parentLinks.length > 0 && childLinks.length > 0) {
                    const parent = parentLinks[0].source;
                    const children = childLinks.map(l => l.target);

                    const parentX = parent.fx || parent.x;
                    const parentY = parent.fy || parent.y;
                    const childrenX = children.map(c => c.fx || c.x);
                    const childrenY = children.map(c => c.fy || c.y);
                    const avgChildX = d3.mean(childrenX);
                    const avgChildY = d3.mean(childrenY);

                    // Update pseudo node position - allow both X and Y movement
                    pseudoNode.fx = (parentX + avgChildX) / 2;
                    pseudoNode.fy = (parentY + avgChildY) / 2;
                }
            }
        });

        // Restart simulation with lower alpha to smooth the transition
        simulation.alpha(0.1).restart();
    }


    // t -> ref to "this" from d3
    function nodeClick(event, node, t) {
        console.log(node);
        d3.select(t).select("circle").classed("highlighted", !d3.select(t).select("circle").classed("highlighted"));
    }

    // Wait before showing popup (ms)
    var popupWaitBeforeShow = 750;

    // Custom popover element
    let popoverTimeout = null;

    function createPopover() {
        const popover = document.createElement('div');
        popover.id = 'custom-popover';
        popover.className = 'fixed z-50';
        popover.style.cssText = `
            background: #ffffff;
            border: 1px solid #d1d5db;
            box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
            max-width: 320px;
            padding: 0.75rem;
            border-radius: 0.5rem;
            opacity: 0;
            visibility: hidden;
            transition: opacity 150ms ease-in-out, visibility 150ms ease-in-out;
            pointer-events: auto;
        `;
        popover.setAttribute('role', 'tooltip');
        popover.innerHTML = `
            <div class="font-semibold mb-2 popover-title" style="font-weight: 600; margin-bottom: 0.5rem;"></div>
            <div class="text-sm popover-content" style="font-size: 0.875rem;"></div>
        `;

        // Add styles for content images
        const style = document.createElement('style');
        style.textContent = `
            #custom-popover img {
                max-width: 100%;
                height: auto;
                display: block;
                margin: 0.5rem 0;
            }
            #custom-popover a {
                color: #2563eb;
                text-decoration: none;
            }
            #custom-popover a:hover {
                text-decoration: underline;
            }
        `;
        if (!document.getElementById('popover-styles')) {
            style.id = 'popover-styles';
            document.head.appendChild(style);
        }

        // Keep popover open when hovering over it
        popover.addEventListener('mouseenter', () => {
            if (popoverTimeout) {
                clearTimeout(popoverTimeout);
                popoverTimeout = null;
            }
        });

        popover.addEventListener('mouseleave', () => {
            hidePopover();
        });

        document.body.appendChild(popover);
        return popover;
    }

    function getPopover() {
        return document.getElementById('custom-popover') || createPopover();
    }

    function showPopover(element, title, content) {
        const popover = getPopover();
        popover.querySelector('.popover-title').textContent = title;
        popover.querySelector('.popover-content').innerHTML = content;

        // Make visible to measure
        popover.style.visibility = 'hidden';
        popover.style.opacity = '0';
        popover.style.display = 'block';

        // Smart positioning - avoid viewport overflow
        const padding = 10;
        const popoverRect = popover.getBoundingClientRect();
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;

        let left = clientX + 15;
        let top = clientY - (popoverRect.height / 2);

        // Prevent right overflow
        if (left + popoverRect.width > viewportWidth - padding) {
            left = clientX - popoverRect.width - 15;
        }

        // Prevent bottom overflow
        if (top + popoverRect.height > viewportHeight - padding) {
            top = viewportHeight - popoverRect.height - padding;
        }

        // Prevent top overflow
        if (top < padding) {
            top = padding;
        }

        popover.style.top = `${top}px`;
        popover.style.left = `${left}px`;
        popover.style.visibility = 'visible';
        popover.style.opacity = '1';

        currentElement = element;
    }

    function hidePopover() {
        const popover = getPopover();
        popover.style.opacity = '0';
        popover.style.visibility = 'hidden';
        currentElement = null;
    }

    function pop_add(objects, title, contentFunction) {
        objects.each(function (d) {
            const element = this;

            element.addEventListener('mouseenter', () => {
                if (popoverTimeout) clearTimeout(popoverTimeout);

                popoverTimeout = setTimeout(() => {
                    if (element.matches(':hover')) {
                        const content = contentFunction(d);
                        showPopover(element, title, content);
                    }
                }, popupWaitBeforeShow);
            });

            element.addEventListener('mouseleave', () => {
                if (popoverTimeout) {
                    clearTimeout(popoverTimeout);
                    popoverTimeout = null;
                }

                // Delay hide to allow moving to popover
                setTimeout(() => {
                    const popover = getPopover();
                    if (!popover.matches(':hover') && !element.matches(':hover')) {
                        hidePopover();
                    }
                }, 100);
            });
        });
    }

    function node_popup(n) {
        popupContent = "";

        if (timeseriesViewEnabled && n.timeseries && n.timeseries.measurements) {
            for (var s of n.scenarios) {
                popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
            }

            popupContent += '<div style="width:100%;height:120px"><canvas id="ts-popover-canvas"></canvas></div>';
            const tsMeasurements = n.timeseries.measurements;
            setTimeout(() => {
                const canvas = document.getElementById('ts-popover-canvas');
                if (canvas && window.Chart) {
                    const valid = tsMeasurements
                        .filter(m => m.timestamp != null && m.value != null)
                        .map(m => ({ ...m, timestamp: typeof m.timestamp === 'number' ? m.timestamp : new Date(m.timestamp).getTime() }))
                        .sort((a, b) => a.timestamp - b.timestamp);

                    const datasets = [];

                    // Error band (lower + upper with fill between)
                    const withErrors = valid.filter(m => m.error != null && m.error > 0);
                    if (withErrors.length > 0) {
                        datasets.push({
                            data: withErrors.map(m => ({ x: m.timestamp, y: m.value - m.error })),
                            borderColor: 'rgba(59,130,246,0.3)',
                            backgroundColor: 'rgba(59,130,246,0.15)',
                            pointRadius: 0,
                            fill: false,
                            tension: 0.1,
                        });
                        datasets.push({
                            data: withErrors.map(m => ({ x: m.timestamp, y: m.value + m.error })),
                            borderColor: 'rgba(59,130,246,0.3)',
                            backgroundColor: 'rgba(59,130,246,0.15)',
                            pointRadius: 0,
                            fill: '-1',
                            tension: 0.1,
                        });
                    }

                    // Main value line
                    datasets.push({
                        data: valid.map(m => ({ x: m.timestamp, y: m.value })),
                        borderColor: 'rgb(59,130,246)',
                        pointRadius: 0,
                        tension: 0.1,
                        fill: false,
                    });

                    new Chart(canvas.getContext('2d'), {
                        type: 'line',
                        data: { datasets },
                        options: {
                            responsive: true,
                            maintainAspectRatio: false,
                            plugins: {
                                legend: { display: false },
                                tooltip: { enabled: false },
                            },
                            scales: {
                                x: {
                                    type: 'linear',
                                    ticks: { font: { size: 10 } },
                                    title: { display: false },
                                },
                                y: {
                                    ticks: { font: { size: 10 } },
                                    title: { display: false },
                                },
                            },
                        },
                    });
                }
            }, 0);

            return popupContent;
        }

        if (n.stereo_removed) {
            popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
        }
        popupContent += "<a href='" + n.url + "'>" + n.plain_name + "</a><br>";
        popupContent += "Depth " + n.depth + "<br>"

        if (appDomainViewEnabled) {
            if (n.app_domain != null) {
                popupContent += "This compound is " + (n.app_domain['inside_app_domain'] ? "inside" : "outside") + " the Applicability Domain derived from the chemical (PCA) space constructed using the training data." + "<br>"
                if (n.app_domain['uncovered_functional_groups']) {
                    popupContent += "Compound contains functional groups not covered by the training set  <br>"
                }
            }
        }

        if (predictedPropertyViewEnabled) {

            var tempContent = "";

            if (Object.keys(n.predicted_properties).length > 0) {

                if ("PepperPrediction" in n.predicted_properties) {
                    // TODO needs to be generic once we store it as AddInf
                    for (var s of n.predicted_properties["PepperPrediction"]) {
                        if (s["mean"] != null) {
                            tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + " days<br>"
                        }
                    }
                }
            }

            if (tempContent === "") {
                tempContent = "<b>No predicted properties for this Node</b><br>";
            }

            popupContent += tempContent
        }

        popupContent += "<img src='" + n.image + "'><br>"
        if (n.scenarios.length > 0) {
            popupContent += '<b>Half-lives and related scenarios:</b><br>'
            for (var s of n.scenarios) {
                popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
                for (var prop in n.proposed) {
                    if (n.proposed[prop].scenarioId === s.url) {
                        if("proposed" in n.proposed[prop]){
                            popupContent += "This compound is a proposed intermediate" + "<br>";
                        }
                        if("Confidence" in n.proposed[prop]){
                            popupContent += "Confidence: " + n.proposed[prop]["Confidence"] + "<br>";
                        }
                        if("Transformation product importance" in n.proposed[prop]){
                            popupContent += "Transformation product importance: " + n.proposed[prop]["Transformation product importance"] + "<br>";
                        }
                    }
                }
                popupContent += "<br>";

            }
        }

        var isLeaf = pathway.links.filter(obj => obj.source.id === n.id).length === 0;
        if (pathway.isIncremental && isLeaf) {
            popupContent += '<br><a class="btn btn-primary btn-sm mt-2" onclick="predictFromNode(\'' + n.url + '\')" href="#">Predict from here</a><br>';
        }

        return popupContent;
    }

    function edge_popup(e) {
        popupContent = "<a href='" + e.url + "'>" + e.plain_name + "</a><br><br>";

        if (e.reaction.rules) {
            for (var rule of e.reaction.rules) {
                popupContent += "Rule <a href='" + rule.url + "'>" + rule.name + "</a><br>";
            }
        }

        if (e.app_domain) {
            adcontent = "<p>";
            if (e.app_domain["times_triggered"]) {
                adcontent += "This rule triggered " + e.app_domain["times_triggered"] + " times in the training set<br>";
            }
            adcontent += "Reliability " + e.app_domain["reliability"].toFixed(2) + " (" + (e.app_domain["reliability"] > e.app_domain["reliability_threshold"] ? "&gt" : "&lt") + " Reliability Threshold of " + e.app_domain["reliability_threshold"] + ")<br>";
            adcontent += "Local Compatibility " + e.app_domain["local_compatibility"].toFixed(2) + " (" + (e.app_domain["local_compatibility"] > e.app_domain["local_compatibility_threshold"] ? "&gt" : "&lt") + " Local Compatibility Threshold of " + e.app_domain["local_compatibility_threshold"] + ")<br>";
            adcontent += "</p>";
            popupContent += adcontent;
        }

        popupContent += "<img src='" + e.image + "'><br>"
        if (e.reaction_probability) {
            popupContent += '<b>Probability:</b><br>' + e.reaction_probability.toFixed(3) + '<br>';
        }

        if (e.scenarios.length > 0) {
            popupContent += '<b>Half-lives and related scenarios:</b><br>'
            for (var s of e.scenarios) {
                popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
            }
        }

        return popupContent;
    }

    var clientX;
    var clientY;
    document.addEventListener('mousemove', function (event) {
        clientX = event.clientX;
        clientY = event.clientY;
    });

    const zoomable = d3.select("#zoomable");
    const svg = d3.select("#pwsvg");
    const container = d3.select("#vizdiv");

    // Set explicit SVG dimensions for proper zoom behavior
    svg.attr("width", width)
       .attr("height", height);

    // Add background rectangle FIRST to enable pan/zoom on empty space
    // This must be inserted before zoomable group so it's behind everything
    const rect = svg.insert("rect", "#zoomable")
        .attr("x", 0)
        .attr("y", 0)
        .attr("width", width)
        .attr("height", height)
        .attr("fill", "transparent")
        .attr("pointer-events", "all")
        .style("cursor", "grab");

    // Click on empty SVG space (not on a node or link)
    rect.on("click", function(event) {
        // Only treat it as a background click if the direct target is the rect itself
        // (clicks on nodes/links bubble up but originate from different elements)
        const target = event.target;
        const isNode = target.closest && (
            target.closest("circle") !== null ||
            target.closest("image") !== null ||
            target.closest(".node") !== null
        );
        const isLink = target.closest && (
            target.closest("path.link") !== null ||
            target.closest("path.link_no_arrow") !== null
        );
        if (!isNode && !isLink) {
            // console.log("Clicked outside a node or link");
            // Clear all highlights
            d3.selectAll("circle").classed("highlighted", false);
            d3.selectAll("path").classed("highlighted", false);
            d3.selectAll("path").classed("inedge", false);
            d3.selectAll("path").classed("outedge", false);
        }
    });

    // Zoom Funktion aktivieren
    const zoom = d3.zoom()
        .scaleExtent([0.5, 5])
        .on("zoom", (event) => {
            zoomable.attr("transform", event.transform);
        });

    // Apply zoom to the SVG element - this enables wheel zoom
    svg.call(zoom);

    svg.call(zoom.scaleBy, initialzoom);

    // Also apply zoom to container to catch events that might not reach SVG
    // This ensures drag-to-pan works even when clicking on empty space
    container.call(zoom);

    nodes = pathway['nodes'];
    links = pathway['links'];

    // Use "max" depth for a neater viz
    nodes.forEach(n => {
        const parents = links
            .filter(l => l.target === n.id)
            .map(l => l.source);

        orig_depth = n.depth
        // console.log(n.id, parents)
        for (idx in parents) {
            p = nodes[parents[idx]]
            // console.log(p.depth)
            // if (p.depth >= n.depth) {
            //     // keep the .5 steps for pseudo nodes
            //     n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
            //     // console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
            // }
        }
    });

    assignPositions(nodes);

    const simulation = d3.forceSimulation(nodes)
        .force("link", d3.forceLink(links).id(d => d.id).distance(linkDistance))
        .force("charge", d3.forceManyBody().strength(chargeStrength))
        .force("center", d3.forceCenter(width / 2, height / 4))
        .on("tick", ticked);

    // Draw Edges
    const linkGroup = zoomable.append("g")
      .selectAll("g")
      .data(links)
      .enter()
      .append("g")
      .attr("class", "link-group");

    const link = linkGroup
      .append("path")
      .attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
      .attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)")
      .attr("fill", "none")

      // Check if target is pseudo and draw marker only if not pseudo
      .attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
      .attr("marker-end", d => {
        if (d.target.pseudo) return '';
        if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
          return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
        }
      )
      .attr("fill", "none")
      .on("click", function(event, d) {
          const wasHighlighted = d3.select(this).classed("highlighted");
          d3.selectAll("path").classed("highlighted", false);
          if (!wasHighlighted) {
            const toHighlight = [];
            toHighlight.push(d.el);

            if (d.source.pseudo || d.target.pseudo) {
              if (d.target.pseudo) {
                d3.selectAll("path").each(e => {
                  if (e !== undefined && e.source.id === d.target.id) {
                    toHighlight.push(e.el);
                  }
                });
              } else {
                d3.selectAll("path").each(e => {
                  if (e !== undefined && (e.target.id === d.source.id || e.source.id === d.source.id)) {
                    toHighlight.push(e.el);
                  }
                });
              }
            }

            for (const e of toHighlight) {
              d3.select(e).classed("highlighted", true);
            }
          }
        });

    function reaction_name(d) {
        const to_pseudo = d?.to_pseudo || false;

        if (to_pseudo) {
            return "";
        }

        var suffix = "";
        if ("reaction_probability" in d && d["reaction_probability"] !== null) {
            suffix = " (p= " + d["reaction_probability"].toFixed(2) + ")"
        }
        return d.plain_name + suffix;
    }

    const linkText = linkGroup
      .append("text")
      .attr("class", "link-label")
      .attr("text-anchor", "middle")
      .attr("dy", -6)
      .style("font-size", "4px")
      .style("pointer-events", "none")
      .style("display", "none")
      .text(d => reaction_name(d))
      .attr("x", d => (d.source.x + d.target.x) / 2)
      .attr("y", d => (d.source.y + d.target.y) / 2);

    // add element to links array
    link.each(function (d) {
        d.el = this;  // attach the DOM element to the data object
    });

    pop_add(link, "Reaction", edge_popup);

    // Draw Nodes
    const node = zoomable.append("g")
        .selectAll("g")
        .data(nodes)
        .enter().append("g")
        .call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended)
        )
        .on("click", function (event, d) {
          const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
          d3.selectAll('circle.highlighted').classed('highlighted', false);
          d3.selectAll("path").classed("inedge", false);
          d3.selectAll("path").classed("outedge", false);

          if (!wasHighlighted) {
            d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
            const inEdges = findPaths(d.id, links);
            const outEdges = []
            // Colorize out edges green
            for (const l of links) {
              if (l.source.id === d.id) {
                outEdges.push(l);
                if (l.target.pseudo) {
                  for (const l2 of links) {
                    if (l.target.id === l2.source.id) {
                      outEdges.push(l2);
                    }
                  }
                }
              }
            }

            for (const e of inEdges) {
              d3.select(e.el).classed("inedge", true);
            }

            for (const e of outEdges) {
              d3.select(e.el).classed("outedge", true);
            }
          }
        });

    // Kreise für die Knoten hinzufügen
    node.append("circle")
        // make radius "invisible" for pseudo nodes
        .attr("r", d => d.pseudo ? 0.01 : nodeRadius)
        .style("fill", d => d.is_engineered_intermediate ? "#42eff5" : "#e8e8e8");

    // Add image only for non pseudo nodes
    node.filter(d => !d.pseudo).each(function (d, i) {
        const g = d3.select(this);

        if (d.image_type === "svg") {
            // Parse the SVG string
            const parser = new DOMParser();
            const svgDoc = parser.parseFromString(d.image_svg, "image/svg+xml");
            const svgElem = svgDoc.documentElement;

            // Create a unique prefix per node
            const prefix = `node-${i}-`;

            // Rename all IDs and fix <use> references
            svgElem.querySelectorAll("[id]").forEach(el => {
                const oldId = el.id;
                const newId = prefix + oldId;
                el.id = newId;

                const XLINK_NS = "http://www.w3.org/1999/xlink";
                // Update <use> elements that reference this old ID
                const uses = Array.from(svgElem.querySelectorAll("use")).filter(
                    u => u.getAttributeNS(XLINK_NS, "href") === `#${oldId}`
                );

                uses.forEach(u => {
                    u.setAttributeNS(XLINK_NS, "href", `#${newId}`);
                });
            });

            g.node().appendChild(svgElem);

            const vb = svgElem.viewBox.baseVal;
            const svgWidth = vb.width || 40;
            const svgHeight = vb.height || 40;

            const scale = (nodeRadius * 2) / Math.max(svgWidth, svgHeight);

            g.select("svg")
                .attr("width", svgWidth * scale)
                .attr("height", svgHeight * scale)
                .attr("x", -svgWidth * scale / 2)
                .attr("y", -svgHeight * scale / 2);
        } else {
            // We have a image type different than svg
            // include it via img url
            g.append("svg:image")
                .attr("xlink:href", d.image)
                .attr("width", 40)
                .attr("height", 40)
                .attr("x", -20)
                .attr("y", -20);
        }

    });

    // add element to nodes array
    node.each(function (d) {
        d.el = this;  // attach the DOM element to the data object
    });

    pop_add(node, "Compound", node_popup);
}

function serializeSVG(svgElement) {

    svgElement.querySelectorAll("path.link").forEach(line => {
        const style = getComputedStyle(line);
        line.setAttribute("stroke", style.stroke);
        line.setAttribute("stroke-width", style.strokeWidth);
        line.setAttribute("fill", style.fill);
    });

    svgElement.querySelectorAll("path.link_no_arrow").forEach(line => {
        const style = getComputedStyle(line);
        line.setAttribute("stroke", style.stroke);
        line.setAttribute("stroke-width", style.strokeWidth);
        line.setAttribute("fill", style.fill);
    });

    const serializer = new XMLSerializer();
    let svgString = serializer.serializeToString(svgElement);

    // Add namespace if missing
    if (!svgString.includes('xmlns="http://www.w3.org/2000/svg"')) {
        svgString = svgString.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
    }

    return svgString;
}

function shrinkSVG(svgSelector) {

    const svg = d3.select(svgSelector);
    const node = svg.node();

    // Compute bounding box of everything inside the SVG
    const bbox = node.getBBox();

    const padding = 10;
    svg.attr("viewBox",
        `${bbox.x - padding} ${bbox.y - padding} ${bbox.width + 2 * padding} ${bbox.height + 2 * padding}`
    )
        .attr("width", bbox.width + 2 * padding)
        .attr("height", bbox.height + 2 * padding);

    return bbox;
}

function downloadSVG(svgElement, filename = 'chart.svg') {
    shrinkSVG("#" + svgElement.id);
    const svgString = serializeSVG(svgElement);
    const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
    const url = URL.createObjectURL(blob);

    const link = document.createElement('a');
    link.href = url;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
}
