// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as d3 from "d3";
import { layoutNodeGraph } from "../src/graph-layout";
import { GNode, nodeToStr } from "../src/node";
import { NODE_INPUT_WIDTH } from "../src/node";
import { DEFAULT_NODE_BUBBLE_RADIUS } from "../src/node";
import { Edge, edgeToStr } from "../src/edge";
import { PhaseView } from "../src/view";
import { MySelection } from "../src/selection";
import { partial } from "../src/util";
import { NodeSelectionHandler, ClearableHandler } from "./selection-handler";
import { Graph } from "./graph";
import { SelectionBroker } from "./selection-broker";

function nodeToStringKey(n: GNode) {
  return "" + n.id;
}

interface GraphState {
  showTypes: boolean;
  selection: MySelection;
  mouseDownNode: any;
  justDragged: boolean;
  justScaleTransGraph: boolean;
  hideDead: boolean;
}

export class GraphView extends PhaseView {
  divElement: d3.Selection<any, any, any, any>;
  svg: d3.Selection<any, any, any, any>;
  showPhaseByName: (p: string, s: Set<any>) => void;
  state: GraphState;
  selectionHandler: NodeSelectionHandler & ClearableHandler;
  graphElement: d3.Selection<any, any, any, any>;
  visibleNodes: d3.Selection<any, GNode, any, any>;
  visibleEdges: d3.Selection<any, Edge, any, any>;
  drag: d3.DragBehavior<any, GNode, GNode>;
  panZoom: d3.ZoomBehavior<SVGElement, any>;
  visibleBubbles: d3.Selection<any, any, any, any>;
  transitionTimout: number;
  graph: Graph;
  broker: SelectionBroker;
  phaseName: string;
  toolbox: HTMLElement;

  createViewElement() {
    const pane = document.createElement('div');
    pane.setAttribute('id', "graph");
    return pane;
  }

  constructor(idOrContainer: string | HTMLElement, broker: SelectionBroker,
    showPhaseByName: (s: string) => void, toolbox: HTMLElement) {
    super(idOrContainer);
    const view = this;
    this.broker = broker;
    this.showPhaseByName = showPhaseByName;
    this.divElement = d3.select(this.divNode);
    this.phaseName = "";
    this.toolbox = toolbox;
    const svg = this.divElement.append("svg")
      .attr('version', '2.0')
      .attr("width", "100%")
      .attr("height", "100%");
    svg.on("click", function (d) {
      view.selectionHandler.clear();
    });
    // Listen for key events. Note that the focus handler seems
    // to be important even if it does nothing.
    svg
      .attr("focusable", false)
      .on("focus", e => { })
      .on("keydown", e => { view.svgKeyDown(); });

    view.svg = svg;

    this.state = {
      selection: null,
      mouseDownNode: null,
      justDragged: false,
      justScaleTransGraph: false,
      showTypes: false,
      hideDead: false
    };

    this.selectionHandler = {
      clear: function () {
        view.state.selection.clear();
        broker.broadcastClear(this);
        view.updateGraphVisibility();
      },
      select: function (nodes: Array<GNode>, selected: boolean) {
        const locations = [];
        for (const node of nodes) {
          if (node.nodeLabel.sourcePosition) {
            locations.push(node.nodeLabel.sourcePosition);
          }
          if (node.nodeLabel.origin && node.nodeLabel.origin.bytecodePosition) {
            locations.push({ bytecodePosition: node.nodeLabel.origin.bytecodePosition });
          }
        }
        view.state.selection.select(nodes, selected);
        broker.broadcastSourcePositionSelect(this, locations, selected);
        view.updateGraphVisibility();
      },
      brokeredNodeSelect: function (locations, selected: boolean) {
        if (!view.graph) return;
        const selection = view.graph.nodes(n => {
          return locations.has(nodeToStringKey(n))
            && (!view.state.hideDead || n.isLive());
        });
        view.state.selection.select(selection, selected);
        // Update edge visibility based on selection.
        for (const n of view.graph.nodes()) {
          if (view.state.selection.isSelected(n)) {
            n.visible = true;
            n.inputs.forEach(e => {
              e.visible = e.visible || view.state.selection.isSelected(e.source);
            });
            n.outputs.forEach(e => {
              e.visible = e.visible || view.state.selection.isSelected(e.target);
            });
          }
        }
        view.updateGraphVisibility();
      },
      brokeredClear: function () {
        view.state.selection.clear();
        view.updateGraphVisibility();
      }
    };

    view.state.selection = new MySelection(nodeToStringKey);

    const defs = svg.append('svg:defs');
    defs.append('svg:marker')
      .attr('id', 'end-arrow')
      .attr('viewBox', '0 -4 8 8')
      .attr('refX', 2)
      .attr('markerWidth', 2.5)
      .attr('markerHeight', 2.5)
      .attr('orient', 'auto')
      .append('svg:path')
      .attr('d', 'M0,-4L8,0L0,4');

    this.graphElement = svg.append("g");
    view.visibleEdges = this.graphElement.append("g");
    view.visibleNodes = this.graphElement.append("g");

    view.drag = d3.drag<any, GNode, GNode>()
      .on("drag", function (d) {
        d.x += d3.event.dx;
        d.y += d3.event.dy;
        view.updateGraphVisibility();
      });

    function zoomed() {
      if (d3.event.shiftKey) return false;
      view.graphElement.attr("transform", d3.event.transform);
      return true;
    }

    const zoomSvg = d3.zoom<SVGElement, any>()
      .scaleExtent([0.2, 40])
      .on("zoom", zoomed)
      .on("start", function () {
        if (d3.event.shiftKey) return;
        d3.select('body').style("cursor", "move");
      })
      .on("end", function () {
        d3.select('body').style("cursor", "auto");
      });

    svg.call(zoomSvg).on("dblclick.zoom", null);

    view.panZoom = zoomSvg;

  }

