import { IGraph, ITooltip } from "@hpcc-js/api";
import { d3Event, drag as d3Drag, ISize, Platform, select as d3Select, Spacer, SVGGlowFilter, SVGZoomWidget, ToggleButton, Utility, Widget } from "@hpcc-js/common";

import "d3-transition";
import { Edge } from "./Edge.ts";
import { GraphData } from "./GraphData.ts";
import * as GraphLayouts from "./GraphLayouts.ts";
import { Subgraph } from "./Subgraph.ts";
import { Vertex } from "./Vertex.ts";

import "./Graph.css";

export interface Lineage {
    parent: Widget;
    child: Widget;
}
export interface IGraphData {
    subgraphs?: Widget[];
    vertices: Widget[];
    edges: Edge[];
    hierarchy?: Lineage[];
}
export type GraphLayoutType = "Hierarchy" | "DOT" | "ForceDirected" | "ForceDirected2" | "Neato" | "FDP" | "Circle" | "TwoPI" | "Circo" | "None";

export class Graph extends SVGZoomWidget {
    static Subgraph = Subgraph;
    static Vertex = Vertex;
    static Edge = Edge;

    private _toggleHierarchy = new ToggleButton().faChar("fa-sitemap").tooltip("Hierarchy").on("click", () => this.layoutClick("Hierarchy"));
    private _toggleForceDirected = new ToggleButton().faChar("fa-expand").tooltip("Force Directed").on("click", () => this.layoutClick("ForceDirected"));
    private _toggleForceDirected2 = new ToggleButton().faChar("fa-arrows").tooltip("Spring").on("click", () => this.layoutClick("ForceDirected2"));
    private _toggleCircle = new ToggleButton().faChar("fa-circle-o").tooltip("Circle").on("click", () => this.layoutClick("Circle"));

    private _graphData: GraphData;
    protected highlight;
    protected _selection;
    protected _dragging;
    protected forceLayout;
    protected _d3Drag;
    protected defs;
    protected _centroidFilter: SVGGlowFilter;
    protected svgFragment;
    protected svg;
    protected svgC;
    protected svgE;
    protected svgV;

    constructor() {
        super();
        IGraph.call(this);
        ITooltip.call(this);
        this.tooltipHTML(function (d: any) {
            let content;
            if (d instanceof Subgraph) {
                content = d.title().replace(/\n/g, "<br>");
            } else if (d instanceof Vertex || d instanceof Edge) {
                content = d.text().replace(/\n/g, "<br>");
            }
            if (content) {
                return `<p style="text-align:center">${content}</p>`;
            }
            return null;
        });

        this._drawStartPos = "origin";

        const buttons: Widget[] = [
            this._toggleHierarchy,
            this._toggleForceDirected,
            this._toggleForceDirected2,
            this._toggleCircle,
            new Spacer()];
        this._iconBar.buttons(buttons.concat(this._iconBar.buttons()));

        this._graphData = new GraphData();
        this.highlight = {
            zoom: 1.1,
            opacity: 0.33,
            edge: "1.25px"
        };
        this._selection = new Utility.Selection(this);
        this.zoomToFitLimit(1);
    }

    iconBarButtons(): Widget[] {
        return this._iconBar.buttons();
    }

    layoutClick(layout: GraphLayoutType) {
        this.layout(layout);
        if (layout !== "ForceDirected2") this.applyScaleOnLayout(true);
        this
            .layout(layout)
            .render(w => {
                this.applyScaleOnLayout(false);
            });
    }

    //  Properties  ---
    getOffsetPos() {
        return { x: 0, y: 0 };
    }

    size(): ISize;
    size(_): this;
    size(_?): ISize | this {
        const retVal = super.size.apply(this, arguments as any);
        return retVal;
    }

    clear() {
        this.data({ subgraphs: [], vertices: [], edges: [], hierarchy: [] }, false);
    }

    _dataHash = 0;
    data(): IGraphData;
    data(_: IGraphData, merge?: boolean): this;
    data(_?: IGraphData, merge?: boolean): IGraphData | this {
        const retVal = super.data.apply(this, arguments as any);
        if (arguments.length) {
            if (!merge) {
                this._graphData = new GraphData();
                this._renderCount = 0;
            }
            const data = this._graphData.setData(_.subgraphs || [], _.vertices || [], _.edges || [], _.hierarchy || [], merge || false);
            if (data.addedVertices.length) {
                this._dataHash++;
            }

            const context = this;
            data.addedVertices.forEach(function (item) {
                item._graphID = context._id;
            });
            data.addedEdges.forEach(function (item) {
                item._graphID = context._id;
            });

            //  Recalculate edge arcs  ---
            const dupMap = {};
            this._graphData.edges().forEach(function (item) {
                if (!dupMap[item._sourceVertex._id]) {
                    dupMap[item._sourceVertex._id] = {};
                }
                if (!dupMap[item._sourceVertex._id][item._targetVertex._id]) {
                    dupMap[item._sourceVertex._id][item._targetVertex._id] = 0;
                }
                const dupEdgeCount = ++dupMap[item._sourceVertex._id][item._targetVertex._id];
                item.arcDepth(16 * dupEdgeCount);
            });
        }
        return retVal;
    }

