/*!
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/*
  global d3, localStorage, dagreD3, dagNodes, edges, arrange, document,
*/

const highlightColor = "#000000";
const upstreamColor = "#2020A0";
const downstreamColor = "#0000FF";
const initialStrokeWidth = "3px";
const highlightStrokeWidth = "5px";
const duration = 500;

let nodes = dagNodes;
const fullNodes = nodes;
const filteredNodes = nodes.filter((n) =>
  edges.some((e) => e.u === n.id || e.v === n.id)
);

// Preparation of DagreD3 data structures
let g = new dagreD3.graphlib.Graph()
  .setGraph({
    nodesep: 15,
    ranksep: 15,
    rankdir: arrange,
  })
  .setDefaultEdgeLabel(() => ({ lineInterpolate: "basis" }));

const render = dagreD3.render();
const svg = d3.select("#graph-svg");
const innerSvg = d3.select("#graph-svg g");

// Returns true if a node's id or its children's id matches search_text
function nodeMatches(nodeId, searchText) {
  if (nodeId.indexOf(searchText) > -1) return true;
  return false;
}

function highlightNodes(nodesToHighlight, color, strokeWidth) {
  nodesToHighlight.forEach((nodeid) => {
    const myNode = g.node(nodeid).elem;
    d3.select(myNode)
      .selectAll("rect,circle")
      .style("stroke", color)
      .style("stroke-width", strokeWidth);
  });
}

let zoom = null;

function setUpZoomSupport() {
  // Set up zoom support for Graph
  zoom = d3.behavior.zoom().on("zoom", () => {
    innerSvg.attr(
      "transform",
      `translate(${d3.event.translate})scale(${d3.event.scale})`
    );
  });
  svg.call(zoom);

  // Centering the DAG on load
  // Get Dagre Graph dimensions
  const graphWidth = g.graph().width;
  const graphHeight = g.graph().height;
  // Get SVG dimensions
  const padding = 20;
  const svgBb = svg.node().getBoundingClientRect();
  const width = svgBb.width - padding * 2;
  const height = svgBb.height - padding; // we are not centering the dag vertically

  // Calculate applicable scale for zoom
  const zoomScale = Math.min(
    Math.min(width / graphWidth, height / graphHeight),
    1.5 // cap zoom level to 1.5 so nodes are not too large
  );

  zoom.translate([width / 2 - (graphWidth * zoomScale) / 2 + padding, padding]);
  zoom.scale(zoomScale);
  zoom.event(innerSvg);
}

function setUpNodeHighlighting(focusItem = null) {
  // eslint-disable-next-line func-names
  d3.selectAll("g.node").on("mouseover", function (d) {
    d3.select(this).selectAll("rect").style("stroke", highlightColor);
    highlightNodes(g.predecessors(d), upstreamColor, highlightStrokeWidth);
    highlightNodes(g.successors(d), downstreamColor, highlightStrokeWidth);
    const adjacentNodeNames = [d, ...g.predecessors(d), ...g.successors(d)];
    d3.selectAll("g.nodes g.node")
      .filter((x) => !adjacentNodeNames.includes(x))
      .style("opacity", 0.2);
    const adjacentEdges = g.nodeEdges(d);
    d3.selectAll("g.edgePath")[0]
      // eslint-disable-next-line no-underscore-dangle
      .filter((x) => !adjacentEdges.includes(x.__data__))
      .forEach((x) => {
        d3.select(x).style("opacity", 0.2);
      });
  });

  // eslint-disable-next-line func-names
  d3.selectAll("g.node").on("mouseout", function (d) {
    d3.select(this).selectAll("rect,circle").style("stroke", null);
    highlightNodes(g.predecessors(d), null, initialStrokeWidth);
    highlightNodes(g.successors(d), null, initialStrokeWidth);
    d3.selectAll("g.node").style("opacity", 1);
    d3.selectAll("g.node rect").style("stroke-width", initialStrokeWidth);
    d3.selectAll("g.edgePath").style("opacity", 1);
    if (focusItem) {
      localStorage.removeItem(focusItem);
    }
  });
}

function searchboxHighlighting(s) {
  let match = null;

  d3.selectAll("g.nodes g.node").filter(function forEach(d) {
    if (s === "") {
      d3.select("g.edgePaths")
        .transition()
        .duration(duration)
        .style("opacity", 1);
      d3.select(this)
        .transition()
        .duration(duration)
        .style("opacity", 1)
        .selectAll("rect")
        .style("stroke-width", initialStrokeWidth);
    } else {
      d3.select("g.edgePaths")
        .transition()
        .duration(duration)
        .style("opacity", 0.2);
      if (nodeMatches(d, s)) {
        if (!match) match = this;
        d3.select(this)
          .transition()
          .duration(duration)
          .style("opacity", 1)
          .selectAll("rect")
          .style("stroke-width", highlightStrokeWidth);
      } else {
        d3.select(this)
          .transition()
          .style("opacity", 0.2)
          .duration(duration)
          .selectAll("rect")
          .style("stroke-width", initialStrokeWidth);
      }
    }
    return null;
  });

  // This moves the matched node to the center of the graph area
  if (match) {
    const transform = d3.transform(d3.select(match).attr("transform"));

    const svgBb = svg.node().getBoundingClientRect();
    transform.translate = [
      svgBb.width / 2 - transform.translate[0],
      svgBb.height / 2 - transform.translate[1],
    ];
    transform.scale = [1, 1];

    if (zoom !== null) {
      zoom.translate(transform.translate);
      zoom.scale(1);
      zoom.event(innerSvg);
    }
  }
}

d3.select("#searchbox").on("keyup", () => {
  const s = document.getElementById("searchbox").value;
  searchboxHighlighting(s);
});

const renderGraph = () => {
  g = new dagreD3.graphlib.Graph()
    .setGraph({
      nodesep: 15,
      ranksep: 15,
      rankdir: arrange,
    })
    .setDefaultEdgeLabel(() => ({ lineInterpolate: "basis" }));

  // set nodes
  nodes.forEach((node) => {
    g.setNode(node.id, node.value);
  });

  // filter out edges that point to non-existent nodes
  const realEdges = edges.filter((e) => {
    const edgeNodes = nodes.filter((n) => n.id === e.u || n.id === e.v);
    return edgeNodes.length === 2;
  });

  // Set edges
  realEdges.forEach((edge) => {
    g.setEdge(edge.u, edge.v, {
      curve: d3.curveBasis,
      arrowheadClass: "arrowhead",
    });
  });

  innerSvg.call(render, g);
  setUpNodeHighlighting();
  setUpZoomSupport();
};

// rerender graph when filtering dags with dependencies or not
document
  .getElementById("deps-filter")
  .addEventListener("change", function onChange() {
    // reset searchbox
    document.getElementById("searchbox").value = "";
    if (this.checked) {
      nodes = filteredNodes;
    } else {
      nodes = fullNodes;
    }
    renderGraph();
  });

// initial filter check and render
if (document.getElementById("deps-filter").checked) {
  nodes = filteredNodes;
}

renderGraph();