  getEdgeFrontier(nodes: Iterable<GNode>, inEdges: boolean,
    edgeFilter: (e: Edge, i: number) => boolean) {
    const frontier: Set<Edge> = new Set();
    for (const n of nodes) {
      const edges = inEdges ? n.inputs : n.outputs;
      let edgeNumber = 0;
      edges.forEach((edge: Edge) => {
        if (edgeFilter == undefined || edgeFilter(edge, edgeNumber)) {
          frontier.add(edge);
        }
        ++edgeNumber;
      });
    }
    return frontier;
  }

  getNodeFrontier(nodes: Iterable<GNode>, inEdges: boolean,
    edgeFilter: (e: Edge, i: number) => boolean) {
    const view = this;
    const frontier: Set<GNode> = new Set();
    let newState = true;
    const edgeFrontier = view.getEdgeFrontier(nodes, inEdges, edgeFilter);
    // Control key toggles edges rather than just turning them on
    if (d3.event.ctrlKey) {
      edgeFrontier.forEach(function (edge: Edge) {
        if (edge.visible) {
          newState = false;
        }
      });
    }
    edgeFrontier.forEach(function (edge: Edge) {
      edge.visible = newState;
      if (newState) {
        const node = inEdges ? edge.source : edge.target;
        node.visible = true;
        frontier.add(node);
      }
    });
    view.updateGraphVisibility();
    if (newState) {
      return frontier;
    } else {
      return undefined;
    }
  }

  initializeContent(data, rememberedSelection) {
    this.show();
    function createImgInput(id: string, title: string, onClick): HTMLElement {
      const input = document.createElement("input");
      input.setAttribute("id", id);
      input.setAttribute("type", "image");
      input.setAttribute("title", title);
      input.setAttribute("src", `img/${id}-icon.png`);
      input.className = "button-input graph-toolbox-item";
      input.addEventListener("click", onClick);
      return input;
    }
    this.toolbox.appendChild(createImgInput("layout", "layout graph",
      partial(this.layoutAction, this)));
    this.toolbox.appendChild(createImgInput("show-all", "show all nodes",
      partial(this.showAllAction, this)));
    this.toolbox.appendChild(createImgInput("show-control", "show all nodes",
      partial(this.showControlAction, this)));
    this.toolbox.appendChild(createImgInput("toggle-hide-dead", "show only live nodes",
      partial(this.toggleHideDead, this)));
    this.toolbox.appendChild(createImgInput("hide-unselected", "show only live nodes",
      partial(this.hideUnselectedAction, this)));
    this.toolbox.appendChild(createImgInput("hide-selected", "show only live nodes",
      partial(this.hideSelectedAction, this)));
    this.toolbox.appendChild(createImgInput("zoom-selection", "show only live nodes",
      partial(this.zoomSelectionAction, this)));
    this.toolbox.appendChild(createImgInput("toggle-types", "show only live nodes",
      partial(this.toggleTypesAction, this)));

    this.phaseName = data.name;
    this.createGraph(data.data, rememberedSelection);
    this.broker.addNodeHandler(this.selectionHandler);

    if (rememberedSelection != null && rememberedSelection.size > 0) {
      this.attachSelection(rememberedSelection);
      this.connectVisibleSelectedNodes();
      this.viewSelection();
    } else {
      this.viewWholeGraph();
    }
  }