    graphData(): any {
        return this._graphData;
    }

    selection(_: Widget[]): this;
    selection(): Widget[];
    selection(_?: Widget[]): Widget[] | this {
        if (!arguments.length) return this._selection.get();
        this._selection.set(_);
        return this;
    }

    private _linkcolor: string;
    linkcolor_default(): string;
    linkcolor_default(_: string): this;
    linkcolor_default(_?: string): string | this {
        if (!arguments.length) return this._linkcolor;
        this._linkcolor = _;
        return this;
    }

    private _linktooltip: string;
    linktooltip_default(): string;
    linktooltip_default(_: string): this;
    linktooltip_default(_?: string): string | this {
        if (!arguments.length) return this._linktooltip;
        this._linktooltip = _;
        return this;
    }

    //  Drag  ---
    private _neighborOffsets: Array<{ neighbor: Vertex, offsetX: number, offsetY: number }> = [];
    dragstart(d) {
        if (this.allowDragging()) {
            const event = d3Event();
            event.sourceEvent.stopPropagation();

            d.__drag_dx = event.x - d.x();
            d.__drag_dy = event.y - d.y();
            this._dragging = true;
            if (this.forceLayout) {
                if (!event.active) this.forceLayout.force.alphaTarget(0.3).restart();
                const forceNode = this.forceLayout.vertexMap[d.id()];
                forceNode.fixed = true;
                forceNode.fx = forceNode.x;
                forceNode.fy = forceNode.y;
            }

            this._neighborOffsets = [];
            if (this.dragSingleNeighbors()) {
                this._neighborOffsets = this._graphData.singleNeighbors(d.id()).map((neighbor: any) => {
                    d3Select(neighbor.target()).raise();
                    return {
                        neighbor,
                        offsetX: d.x() - neighbor.x(),
                        offsetY: d.y() - neighbor.y()
                    };
                });
            }

            //  Safe Raise - does not interfere with current click event  ---
            const target = d.target();
            let nextSibling = target.nextSibling;
            while (nextSibling) {
                target.parentNode.insertBefore(nextSibling, target);
                nextSibling = target.nextSibling;
            }

            if (Platform.svgMarkerGlitch) {
                this._graphData.nodeEdges(d.id()).forEach(glEdge => {
                    const edge = this._graphData.edge(glEdge);
                    this._pushMarkers(edge.element());
                });
            }
        }
    }

    dragging(d) {
        if (this.allowDragging()) {
            const event = d3Event();
            event.sourceEvent.stopPropagation();
            d.move({ x: event.x - d.__drag_dx, y: event.y - d.__drag_dy });
            if (this.forceLayout) {
                const forceNode = this.forceLayout.vertexMap[d.id()];
                forceNode.fixed = true;
                forceNode.fx = event.x - d.__drag_dx;
                forceNode.fy = event.y - d.__drag_dy;
            }

            // Drag singleton child nodes
            this._neighborOffsets.forEach(neighborOffset => {
                const neighborX = event.x - d.__drag_dx - neighborOffset.offsetX;
                const neighborY = event.y - d.__drag_dy - neighborOffset.offsetY;
                if (this.forceLayout) {
                    const forceNode = this.forceLayout.vertexMap[neighborOffset.neighbor.id()];
                    forceNode.fixed = true;
                    forceNode.fx = neighborX;
                    forceNode.fy = neighborY;
                }
                neighborOffset.neighbor.move({ x: neighborX, y: neighborY });
            });

            this.refreshIncidentEdges(d, true);
        }
    }

    dragend(d) {
        if (this.allowDragging()) {
            d3Event().sourceEvent.stopPropagation();
            this._dragging = false;
            if (this.snapToGrid()) {
                const snapLoc = d.calcSnap(this.snapToGrid());
                d.move(snapLoc[0]);
                this.refreshIncidentEdges(d, true);
            }
            if (this.forceLayout) {
                const forceNode = this.forceLayout.vertexMap[d.id()];
                forceNode.fixed = false;
                forceNode.fx = null;
                forceNode.fy = null;

                this._neighborOffsets.forEach(neighborOffset => {
                    const forceNode = this.forceLayout.vertexMap[neighborOffset.neighbor.id()];
                    forceNode.fixed = false;
                    forceNode.fx = null;
                    forceNode.fy = null;
                });
            }
            this._neighborOffsets = [];

            if (Platform.svgMarkerGlitch) {
                this._graphData.nodeEdges(d.id()).forEach(function (this, id) {
                    const edge = this._graphData.edge(id);
                    this._popMarkers(edge.element());
                });
            }
        }
    }

