// a few constants
var MAX_TREE_SIZE_TO_PLOT = 100;

function getTxID() {
  var match = RegExp("[\\?&]id=([^&#]*)").exec(window.location.search);
  return match && decodeURIComponent(match[1].replace(/\+/g, " "));
}

function urlBase() {
  var pathArray = window.location.pathname.split("/"),
      basePath = [];

  // read until tx.html
  for (var i=0; i < pathArray.length; i++) {
    if (pathArray[i] == "tx.html") {
      break;
    } else {
      basePath.push(pathArray[i]);
    }
  }
  return basePath.join("/");
}

function urlFor(relpath) {
  return urlBase() + "/" + relpath;
}

var fmtMs = d3.format(".2f")
function fmtNs(ns) {
  return fmtMs(ns / 1e6)
}

var txID = getTxID(),
    report = d3.select("#report-container");

var blockWidth = 200, blockHeight = 20;
var blockScale = d3.scale.linear().domain([0, 1.0]).range([0, blockWidth]);

// Returns an array of pairs: a field name and a field value in each
// case. The data describes the given transaction. No attempt is made
// to traverse the children in the call graph.
function summarize(txNode)  {
  var txSummary = [
    ["Client", txNode["Client"]],
    ["Client Host", txNode["ClientHost"]],
    ["Client Pid", txNode["ClientPid"]],
    ["Server", txNode["Server"]],
    ["Server Host", txNode["ServerHost"]],
    ["Server Pid", txNode["ServerPid"]],
    ["Signature", txNode["Signature"]],
    ["Duration (ns)", txNode["ClientNanos"] || txNode["ServerNanos"]]
  ];
  if ("RPC" in txNode) {
    if (txNode["RPC"]["Protocol"] === "HTTP") {
      txSummary = txSummary.concat([
        ["URI", txNode["RPC"]["HTTPURIPath"]],
        ["Status", txNode["RPC"]["HTTPStatus"]],
        ["Method", txNode["RPC"]["HTTPMethod"]]
      ]);
    }
  }
  txSummary.push(["Raw JSON", "<a href=\"tx/" + txID + ".json\">link</a>"]);
  return txSummary;
}

// Updates an html table element to hold summary data from the txData
function updateSummary(txData, table) {
  var rows = table.select("tbody")
      .selectAll("tr")
      .data(summarize(txData));

  rows.enter()
    .append("tr")
    .attr("class", "tx-field")

  var fieldNames = rows.selectAll("td.tx-field").data(function (d) { return d; });

  fieldNames.enter().append("td")
    .attr("class", "tx-field-name")
    .html(function (d) { return d });
}

function CPUTimeByService(txData) {
  var clientTimes = d3.map(),
      serverTimes = d3.map(),
      services = [];

  function gatherCPUTime(txNode) {
    var cName = txNode["Client"],
        cTime = txNode["ClientNanos"],
        sName = txNode["Server"],
        sTime = txNode["ServerNanos"];

    if (cName !== null && cTime !== null) {
      if (!clientTimes.has(cName)) {
        clientTimes.set(cName, cTime);
      } else {
        clientTimes.set(cName, clientTimes.get(cName) + cTime);
      }
      if (services.indexOf(cName) === -1) {
        services.push(cName);
      }
    }
    if (sName !== null && sTime !== null) {
      if (!serverTimes.has(sName)) {
        serverTimes.set(sName, sTime);
      } else {
        serverTimes.set(sName, serverTimes.get(sName) + sTime);
      }
      if (services.indexOf(sName) === -1) {
        services.push(sName);
      }
    }
  }
  walk(txData, gatherCPUTime);

  var resultTable = [];
  services.forEach(function (svc) {
    resultTable.push(d3.map({
      "service": svc,
      "clientCPU": clientTimes.has(svc) ? clientTimes.get(svc) : 0,
      "serverCPU": serverTimes.has(svc) ? serverTimes.get(svc) : 0,
    }));
  });

  return resultTable
}

function callTableData(txData) {
  var callData = txCallIterator(txData, null),
      rowData = [];

  var txStart = callData[0].ClientStart || callData[0].ServerStart;
  var start, offset, duration;
  callData.forEach(function (call) {
    start = call.ClientStart || call.ServerStart;
    offset = start - txStart;
    duration = (call.ClientEnd || call.ServerEnd) - start;
    rowData.push(d3.map({
      clientStart: call.ClientStart ? call.ClientStart - txStart : undefined,
      clientEnd: call.ClientEnd ? call.ClientEnd - txStart : undefined,
      serverStart: call.ServerStart ? call.ServerStart - txStart : undefined,
      serverEnd: call.ServerEnd ? call.ServerEnd - txStart : undefined,
      offset: offset,
      path: call.Path,
      duration: duration,
      client: call.Client,
      server: call.Server,
      description: describeCall(call)
    }));
  });
  return rowData;
}