  deleteContent() {
    for (const item of this.toolbox.querySelectorAll(".graph-toolbox-item")) {
      item.parentElement.removeChild(item);
    }

    for (const n of this.graph.nodes()) {
      n.visible = false;
    }
    this.graph.forEachEdge((e: Edge) => {
      e.visible = false;
    });
    this.updateGraphVisibility();
  }

  public hide(): void {
    super.hide();
    this.deleteContent();
  }

  createGraph(data, rememberedSelection) {
    this.graph = new Graph(data);

    this.showControlAction(this);

    if (rememberedSelection != undefined) {
      for (const n of this.graph.nodes()) {
        n.visible = n.visible || rememberedSelection.has(nodeToStringKey(n));
      }
    }

    this.graph.forEachEdge(e => e.visible = e.source.visible && e.target.visible);

    this.layoutGraph();
    this.updateGraphVisibility();
  }

  connectVisibleSelectedNodes() {
    const view = this;
    for (const n of view.state.selection) {
      n.inputs.forEach(function (edge: Edge) {
        if (edge.source.visible && edge.target.visible) {
          edge.visible = true;
        }
      });
      n.outputs.forEach(function (edge: Edge) {
        if (edge.source.visible && edge.target.visible) {
          edge.visible = true;
        }
      });
    }
  }

  updateInputAndOutputBubbles() {
    const view = this;
    const g = this.graph;
    const s = this.visibleBubbles;
    s.classed("filledBubbleStyle", function (c) {
      const components = this.id.split(',');
      if (components[0] == "ib") {
        const edge = g.nodeMap[components[3]].inputs[components[2]];
        return edge.isVisible();
      } else {
        return g.nodeMap[components[1]].areAnyOutputsVisible() == 2;
      }
    }).classed("halfFilledBubbleStyle", function (c) {
      const components = this.id.split(',');
      if (components[0] == "ib") {
        return false;
      } else {
        return g.nodeMap[components[1]].areAnyOutputsVisible() == 1;
      }
    }).classed("bubbleStyle", function (c) {
      const components = this.id.split(',');
      if (components[0] == "ib") {
        const edge = g.nodeMap[components[3]].inputs[components[2]];
        return !edge.isVisible();
      } else {
        return g.nodeMap[components[1]].areAnyOutputsVisible() == 0;
      }
    });
    s.each(function (c) {
      const components = this.id.split(',');
      if (components[0] == "ob") {
        const from = g.nodeMap[components[1]];
        const x = from.getOutputX();
        const y = from.getNodeHeight(view.state.showTypes) + DEFAULT_NODE_BUBBLE_RADIUS;
        const transform = "translate(" + x + "," + y + ")";
        this.setAttribute('transform', transform);
      }
    });
  }

  attachSelection(s) {
    if (!(s instanceof Set)) return;
    this.selectionHandler.clear();
    const selected = [...this.graph.nodes(n =>
      s.has(this.state.selection.stringKey(n)) && (!this.state.hideDead || n.isLive()))];
    this.selectionHandler.select(selected, true);
  }

  detachSelection() {
    return this.state.selection.detachSelection();
  }

  selectAllNodes() {
    if (!d3.event.shiftKey) {
      this.state.selection.clear();
    }
    const allVisibleNodes = [...this.graph.nodes(n => n.visible)];
    this.state.selection.select(allVisibleNodes, true);
    this.updateGraphVisibility();
  }

  layoutAction(graph: GraphView) {
    graph.layoutGraph();
    graph.updateGraphVisibility();
    graph.viewWholeGraph();
  }

  showAllAction(view: GraphView) {
    for (const n of view.graph.nodes()) {
      n.visible = !view.state.hideDead || n.isLive();
    }
    view.graph.forEachEdge((e: Edge) => {
      e.visible = e.source.visible || e.target.visible;
    });
    view.updateGraphVisibility();
    view.viewWholeGraph();
  }