    enter(domNode, element) {
        super.enter(domNode, element);

        this._zoomGrab.on("click.clear", () => {
            if (this.selectionClearOnBackgroundClick()) {
                this._selection.clear();
            }
        });

        this._d3Drag = d3Drag()
            // .origin(function (d) {
            //    return d.pos();
            // })
            .on("start", d => this.dragstart(d))
            .on("end", d => this.dragend(d))
            .on("drag", d => this.dragging(d))
            ;
        //  SVG  ---
        this.defs = this._renderElement.append("defs");
        this.addMarkers();
        this._centroidFilter = new SVGGlowFilter(this.defs, this._id + "_glow");

        // element.call(this.zoom);
        this.svg = this._renderElement.append("svg:g");
        // this._svgBrush = this.svg.append("g").attr("class", "selectionBrush").call(this.brush);
        // this._svgBrush.select(".background").style("cursor", null);
        // context._svgBrush.call(context.brush.clear());
        this.svgC = this.svg.append("g").attr("id", this._id + "C");
        this.svgE = this.svg.append("g").attr("id", this._id + "E");
        this.svgV = this.svg.append("g").attr("id", this._id + "V");
    }

    getBounds(items, layoutEngine?) {
        const vBounds = [[null, null], [null, null]];
        items.forEach(function (item) {
            const pos = layoutEngine ? layoutEngine.nodePos(item._id) : { x: item.x(), y: item.y(), width: item.width(), height: item.height() };
            const leftX = pos.x - pos.width / 2;
            const rightX = pos.x + pos.width / 2;
            const topY = pos.y - pos.height / 2;
            const bottomY = pos.y + pos.height / 2;
            if (vBounds[0][0] === null || vBounds[0][0] > leftX) {
                vBounds[0][0] = leftX;
            }
            if (vBounds[0][1] === null || vBounds[0][1] > topY) {
                vBounds[0][1] = topY;
            }
            if (vBounds[1][0] === null || vBounds[1][0] < rightX) {
                vBounds[1][0] = rightX;
            }
            if (vBounds[1][1] === null || vBounds[1][1] < bottomY) {
                vBounds[1][1] = bottomY;
            }
        });
        return vBounds;
    }

    getVertexBounds(layoutEngine) {
        return this.getBounds(this._graphData.nodes(), layoutEngine);
    }

    getSelectionBounds(layoutEngine) {
        return this.getBounds(this._selection.get(), layoutEngine);
    }

    centerOnItem(item: Widget) {
        const bbox = item.getBBox(true);
        const deltaX = bbox.x + bbox.width / 2;
        const deltaY = bbox.y + bbox.height / 2;
        const itemBBox = {
            x: item.x() + deltaX - bbox.width / 2,
            y: item.y() + deltaY - bbox.height / 2,
            width: bbox.width,
            height: bbox.height
        };
        this.centerOnBBox(itemBBox);
    }

    zoomToItem(item: Widget) {
        const bbox = item.getBBox(true);
        const deltaX = bbox.x + bbox.width / 2;
        const deltaY = bbox.y + bbox.height / 2;
        const itemBBox = {
            x: item.x() + deltaX - bbox.width / 2,
            y: item.y() + deltaY - bbox.height / 2,
            width: bbox.width,
            height: bbox.height
        };
        this.zoomToBBox(itemBBox);
    }