// returns UTC nanoseconds for a timestamp
function parseTime(timestamp) {
  return timestamp
}

// Return a short text description of what went down in the call
function describeCall(call) {
  var descr;
  // try to be as helpful as we can: if there's RPC info, great!
  if (call.RPC !== undefined) {
    if (call.RPC.Protocol === "HTTP") {
      return "HTTP: "  + call.RPC.HTTPMethod + " to " + call.RPC.HTTPURIPath + ", got " + call.RPC.HTTPStatus;
    }
    if (call.RPC.Protocol === "SQL") {
      return "SQL: " + call.RPC.StrippedQuery + " on " + call.RPC.DatabaseName + " as " + call.RPC.DatabaseUser;
    }
    if (call.RPC.Protocol === "Memcached") {
      return "Memcached: " + call.RPC.Name + ", " + call.RPC.KeysReceived + " of " + call.RPC.KeysRequested;
    }
    if (call.RPC.Protocol === "gRPC") {
      return "gRPC: " + call.RPC.Method;
    }
  } else {
    // Try to describe why this call is mostly unknown
    if (!call.Client) {
      return "Unknown client";
    // Give up
    } else {
      return "Unknown call";
    }
  }
}

// apply a function to each node of the dataset. If you need to gather
// values, use a closure - this wrapper function does not return
// anything. Traversal is depth-first.
function walk(txData, callback) {
  for (var path in txData.Children) {
    var child = txData.Children[path];
    walk(child, callback);
  }
  callback(txData);
}

// returns an array of the calls in a transaction, sorted in
// path-based execution order.
function txCallIterator(txData, parent) {
  var result = [];
  // First, a node
  var node = bareCopy(txData);
  node.parent = parent;
  result.push(node)
  // Then, its sorted children
  var childPaths = [];
  for (var path in txData.Children) {
    childPaths.push(path);
  }
  childPaths.sort(function(a, b) { return d3.ascending(parseInt(a, 10), parseInt(b, 10)) });
  // Mark this node as the parent of each child
  childPaths.forEach(function (p) {
    result = result.concat(txCallIterator(txData.Children[p], node));
  });
  return result
}

// remove the 'children' and 'raw' fields from a node, just giving its value
function bareCopy(txNode) {
  var copy = {};
  for (var attr in txNode) {
    if (txNode.hasOwnProperty(attr) && attr !== "children" && attr !== "raw") {
      copy[attr] = txNode[attr];
    }
  }
  return copy;
}

function getCallEnd(c) {
  return c.get("clientEnd") > c.get("serverEnd") ? c.get("clientEnd") : c.get("serverEnd");
}

function updateCallTable(txData, table) {
  var tableData = callTableData(txData);
  var endOffset = d3.max(tableData, getCallEnd);

  blockScale = blockScale.domain([0, endOffset]);

  var rows = table.select("tbody")
      .selectAll("tr")
      .data(tableData)
      .enter()
      .append("tr");

  rows.selectAll("td.tx-calls-path")
    .data(function(d) {
      return d3.values(d);
    })
    .enter()
    .append("td")
    .attr("class", "tx-calls-path")
    .text(function (d) { return d["path"]; });

  rows.selectAll("td.tx-calls-offset")
    .data(function (d) {
      return d3.values(d);
    })
    .enter()
    .append("td")
    .attr("class", "tx-calls-offset numeric")
    .text(function (d) {
      return fmtNs(d["offset"])
    });

  rows.selectAll("td.tx-calls-duration")
    .data(function (d) { return d3.values(d) })
    .enter()
    .append("td")
    .attr("class", "tx-calls-duration numeric")
    .text(function (d) {
      return fmtNs(d["duration"]);
    });

  var blocks = rows.selectAll("td.tx-calls-block")
      .data(function (d) { return d3.values(d) })
      .enter()
      .append("td")
      .attr("class", "tx-calls-block")
      .style("width", blockWidth + "px")
      .append("svg")
      .attr("width", blockWidth + "px")
      .attr("height", blockHeight + "px")

  var clientBlockThickness = 5,
      serverBlockThickness = 10;

  var cBYOffset = blockHeight / 2 - clientBlockThickness / 2;
  var sBYOffset = blockHeight / 2 - serverBlockThickness / 2;
  blocks.append("svg:rect")
    .attr("x", function(d) { return blockScale(d["clientStart"]) })
    .attr("width", function(d) { return blockScale(d["clientEnd"] - d["clientStart"]) })
    .attr("y", cBYOffset)
    .attr("height", clientBlockThickness)
    .attr("fill", "darkgreen")
    .attr("fill-opacity", 0.5)

  blocks.append("svg:rect")
    .attr("x", function(d) { return blockScale(d["serverStart"]) })
    .attr("width", function(d) { return blockScale(d["serverEnd"] - d["serverStart"]) })
    .attr("y", sBYOffset)
    .attr("height", serverBlockThickness)
    .attr("fill", "darkred")
    .attr("fill-opacity", 0.5)

  rows.selectAll("td.tx-calls-client")
    .data(function (d) { return d3.values(d) })
    .enter()
    .append("td")
    .attr("class", "tx-calls-client")
    .append("a")
    .attr("href", function(d) {
      return urlFor("bin/" + d["client"] + ".html");
    })
    .text(function (d) {
      return d["client"];
    });
  rows.selectAll("td.tx-calls-server")
    .data(function (d) { return d3.values(d) })
    .enter()
    .append("td")
    .attr("class", "tx-calls-server")
    .append("a")
    .attr("href", function(d) {
      return urlFor("bin/" + d["server"] + ".html");
    })
    .text(function (d) {
      return d["server"];
    });
  rows.selectAll("td.tx-calls-description")
    .data(function (d) { return d3.values(d) })
    .enter()
    .append("td")
    .attr("class", "tx-calls-description")
    .text(function (d) {
      return d["description"];
    });

}

