import * as powergraph from './powergraph'
import {LinkLengthAccessor, symmetricDiffLinkLengths, jaccardLinkLengths, generateDirectedEdgeConstraints} from './linklengths'
import {Descent} from './descent'
import {Rectangle, Projection, makeEdgeTo, makeEdgeBetween} from './rectangle'
import {Calculator} from './shortestpaths'
import {TangentVisibilityGraph, TVGPoint} from './geom'
import {separateGraphs, applyPacking} from './handledisconnected'
    /**
     * The layout process fires three events:
     *  - start: layout iterations started
     *  - tick: fired once per iteration, listen to this to animate
     *  - end: layout converged, you might like to zoom-to-fit or something at notification of this event
     */
    export enum EventType { start, tick, end };
    export interface Event {
        type: EventType;
        alpha: number;
        stress?: number;
        listener?: () => void;
    }
    export interface InputNode {
        /**
         * index in nodes array, this is initialized by Layout.start()
         */
        index?: number;
        /**
         * x and y will be computed by layout as the Node's centroid
         */
        x?: number;
        /**
         * x and y will be computed by layout as the Node's centroid
         */
        y?: number;
        /**
         * specify a width and height of the node's bounding box if you turn on avoidOverlaps
         */
        width?: number;
        /**
         * specify a width and height of the node's bounding box if you turn on avoidOverlaps
         */
        height?: number;
        /**
         * selective bit mask.  !=0 means layout will not move.
         */
        fixed?: number;
    }
    export interface Node extends InputNode {
      // Client-passed node may be missing these properties, which will be set
      // upon ingestion
      x: number;
      y: number;
    }

    export interface Group {
        bounds?: Rectangle;
        leaves?: Node[];
        groups?: Group[];
        padding: number;
    }

    function isGroup(g: any): g is Group {
        return typeof g.leaves !== 'undefined' || typeof g.groups !== 'undefined';
    }

    export interface Link<NodeRefType> {
        source: NodeRefType;
        target: NodeRefType;

        // ideal length the layout should try to achieve for this link
        length?: number;

        // how hard we should try to satisfy this link's ideal length
        // must be in the range: 0 < weight <= 1
        // if unspecified 1 is the default
        weight?: number;
    }

    export type LinkNumericPropertyAccessor = (t: Link<Node | number>) => number;

    export interface LinkLengthTypeAccessor extends LinkLengthAccessor<Link<Node | number>> {
        getType: LinkNumericPropertyAccessor;
    }
    /**
     * Main interface to cola layout.
     * @class Layout
     */
    export class Layout {
        private _canvasSize = [1, 1];
        private _linkDistance: number | LinkNumericPropertyAccessor = 20;
        private _defaultNodeSize: number = 10;
        private _linkLengthCalculator = null;
        private _linkType = null;
        private _avoidOverlaps = false;
        private _handleDisconnected = true;
        private _alpha;
        private _lastStress;
        private _running = false;
        private _nodes = [];
        private _groups = [];
        private _rootGroup = null;
        private _links: Link<Node | number>[] = [];
        private _constraints = [];
        private _distanceMatrix = null;
        private _descent: Descent = null;
        private _directedLinkConstraints = null;
        private _threshold = 0.01;
        private _visibilityGraph = null;
        private _groupCompactness = 1e-6;

        // sub-class and override this property to replace with a more sophisticated eventing mechanism
        protected event = null;

        // subscribe a listener to an event
        // sub-class and override this method to replace with a more sophisticated eventing mechanism
        public on(e: EventType | string, listener: (event?: Event) => void): this {
            // override me!
            if (!this.event) this.event = {};
            if (typeof e === 'string') {
                this.event[EventType[e]] = listener;
            } else {
                this.event[e] = listener;
            }
            return this;
        }

        // a function that is notified of events like "tick"
        // sub-classes can override this method to replace with a more sophisticated eventing mechanism
        protected trigger(e: Event) {
            if (this.event && typeof this.event[e.type] !== 'undefined') {
                this.event[e.type](e);
            }
        }

        // a function that kicks off the iteration tick loop
        // it calls tick() repeatedly until tick returns true (is converged)
        // subclass and override it with something fancier (e.g. dispatch tick on a timer)
        protected kick(): void {
            while (!this.tick());
        }

        /**
         * iterate the layout.  Returns true when layout converged.
         */
        protected tick(): boolean {
            if (this._alpha < this._threshold) {
                this._running = false;
                this.trigger({ type: EventType.end, alpha: this._alpha = 0, stress: this._lastStress });
                return true;
            }
            const n = this._nodes.length,
                  m = this._links.length;
            let o, i;

            this._descent.locks.clear();
            for (i = 0; i < n; ++i) {
                o = this._nodes[i];
                if (o.fixed) {
                    if (typeof o.px === 'undefined' || typeof o.py === 'undefined') {
                        o.px = o.x;
                        o.py = o.y;
                    }
                    var p = [o.px, o.py];
                    this._descent.locks.add(i, p);
                }
            }

            let s1 = this._descent.rungeKutta();
            //var s1 = descent.reduceStress();
            if (s1 === 0) {
                this._alpha = 0;
            } else if (typeof this._lastStress !== 'undefined') {
                this._alpha = s1; //Math.abs(Math.abs(this._lastStress / s1) - 1);
            }
            this._lastStress = s1;

            this.updateNodePositions();

            this.trigger({ type: EventType.tick, alpha: this._alpha, stress: this._lastStress });
            return false;
        }

        // copy positions out of descent instance into each of the nodes' center coords
        private updateNodePositions(): void {
            const x = this._descent.x[0], y = this._descent.x[1];
            let o, i = this._nodes.length;
            while (i--) {
                o = this._nodes[i];
                o.x = x[i];
                o.y = y[i];
            }
        }

        /**
         * the list of nodes.
         * If nodes has not been set, but links has, then we instantiate a nodes list here, of the correct size,
         * before returning it.
         * @property nodes {Array}
         * @default empty list
         */
        nodes(): Array<Node>
        nodes(v: Array<InputNode>): this
        nodes(v?: any): any {
            if (!v) {
                if (this._nodes.length === 0 && this._links.length > 0) {
                    // if we have links but no nodes, create the nodes array now with empty objects for the links to point at.
                    // in this case the links are expected to be numeric indices for nodes in the range 0..n-1 where n is the number of nodes
                    var n = 0;
                    this._links.forEach(function (l) {
                        n = Math.max(n, <number>l.source, <number>l.target);
                    });
                    this._nodes = new Array(++n);
                    for (var i = 0; i < n; ++i) {
                        this._nodes[i] = {};
                    }
                }
                return this._nodes;
            }
            this._nodes = v;
            return this;
        }

        /**
         * a list of hierarchical groups defined over nodes
         * @property groups {Array}
         * @default empty list
         */
        groups(): Array<Group>
        groups(x: Array<Group>): this
        groups(x?: Array<Group>): any {
            if (!x) return this._groups;
            this._groups = x;
            this._rootGroup = {};
            this._groups.forEach(g => {
                if (typeof g.padding === "undefined")
                    g.padding = 1;
                if (typeof g.leaves !== "undefined") {
                    g.leaves.forEach((v, i) => {
                        if (typeof v === 'number')
                            (g.leaves[i] = this._nodes[v]).parent = g
                    });
                }
                if (typeof g.groups !== "undefined") {
                    g.groups.forEach((gi, i) => {
                        if (typeof gi === 'number')
                            (g.groups[i] = this._groups[gi]).parent = g
                    });
                }
            });
            this._rootGroup.leaves = this._nodes.filter(v => typeof v.parent === 'undefined');
            this._rootGroup.groups = this._groups.filter(g => typeof g.parent === 'undefined');
            return this;
        }

        powerGraphGroups(f: Function): this {
            var g = powergraph.getGroups(this._nodes, this._links, this.linkAccessor, this._rootGroup);
            this.groups(g.groups);
            f(g);
            return this;
        }

        /**
         * if true, the layout will not permit overlaps of the node bounding boxes (defined by the width and height properties on nodes)
         * @property avoidOverlaps
         * @type bool
         * @default false
         */
        avoidOverlaps(): boolean
        avoidOverlaps(v: boolean): this
        avoidOverlaps(v?: boolean): any {
            if (!arguments.length) return this._avoidOverlaps;
            this._avoidOverlaps = v;
            return this;
        }

        /**
         * if true, the final step of the start method will be to nicely pack connected components of the graph.
         * works best if start() is called with a reasonable number of iterations specified and
         * each node has a bounding box (defined by the width and height properties on nodes).
         * @property handleDisconnected
         * @type bool
         * @default true
         */
        handleDisconnected(): boolean
        handleDisconnected(v: boolean): this
        handleDisconnected(v?: boolean): any {
            if (!arguments.length) return this._handleDisconnected;
            this._handleDisconnected = v;
            return this;
        }

        /**
         * causes constraints to be generated such that directed graphs are laid out either from left-to-right or top-to-bottom.
         * a separation constraint is generated in the selected axis for each edge that is not involved in a cycle (part of a strongly connected component)
         * @param axis {string} 'x' for left-to-right, 'y' for top-to-bottom
         * @param minSeparation {number|link=>number} either a number specifying a minimum spacing required across all links or a function to return the minimum spacing for each link
         */
        flowLayout(axis: string, minSeparation: number|((t: any)=>number)): this {
            if (!arguments.length) axis = 'y';
            this._directedLinkConstraints = {
                axis: axis,
                getMinSeparation: typeof minSeparation === 'number' ? function () { return minSeparation } : minSeparation
            };
            return this;
        }

        /**
         * links defined as source, target pairs over nodes
         * @property links {array}
         * @default empty list
         */
        links(): Array<Link<Node|number>>
        links(x: Array<Link<Node|number>>): this
        links(x?: Array<Link<Node|number>>): any {
            if (!arguments.length) return this._links;
            this._links = x;
            return this;
        }

        /**
         * list of constraints of various types
         * @property constraints
         * @type {array}
         * @default empty list
         */
        constraints(): Array<any>
        constraints(c: Array<any>): this
        constraints(c?: Array<any>): any {
            if (!arguments.length) return this._constraints;
            this._constraints = c;
            return this;
        }

        /**
         * Matrix of ideal distances between all pairs of nodes.
         * If unspecified, the ideal distances for pairs of nodes will be based on the shortest path distance between them.
         * @property distanceMatrix
         * @type {Array of Array of Number}
         * @default null
         */
        distanceMatrix(): Array<Array<number>>
        distanceMatrix(d: Array<Array<number>>): this
        distanceMatrix(d?: any): any {
            if (!arguments.length) return this._distanceMatrix;
            this._distanceMatrix = d;
            return this;
        }

        /**
         * Size of the layout canvas dimensions [x,y]. Currently only used to determine the midpoint which is taken as the starting position
         * for nodes with no preassigned x and y.
         * @property size
         * @type {Array of Number}
         */
        size(): Array<number>
        size(x: Array<number>): this
        size(x?: Array<number>): any {
            if (!x) return this._canvasSize;
            this._canvasSize = x;
            return this;
        }

        /**
         * Default size (assume nodes are square so both width and height) to use in packing if node width/height are not specified.
         * @property defaultNodeSize
         * @type {Number}
         */
        defaultNodeSize(): number
        defaultNodeSize(x: number): this
        defaultNodeSize(x?: any): any {
            if (!x) return this._defaultNodeSize;
            this._defaultNodeSize = x;
            return this;
        }

        /**
         * The strength of attraction between the group boundaries to each other.
         * @property defaultNodeSize
         * @type {Number}
         */
        groupCompactness(): number
        groupCompactness(x: number): this
        groupCompactness(x?: any): any {
            if (!x) return this._groupCompactness;
            this._groupCompactness = x;
            return this;
        }

        /**
         * links have an ideal distance, The automatic layout will compute layout that tries to keep links (AKA edges) as close as possible to this length.
         */
        linkDistance(): number
        linkDistance(): LinkNumericPropertyAccessor
        linkDistance(x: number): this
        linkDistance(x: LinkNumericPropertyAccessor): this
        linkDistance(x?: any): any {
            if (!x) {
                return this._linkDistance;
            }
            this._linkDistance = typeof x === "function" ? x : +x;
            this._linkLengthCalculator = null;
            return this;
        }

        linkType(f: Function | number): this {
            this._linkType = f;
            return this;
        }

        convergenceThreshold(): number
        convergenceThreshold(x: number): this
        convergenceThreshold(x?: number): any {
            if (!x) return this._threshold;
            this._threshold = typeof x === "function" ? x : +x;
            return this;
        }

        alpha(): number
        alpha(x: number): this
        alpha(x?: number): any {
            if (!arguments.length) return this._alpha;
            else {
                x = +x;
                if (this._alpha) { // if we're already running
                    if (x > 0) this._alpha = x; // we might keep it hot
                    else this._alpha = 0; // or, next tick will dispatch "end"
                } else if (x > 0) { // otherwise, fire it up!
                    if (!this._running) {
                        this._running = true;
                        this.trigger({ type: EventType.start, alpha: this._alpha = x});
                        this.kick();
                    }
                }
                return this;
            }
        }

        getLinkLength(link: Link<Node | number>): number {
            return typeof this._linkDistance === "function" ? +((<LinkNumericPropertyAccessor>this._linkDistance)(link)) : <number>this._linkDistance;
        }

        static setLinkLength(link: Link<Node|number>, length: number) {
            link.length = length;
        }

        getLinkType(link: Link<Node | number>): number {
            return typeof this._linkType === "function" ? this._linkType(link) : 0;
        }

        linkAccessor: LinkLengthTypeAccessor = {
            getSourceIndex: Layout.getSourceIndex,
            getTargetIndex: Layout.getTargetIndex,
            setLength: Layout.setLinkLength,
            getType: l => typeof this._linkType === "function" ? this._linkType(l) : 0
        };

        /**
         * compute an ideal length for each link based on the graph structure around that link.
         * you can use this (for example) to create extra space around hub-nodes in dense graphs.
         * In particular this calculation is based on the "symmetric difference" in the neighbour sets of the source and target:
         * i.e. if neighbours of source is a and neighbours of target are b then calculation is: sqrt(|a union b| - |a intersection b|)
         * Actual computation based on inspection of link structure occurs in start(), so links themselves
         * don't have to have been assigned before invoking this function.
         * @param {number} [idealLength] the base length for an edge when its source and start have no other common neighbours (e.g. 40)
         * @param {number} [w] a multiplier for the effect of the length adjustment (e.g. 0.7)
         */
        symmetricDiffLinkLengths(idealLength: number, w: number = 1): this {
            this.linkDistance(l => idealLength * l.length);
            this._linkLengthCalculator = () => symmetricDiffLinkLengths(this._links, this.linkAccessor, w);
            return this;
        }

        /**
         * compute an ideal length for each link based on the graph structure around that link.
         * you can use this (for example) to create extra space around hub-nodes in dense graphs.
         * In particular this calculation is based on the "symmetric difference" in the neighbour sets of the source and target:
         * i.e. if neighbours of source is a and neighbours of target are b then calculation is: |a intersection b|/|a union b|
         * Actual computation based on inspection of link structure occurs in start(), so links themselves
         * don't have to have been assigned before invoking this function.
         * @param {number} [idealLength] the base length for an edge when its source and start have no other common neighbours (e.g. 40)
         * @param {number} [w] a multiplier for the effect of the length adjustment (e.g. 0.7)
         */
        jaccardLinkLengths(idealLength: number, w: number = 1): this {
            this.linkDistance(l => idealLength * l.length);
            this._linkLengthCalculator = () => jaccardLinkLengths(this._links, this.linkAccessor, w);
            return this;
        }

        /**
         * start the layout process
         * @method start
         * @param {number} [initialUnconstrainedIterations=0] unconstrained initial layout iterations
         * @param {number} [initialUserConstraintIterations=0] initial layout iterations with user-specified constraints
         * @param {number} [initialAllConstraintsIterations=0] initial layout iterations with all constraints including non-overlap
         * @param {number} [gridSnapIterations=0] iterations of "grid snap", which pulls nodes towards grid cell centers - grid of size node[0].width - only really makes sense if all nodes have the same width and height
         * @param [keepRunning=true] keep iterating asynchronously via the tick method
         * @param [centerGraph=true] Center graph on restart
         */
        start(
            initialUnconstrainedIterations: number = 0,
            initialUserConstraintIterations: number = 0,
            initialAllConstraintsIterations: number = 0,
            gridSnapIterations: number = 0,
            keepRunning = true,
            centerGraph = true,
        ): this {
            var i: number,
                j: number,
                n = (<Array<any>>this.nodes()).length,
                N = n + 2 * this._groups.length,
                m = this._links.length,
                w = this._canvasSize[0],
                h = this._canvasSize[1];

            var x = new Array(N), y = new Array(N);

            var G = null;

            var ao = this._avoidOverlaps;

            this._nodes.forEach((v, i) => {
                v.index = i;
                if (typeof v.x === 'undefined') {
                    v.x = w / 2, v.y = h / 2;
                }
                x[i] = v.x, y[i] = v.y;
            });

            if (this._linkLengthCalculator) this._linkLengthCalculator();

            //should we do this to clearly label groups?
            //this._groups.forEach((g, i) => g.groupIndex = i);

            var distances;
            if (this._distanceMatrix) {
                // use the user specified distanceMatrix
                distances = this._distanceMatrix;
            } else {
                // construct an n X n distance matrix based on shortest paths through graph (with respect to edge.length).
                distances = (new Calculator(N, this._links, Layout.getSourceIndex, Layout.getTargetIndex, l=> this.getLinkLength(l))).DistanceMatrix();

                // G is a square matrix with G[i][j] = 1 iff there exists an edge between node i and node j
                // otherwise 2. (
                G = Descent.createSquareMatrix(N, () => 2);
                this._links.forEach(l => {
                    if (typeof l.source == "number") l.source = this._nodes[<number>l.source];
                    if (typeof l.target == "number") l.target = this._nodes[<number>l.target];
                });
                this._links.forEach(e => {
                    const u = Layout.getSourceIndex(e), v = Layout.getTargetIndex(e);
                    G[u][v] = G[v][u] = e.weight || 1;
                });
            }

            var D = Descent.createSquareMatrix(N, function (i, j) {
                return distances[i][j];
            });

            if (this._rootGroup && typeof this._rootGroup.groups !== 'undefined') {
                var i = n;
                var addAttraction = (i, j, strength, idealDistance) => {
                    G[i][j] = G[j][i] = strength;
                    D[i][j] = D[j][i] = idealDistance;
                };
                this._groups.forEach(g => {
                    addAttraction(i, i + 1, this._groupCompactness, 0.1);

                    // todo: add terms here attracting children of the group to the group dummy nodes
                    //if (typeof g.leaves !== 'undefined')
                    //    g.leaves.forEach(l => {
                    //        addAttraction(l.index, i, 1e-4, 0.1);
                    //        addAttraction(l.index, i + 1, 1e-4, 0.1);
                    //    });
                    //if (typeof g.groups !== 'undefined')
                    //    g.groups.forEach(g => {
                    //        var gid = n + g.groupIndex * 2;
                    //        addAttraction(gid, i, 0.1, 0.1);
                    //        addAttraction(gid + 1, i, 0.1, 0.1);
                    //        addAttraction(gid, i + 1, 0.1, 0.1);
                    //        addAttraction(gid + 1, i + 1, 0.1, 0.1);
                    //    });

                    x[i] = 0, y[i++] = 0;
                    x[i] = 0, y[i++] = 0;
                });
            } else this._rootGroup = { leaves: this._nodes, groups: [] };

            var curConstraints = this._constraints || [];
            if (this._directedLinkConstraints) {
                (<any>this.linkAccessor).getMinSeparation = this._directedLinkConstraints.getMinSeparation;
                curConstraints = curConstraints.concat(generateDirectedEdgeConstraints(n, this._links, this._directedLinkConstraints.axis, <any>(this.linkAccessor)));

                // todo: add containment constraints between group dummy nodes and their children
            }

            this.avoidOverlaps(false);
            this._descent = new Descent([x, y], D);

            this._descent.locks.clear();
            for (var i = 0; i < n; ++i) {
                var o = this._nodes[i];
                if (o.fixed) {
                    o.px = o.x;
                    o.py = o.y;
                    var p = [o.x, o.y];
                    this._descent.locks.add(i, p);
                }
            }
            this._descent.threshold = this._threshold;

            // apply initialIterations without user constraints or nonoverlap constraints
            // if groups are specified, dummy nodes and edges will be added to untangle
            // with respect to group connectivity
            this.initialLayout(initialUnconstrainedIterations, x, y);

            // apply initialIterations with user constraints but no nonoverlap constraints
            if (curConstraints.length > 0) this._descent.project = new Projection(this._nodes, this._groups, this._rootGroup, curConstraints).projectFunctions();
            this._descent.run(initialUserConstraintIterations);
            this.separateOverlappingComponents(w, h, centerGraph);

            // subsequent iterations will apply all constraints
            this.avoidOverlaps(ao);
            if (ao) {
                this._nodes.forEach(function (v, i) { v.x = x[i], v.y = y[i]; });
                this._descent.project = new Projection(this._nodes, this._groups, this._rootGroup, curConstraints, true).projectFunctions();
                this._nodes.forEach(function (v, i) { x[i] = v.x, y[i] = v.y; });
            }

            // allow not immediately connected nodes to relax apart (p-stress)
            this._descent.G = G;
            this._descent.run(initialAllConstraintsIterations);

            if (gridSnapIterations) {
                this._descent.snapStrength = 1000;
                this._descent.snapGridSize = this._nodes[0].width;
                this._descent.numGridSnapNodes = n;
                this._descent.scaleSnapByMaxH = n != N; // if we have groups then need to scale hessian so grid forces still apply
                var G0 = Descent.createSquareMatrix(N,(i, j) => {
                    if (i >= n || j >= n) return G[i][j];
                    return 0
                });
                this._descent.G = G0;
                this._descent.run(gridSnapIterations);
            }

            this.updateNodePositions();
            this.separateOverlappingComponents(w, h, centerGraph);
            return keepRunning ? this.resume() : this;
        }

        private initialLayout(iterations: number, x: number[], y: number[]) {
            if (this._groups.length > 0 && iterations > 0) {
                // construct a flat graph with dummy nodes for the groups and edges connecting group dummy nodes to their children
                // todo: edges attached to groups are replaced with edges connected to the corresponding group dummy node
                var n = this._nodes.length;
                var edges = this._links.map(e => <any>{ source: (<Node>e.source).index, target: (<Node>e.target).index });
                var vs = this._nodes.map(v => <any>{ index: v.index });
                this._groups.forEach((g, i) => {
                    vs.push(<any>{ index: g.index = n + i });
                });
                this._groups.forEach((g, i) => {
                    if (typeof g.leaves !== 'undefined')
                        g.leaves.forEach(v => edges.push({ source: g.index, target: v.index }));
                    if (typeof g.groups !== 'undefined')
                        g.groups.forEach(gg => edges.push({ source: g.index, target: gg.index }));
                });

                // layout the flat graph with dummy nodes and edges
                new Layout()
                    .size(this.size())
                    .nodes(vs)
                    .links(edges)
                    .avoidOverlaps(false)
                    .linkDistance(this.linkDistance())
                    .symmetricDiffLinkLengths(5)
                    .convergenceThreshold(1e-4)
                    .start(iterations, 0, 0, 0, false);

                this._nodes.forEach(v => {
                    x[v.index] = vs[v.index].x;
                    y[v.index] = vs[v.index].y;
                });
            } else {
                this._descent.run(iterations);
            }
        }

        // recalculate nodes position for disconnected graphs
        private separateOverlappingComponents(width: number, height: number, centerGraph: boolean = true): void {
            // recalculate nodes position for disconnected graphs
            if (!this._distanceMatrix && this._handleDisconnected) {
                let x = this._descent.x[0], y = this._descent.x[1];
                this._nodes.forEach(function (v, i) { v.x = x[i], v.y = y[i]; });
                var graphs = separateGraphs(this._nodes, this._links);
                applyPacking(graphs, width, height, this._defaultNodeSize, 1, centerGraph);
                this._nodes.forEach((v, i) => {
                    this._descent.x[0][i] = v.x, this._descent.x[1][i] = v.y;
                    if (v.bounds) {
                        v.bounds.setXCentre(v.x);
                        v.bounds.setYCentre(v.y);
                    }
                });
            }
        }

        resume(): this {
            return this.alpha(0.1);
        }

        stop(): this {
            return this.alpha(0);
        }

        /// find a visibility graph over the set of nodes.  assumes all nodes have a
        /// bounds property (a rectangle) and that no pair of bounds overlaps.
        prepareEdgeRouting(nodeMargin: number = 0) {
            this._visibilityGraph = new TangentVisibilityGraph(
                this._nodes.map(function (v) {
                    return v.bounds.inflate(-nodeMargin).vertices();
                }));
        }

        /**
         * find a route avoiding node bounds for the given edge.
         * assumes the visibility graph has been created (by prepareEdgeRouting method)
         * and also assumes that nodes have an index property giving their position in the
         * node array.  This index property is created by the start() method.
         * @param [edge] The edge to generate a route for.
         * @param {number} [ah] The size of the arrow head, a distance to shorten the end
         *                      of the edge by.  Defaults to 5.
         */
        routeEdge(edge, ah: number = 5, draw) {
            var lineData = [];
            //if (d.source.id === 10 && d.target.id === 11) {
            //    debugger;
            //}
            var vg2 = new TangentVisibilityGraph(this._visibilityGraph.P, { V: this._visibilityGraph.V, E: this._visibilityGraph.E }),
                port1 = <TVGPoint>{ x: edge.source.x, y: edge.source.y },
                port2 = <TVGPoint>{ x: edge.target.x, y: edge.target.y },
                start = vg2.addPoint(port1, edge.source.index),
                end = vg2.addPoint(port2, edge.target.index);
            vg2.addEdgeIfVisible(port1, port2, edge.source.index, edge.target.index);
            if (typeof draw !== 'undefined') {
                draw(vg2);
            }
            var sourceInd = e => e.source.id, targetInd = e => e.target.id, length = e => e.length(),
                spCalc = new Calculator(vg2.V.length, vg2.E, sourceInd, targetInd, length),
                shortestPath = spCalc.PathFromNodeToNode(start.id, end.id);
            if (shortestPath.length === 1 || shortestPath.length === vg2.V.length) {
                let route = makeEdgeBetween(edge.source.innerBounds, edge.target.innerBounds, ah);
                lineData = [route.sourceIntersection, route.arrowStart];
            } else {
                var n = shortestPath.length - 2,
                    p = vg2.V[shortestPath[n]].p,
                    q = vg2.V[shortestPath[0]].p,
                    lineData = [edge.source.innerBounds.rayIntersection(p.x, p.y)];
                for (var i = n; i >= 0; --i)
                    lineData.push(vg2.V[shortestPath[i]].p);
                lineData.push(makeEdgeTo(q, edge.target.innerBounds, ah));
            }
            //lineData.forEach((v, i) => {
            //    if (i > 0) {
            //        var u = lineData[i - 1];
            //        this._nodes.forEach(function (node) {
            //            if (node.id === getSourceIndex(d) || node.id === getTargetIndex(d)) return;
            //            var ints = node.innerBounds.lineIntersections(u.x, u.y, v.x, v.y);
            //            if (ints.length > 0) {
            //                debugger;
            //            }
            //        })
            //    }
            //})
            return lineData;
        }

        //The link source and target may be just a node index, or they may be references to nodes themselves.
        static getSourceIndex(e: Link<Node | number>): number {
            return typeof e.source === 'number' ? <number>e.source : (<Node>e.source).index;
        }

        //The link source and target may be just a node index, or they may be references to nodes themselves.
        static getTargetIndex(e: Link<Node | number>): number {
            return typeof e.target === 'number' ? <number>e.target : (<Node>e.target).index;
        }

        // Get a string ID for a given link.
        static linkId(e: Link<Node | number>): string {
            return Layout.getSourceIndex(e) + "-" + Layout.getTargetIndex(e);
        }

        // The fixed property has three bits:
        // Bit 1 can be set externally (e.g., d.fixed = true) and show persist.
        // Bit 2 stores the dragging state, from mousedown to mouseup.
        // Bit 3 stores the hover state, from mouseover to mouseout.
        static dragStart(d: Node | Group) {
            if (isGroup(d)) {
                Layout.storeOffset(d, Layout.dragOrigin(d));
            } else {
                Layout.stopNode(d);
                d.fixed |= 2; // set bit 2
            }
        }

        // we clobber any existing desired positions for nodes
        // in case another tick event occurs before the drag
        private static stopNode(v: Node) {
            (<any>v).px = v.x;
            (<any>v).py = v.y;
        }

        // we store offsets for each node relative to the centre of the ancestor group
        // being dragged in a pair of properties on the node
        private static storeOffset(d: Group, origin: { x: number, y: number }) {
            if (typeof d.leaves !== 'undefined') {
                d.leaves.forEach(v => {
                    v.fixed |= 2;
                    Layout.stopNode(v);
                    (<any>v)._dragGroupOffsetX = v.x - origin.x;
                    (<any>v)._dragGroupOffsetY = v.y - origin.y;
                });
            }
            if (typeof d.groups !== 'undefined') {
                d.groups.forEach(g => Layout.storeOffset(g, origin));
            }
        }

        // the drag origin is taken as the centre of the node or group
        static dragOrigin(d: Node | Group): { x: number, y: number } {
            if (isGroup(d)) {
                return {
                    x: d.bounds.cx(),
                    y: d.bounds.cy()
                };
            } else {
                return d;
            }
        }

        // for groups, the drag translation is propagated down to all of the children of
        // the group.
        static drag(d: Node | Group, position: { x: number, y: number }) {
            if (isGroup(d)) {
                if (typeof d.leaves !== 'undefined') {
                    d.leaves.forEach(v => {
                        d.bounds.setXCentre(position.x);
                        d.bounds.setYCentre(position.y);
                        (<any>v).px = (<any>v)._dragGroupOffsetX + position.x;
                        (<any>v).py = (<any>v)._dragGroupOffsetY + position.y;
                    });
                }
                if (typeof d.groups !== 'undefined') {
                    d.groups.forEach(g => Layout.drag(g, position));
                }
            } else {
                (<any>d).px = position.x;
                (<any>d).py = position.y;
            }
        }

        // we unset only bits 2 and 3 so that the user can fix nodes with another a different
        // bit such that the lock persists between drags
        static dragEnd(d) {
            if (isGroup(d)) {
                if (typeof d.leaves !== 'undefined') {
                    d.leaves.forEach(v => {
                        Layout.dragEnd(v);
                        delete (<any>v)._dragGroupOffsetX;
                        delete (<any>v)._dragGroupOffsetY;
                    });
                }
                if (typeof d.groups !== 'undefined') {
                    d.groups.forEach(Layout.dragEnd);
                }
            } else {
                d.fixed &= ~6; // unset bits 2 and 3
                //d.fixed = 0;
            }
        }

        // in d3 hover temporarily locks nodes, currently not used in cola
        static mouseOver(d) {
            d.fixed |= 4; // set bit 3
            d.px = d.x, d.py = d.y; // set velocity to zero
        }

        // in d3 hover temporarily locks nodes, currently not used in cola
        static mouseOut(d) {
            d.fixed &= ~4; // unset bit 3
        }
    }