    //  Render  ---
    updateVertices(rootElement, rootSuffix, data) {
        const width = this.width();
        const height = this.height();
        const context = this;

        const vertexElements = rootElement.selectAll("#" + this._id + rootSuffix + " > .graphVertex").data(data, function (d) { return d.id(); });
        vertexElements.enter().append("g")
            .attr("class", "graphVertex")
            .style("opacity", 1e-6)
            //  TODO:  Events need to be optional  ---
            .on("click.selectionBag", function (d) {
                context._selection.click(d, d3Event());
                context.selectionChanged();
            })
            .on("click", function (this: SVGElement, d) {
                const vertexElement = d3Select(this).select(".graph_Vertex");
                let selected = false;
                if (!vertexElement.empty()) {
                    selected = vertexElement.classed("selected");
                }
                context.vertex_click(context.rowToObj(d.data()), "", selected, {
                    vertex: d
                });
            })
            .on("dblclick", function (this: SVGElement, d) {
                d3Event().stopPropagation();
                const vertexElement = d3Select(this).select(".graph_Vertex");
                let selected = false;
                if (!vertexElement.empty()) {
                    selected = vertexElement.classed("selected");
                }
                context.vertex_dblclick(context.rowToObj(d.data()), "", selected, {
                    vertex: d
                });
            })
            .on("contextmenu", function (this: SVGElement, d) {
                const vertexElement = d3Select(this).select(".graph_Vertex");
                let selected = false;
                if (!vertexElement.empty()) {
                    selected = vertexElement.classed("selected");
                }
                context.vertex_contextmenu(context.rowToObj(d.data()), "", selected, {
                    vertex: d
                });
            })
            .on("mouseout.tooltip", this.tooltip.hide)
            .on("mousemove.tooltip", this.tooltip.show)
            .on("mouseover", function (this: SVGElement, d) {
                if (context._dragging)
                    return;
                context.vertex_mouseover(d3Select(this), d);
            })
            .on("mouseout", function (this: SVGElement, d) {
                if (context._dragging)
                    return;
                context.vertex_mouseout(d3Select(this), d);
            })
            .each(createV)
            .transition()
            .duration(750)
            .style("opacity", 1)
            ;
        function createV(this: SVGElement, d) {
            d3Select(this).style("cursor", context.allowDragging() ? "move" : "pointer");
            d
                .target(this)
                .pos({ x: d.x() || width / 2, y: d.y() || height / 2 })
                .animationFrameRender()
                ;
            if (context.allowDragging()) {
                d3Select(this)
                    .call(context._d3Drag)
                    ;
            }
            if (d.dispatch) {
                d.dispatch.on("sizestart", function (d2) {
                    d2.allowResize(context.allowDragging());
                    if (context.allowDragging()) {
                        context._dragging = true;
                    }
                });
                d.dispatch.on("size", function (d2) {
                    context.refreshIncidentEdges(d2, false);
                });
                d.dispatch.on("sizeend", function (d2) {
                    context._dragging = false;
                    if (context.snapToGrid()) {
                        const snapLoc = d2.calcSnap(context.snapToGrid());
                        d2
                            .pos(snapLoc[0])
                            .size(snapLoc[1])
                            .render()
                            ;
                        context.refreshIncidentEdges(d2, false);
                    }
                });
            }
        }

        vertexElements
            .each(updateV)
            ;
        function updateV(d) {
            d
                .animationFrameRender()
                ;
        }

        vertexElements.exit()
            .each(function (d) {
                d.target(null);
            })
            .remove()
            ;

        vertexElements.order();
    }

    update(domNode, element) {
        super.update(domNode, element);
        this.tooltip.hide();
        this._centroidFilter.update(this.centroidColor());

        //  IconBar  ---
        const layout = this.layout();
        this._toggleHierarchy.selected(layout === "Hierarchy").render();
        this._toggleForceDirected.selected(layout === "ForceDirected").render();
        this._toggleForceDirected2.selected(layout === "ForceDirected2").render();
        this._toggleCircle.selected(layout === "Circle").render();

        //  Create  ---
        const context = this;

        this.updateVertices(this.svgC, "C", this._graphData.nodes().filter(v => this.layout() === "Hierarchy" ? (v instanceof Subgraph) : false));
        this.updateVertices(this.svgV, "V", this._graphData.nodes().filter(v => !(v instanceof Subgraph)));

        const edgeElements = this.svgE.selectAll("#" + this._id + "E > .graphEdge").data(this.showEdges() ? this._graphData.edges() : [], function (d) { return d.id(); });
        edgeElements.enter().append("g")
            .attr("class", "graphEdge")
            .style("opacity", 1e-6)
            .on("click.selectionBag", function (d) {
                context._selection.click(d, d3Event());
            })
            .on("click", function (this: SVGElement, d) {
                const edgeElement = d3Select(this).select(".graph_Edge");
                let selected = false;
                if (!edgeElement.empty()) {
                    selected = edgeElement.classed("selected");
                }
                context.edge_click(context.rowToObj(d.data()), "", selected, {
                    edge: d
                });
            })
            .on("dblclick", function (this: SVGElement, d) {
                const edgeElement = d3Select(this).select(".graph_Edge");
                let selected = false;
                if (!edgeElement.empty()) {
                    selected = edgeElement.classed("selected");
                }
                context.edge_dblclick(context.rowToObj(d.data()), "", selected, {
                    edge: d
                });
            })
            .on("mouseout.tooltip", this.tooltip.hide)
            .on("mousemove.tooltip", this.tooltip.show)
            .on("mouseover", function (this: SVGElement, d) {
                if (context._dragging)
                    return;
                context.edge_mouseover(d3Select(this), d);
            })
            .on("mouseout", function (this: SVGElement, d) {
                if (context._dragging)
                    return;
                context.edge_mouseout(d3Select(this), d);
            })
            .each(createE)
            .transition()
            .duration(750)
            .style("opacity", 1)
            ;
        function createE(this: SVGElement, d) {
            d
                .target(this)
                .animationFrameRender()
                ;
        }

        edgeElements
            .each(updateE)
            ;
        function updateE(d) {
            d
                .animationFrameRender()
                ;
        }

        edgeElements.exit()
            .each(function (d) {
                d.target(null);
            })
            .remove()
            ;

        if (!this._renderCount) {
            this._renderCount++;
            this.layout(this.layout());
        }
    }