function updateTimingTable(txData, table) {
  var rows = table.select("tbody")
      .selectAll("tr")
      .data(CPUTimeByService(txData));

  var newRows = rows.enter()
    .append("tr")
    .attr("class", "tx-timing-row");

  newRows.selectAll("td.tx-timing-svcname")
    .data(function (d) { return d3.values(d) })
    .enter()
    .append("td")
    .attr("class", "tx-timing-svcname")
    .html(function (d) {
      return d["service"];
    });
  newRows.selectAll("td.tx-timing-client")
    .data(function (d) { return d3.values(d) })
    .enter()
    .append("td")
    .attr("class", "tx-timing-client numeric")
    .text(function (d) {
      return fmtNs(d["clientCPU"]);
    });
  newRows.selectAll("td.tx-timing-server")
    .data(function (d) { return d3.values(d) })
    .enter()
    .append("td")
    .attr("class", "tx-timing-server numeric")
    .text(function (d) {
      return fmtNs(d["serverCPU"]);
    });
  newRows.selectAll("td.tx-timing-total")
    .data(function (d) { return d3.values(d) })
    .enter()
    .append("td")
    .attr("class", "tx-timing-total numeric")
    .text(function (d) {
      return fmtNs(d["clientCPU"] + d["serverCPU"]);
    });

}

function writeTree(txData, container) {
  var margin = {top: 10, bottom: 10,
                right: 100, left: 100},
      width = parseInt(container.style('width')),
      width = width - margin.left - margin.right,
      height = width / 1.6 - margin.top - margin.bottom;

  var svg = container.select("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)

  var plot = svg.append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  var tree = d3.layout.tree()
      .children(function (d) {
        var children = [];
        for (var path in d.Children) {
          children.push(d.Children[path]);
        }
        return children;
      })
      .size([height, width]);

  var diagonal = d3.svg.diagonal()
      .projection(function(d) { return [d.y, d.x]; });

  var nodes = tree.nodes(txData);

  if (nodes.length > MAX_TREE_SIZE_TO_PLOT) {
    height = 30;
    svg.attr("height", height);
    plot.attr("transform", "")
      .append("text")
      .attr("text-anchor", "center")
      .attr("x", width / 2)
      .attr("y", height / 2)
      .text("Tree too large to plot (" + nodes.length + " nodes)")
    return
  }

  plot.selectAll(".link")
    .data(tree.links(nodes))
    .enter().append("path")
    .attr("class", "link")
    .attr("d", diagonal)
    .style({"fill": "none", "stroke": "black"});

  var nodeElements = plot.selectAll("g.node")
      .data(nodes)
      .enter()
      .append("g")
      .attr("class", "node")
      .attr("transform", function(d) {
        return "translate(" + d.y + "," + d.x + ")"
      });

  nodeElements.append("circle")
    .attr("r", width / 100)
    .attr("cx", 0)
    .attr("cy", 0)
    .style({"stroke": "black"});

  var label = nodeElements
      .append("g")
      .attr("class", "call-label")
      .attr("transform", "translate(-50)")
      .append("text")
      .attr("text-anchor", "left")
      .attr("font-size", "0.8em")
      .attr("font-family", "Helvetica")

  label.append("tspan")
    .text(function (d) {
      return "dest: " + (d.Server || "???");
    })
    .attr("dy", "1.2em")
    .attr("x", 0);

  label.append("tspan")
    .text(describeCall)
    .attr("dy", "1.2em")
    .attr("x", 0);
}

// Do it!
d3.json("tx/" + txID + ".json", function(txData) {
  updateSummary(txData, report.select("table.tx-summary"));
  updateTimingTable(txData, report.select("table.tx-timings"));
  updateCallTable(txData, report.select("table.tx-calls"));
  writeTree(txData, report.select(".tx-tree"));
})
