import type { SimulationLinkDatum, SimulationNodeDatum } from "d3";
import * as d3 from "d3";
import { createEdges, TaskDescriptor, TasksFile } from "./tasks";

type TaskId = {
    id: string;
    task: TaskDescriptor;
} & SimulationNodeDatum;

function toMapById(nodes: TaskId[]): Map<string, TaskId> {
    let nodeMap = new Map<string, TaskId>();
    for (let task of nodes) {
        if (task.id in nodeMap)
            throw 'duplicate IDs';
        nodeMap.set(task.id, task);
    }
    return nodeMap;
}

function taskForce(): d3.Force<TaskId, undefined> {
    let myNodes: TaskId[] | null = null;
    let deps: Map<string, number> = new Map();
    let idMap: Map<string, TaskId> = new Map();

    function getNumberOfDeps(task: TaskId): number {
        if (deps.has(task.id)) return deps.get(task.id)!;

        if (task.task.requires.length == 0) return 0;

        let res = 0;
        for (let r of task.task.requires) {
            res += getNumberOfDeps(idMap.get(r)!) + 1;
        }
        deps.set(task.id, res);
        return res;
    }

    let force: d3.Force<TaskId, undefined> = function (alpha: number) {
        if (myNodes == null) throw 'nodes not initialized';

        for (let task of myNodes) {
            if (task.vy == null) {
                task.vy = 0
            }

            task.vy += getNumberOfDeps(task) * 25 * alpha;
        }
    }

    force.initialize = function (nodes: TaskId[]) {
        myNodes = nodes;
        idMap = toMapById(myNodes);
    }

    return force;
}

/**
 * 
 * @param nodes 
 * @param edges 
 * @param ticked function that gets run every tick of a running simulation
 */
export function forceSimulation(tasks: TasksFile, ticked: (positions: Map<string, [number, number]>) => void, repulsionForce?: number) {
    repulsionForce = repulsionForce ?? -1000;

    let nodes: TaskId[] = tasks.tasks.map(
        (t) => {
            return {
                id: t.id,
                task: t,
                x: (t.position ?? [0, 0])[0],
                y: (t.position ?? [0, 0])[1]
            }
        })
    let edges: SimulationLinkDatum<TaskId>[] = createEdges(tasks.tasks).map((e) => {
        return {
            // FIXME are we sure its not the other way round?
            source: nodes.find((n) => n.id == e.dependee.id)!,
            target: nodes.find((n) => n.id == e.dependency.id)!
        }
    });

    function tickHandler() {
        ticked(new Map(nodes.map((n) => [n.id!, [n.x!, n.y!]])))
    }

    // Let's list the force we wanna apply on the network
    let simulation = d3
        .forceSimulation(nodes) // Force algorithm is applied to data.nodes
        .force(
            "link",
            d3
                .forceLink<TaskId, SimulationLinkDatum<TaskId>>() // This force provides links between nodes
                .id(d => d.id) // This provide  the id of a node
                .links(edges) // and this the list of links
        )
        .force("charge", d3.forceManyBody().strength(repulsionForce)) // This adds repulsion between nodes. Play with the -400 for the repulsion strength
        .force("x", d3.forceX()) // attracts elements to the zero X coord
        .force("y", d3.forceY().strength(0.5)) // attracts elements to the zero Y coord
        .force("dependencies", taskForce())
        .on("tick", tickHandler)
        .on("end", tickHandler);
}