    exit(domNode, element) {
        this._graphData.nodes().forEach(v => v.target(null));
        this._graphData.edges().forEach(e => e.target(null));
        super.exit(domNode, element);
    }

    static profileID = 0;
    render(callback?: (w: Widget) => void): this {
        this.progress("start");
        super.render(w => {
            this.doLayout().then(() => {
                this.progress("end");
                if (callback) {
                    callback(w);
                }
            });
        });
        return this;
    }

    //  Methods  ---
    _prevLayout;
    _prevDataHash;
    doLayout(transitionDuration = 0): Promise<void> {
        return new Promise((resolve, reject) => {
            requestAnimationFrame(() => {
                if (this._prevLayout !== this.layout() || this._prevDataHash !== this._dataHash) {
                    this._prevLayout = this.layout();
                    this._prevDataHash = this._dataHash;
                    this._doLayout(transitionDuration).then(() => {
                        resolve();
                    });
                } else {
                    resolve();
                }
            });
        });
    }

    _doLayout(transitionDuration = 0): Promise<void> {
        return new Promise((resolve, reject) => {
            this.progress("layout-start");
            if (this.forceLayout) {
                this.forceLayout.force.stop();
                this.forceLayout = null;
            }

            const context = this;
            const layoutEngine = this.getLayoutEngine() as GraphLayouts.ForceDirected;
            if (this.layout() === "ForceDirected2") {
                this.forceLayout = layoutEngine;
                this.forceLayout.force
                    .on("tick", function (this: SVGElement) {
                        context.progress("layout-tick");
                        layoutEngine.vertices.forEach(function (item) {
                            if (item.fixed) {
                                // item.x = item.px;
                                // item.y = item.py;
                            } else {
                                // item.px = item.x;
                                // item.py = item.y;

                                //  Might have been cleared ---
                                const vertex = context._graphData.node(item.id);
                                if (vertex) {
                                    vertex
                                        .move({ x: item.x, y: item.y })
                                        ;
                                }
                            }
                        });
                        context._graphData.edges().forEach((item: any) => {
                            item
                                .points([], false, false)
                                ;
                        });
                        if (context.applyScaleOnLayout()) {
                            // const vBounds = context.getVertexBounds(layoutEngine);
                            // context.shrinkToFit(vBounds);
                        }
                    })
                    .on("end", function (this: SVGElement) {
                        context.progress("layout-end");
                    })
                    ;
                this.forceLayout.force.restart();
                resolve();
            } else if (layoutEngine) {
                this.forceLayout = null;
                context._dragging = true;
                context._graphData.nodes().forEach(function (item) {
                    const pos = layoutEngine.nodePos(item.id());
                    if (item instanceof Graph.Subgraph) {
                        item
                            .pos({ x: pos.x, y: pos.y })
                            .size({ width: pos.width, height: pos.height })
                            .animationFrameRender()
                            ;
                    } else {
                        item.move({ x: pos.x, y: pos.y }, 0);
                    }
                });
                context._graphData.edges().forEach((item: any) => {
                    const points = layoutEngine.edgePoints(item);
                    item.points(points, transitionDuration);
                });
                if (context.applyScaleOnLayout()) {
                    requestAnimationFrame(() => {
                        context.zoomToFit();
                        resolve();
                    });
                }
                this._fixIEMarkers();
                setTimeout(function () {
                    context._dragging = false;
                }, transitionDuration ? transitionDuration + 50 : 50);  //  Prevents highlighting during morph  ---
                this.progress("layout-end");
                if (!context.applyScaleOnLayout()) {
                    resolve();
                }
            }
        });
    }