  showControlAction(view: GraphView) {
    for (const n of view.graph.nodes()) {
      n.visible = n.cfg && (!view.state.hideDead || n.isLive());
    }
    view.graph.forEachEdge((e: Edge) => {
      e.visible = e.type == 'control' && e.source.visible && e.target.visible;
    });
    view.updateGraphVisibility();
    view.viewWholeGraph();
  }

  toggleHideDead(view: GraphView) {
    view.state.hideDead = !view.state.hideDead;
    if (view.state.hideDead) view.hideDead();
    const element = document.getElementById('toggle-hide-dead');
    element.classList.toggle('button-input-toggled', view.state.hideDead);
  }

  hideDead() {
    for (const n of this.graph.nodes()) {
      if (!n.isLive()) {
        n.visible = false;
        this.state.selection.select([n], false);
      }
    }
    this.updateGraphVisibility();
  }

  hideUnselectedAction(view: GraphView) {
    for (const n of view.graph.nodes()) {
      if (!view.state.selection.isSelected(n)) {
        n.visible = false;
      }
    }
    view.updateGraphVisibility();
  }

  hideSelectedAction(view: GraphView) {
    for (const n of view.graph.nodes()) {
      if (view.state.selection.isSelected(n)) {
        n.visible = false;
      }
    }
    view.selectionHandler.clear();
  }

  zoomSelectionAction(view: GraphView) {
    view.viewSelection();
  }

  toggleTypesAction(view: GraphView) {
    view.toggleTypes();
  }

  searchInputAction(searchBar: HTMLInputElement, e: KeyboardEvent, onlyVisible: boolean) {
    if (e.keyCode == 13) {
      this.selectionHandler.clear();
      const query = searchBar.value;
      window.sessionStorage.setItem("lastSearch", query);
      if (query.length == 0) return;

      const reg = new RegExp(query);
      const filterFunction = (n: GNode) => {
        return (reg.exec(n.getDisplayLabel()) != null ||
          (this.state.showTypes && reg.exec(n.getDisplayType())) ||
          (reg.exec(n.getTitle())) ||
          reg.exec(n.nodeLabel.opcode) != null);
      };

      const selection = [...this.graph.nodes(n => {
        if ((e.ctrlKey || n.visible || !onlyVisible) && filterFunction(n)) {
          if (e.ctrlKey || !onlyVisible) n.visible = true;
          return true;
        }
        return false;
      })];

      this.selectionHandler.select(selection, true);
      this.connectVisibleSelectedNodes();
      this.updateGraphVisibility();
      searchBar.blur();
      this.viewSelection();
    }
    e.stopPropagation();
  }

  svgKeyDown() {
    const view = this;
    const state = this.state;

    const showSelectionFrontierNodes = (inEdges: boolean, filter: (e: Edge, i: number) => boolean, doSelect: boolean) => {
      const frontier = view.getNodeFrontier(state.selection, inEdges, filter);
      if (frontier != undefined && frontier.size) {
        if (doSelect) {
          if (!d3.event.shiftKey) {
            state.selection.clear();
          }
          state.selection.select([...frontier], true);
        }
        view.updateGraphVisibility();
      }
    };

    let eventHandled = true; // unless the below switch defaults
    switch (d3.event.keyCode) {
      case 49:
      case 50:
      case 51:
      case 52:
      case 53:
      case 54:
      case 55:
      case 56:
      case 57:
        // '1'-'9'
        showSelectionFrontierNodes(true,
          (edge: Edge, index: number) => index == (d3.event.keyCode - 49),
          !d3.event.ctrlKey);
        break;
      case 97:
      case 98:
      case 99:
      case 100:
      case 101:
      case 102:
      case 103:
      case 104:
      case 105:
        // 'numpad 1'-'numpad 9'
        showSelectionFrontierNodes(true,
          (edge, index) => index == (d3.event.keyCode - 97),
          !d3.event.ctrlKey);
        break;
      case 67:
        // 'c'
        showSelectionFrontierNodes(d3.event.altKey,
          (edge, index) => edge.type == 'control',
          true);
        break;
      case 69:
        // 'e'
        showSelectionFrontierNodes(d3.event.altKey,
          (edge, index) => edge.type == 'effect',
          true);
        break;
      case 79:
        // 'o'
        showSelectionFrontierNodes(false, undefined, false);
        break;
      case 73:
        // 'i'
        if (!d3.event.ctrlKey && !d3.event.shiftKey) {
          showSelectionFrontierNodes(true, undefined, false);
        } else {
          eventHandled = false;
        }
        break;
      case 65:
        // 'a'
        view.selectAllNodes();
        break;
      case 38:
      // UP
      case 40: {
        // DOWN
        showSelectionFrontierNodes(d3.event.keyCode == 38, undefined, true);
        break;
      }
      case 82:
        // 'r'
        if (!d3.event.ctrlKey && !d3.event.shiftKey) {
          this.layoutAction(this);
        } else {
          eventHandled = false;
        }
        break;
      case 83:
        // 's'
        view.selectOrigins();
        break;
      default:
        eventHandled = false;
        break;
    }
    if (eventHandled) {
      d3.event.preventDefault();
    }
  }

