/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const {
  Component,
} = require("resource://devtools/client/shared/vendor/react.js");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const { isSavedFrame } = require("resource://devtools/shared/DevToolsUtils.js");
const {
  getSourceNames,
} = require("resource://devtools/client/shared/source-utils.js");
const { L10N } = require("resource://devtools/client/memory/utils.js");

const GRAPH_DEFAULTS = {
  translate: [20, 20],
  scale: 1,
};

const NO_STACK = "noStack";
const NO_FILENAME = "noFilename";
const ROOT_LIST = "JS::ubi::RootList";

function stringifyLabel(label, id) {
  const sanitized = [];

  for (let i = 0, length = label.length; i < length; i++) {
    const piece = label[i];

    if (isSavedFrame(piece)) {
      const { short } = getSourceNames(piece.source);
      sanitized[i] =
        `${piece.functionDisplayName} @ ` +
        `${short}:${piece.line}:${piece.column}`;
    } else if (piece === NO_STACK) {
      sanitized[i] = L10N.getStr("tree-item.nostack");
    } else if (piece === NO_FILENAME) {
      sanitized[i] = L10N.getStr("tree-item.nofilename");
    } else if (piece === ROOT_LIST) {
      // Don't use the usual labeling machinery for root lists: replace it
      // with the "GC Roots" string.
      sanitized.splice(0, label.length);
      sanitized.push(L10N.getStr("tree-item.rootlist"));
      break;
    } else {
      sanitized[i] = "" + piece;
    }
  }

  return `${sanitized.join(" › ")} @ 0x${id.toString(16)}`;
}

class ShortestPaths extends Component {
  static get propTypes() {
    return {
      graph: PropTypes.shape({
        nodes: PropTypes.arrayOf(PropTypes.object),
        edges: PropTypes.arrayOf(PropTypes.object),
      }),
    };
  }

  constructor(props) {
    super(props);
    this.state = { zoom: null };
    this._renderGraph = this._renderGraph.bind(this);
  }

  componentDidMount() {
    if (this.props.graph) {
      this._renderGraph(this.refs.container, this.props.graph);
    }
  }

  shouldComponentUpdate(nextProps) {
    return this.props.graph != nextProps.graph;
  }

  componentDidUpdate() {
    if (this.props.graph) {
      this._renderGraph(this.refs.container, this.props.graph);
    }
  }

  componentWillUnmount() {
    if (this.state.zoom) {
      this.state.zoom.on("zoom", null);
    }
  }

  _renderGraph(container, { nodes, edges }) {
    if (!container.firstChild) {
      const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
      svg.setAttribute("id", "graph-svg");
      svg.setAttribute("xlink", "http://www.w3.org/1999/xlink");
      svg.style.width = "100%";
      svg.style.height = "100%";

      const target = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "g"
      );
      target.setAttribute("id", "graph-target");
      target.style.width = "100%";
      target.style.height = "100%";

      svg.appendChild(target);
      container.appendChild(svg);
    }

    const graph = new dagreD3.Digraph();

    for (let i = 0; i < nodes.length; i++) {
      graph.addNode(nodes[i].id, {
        id: nodes[i].id,
        label: stringifyLabel(nodes[i].label, nodes[i].id),
      });
    }

    for (let i = 0; i < edges.length; i++) {
      graph.addEdge(null, edges[i].from, edges[i].to, {
        label: edges[i].name,
      });
    }

    const renderer = new dagreD3.Renderer();
    renderer.drawNodes();
    renderer.drawEdgePaths();

    const svg = d3.select("#graph-svg");
    const target = d3.select("#graph-target");

    let zoom = this.state.zoom;
    if (!zoom) {
      zoom = d3.behavior.zoom().on("zoom", function () {
        target.attr(
          "transform",
          `translate(${d3.event.translate}) scale(${d3.event.scale})`
        );
      });
      svg.call(zoom);
      this.setState({ zoom });
    }

    const { translate, scale } = GRAPH_DEFAULTS;
    zoom.scale(scale);
    zoom.translate(translate);
    target.attr("transform", `translate(${translate}) scale(${scale})`);

    const layout = dagreD3.layout();
    renderer.layout(layout).run(graph, target);
  }

  render() {
    let contents;
    if (this.props.graph) {
      // Let the componentDidMount or componentDidUpdate method draw the graph
      // with DagreD3. We just provide the container for the graph here.
      contents = dom.div({
        ref: "container",
        style: {
          flex: 1,
          height: "100%",
          width: "100%",
        },
      });
    } else {
      contents = dom.div(
        {
          id: "shortest-paths-select-node-msg",
        },
        L10N.getStr("shortest-paths.select-node")
      );
    }

    return dom.div(
      {
        id: "shortest-paths",
        className: "vbox",
      },
      dom.label(
        {
          id: "shortest-paths-header",
          className: "header",
        },
        L10N.getStr("shortest-paths.header")
      ),
      contents
    );
  }
}

module.exports = ShortestPaths;