    getLayoutEngine() {
        switch (this.layout()) {
            case "Circle":
                return new GraphLayouts.Circle(this._graphData, this._size.width, this._size.height);
            case "ForceDirected":
                return new GraphLayouts.ForceDirected(this._graphData, this._size.width, this._size.height, {
                    oneShot: true,
                    linkDistance: this.forceDirectedLinkDistance(),
                    linkStrength: this.forceDirectedLinkStrength(),
                    friction: this.forceDirectedFriction(),
                    charge: this.forceDirectedCharge(),
                    chargeDistance: this.forceDirectedChargeDistance(),
                    theta: this.forceDirectedTheta(),
                    gravity: this.forceDirectedGravity()
                });
            case "ForceDirected2":
                return new GraphLayouts.ForceDirected(this._graphData, this._size.width, this._size.height, {
                    linkDistance: this.forceDirectedLinkDistance(),
                    linkStrength: this.forceDirectedLinkStrength(),
                    friction: this.forceDirectedFriction(),
                    charge: this.forceDirectedCharge(),
                    chargeDistance: this.forceDirectedChargeDistance(),
                    theta: this.forceDirectedTheta(),
                    gravity: this.forceDirectedGravity()
                });
            case "Hierarchy":
                return new GraphLayouts.Hierarchy(this._graphData, this._size.width, this._size.height, {
                    rankdir: this.hierarchyRankDirection(),
                    nodesep: this.hierarchyNodeSeparation(),
                    edgesep: this.hierarchyEdgeSeparation(),
                    ranksep: this.hierarchyRankSeparation(),
                    digraph: this.hierarchyDigraph()
                });
            default:
        }
        return null; // new GraphLayouts.None(this.graphData, this._size.width, this._size.height);
    }

    getNeighborMap(vertex) {
        const vertices = {};
        const edges = {};

        if (vertex) {
            const nedges = this._graphData.nodeEdges(vertex.id());
            for (let i = 0; i < nedges.length; ++i) {
                const edge = this._graphData.edge(nedges[i]);
                edges[edge.id()] = edge;
                if (edge._sourceVertex.id() !== vertex.id()) {
                    vertices[edge._sourceVertex.id()] = edge._sourceVertex;
                }
                if (edge._targetVertex.id() !== vertex.id()) {
                    vertices[edge._targetVertex.id()] = edge._targetVertex;
                }
            }
        }

        return {
            vertices,
            edges
        };
    }

    highlightVerticies(vertexMap?: { [id: string]: boolean }) {
        const context = this;
        const vertexElements = this.svgV.selectAll(".graphVertex");
        vertexElements
            .classed("graphVertex-highlighted", d => !vertexMap || vertexMap[d.id()])
            .style("filter", d => vertexMap && vertexMap[d.id()] ? "url(#" + this.id() + "_glow)" : null)
            .transition().duration(this.transitionDuration())
            .on("end", function (d) {
                if (vertexMap && vertexMap[d.id()]) {
                    if (d._placeholderElement.node() && d._placeholderElement.node().parentNode) {
                        d._placeholderElement.node().parentNode.appendChild(d._placeholderElement.node());
                    }
                }
            })
            .style("opacity", function (d) {
                if (!vertexMap || vertexMap[d.id()]) {
                    return 1;
                }
                return context.highlight.opacity;
            })
            ;
        return this;
    }

    highlightEdges(edgeMap) {
        const context = this;
        const edgeElements = this.svgE.selectAll(".graphEdge");
        edgeElements
            .classed("graphEdge-highlighted", function (d) { return !edgeMap || edgeMap[d.id()]; })
            .style("stroke-width", function (o) {
                if (edgeMap && edgeMap[o.id()]) {
                    return context.highlight.edge;
                }
                return "1px";
            }).transition().duration(this.transitionDuration())
            .style("opacity", function (o) {
                if (!edgeMap || edgeMap[o.id()]) {
                    return 1;
                }
                return context.highlight.opacity;
            })
            ;
        return this;
    }

    highlightVertex(_element, d) {
        if (this.highlightOnMouseOverVertex()) {
            if (d) {
                const highlight = this.getNeighborMap(d);
                highlight.vertices[d.id()] = d;
                this.highlightVerticies(highlight.vertices);
                this.highlightEdges(highlight.edges);
            } else {
                this.highlightVerticies(null);
                this.highlightEdges(null);
            }
        }
    }

    highlightEdge(_element, d) {
        if (this.highlightOnMouseOverEdge()) {
            if (d) {
                const vertices = {};
                vertices[d._sourceVertex.id()] = d._sourceVertex;
                vertices[d._targetVertex.id()] = d._targetVertex;
                const edges = {};
                edges[d.id()] = d;
                this.highlightVerticies(vertices);
                this.highlightEdges(edges);
            } else {
                this.highlightVerticies(null);
                this.highlightEdges(null);
            }
        }
    }

    refreshIncidentEdges(d, skipPushMarkers) {
        this._graphData.nodeEdges(d.id()).forEach(glEdge => {
            const edge: any = this._graphData.edge(glEdge);
            edge
                .points([], false, skipPushMarkers)
                ;
        });
    }

    //  Events  ---
    centroids(): Vertex[] {
        return (this._graphData.nodes() as any[]).filter(vertex => vertex.centroid());
    }

    selectionChanged() {
        if (this.highlightSelectedPathToCentroid()) {
            const highlightedEdges = {};
            this.centroids().forEach(centroid => {
                this.selection().forEach(selection => {
                    this._graphData.undirectedShortestPath(centroid.id(), selection.id()).forEach(e => {
                        highlightedEdges[e.id()] = true;
                    });
                });
            });
            this.svgE.selectAll(".graphEdge")
                .classed("shortest-path", d => highlightedEdges[d.id()] === true)
                ;
        }
    }