  layoutGraph() {
    console.time("layoutGraph");
    layoutNodeGraph(this.graph, this.state.showTypes);
    const extent = this.graph.redetermineGraphBoundingBox(this.state.showTypes);
    this.panZoom.translateExtent(extent);
    this.minScale();
    console.timeEnd("layoutGraph");
  }

  selectOrigins() {
    const state = this.state;
    const origins = [];
    let phase = this.phaseName;
    const selection = new Set<any>();
    for (const n of state.selection) {
      const origin = n.nodeLabel.origin;
      if (origin) {
        phase = origin.phase;
        const node = this.graph.nodeMap[origin.nodeId];
        if (phase === this.phaseName && node) {
          origins.push(node);
        } else {
          selection.add(`${origin.nodeId}`);
        }
      }
    }
    // Only go through phase reselection if we actually need
    // to display another phase.
    if (selection.size > 0 && phase !== this.phaseName) {
      this.showPhaseByName(phase, selection);
    } else if (origins.length > 0) {
      this.selectionHandler.clear();
      this.selectionHandler.select(origins, true);
    }
  }

  // call to propagate changes to graph
  updateGraphVisibility() {
    const view = this;
    const graph = this.graph;
    const state = this.state;
    if (!graph) return;

    const filteredEdges = [...graph.filteredEdges(function (e) {
      return e.source.visible && e.target.visible;
    })];
    const selEdges = view.visibleEdges.selectAll<SVGPathElement, Edge>("path").data(filteredEdges, edgeToStr);

    // remove old links
    selEdges.exit().remove();

    // add new paths
    const newEdges = selEdges.enter()
      .append('path');

    newEdges.style('marker-end', 'url(#end-arrow)')
      .attr("id", function (edge) { return "e," + edge.stringID(); })
      .on("click", function (edge) {
        d3.event.stopPropagation();
        if (!d3.event.shiftKey) {
          view.selectionHandler.clear();
        }
        view.selectionHandler.select([edge.source, edge.target], true);
      })
      .attr("adjacentToHover", "false")
      .classed('value', function (e) {
        return e.type == 'value' || e.type == 'context';
      }).classed('control', function (e) {
        return e.type == 'control';
      }).classed('effect', function (e) {
        return e.type == 'effect';
      }).classed('frame-state', function (e) {
        return e.type == 'frame-state';
      }).attr('stroke-dasharray', function (e) {
        if (e.type == 'frame-state') return "10,10";
        return (e.type == 'effect') ? "5,5" : "";
      });

    const newAndOldEdges = newEdges.merge(selEdges);

    newAndOldEdges.classed('hidden', e => !e.isVisible());

    // select existing nodes
    const filteredNodes = [...graph.nodes(n => n.visible)];
    const allNodes = view.visibleNodes.selectAll<SVGGElement, GNode>("g");
    const selNodes = allNodes.data(filteredNodes, nodeToStr);

    // remove old nodes
    selNodes.exit().remove();

    // add new nodes
    const newGs = selNodes.enter()
      .append("g");

    newGs.classed("turbonode", function (n) { return true; })
      .classed("control", function (n) { return n.isControl(); })
      .classed("live", function (n) { return n.isLive(); })
      .classed("dead", function (n) { return !n.isLive(); })
      .classed("javascript", function (n) { return n.isJavaScript(); })
      .classed("input", function (n) { return n.isInput(); })
      .classed("simplified", function (n) { return n.isSimplified(); })
      .classed("machine", function (n) { return n.isMachine(); })
      .on('mouseenter', function (node) {
        const visibleEdges = view.visibleEdges.selectAll<SVGPathElement, Edge>('path');
        const adjInputEdges = visibleEdges.filter(e => e.target === node);
        const adjOutputEdges = visibleEdges.filter(e => e.source === node);
        adjInputEdges.attr('relToHover', "input");
        adjOutputEdges.attr('relToHover', "output");
        const adjInputNodes = adjInputEdges.data().map(e => e.source);
        const visibleNodes = view.visibleNodes.selectAll<SVGGElement, GNode>("g");
        visibleNodes.data<GNode>(adjInputNodes, nodeToStr).attr('relToHover', "input");
        const adjOutputNodes = adjOutputEdges.data().map(e => e.target);
        visibleNodes.data<GNode>(adjOutputNodes, nodeToStr).attr('relToHover', "output");
        view.updateGraphVisibility();
      })
      .on('mouseleave', function (node) {
        const visibleEdges = view.visibleEdges.selectAll<SVGPathElement, Edge>('path');
        const adjEdges = visibleEdges.filter(e => e.target === node || e.source === node);
        adjEdges.attr('relToHover', "none");
        const adjNodes = adjEdges.data().map(e => e.target).concat(adjEdges.data().map(e => e.source));
        const visibleNodes = view.visibleNodes.selectAll<SVGPathElement, GNode>("g");
        visibleNodes.data(adjNodes, nodeToStr).attr('relToHover', "none");
        view.updateGraphVisibility();
      })
      .on("click", d => {
        if (!d3.event.shiftKey) view.selectionHandler.clear();
        view.selectionHandler.select([d], undefined);
        d3.event.stopPropagation();
      })
      .call(view.drag);

    newGs.append("rect")
      .attr("rx", 10)
      .attr("ry", 10)
      .attr('width', function (d) {
        return d.getTotalNodeWidth();
      })
      .attr('height', function (d) {
        return d.getNodeHeight(view.state.showTypes);
      });

    function appendInputAndOutputBubbles(g, d) {
      for (let i = 0; i < d.inputs.length; ++i) {
        const x = d.getInputX(i);
        const y = -DEFAULT_NODE_BUBBLE_RADIUS;
        g.append('circle')
          .classed("filledBubbleStyle", function (c) {
            return d.inputs[i].isVisible();
          })
          .classed("bubbleStyle", function (c) {
            return !d.inputs[i].isVisible();
          })
          .attr("id", "ib," + d.inputs[i].stringID())
          .attr("r", DEFAULT_NODE_BUBBLE_RADIUS)
          .attr("transform", function (d) {
            return "translate(" + x + "," + y + ")";
          })
          .on("click", function (this: SVGCircleElement, d) {
            const components = this.id.split(',');
            const node = graph.nodeMap[components[3]];
            const edge = node.inputs[components[2]];
            const visible = !edge.isVisible();
            node.setInputVisibility(components[2], visible);
            d3.event.stopPropagation();
            view.updateGraphVisibility();
          });
      }
      if (d.outputs.length != 0) {
        const x = d.getOutputX();
        const y = d.getNodeHeight(view.state.showTypes) + DEFAULT_NODE_BUBBLE_RADIUS;
        g.append('circle')
          .classed("filledBubbleStyle", function (c) {
            return d.areAnyOutputsVisible() == 2;
          })
          .classed("halFilledBubbleStyle", function (c) {
            return d.areAnyOutputsVisible() == 1;
          })
          .classed("bubbleStyle", function (c) {
            return d.areAnyOutputsVisible() == 0;
          })
          .attr("id", "ob," + d.id)
          .attr("r", DEFAULT_NODE_BUBBLE_RADIUS)
          .attr("transform", function (d) {
            return "translate(" + x + "," + y + ")";
          })
          .on("click", function (d) {
            d.setOutputVisibility(d.areAnyOutputsVisible() == 0);
            d3.event.stopPropagation();
            view.updateGraphVisibility();
          });
      }
    }

    newGs.each(function (d) {
      appendInputAndOutputBubbles(d3.select(this), d);
    });

    newGs.each(function (d) {
      d3.select(this).append("text")
        .classed("label", true)
        .attr("text-anchor", "right")
        .attr("dx", 5)
        .attr("dy", 5)
        .append('tspan')
        .text(function (l) {
          return d.getDisplayLabel();
        })
        .append("title")
        .text(function (l) {
          return d.getTitle();
        });
      if (d.nodeLabel.type != undefined) {
        d3.select(this).append("text")
          .classed("label", true)
          .classed("type", true)
          .attr("text-anchor", "right")
          .attr("dx", 5)
          .attr("dy", d.labelbbox.height + 5)
          .append('tspan')
          .text(function (l) {
            return d.getDisplayType();
          })
          .append("title")
          .text(function (l) {
            return d.getType();
          });
      }
    });

    const newAndOldNodes = newGs.merge(selNodes);

    newAndOldNodes.select<SVGTextElement>('.type').each(function (d) {
      this.setAttribute('visibility', view.state.showTypes ? 'visible' : 'hidden');
    });

    newAndOldNodes
      .classed("selected", function (n) {
        if (state.selection.isSelected(n)) return true;
        return false;
      })
      .attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; })
      .select('rect')
      .attr('height', function (d) { return d.getNodeHeight(view.state.showTypes); });

    view.visibleBubbles = d3.selectAll('circle');

    view.updateInputAndOutputBubbles();

    graph.maxGraphX = graph.maxGraphNodeX;
    newAndOldEdges.attr("d", function (edge) {
      return edge.generatePath(graph, view.state.showTypes);
    });
  }

  getSvgViewDimensions() {
    return [this.container.clientWidth, this.container.clientHeight];
  }

  getSvgExtent(): [[number, number], [number, number]] {
    return [[0, 0], [this.container.clientWidth, this.container.clientHeight]];
  }

  minScale() {
    const dimensions = this.getSvgViewDimensions();
    const minXScale = dimensions[0] / (2 * this.graph.width);
    const minYScale = dimensions[1] / (2 * this.graph.height);
    const minScale = Math.min(minXScale, minYScale);
    this.panZoom.scaleExtent([minScale, 40]);
    return minScale;
  }

  onresize() {
    const trans = d3.zoomTransform(this.svg.node());
    const ctrans = this.panZoom.constrain()(trans, this.getSvgExtent(), this.panZoom.translateExtent());
    this.panZoom.transform(this.svg, ctrans);
  }

  toggleTypes() {
    const view = this;
    view.state.showTypes = !view.state.showTypes;
    const element = document.getElementById('toggle-types');
    element.classList.toggle('button-input-toggled', view.state.showTypes);
    view.updateGraphVisibility();
  }

  viewSelection() {
    const view = this;
    let minX;
    let maxX;
    let minY;
    let maxY;
    let hasSelection = false;
    view.visibleNodes.selectAll<SVGGElement, GNode>("g").each(function (n) {
      if (view.state.selection.isSelected(n)) {
        hasSelection = true;
        minX = minX ? Math.min(minX, n.x) : n.x;
        maxX = maxX ? Math.max(maxX, n.x + n.getTotalNodeWidth()) :
          n.x + n.getTotalNodeWidth();
        minY = minY ? Math.min(minY, n.y) : n.y;
        maxY = maxY ? Math.max(maxY, n.y + n.getNodeHeight(view.state.showTypes)) :
          n.y + n.getNodeHeight(view.state.showTypes);
      }
    });
    if (hasSelection) {
      view.viewGraphRegion(minX - NODE_INPUT_WIDTH, minY - 60,
        maxX + NODE_INPUT_WIDTH, maxY + 60);
    }
  }

  viewGraphRegion(minX, minY, maxX, maxY) {
    const [width, height] = this.getSvgViewDimensions();
    const dx = maxX - minX;
    const dy = maxY - minY;
    const x = (minX + maxX) / 2;
    const y = (minY + maxY) / 2;
    const scale = Math.min(width / (1.1 * dx), height / (1.1 * dy));
    this.svg
      .transition().duration(300).call(this.panZoom.translateTo, x, y)
      .transition().duration(300).call(this.panZoom.scaleTo, scale)
      .transition().duration(300).call(this.panZoom.translateTo, x, y);
  }

  viewWholeGraph() {
    this.panZoom.scaleTo(this.svg, 0);
    this.panZoom.translateTo(this.svg,
      this.graph.minGraphX + this.graph.width / 2,
      this.graph.minGraphY + this.graph.height / 2);
  }
}
