import * as config from './config.js'
import * as utils from './utils.js'
import rectCollide from './collide.js';
import { getNode, createDistanceMatrix, getBoundingBox, filterLinksByWord, lowlight, dehighlight, highlight, dehighlight_edge, highlight_edge, highlightEdgeColor, show, hide, getColor } from './utils.js';
import { saveMemento } from './memento.js';

export default function(graph, width, height, nodes, links, updateMetrics, updateMetricGuides) {
    let simulation, forces;
    let linkForce, linkForceDecreasingByLevel, collisionForce, centeringForce, anchoringForce, draggedLinkForce, draggedCollisionForce;

    let dragged;

    simulation = d3.forceSimulation(graph.nodes);
    initializeForces();

    let forceModes = {
        initial: 0,
        drag: 1,
        recompute: 2
    }

    let movementMode = {
        neighbors: false,
        holes: false
    }

    let forceMode = forceModes.initial;
            
    function initializeForces() {
        /*** MAIN FORCES ***/

        // attractive force between nodes based on their similiarity
        linkForce = d3.forceLink(graph.links)
            // if one of the adjacent nodes is currently deactivated, disable this link force
            .strength(l => (l.source.forceMode == 0 && l.target.forceMode == 0) ? 
            l.similarity*config.LINK_FORCE_STRENGTH : 
            0) 

        // repelling force avoiding node overlap // custom force (see ./collide.js)
        collisionForce = rectCollide()
            // if the node is currently deactivated, we set the bounding box to an empty rect to disable collision (necessary workaround as this is a custom force)
            .size(d => d.forceMode == 0 ? getBoundingBox(getNode(d.id)).arr : utils.emptyRectangle)
            .strength(config.COLLISION_FORCE_STRENGTH)

        // positioning force that centers nodes (TODO: and ensures custom aspect ratio)
        centeringForce = {
            x: d3.forceX(width/2).strength(config.INITIAL_X_FORCE_STRENGTH),
            y: d3.forceY(height/2).strength(config.INITIAL_Y_FORCE_STRENGTH),
        }

        // positioning force that anchors nodes at their position
        anchoringForce = {
            x: d3.forceX(d => d.x).strength(d => d.forceMode == 0 ? config.FINAL_POSITION_FORCE_STRENGTH : 0),
            y: d3.forceY(d => d.y).strength(d => d.forceMode == 0 ? config.FINAL_POSITION_FORCE_STRENGTH : 0)
        }

        /*** FORCES FOR DRAGGED NODE AND ITS SELECTED NEIGHBORS ***/

        // attractive force between dragged node and its neighbors
        draggedLinkForce = d3.forceLink(graph.links)
            // enable this force only for links between dragged word and selected neighbors // TODO: Fix dragged variable here
            .strength(l => (l.source.forceMode == 1 && l.target.forceMode == 1 && (l.source.id == dragged.id || l.target.id == dragged.id)) ? 1 : 0) 
            // we want them to be attached closely to the dragged node, so we set the desired distance to 0
            .distance(0)

        // repelling force avoiding overlap between dragged node and its neighbors
        draggedCollisionForce = rectCollide()
                .size(d => d.forceMode == 1 ? getBoundingBox(getNode(d.id)).arr : utils.emptyRectangle)
                .strength(config.COLLISION_FORCE_STRENGTH);
    }

    let time = 0;

    function initialLayout() {
        //console.log("initial layout");
        forceMode = forceModes.initial;

        simulation.force("link", linkForce);
        simulation.force("collide", collisionForce);
        simulation.force("x", centeringForce.x);
        simulation.force("y", centeringForce.y);
        simulation.force("link2", draggedLinkForce);
        simulation.force("collide2", draggedCollisionForce);

        // if we want to animate the initial forces, we just attach the tick handler
        if (config.FIRST_PASS_ANIMATED) {
            simulation.on("tick", ticked)
                .on("end", _ => { 
                    updateMetrics(); 
                    simulation.force("x", anchoringForce.x);
                    simulation.force("y", anchoringForce.y); 
                    console.log("Simulation finished");
                    //storeMemento(graph);
                });
        } else { 
            computeStaticLayout(simulation);
            simulation.on("tick", ticked)
                .on("end.metrics", _ => updateMetrics())
                .on("end.metric-guides", _ => updateMetricGuides())
                .on("end.anchoring", _ => {
                    simulation.force("x", anchoringForce.x);
                    simulation.force("y", anchoringForce.y);
                    time = 0;
                })
                .on("end.console", _ => console.log("Simulation finished"))
                //.on("end.memento", _ => storeMemento(graph));
            simulation.restart();
        }
    }

    function dragging() {
        console.log("dragging");
        forceMode = forceModes.drag;

        // we disable all forces except the anchoring force
        simulation.force("link", null);
        simulation.force("collide", null);
        simulation.force("x", anchoringForce.x);
        simulation.force("y", anchoringForce.y);
    }

    function recomputeLayout() {
        console.log("recompute");
        forceMode = forceModes.recompute;

        // enable all forces again
        simulation.force("link", linkForce.strength(0));
        simulation.force("collide", collisionForce);
        simulation.force("x", anchoringForce.x);
        simulation.force("y", anchoringForce.y);
    }

    function ticked() {
        for (let i = 0; i < 10; i++) {
            if (forceMode == forceModes.recompute && config.VARIABLE_RECOMPUTATION_STRENGTH) {
                //tickStrength();
            }
            simulation.tick();
        }

        links
            .attr("x1", d => d.source.x )
            .attr("y1", d => d.source.y )
            .attr("x2", d => d.target.x )
            .attr("y2", d => d.target.y );

        nodes
            .attr("transform", d => "translate(" + d.x + "," + d.y + ")")
    }

    function tickStrength() {
        let totalTime = 20 * nodes.size();
        let x = time / totalTime;
        console.log(time);

        if (time == 0) {
            simulation.alphaDecay(0);
            simulation.force("collide").strength(config.COLLISION_FORCE_STRENGTH);
            simulation.force("x").strength(d => d.level > 0 ?
                (1-1/Math.pow(d.level,2))*config.ANCHORING_FORCE_STRENGTH : 0);
            simulation.force("y").strength(d => d.level > 0 ?
                (1-1/Math.pow(d.level,2))*config.ANCHORING_FORCE_STRENGTH : 0);
        }

        // collision strength is a logistic function of time
        let logisticInc = 1 / (1 + Math.exp(-10*x + 5));

        // x/y anchoring strength of neighbors is a inverse logistic function of time
        let logisticDec = (1 - 1 / (1 + Math.exp(-10*x + 5)));
        console.log(logisticDec);

        if (time < totalTime) {
            simulation.force("link").strength(l => (1/(Math.max(l.source.level, l.target.level)-1))*l.similarity > 0.1 ?
                (1/(Math.max(l.source.level, l.target.level)-1))*l.similarity * logisticDec : 0);

            

            
            //simulation.force("collide").strength(config.COLLISION_FORCE_STRENGTH * logisticInc);


            //simulation.force("x").strength(d => d.level == 1 ? 5 : 5*config.ANCHORING_FORCE_STRENGTH * logisticDec);
            //simulation.force("y").strength(d => d.level == 1 ? 5 : 5*config.ANCHORING_FORCE_STRENGTH * logisticDec);
        } 

        if (time == totalTime) {
            simulation.alphaDecay(0.1);
            simulation.force("link").strength(0);
            simulation.force("collide").strength(config.COLLISION_FORCE_STRENGTH);
            simulation.force("x").strength(0);
            simulation.force("y").strength(0);
        }

        time++;
/*
            // collision strength is a logistic function of time
            let logisticInc = 1 / (1 + Math.exp(-5*x + 5));

            // x/y anchoring strength of neighbors is a inverse logistic function of time
            let logisticDec = 5 * (1 - 1 / (1 + Math.exp(-5*x + 5)));

            // x/y anchoring strength of newly placed word stays very high up to 70% of the time, then decreases to the configured value
            let anchoringStrengthNew;
            if (time < 0.7*totalTime) {
                anchoringStrengthNew = config.ANCHORING_FORCE_STRENGTH * 2;
            } else if (time < totalTime) {
                let x2 = (time - 0.7*totalTime) / (0.3*totalTime);
                anchoringStrengthNew = config.ANCHORING_FORCE_STRENGTH * (2 - Math.pow((x2-1), 2));
            } else {
                anchoringStrengthNew = config.ANCHORING_FORCE_STRENGTH;
            }

            // set the strengths of the forces
            //simulation.force("collide").strength(0);
            //simulation.force("link", linkForce.strength(l => (1/Math.pow(Math.min(l.source.level, l.target.level),3))*0.5 * logisticDec));
            //simulation.force("y", anchoringForce.y.strength(d => d.forceMode == 0 ? 1 : 0));
            //simulation.force("x", anchoringForce.x.strength(d => d.forceMode == 0 ? 1 : 0));
        //} else {
            //simulation.force("collide").strength(config.COLLISION_FORCE_STRENGTH);
        //}*/
    }

    async function updateStaticPositions() {
        links
            .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
            .transition()
            .attr("transform", d => "translate(" + d.x + "," + d.y + ")")
        .end();

        updateMetrics();
        updateMetricGuides();
    }

    // to get a static layout without animation, we perform a fixed number of iterations
    function computeStaticLayout() {
        let startTime = performance.now();
        for (let i = 0; i < 20*nodes.size(); i++) {
            simulation.tick();
        }
        simulation.stop();
        let endTime = performance.now();
        console.log("Static layout computed in " + (endTime - startTime) + " ms");
        updateStaticPositions();
    }

    function convergeStaticLayout() {

        if (movementMode.neighbors == true) {
            simulation.force("collide").strength(0.1*config.COLLISION_FORCE_STRENGTH);
            simulation.force("x").strength(d => d.moves ? d.level !== 1 ? 0.05*config.ANCHORING_FORCE_STRENGTH : 0.1*config.ANCHORING_FORCE_STRENGTH : config.ANCHORING_FORCE_STRENGTH);
            simulation.force("y").strength(d => d.moves ? d.level !== 1 ? 0.05*config.ANCHORING_FORCE_STRENGTH : 0.1*config.ANCHORING_FORCE_STRENGTH : config.ANCHORING_FORCE_STRENGTH);
            simulation.force("link").strength(l => l.source.moves && l.target.moves ?
                (1/(Math.max(l.source.level, l.target.level)-1))*0.2*l.similarity : 0);

            let startTime = performance.now();
            for (let i = 0; i < 100; i++) {
                simulation.tick();
            }
            let endTime = performance.now();
            console.log("Static layout 1 converged in " + (endTime - startTime) + " ms");

            simulation.force("collide").strength(config.COLLISION_FORCE_STRENGTH);
        } else {
            // enable all forces again
            simulation.force("link", linkForce);
            simulation.force("collide", collisionForce);
            simulation.force("x", anchoringForce.x);
            simulation.force("y", anchoringForce.y);
        }

        if (movementMode.holes == true) {
            simulation.force("x", centeringForce.x.strength(0.25*config.INITIAL_X_FORCE_STRENGTH));
            simulation.force("y", centeringForce.y.strength(0.25*config.INITIAL_Y_FORCE_STRENGTH));
            simulation.force("xA", anchoringForce.x.strength(config.INITIAL_X_FORCE_STRENGTH));
            simulation.force("yA", anchoringForce.y.strength(config.INITIAL_Y_FORCE_STRENGTH));
            simulation.force("collide").strength(config.COLLISION_FORCE_STRENGTH);
        }

        let startTime = performance.now();
        for (let i = 0; i < 250; i++) {
            simulation.tick();
        }
        let endTime = performance.now();
        console.log("Static layout 2 converged in " + (endTime - startTime) + " ms");

        simulation.force("xA", null);
        simulation.force("yA", null);

        simulation.stop();
        updateStaticPositions();
        //updateMetrics();
        //updateMetricGuides();
    }

    return forces = {

        simulation: simulation,

        stop: () => simulation.stop(),

        initialLayout: initialLayout,

        enableDragging: (nodes, dragged) => {
            dragged = dragged;
            dragging();
            simulation.nodes(nodes)
                .alpha(0.01)
                .alphaTarget(0.01)
                .restart();
        },

        reheat: (nodes) => {
            recomputeLayout();
            simulation.nodes(nodes)
                .alpha(1)
                .alphaTarget(0.1)
                
            if (config.DYNAMIC_RECOMPUTATION) {
                simulation.restart();
            } else {
                convergeStaticLayout();
            }
        },

        disableDragging: (nodes) => {
            simulation.nodes(nodes)
                .alpha(1)
                .alphaDecay(0.01) 
                //.velocityDecay(0.8)
                .alphaTarget(0);
            recomputeLayout();
            if (config.DYNAMIC_RECOMPUTATION) {
                simulation.restart();
            } else {
                convergeStaticLayout();
            }
        },

        restart: () => {
            simulation.restart();
        },

        updateNodes: (nodes) => {
            simulation.nodes(nodes);
        },

        updateLinks: (links) => {
            simulation.force("link").links(links);
        },

        reinitialize: (nodes, links) => {
            simulation.nodes(nodes);
            simulation.force("link").links(links);
            initializeForces();
        },

        neighborsFollow: (bool) => {
            movementMode.neighbors = bool;
        },

        fillHoles: (bool) => {
            movementMode.holes = bool;
        }
    }



}