    vertex_click(_row, _col, _sel, more) {
        if (more && more.vertex) {
            more.vertex._placeholderElement.node().parentNode.appendChild(more.vertex._placeholderElement.node());
        }
        IGraph.prototype.vertex_click.apply(this, arguments);
    }

    vertex_dblclick(_row, _col, _sel, _opts) {
    }

    vertex_contextmenu(_row, _col, _sel, _opts) {
    }

    vertex_mouseover(element, d) {
        this.highlightVertex(element, d);
    }

    vertex_mouseout(_element, _d) {
        this.highlightVertex(null, null);
    }

    edge_mouseover(element, d) {
        this.highlightEdge(element, d);
    }

    edge_mouseout(_element, _d) {
        this.highlightEdge(null, null);
    }

    addMarkers(clearFirst: boolean = false) {
        if (clearFirst) {
            this.defs.select("#" + this._id + "_arrowHead").remove();
            this.defs.select("#" + this._id + "_circleFoot").remove();
            this.defs.select("#" + this._id + "_circleHead").remove();
            this.defs.select("#" + this._id + "_glow").remove();
        }
        this.defs.append("marker")
            .attr("class", "marker")
            .attr("id", this._id + "_arrowHead")
            .attr("viewBox", "0 0 10 10")
            .attr("refX", 10)
            .attr("refY", 5)
            .attr("markerWidth", 8)
            .attr("markerHeight", 8)
            .attr("markerUnits", "strokeWidth")
            .attr("orient", "auto")
            .append("polyline")
            .attr("points", "0,0 10,5 0,10 1,5")
            ;
        this.defs.append("marker")
            .attr("class", "marker")
            .attr("id", this._id + "_circleFoot")
            .attr("viewBox", "0 0 10 10")
            .attr("refX", 1)
            .attr("refY", 5)
            .attr("markerWidth", 7)
            .attr("markerHeight", 7)
            .attr("markerUnits", "strokeWidth")
            .attr("orient", "auto")
            .append("circle")
            .attr("cx", 5)
            .attr("cy", 5)
            .attr("r", 4)
            ;
        this.defs.append("marker")
            .attr("class", "marker")
            .attr("id", this._id + "_circleHead")
            .attr("viewBox", "0 0 10 10")
            .attr("refX", 9)
            .attr("refY", 5)
            .attr("markerWidth", 7)
            .attr("markerHeight", 7)
            .attr("markerUnits", "strokeWidth")
            .attr("orient", "auto")
            .append("circle")
            .attr("cx", 5)
            .attr("cy", 5)
            .attr("r", 4)
            ;
    }

    //  Progess Events ---
    progress(what: "start" | "end" | "layout-start" | "layout-tick" | "layout-end") {
    }
}
Graph.prototype._class += " graph_Graph";
Graph.prototype.implements(IGraph.prototype);
Graph.prototype.implements(ITooltip.prototype);

export interface Graph {
    allowDragging(): boolean;
    allowDragging(_: boolean): this;
    dragSingleNeighbors(): boolean;
    dragSingleNeighbors(_: boolean): this;
    layout(): GraphLayoutType;
    layout(_: GraphLayoutType): this;
    // scale(): string;
    // scale(_: string): this;
    applyScaleOnLayout(): boolean;
    applyScaleOnLayout(_: boolean): this;
    highlightOnMouseOverVertex(): boolean;
    highlightOnMouseOverVertex(_: boolean): this;
    highlightOnMouseOverEdge(): boolean;
    highlightOnMouseOverEdge(_: boolean): this;
    transitionDuration(): number;
    transitionDuration(_: number): this;
    showEdges(): boolean;
    showEdges(_: boolean): this;
    snapToGrid(): number;
    snapToGrid(_: number): this;
    selectionClearOnBackgroundClick(): boolean;
    selectionClearOnBackgroundClick(_: boolean): this;

    centroidColor(): string;
    centroidColor(_: string): this;
    highlightSelectedPathToCentroid(): boolean;
    highlightSelectedPathToCentroid(_: boolean): this;

    hierarchyRankDirection(): string;
    hierarchyRankDirection(_: string): this;
    hierarchyNodeSeparation(): number;
    hierarchyNodeSeparation(_: number): this;
    hierarchyEdgeSeparation(): number;
    hierarchyEdgeSeparation(_: number): this;
    hierarchyRankSeparation(): number;
    hierarchyRankSeparation(_: number): this;
    hierarchyDigraph(): boolean;
    hierarchyDigraph(_: boolean): this;

