import rectCollide from './collide.js';
import mds from './mds.js';
import { createDistanceMatrix, getBoundingBox, filterLinksByWord, lowlight, dehighlight, highlight, highlightBoundingBox, dehighlightBoundingBox, dehighlight_edge, highlight_edge, highlightEdgeColor, show, hide, getColor } from './utils.js';
import { runMetrics, approximateDistortion, getMinimumDistanceBetweenRectangles } from './metrics.js';
import * as config from './config.js';
import * as utils from './utils.js';
import forceSimulation from './force-simulation.js';
import * as memento from './memento.js';

// This is encapsulated in a function to minimize the scope of the variables, in fact the initialize function is the entry point
export default function run(data) {
    let width;
    let height;
    let svg;

    let originX;
    let originY;

    // Datastructure holding nodes and links
    let graph;

    // Visual elements representing nodes and links
    let links, nodes;

    let grid;

    // Visual elements nested within nodes
    let words, boundingboxes;

    // Force simulation
    let forces;

    // Global variable that holds selected word
    let selected = null; // {id, node, data}

    // Global variable that holds neighbors of dragged word
    let dragged = null;
    let dragged_neighbors = [];

    // Global bounding box
    let global_boundingbox = null;

    let realized_links;

    // Metric modes
    let COMPACTNESS_MODE = false;
    let DISTORTION_MODE = false;
    let ADJACENCIES_MODE = false;

    // Current metric values
    let current_metrics = null;
    let prev_metrics = null;
    let top_metrics = null;

    let grid_cells;

    function initialize(data) {
        document.addEventListener('contextmenu', event => event.preventDefault());

        // Load graph
        graph = data;

        svg = d3.select("#svg-content");

        // IF WANTED: add a parent group for allowing for zooming (see bottom snippet as well)
        // svg = svg.append("g")

        // set innerHTML of svg to empty
        svg.html("");

        width = svg.node().getBoundingClientRect().width;
        height = svg.node().getBoundingClientRect().height;

        originX = width/2;
        originY = height/2;
        
        // Create selections for SVG elements
        grid = svg.append("g")
            .attr("id", "grid")
            .attr("class", "grid")
        links = svg.append("g")
            .attr("class", "links")
            .selectAll("line");
        nodes = svg.append("g")
            .attr("class", "nodes")
            .selectAll("g");

        // Add force boolean to data
        for (const node of graph.nodes) {
            node.forceMode = 0;
        }

        createOrUpdateWordCloud();

        initialWordPlacement();

        createButtons();

        forces = forceSimulation(graph, width, height, nodes, links, updateMetrics, updateMetricGuides);

        forces.initialLayout();

        initializeDragAndScrollHandler();

        /* ZOOM BEHVAIOUR (if wanted and needed)
        function handleZoom(e) {
            svg.attr('transform', e.transform);
        }

        let zoom = d3.zoom().on('zoom', handleZoom);
        
        d3.select('svg').call(zoom);
        */
    }

    function initialWordPlacement() {
        let MDS = mds(createDistanceMatrix(graph.links, graph.nodes.length));

        for (const entry of MDS) {
            entry[0] *= config.MDS_X_SCALING;
            entry[1] *= config.MDS_Y_SCALING;
            entry[0] += originX;
            entry[1] += originY;
        }

        for (const node of graph.nodes) { 
            node.x = MDS[node.id][0];
            node.y = MDS[node.id][1];
        }
    }

    function createOrUpdateWordCloud() {
        // Create links
        links = links.data(graph.links).join("line").attr("class", "link")
            .attr("stroke-width", d => ((d.similarity > 0 ? 1 : 0) * config.EDGE_BASE_THICKNESS) + d.similarity*config.EDGE_SIMILARITY_THICKNESS )
            .attr("visibility", "hidden")

        // Show links of selected word (if exists)
        if (selected != null) show(links.filter(l => filterLinksByWord(l, selected)));

        // Create nodes
        nodes = nodes.data(graph.nodes).join("g").attr("class", "group").attr("id", d => "group" + d.id);
        words = nodes.append("text")
            .text(d => d.word)
            .attr("id", d=>"word"+d.id)
            .attr("alignment-baseline", "central")
            .attr("text-anchor", "middle")
            .attr("class", "word")
            .attr("fill", d => getColor(d.id))
            .style("font-size", d => d.weight*config.WORD_SIZE + 'px')
            .on("contextmenu", wordClick);
        boundingboxes = nodes.append("rect")
            .attr("x", d => getBoundingBox(getNode(d.id)).x)
            .attr("y", d => getBoundingBox(getNode(d.id)).y) 
            .attr("width", d => getBoundingBox(getNode(d.id)).width)
            .attr("height", d => getBoundingBox(getNode(d.id)).height)
            .attr("id", d=>"rect"+d.id)
            .attr("fill", "grey")
            .attr("opacity", 0)
            .attr("stroke", "black")
            .attr("class", "boundingbox")
        
        words.raise(); // Bring words to front

        global_boundingbox = svg.append("rect")
    }


    function recomputeFromScratch() {
        forces.stop();
        initialWordPlacement();
        forces.reinitialize(graph.nodes, graph.links);
    }

    let boundingwords = [];
    function enableCompactnessMode() {
        COMPACTNESS_MODE = true;
        drawBoundingBox();

        global_boundingbox.attr("visibility", "visible");
        let boundingrect = global_boundingbox.node().getBoundingClientRect();
        boundingwords = words.nodes().filter(n => {
            let rect = n.getBoundingClientRect();
            return rect.top-boundingrect.top < 20 || rect.bottom-boundingrect.bottom > -20 || rect.left-boundingrect.left < 20 || rect.right-boundingrect.right > -20;
        });
        let non_boundingwords = words.nodes().filter(n => !boundingwords.includes(n));


        boundingwords.forEach(w => {
            highlight(d3.select(w))
        });
        non_boundingwords.forEach(w => {
            lowlight(d3.select(w))
        });
    }

    function disableCompactnessMode() {
        COMPACTNESS_MODE = false;
        global_boundingbox.attr("visibility", "hidden");

        words.nodes().forEach(w => {
            dehighlight(d3.select(w))
        });
    }

    function getGlobalBoundingBox() {
        let boundingrect = global_boundingbox.node().getBoundingClientRect();
        return boundingrect;
    }
        
    function drawBoundingBox() {
        // get maximium bounding box of all words
        let boundingrects = boundingboxes.nodes().map(n => n.getBoundingClientRect());

        let maxTop = Number.POSITIVE_INFINITY;
        let maxBottom = Number.NEGATIVE_INFINITY;
        let maxLeft = Number.POSITIVE_INFINITY;
        let maxRight = Number.NEGATIVE_INFINITY;

        // compute the bounding box of the whole graph
        for (let rect of boundingrects) {    
            maxTop = Math.min(maxTop, rect.top);
            maxBottom = Math.max(maxBottom, rect.bottom);
            maxLeft = Math.min(maxLeft, rect.left);
            maxRight = Math.max(maxRight, rect.right);            
        }

        let svgTopLeft = screenToSVG(maxLeft, maxTop);

        global_boundingbox
            .attr("visibility", "hidden")
            //.transition()
            .attr("x", svgTopLeft.x)
            .attr("y", svgTopLeft.y)
            .attr("width", maxRight - maxLeft)
            .attr("height", maxBottom - maxTop)
            .attr("fill", "none")
            .attr("stroke", "black")
            .attr("stroke-width", 1)
            .attr("id", "boundingbox");
    }

    let top_unrealized_links = [];

    function enableAdjacenciesMode() {
        ADJACENCIES_MODE = true;
        dehighlight_edge(links);
        hide(links);

        let top_unrealized_links_g = graph.links.filter(l => !realized_links.includes(l)).sort((a, b) => b.similarity - a.similarity).slice(0, 10);
        top_unrealized_links = links.filter(l => top_unrealized_links_g.includes(l));

        show(top_unrealized_links);
        highlightEdgeColor(top_unrealized_links, config.UNREALIZED_ADJACENCIES_COLOR);
        showRealizedAdjacencies();

        if (selected != null) {
            enableLocalAdjacencyMode();
        }
    }

    function showRealizedAdjacencies() {
        show(links.filter(l => realized_links.includes(l)));
        highlightEdgeColor(links.filter(l => realized_links.includes(l)), config.REALIZED_ADJACENCIES_COLOR);
    }

    function disableAdjacenciesMode() {
        ADJACENCIES_MODE = false;
        dehighlight_edge(links);
        hide(links);
    }

    function enableDistortionMode() {
        DISTORTION_MODE = true;       
        drawBoundingBox();  

        // get distances between all words in the graph in a 2d array
        let boundingrects = boundingboxes.nodes().map(n => n.getBoundingClientRect());
        let distances = graph.links.map(link => getMinimumDistanceBetweenRectangles(link, boundingrects));
        let diameter = Math.max(...distances);
        let ideal_distances = graph.links.map(link => (1-link.similarity) * diameter/2)
        
        // compute approximate score for each word: sum the difference between the distance and the ideal distance for each neighbor of the word
        let scores = [];
        let num_of_neighbors = [];

        // iterate over graph links
        for (let link of graph.links) {
            let source = link.source.id;
            let target = link.target.id;
            let distance = distances[link.index];
            let ideal_distance = ideal_distances[link.index];

            if (scores[source] == null) {
                scores[source] = 0;
                num_of_neighbors[source] = 0;
            }
            num_of_neighbors[source] += 1;
            if (scores[target] == null) {
                scores[target] = 0;
                num_of_neighbors[target] = 0;
            }
            num_of_neighbors[target] += 1;
            
            if (link.similarity == 0) { // non-related words: we only care if they are too close
                if (distance < ideal_distance) {
                    scores[source] += Math.pow(ideal_distance - distance, 2);
                    scores[target] += Math.pow(ideal_distance - distance, 2);
                }
            } else { // related words: we care in any case if they differ too much from the ideal distance (question is if ideal_distance is accurate)
                scores[source] += Math.abs(ideal_distance - distance);
                scores[target] += Math.abs(ideal_distance - distance);
            }
        }

        // iterate over scores and divide by number of neighbors
        for (let i = 0; i < scores.length; i++) {
            scores[i] = scores[i] / num_of_neighbors[i];
        }

        // sort words by score
        let sorted_words = graph.nodes.map((w, i) => {return {id: w.id, word: w.word, score: scores[i]}}).sort((a, b) => b.score - a.score);

        console.log(sorted_words)

        // highlight top 5 words
        let top_words = sorted_words.slice(0, 5);

        top_words.forEach(w => {
            highlightBoundingBox(getBoundingBoxNode(w.id))
        });

        
        if (selected != null) {
            enableLocalDistortionMode();
        }
    }

    function probeDistortion(word_id) {
        let global_boundingbox = getGlobalBoundingBox();
        let boundingrects = boundingboxes.nodes().map(n => n.getBoundingClientRect());

        let word = boundingrects[word_id]
        
        let left = global_boundingbox.left 
        let right = global_boundingbox.right 
        let top = global_boundingbox.top 
        let bottom = global_boundingbox.bottom 

        let width = right - left;
        let height = bottom - top;
        
        let cellWidth = width / config.GRID_NUMBER_X;
        let cellHeight = height / config.GRID_NUMBER_Y;

        let svg_global_boundingbox_top_left = screenToSVG(left, top);

        let testpoints = utils.generateGridCenterPoints(left, top, cellWidth, cellHeight, config.GRID_NUMBER_X, config.GRID_NUMBER_Y);

        let results = []

        let minDistortion = Number.POSITIVE_INFINITY
        let maxDistortion = 0

        for (let i = 0; i < testpoints.length; i++) {
            let testpoint = testpoints[i];
            let temp = new DOMRect(testpoint.x-word.width/2, testpoint.y-word.height/2, word.width, word.height)
            boundingrects[word_id] = temp;
            let distortion = approximateDistortion(graph.links, boundingrects)
            results.push({id: i, point: screenToSVG(testpoint.x-cellWidth/2, testpoint.y-cellHeight/2), distortion: distortion})
            minDistortion = Math.min(minDistortion, distortion)
            maxDistortion = Math.max(maxDistortion, distortion)
        }

        return { results: results, cellWidth: cellWidth, cellHeight: cellHeight, minDistortion: minDistortion, maxDistortion: maxDistortion };
    }

    
    function disableDistortionMode() {
        DISTORTION_MODE = false;

        d3.selectAll(".boundingbox").style("opacity", 0);
        d3.selectAll(".boundingbox").style("stroke", "black");

        hide(grid);
    }

    function enableLocalAdjacencyMode() {
        hide(top_unrealized_links);
        showRealizedAdjacencies();
        let unrealized_neighbor_adjacencies = links.filter(l => filterLinksByWord(l, selected) && !realized_links.includes(l));
        show(unrealized_neighbor_adjacencies);
        highlightEdgeColor(unrealized_neighbor_adjacencies, config.UNREALIZED_ADJACENCIES_COLOR);
        highlight(selected.node);
    }

    function disableLocalAdjacencyMode() {
        dehighlight(words);
        selected.node.style("fill", d => { return getColor(d.id); });
        let unrealized_neighbor_adjacencies = links.filter(l => filterLinksByWord(l, selected) && !realized_links.includes(l));
        dehighlight_edge(unrealized_neighbor_adjacencies);
        hide(unrealized_neighbor_adjacencies)
        show(top_unrealized_links);
        highlightEdgeColor(top_unrealized_links, config.UNREALIZED_ADJACENCIES_COLOR);
    }

    function enableLocalDistortionMode() {
        let { results, cellWidth, cellHeight, minDistortion, maxDistortion } = probeDistortion(selected.id);
        let color = d3.scaleSequential(config.DISTORTION_COLOR_SCALE)        
        grid.selectAll("rect.grid-cell")
            .data(results, d => d.id)
            .join("rect")
                //.transition()
                .attr("class", "grid-cell")
                .attr("x", d => d.point.x)
                .attr("y", d => d.point.y)
                .attr("width", cellWidth)
                .attr("height", cellHeight)
                .attr("opacity", 0.8)
                .attr("fill", d => color((d.distortion-minDistortion) / (maxDistortion-minDistortion)))

        nodes.raise();
        links.raise();
        show(grid);
    }

    function disableLocalDistortionMode() {
        hide(grid);
    }

    function labelWordsBFS(root) {
        let visited = new Set();
        let level = 1;
        let nextLevel = [];
        let labels = graph.nodes.map(_ => -1);
        let moves = graph.nodes.map(_ => false);

        labels[root.id] = level;
        moves[root.id] = true;
        visited.add(root.id);
        nextLevel.push(root);

        while (nextLevel.length > 0) {
            level++;
            let currentLevel = nextLevel;
            nextLevel = [];
            currentLevel.forEach(node => {
                let neighbors = graph.links.filter(l => l.similarity > 0 && (l.source.id == node.id || l.target.id == node.id)).map(l => { return { node: (l.source.id == node.id ? l.target : l.source), similarity: l.similarity } });
                neighbors.forEach(neighbor => {
                    let n = neighbor.node;
                    if (!visited.has(n.id)) {
                        labels[n.id] = level;
                        moves[n.id] = (1/level)*neighbor.similarity > 0.1 ? true : false;
                        visited.add(n.id);
                        nextLevel.push(n);
                    }
                }
                );
            }
            );

        }

        graph.nodes.forEach(n => { n.level = labels[n.id]; n.moves = moves[n.id]; });
    }

    function initializeDragAndScrollHandler() {
        let sorted_neighbors = [];
        let delta = 0;
        
        // Create a drag handler
        let drag_handler = d3.drag()
            .on("start", dragstarted)
            .on("drag", drag)
            .on("end", dragended)

        // Apply the handler to all word nodes
        drag_handler(nodes);

        // Attach the scroll handler to the svg
        svg.on("wheel", mouseWheel)

        let original;

        function dragstarted(event, d) {
            memento.storeMemento({graph: graph, metrics: 0});

            dragged = d;
            d.forceMode = 1;
            highlight(getWordNode(d.id));

            labelWordsBFS(d);

            // get neighbors sorted by similarity from graph links
            sorted_neighbors = utils.getNeighborsSortedBySimilarity(graph.links, d);

            if (!event.active) {
                forces.enableDragging(graph.nodes, dragged);
            }
            d.fx = d.x;
            d.fy = d.y;
        }

        let fast = true;
        let x;

        function drag(event, d) {
            d.fx = event.x; d.x = event.x;
            d.fy = event.y; d.y = event.y;
        }

        function dragended(event, d) {
            // dehighlight all dragged words
            dehighlight(getWordNode(d.id));
            sorted_neighbors.forEach(d => dehighlight(getWordNode(d.id)) );
            sorted_neighbors.forEach(d => d.forceMode = 0);

            sorted_neighbors = [];
            dragged_neighbors = [];
            d.forceMode = 0;
            if (x) clearTimeout(x);
            if (!event.active) {
                forces.disableDragging(graph.nodes);
            }
            d.fx = null;
            d.fy = null;

            delta = 0;
            dragged = null;
        }

        let timerid = 0;
        function mouseWheel(event) {
            clearTimeout(timerid);
            if (sorted_neighbors.length !== 0) {
                delta = Math.max(0, Math.min(delta - event.deltaY, 5000));

                let deltaPerWord = 100;
                let num_selected_neighbors = Math.max(Math.min(Math.floor(delta/deltaPerWord),sorted_neighbors.length),0);

                let selected = sorted_neighbors.slice(0, num_selected_neighbors);
                let unselected = sorted_neighbors.slice(num_selected_neighbors);

                selected.forEach(d => { highlight(getWordNode(d.id)); d.forceMode = 1; });
                unselected.forEach(d => { dehighlight(getWordNode(d.id)); d.forceMode = 0;  });

                timerid = setTimeout(mouseWheelEnd(selected), 100)
            }

            function mouseWheelEnd(selection) {
                dragged_neighbors = selection;
                forces.updateNodes(graph.nodes);
            }
        }
 
    }

    async function transitionToNewPosition() {
        links.data(graph.links).join("line")
            .transition()
            .attr("x1", d => d.source.x )
            .attr("y1", d => d.source.y )
            .attr("x2", d => d.target.x )
            .attr("y2", d => d.target.y );

        await nodes.data(graph.nodes).join("g")
            .transition()
            .attr("transform", d => "translate(" + d.x + "," + d.y + ")")   
        .end();
        
        updateMetrics();
        updateMetricGuides();
    }

    function createButtons() {
        boundingboxes.active = false;
        d3.select("#btn-update-model")
        .on("click", function(){
        updateModel();
        })

        d3.select("#btn-metrics")
        .on("click", function(){
        updateMetrics();
        })

        d3.select("#btn-recompute")
        .on("click", function(){
        recomputeFromScratch();
        })

        d3.select("#btn-fill-holes")
        .on("click", function(){
            forces.fillHoles(true);
            forces.reheat(graph.nodes);
            forces.fillHoles(false);
        })

        d3.select("#btn-undo")
        .on("click", function(){
            let oldgraph = memento.undo().graph;
            if (oldgraph) {
                graph = oldgraph;
                forces.updateNodes(graph.nodes);
                forces.updateLinks(graph.links);
                transitionToNewPosition();
            }
        })

        d3.select("#btn-save")
        .on("click", function(){
            memento.saveMemento({graph: graph, metrics: current_metrics});

            d3.select("#saved-states-dropdown")
                .selectAll("a.saved-state")
                .data(memento.getSavedMementos())
                .join("a")
                .attr("class", "dropdown-item saved-state")
                .attr("href", "#")
                .text((d,i) => "Saved state " + (i+1))
                .on("click", function(event, d) {
                    event.preventDefault();
                    memento.storeMemento({graph: graph, metrics: current_metrics});
                    graph = structuredClone(d.graph);
                    forces.updateNodes(graph.nodes);
                    forces.updateLinks(graph.links);
                    transitionToNewPosition();
                }
            )

        })

        d3.select("#checkbox-compactness")
        .on("input", function(){
            if (this.checked) {
                enableCompactnessMode();
            } else {
                disableCompactnessMode();
            }
        })

        d3.select("#checkbox-adjacencies")
        .on("input", function(){
            if (this.checked) {
                enableAdjacenciesMode();
            } else {
                disableAdjacenciesMode();
            }
        })

        d3.select("#checkbox-distortion")
        .on("input", function(){
            if (this.checked) {
                enableDistortionMode();
            } else {
                disableDistortionMode();
            }
        })

        d3.select("#checkbox-neighbors")
        .on("input", function(){
            if (this.checked) {
                forces.neighborsFollow(true);
            } else {
                forces.neighborsFollow(false);
            }
        })

        d3.select("#checkbox-holes")
        .on("input", function(){
            if (this.checked) {
                forces.fillHoles(true);
            } else {
                forces.fillHoles(false);
            }
        })


        d3.select("#checkbox-bbox")
            .on("input", function(){
                if (this.checked) {
                    d3.selectAll(".boundingbox").style("opacity", 0.5);
                    boundingboxes.active = true;
                } else {
                    d3.selectAll(".boundingbox").style("opacity", 0);
                    boundingboxes.active = false;
                }
            })

    }

    function updateMetrics() {
        let boundingrects = boundingboxes.nodes().map(n => n.getBoundingClientRect());


        let metrics = runMetrics(graph.links, boundingrects);
        prev_metrics = current_metrics ?? metrics;
        if (current_metrics == null) {
            top_metrics = {};
            top_metrics.realizedAdjacencies = {value: metrics.realizedAdjacencies, graph: structuredClone(graph)};
            top_metrics.distortion = {value: metrics.distortion, graph: structuredClone(graph)};
            top_metrics.compactness = {value: metrics.compactness, graph: structuredClone(graph)};

            d3.select("#metrics-realized-adjacencies-top")
                .text((100*top_metrics.realizedAdjacencies.value).toFixed(2) + '%')
                .on("click", function(event, d) {
                    memento.storeMemento({graph: graph, metrics: current_metrics});
                    graph = structuredClone(top_metrics.realizedAdjacencies.graph);
                    forces.updateNodes(graph.nodes);
                    forces.updateLinks(graph.links);
                    transitionToNewPosition();
                });

            d3.select("#metrics-distortion-top")
                .text((100*top_metrics.distortion.value).toFixed(2) + '%')
                .on("click", function(event, d) {
                    memento.storeMemento({graph: graph, metrics: current_metrics});
                    graph = structuredClone(top_metrics.distortion.graph);
                    forces.updateNodes(graph.nodes);
                    forces.updateLinks(graph.links);
                    transitionToNewPosition();
                });

            d3.select("#metrics-compactness-top")  
                .text((100*top_metrics.compactness.value).toFixed(2) + '%')
                .on("click", function(event, d) {
                    memento.storeMemento({graph: graph, metrics: current_metrics});
                    graph = structuredClone(top_metrics.compactness.graph);
                    forces.updateNodes(graph.nodes);
                    forces.updateLinks(graph.links);
                    transitionToNewPosition();
                });
        }
        current_metrics = metrics;

        if (current_metrics.realizedAdjacencies > top_metrics?.realizedAdjacencies.value ?? 0) {
            top_metrics.realizedAdjacencies.value = current_metrics.realizedAdjacencies;
            top_metrics.realizedAdjacencies.graph = structuredClone(graph);

            d3.select("#metrics-realized-adjacencies-top")
                .text((100*top_metrics.realizedAdjacencies.value).toFixed(2) + '%');
        }

        if (current_metrics.distortion > top_metrics?.distortion.value ?? 0) {
            top_metrics.distortion.value = current_metrics.distortion;
            top_metrics.distortion.graph = structuredClone(graph);

            d3.select("#metrics-distortion-top")
                .text((100*top_metrics.distortion.value).toFixed(2) + '%');
        }

        if (current_metrics.compactness > top_metrics?.compactness.value ?? 0) {
            top_metrics.compactness.value = current_metrics.compactness;
            top_metrics.compactness.graph = structuredClone(graph);

            d3.select("#metrics-compactness-top")
                .text((100*top_metrics.compactness.value).toFixed(2) + '%');
        }

        let absolute_change = {
            realizedAdjacencies: metrics.realizedAdjacencies - prev_metrics.realizedAdjacencies,
            distortion: metrics.distortion - prev_metrics.distortion,
            compactness: metrics.compactness - prev_metrics.compactness
        }

        d3.select("#metrics-realized-adjacencies-prev")
            .text((100*prev_metrics.realizedAdjacencies).toFixed(2) + '%');

        d3.select("#metrics-distortion-prev")
            .text((100*prev_metrics.distortion).toFixed(2) + '%');

        d3.select("#metrics-compactness-prev")
            .text((100*prev_metrics.compactness).toFixed(2) + '%');

        d3.select("#metrics-realized-adjacencies")
            .text((100*metrics.realizedAdjacencies).toFixed(2) + '%' + " (" + (absolute_change.realizedAdjacencies<0?"":"+") + (100*absolute_change.realizedAdjacencies).toFixed(2) + ")");

        d3.select("#metrics-distortion")
            .text((100*metrics.distortion).toFixed(2) + '%' + " (" + (absolute_change.distortion<0?"":"+") + (100*absolute_change.distortion).toFixed(2) + ")");

        d3.select("#metrics-compactness")
            .text((100*metrics.compactness).toFixed(2) + '%' + " (" + (absolute_change.compactness<0?"":"+") + (100*absolute_change.compactness).toFixed(2) + ")");

        realized_links = metrics.realizedLinks;


    }

    function updateMetricGuides() {
        if (COMPACTNESS_MODE) {
            enableCompactnessMode();
        }

        if (ADJACENCIES_MODE) {
            enableAdjacenciesMode();
        }

        if (DISTORTION_MODE) {
            enableDistortionMode();
        }
    }

    let wordClick = function(event){
        event.preventDefault();

        let element = d3.select(this);
        let clicked = {
            node: element,
            data: element.data()[0],
            id: element.data()[0].id,
        };

        // depending on the metrics mode selected, different behaviour is invoked
        // relevant for local/individual investigation are only the Adjacencies and Distortion modes
        
        if (selected == null || selected.id !== clicked.id) {
            if (selected != null) {
                dehighlight(selected.node);

                if (ADJACENCIES_MODE) {
                    let unrealized_neighbor_adjacencies = links.filter(l => filterLinksByWord(l, selected) && !realized_links.includes(l));
                    dehighlight_edge(unrealized_neighbor_adjacencies);
                    hide(unrealized_neighbor_adjacencies);
                }
            }
            highlight(clicked.node);
            selected = clicked;

            if (ADJACENCIES_MODE) enableLocalAdjacencyMode();
            if (DISTORTION_MODE) enableLocalDistortionMode();
            
        } else if (selected.id === clicked.id) {
            if (ADJACENCIES_MODE) disableLocalAdjacencyMode();
            if (DISTORTION_MODE) disableLocalDistortionMode();
            dehighlight(clicked.node);
            selected = null;
        }
    
    } 

    function updateTable() {
        let table = d3.select("#table-container>table");
        let wordSpan = d3.select("#selected-word");

        if (selected !== null) {
            wordSpan.html(selected.data.word);
            table.selectAll("tr").data([]).exit().remove();

            let temp = table.selectAll("tr")
                .data(graph.links.filter(l => (l.source.id == selected.id || l.target.id == selected.id) && !(l.source.id == l.target.id)).sort(bySimilarityLex));
            
            let temp2 = temp.enter().append("tr");

            temp2.append("td").text(d => d.target.id === selected.id ? d.source.word : d.target.word)
                    .attr("class", "table-word px-3")
                    .attr("font-size", 15)

            temp2.append("td").append("input")
                    .attr("type", "number")
                    .attr("step", "0.01")
                    .attr("max", "1")
                    .attr("min", "0")
                    .attr("lang", "en-150")
                    .attr("value", d => d.similarity)
                    .attr("class", "table-similarity-value")
                    .attr("id", d => `similarity_${d.source.id}-${d.target.id}`)
                    .attr("source", d=> d.source.id)
                    .attr("target", d => d.target.id)
        } else {
            wordSpan.html("");
            table.selectAll("tr").data([]).exit().remove();
        }

        // comparator function
        function bySimilarityLex(a,b) {
            if (a.similarity > b.similarity) return -1;
            else if (a.similarity < b.similarity) return 1;
            else {
                let wordA = a.target.id === selected.id ? a.source.word : a.target.word;
                let wordB = b.target.id === selected.id ? b.source.word : b.target.word;
                if (wordA < wordB) return -1;
                if (wordA > wordB) return 1;
                }
        }
    }

    function updateModel() {
        // TODO: Check if this is also possible with D3? Currently using standard JS DOM queries, did not get D3 to work

        let inputElements = document.querySelectorAll("input.table-similarity-value");

        inputElements.forEach(e => {
            // convert value to float
            let newValue = parseFloat(e.value);
            let oldValue = parseFloat(e.getAttribute("value"));
            if (newValue < 0 || newValue > 1) {
                e.value = oldValue; // TODO: Add error message
            } else if (oldValue !== newValue) {
                let sourceId = e.getAttribute("source");
                let targetId = e.getAttribute("target");
                let link = graph.links.find(l => l.source.id == sourceId && l.target.id == targetId); // TODO: Efficiency?
                link.similarity = newValue;
                link.distance = -Math.log(0.95*newValue + 0.05);
            }
        });

        selected.data.forceMode = 2;

        updateTable();
        update();
    }

    function update() {
        createOrUpdateWordCloud();

        console.error("DEPRECATED FUNCTION CALL: update()");
    }

    function getNode(id) {
        return nodes.select("#word"+id).node();
    }

    function getWordNode(id) {
        return d3.select(words.nodes()[id]);
    }

    function getBoundingBoxNode(id) {
        return d3.select(boundingboxes.nodes()[id]);
    }

    function screenToSVG(x, y) {
        let pt = svg.node().createSVGPoint();
        pt.x = x;
        pt.y = y;
        return pt.matrixTransform(svg.node().getScreenCTM().inverse());
    }

    function SVGToScreen(x, y) {
        let pt = svg.node().createSVGPoint();
        pt.x = x;
        pt.y = y;
        return pt.matrixTransform(svg.node().getScreenCTM());
    }

    // MAIN CALL
    initialize(data);
}