    forceDirectedLinkDistance(): number;
    forceDirectedLinkDistance(_: number): this;
    forceDirectedLinkStrength(): number;
    forceDirectedLinkStrength(_: number): this;
    forceDirectedFriction(): number;
    forceDirectedFriction(_: number): this;
    forceDirectedCharge(): number;
    forceDirectedCharge(_: number): this;
    forceDirectedChargeDistance(): number;
    forceDirectedChargeDistance(_: number): this;
    forceDirectedTheta(): number;
    forceDirectedTheta(_: number): this;
    forceDirectedGravity(): number;
    forceDirectedGravity(_: number): this;

    //  IGraph  ---
    edge_click(_row, _col, _sel, _more): void;
    edge_dblclick(_row, _col, _sel, _more): void;

    //  ITooltip  ---
    tooltip;
    tooltipHTML(_): string;
    tooltipFormat(_): string;
}

Graph.prototype.publish("allowDragging", true, "boolean", "Allow Dragging of Vertices", null, { tags: ["Advanced"] });
Graph.prototype.publish("dragSingleNeighbors", false, "boolean", "Dragging a Vertex also moves its singleton neighbors", null, { tags: ["Advanced"] });
Graph.prototype.publish("layout", "Circle", "set", "Default Layout", ["Circle", "ForceDirected", "ForceDirected2", "Hierarchy", "None"], { tags: ["Basic"] });
Graph.prototype.publish("scale", "100%", "set", "Zoom Level", ["all", "width", "selection", "100%", "90%", "75%", "50%", "25%", "10%"], { tags: ["Basic"] });
Graph.prototype.publish("applyScaleOnLayout", false, "boolean", "Shrink to fit on Layout", null, { tags: ["Basic"] });
Graph.prototype.publish("highlightOnMouseOverVertex", false, "boolean", "Highlight Vertex on Mouse Over", null, { tags: ["Basic"] });
Graph.prototype.publish("highlightOnMouseOverEdge", false, "boolean", "Highlight Edge on Mouse Over", null, { tags: ["Basic"] });
Graph.prototype.publish("transitionDuration", 250, "number", "Transition Duration", null, { tags: ["Intermediate"] });
Graph.prototype.publish("showEdges", true, "boolean", "Show Edges", null, { tags: ["Intermediate"] });
Graph.prototype.publish("snapToGrid", 0, "number", "Snap to Grid", null, { tags: ["Private"] });
Graph.prototype.publish("selectionClearOnBackgroundClick", false, "boolean", "Clear selection on background click");

Graph.prototype.publish("centroidColor", "#00A000", "html-color", "Centroid Color", null, { tags: ["Basic"] });
Graph.prototype.publish("highlightSelectedPathToCentroid", false, "boolean", "Highlight path to Center Vertex (for selected vertices)", null, { tags: ["Basic"] });

Graph.prototype.publish("hierarchyRankDirection", "TB", "set", "Direction for Rank Nodes", ["TB", "BT", "LR", "RL"], { tags: ["Advanced"] });
Graph.prototype.publish("hierarchyNodeSeparation", 50, "number", "Number of pixels that separate nodes horizontally in the layout", null, { tags: ["Advanced"] });
Graph.prototype.publish("hierarchyEdgeSeparation", 10, "number", "Number of pixels that separate edges horizontally in the layout", null, { tags: ["Advanced"] });
Graph.prototype.publish("hierarchyRankSeparation", 50, "number", "Number of pixels between each rank in the layout", null, { tags: ["Advanced"] });
Graph.prototype.publish("hierarchyDigraph", true, "boolean", "Directional Graph", null, { tags: ["Advanced"] });

Graph.prototype.publish("forceDirectedLinkDistance", 300, "number", "Target distance between linked nodes", null, { tags: ["Advanced"] });
Graph.prototype.publish("forceDirectedLinkStrength", 1, "number", "Strength (rigidity) of links", null, { tags: ["Advanced"] });
Graph.prototype.publish("forceDirectedFriction", 0.9, "number", "Friction coefficient", null, { tags: ["Advanced"] });
Graph.prototype.publish("forceDirectedCharge", -25, "number", "Charge strength ", null, { tags: ["Advanced"] });
Graph.prototype.publish("forceDirectedChargeDistance", 10000, "number", "Maximum distance over which charge forces are applied", null, { tags: ["Advanced"] });
Graph.prototype.publish("forceDirectedTheta", 0.8, "number", "Barnes–Hut approximation criterion", null, { tags: ["Advanced"] });
Graph.prototype.publish("forceDirectedGravity", 0.1, "number", "Gravitational strength", null, { tags: ["Advanced"] });

const _origScale = Graph.prototype.scale;
//@ts-ignore
Graph.prototype.scale = function (_?, transitionDuration?) {
    const retVal = _origScale.apply(this, arguments as any);
    if (arguments.length) {
        this.zoomTo(_, transitionDuration);
    }
    return retVal;
};
