<!DOCTYPE html>
<html>
  <!--
  Copyright 2017 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.
  -->
<head>
<meta charset="utf-8">
<style>
html, body {
  font-family: sans-serif;
  padding: 0px;
  margin: 0px;
}
h1, h2, h3, section {
  padding-left: 15px;
}

kbd {
  background-color: #eee;
  border-radius: 3px;
  border: 1px solid black;
  display: inline-block;
  font-size: .9em;
  font-weight: bold;
  padding: 0px 4px 2px 4px;
  white-space: nowrap;
}
dl {
  display: grid;
  grid-template-columns: min-content auto;
  grid-gap: 10px;
}
dt {
  text-align: right;
  white-space: nowrap;
}
dd {
  margin: 0;
}

#content {
  opacity: 0.0;
  height: 0px;
  transition: all 0.5s ease-in-out;
}

.success #content {
  height: auto;
  opacity: 1.0;
}

#fileReader {
  width: 100%;
  height: 100px;
  line-height: 100px;
  text-align: center;
  border: solid 1px #000000;
  border-radius: 5px;
  cursor: pointer;
  transition: all 0.5s ease-in-out;
}

.failure #fileReader {
  background-color: #FFAAAA;
}

.success #fileReader {
    height: 20px;
    line-height: 20px;
}

#fileReader:hover {
  background-color: #e0edfe;
}

.loading #fileReader {
  cursor: wait;
}

#fileReader > input {
  display: none;
}


#loader {
  display: none;
}

.loading #loader {
  display: block;
  position: fixed;
  top: 0px;
  left: 0px;
  width: 100%;
  height: 100%;
  background-color: rgba(255, 255, 255, 0.5);
}

#spinner {
  position: absolute;
  width: 100px;
  height: 100px;
  top: 40%;
  left: 50%;
  margin-left: -50px;
  border: 30px solid #000;
  border-top: 30px solid #36E;
  border-radius: 50%;
  animation: spin 1s ease-in-out infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.colorbox {
  width: 10px;
  height: 10px;
  border: 1px black solid;
}

#stats {
  display: flex;
  height: 250px;
}

#stats table {
  flex: 1;
  padding-right: 50px;
  max-height: 250px;
  display: inline-block;
}
#stats table td {
  cursor: pointer;
}
#stats .transitionTable {
  overflow-y: scroll;
}
#stats .transitionTable tr {
  max-width: 200px;

}
#stats .transitionType {
  text-align: right;
  max-width: 380px;
}
#stats .transitionType tr td:nth-child(2) {
  text-align: left;
}
#stats table thead td {
  border-bottom: 1px black dotted;
}

#timeline {
  position: relative;
  height: 300px;
  overflow-y: hidden;
  overflow-x: scroll;
  user-select: none;
}
#timelineLabel {
  transform: rotate(90deg);
  transform-origin: left bottom 0;
  position: absolute;
  left: 0;
  width: 250px;
  text-align: center;
  font-size: 10px;
  opacity: 0.5;
}
#timelineChunks {
  height: 250px;
  position: absolute;
  margin-right: 100px;
}
#timelineCanvas {
  height: 250px;
  position: relative;
  overflow: visible;
  pointer-events: none;
}
.chunk {
  width: 6px;
  border: 0px white solid;
  border-width: 0 2px 0 2px;
  position: absolute;
  background-size: 100% 100%;
  image-rendering: pixelated;
  bottom: 0px;
}
.timestamp {
  height: 250px;
  width: 100px;
  border-left: 1px black dashed;
  padding-left: 4px;
  position: absolute;
  pointer-events: none;
  font-size: 10px;
  opacity: 0.5;
}
#timelineOverview {
  width: 100%;
  height: 50px;
  position: relative;
  margin-top: -50px;
  margin-bottom: 10px;
  background-size: 100% 100%;
  border: 1px black solid;
  border-width: 1px 0 1px 0;
  overflow: hidden;
}
#timelineOverviewIndicator {
  height: 100%;
  position: absolute;
  box-shadow: 0px 2px 20px -5px black inset;
  top: 0px;
  cursor: ew-resize;
}
#timelineOverviewIndicator .leftMask,
#timelineOverviewIndicator .rightMask {
  background-color: rgba(200, 200, 200, 0.5);
  width: 10000px;
  height: 100%;
  position: absolute;
  top: 0px;
}
#timelineOverviewIndicator .leftMask {
  right: 100%;
}
#timelineOverviewIndicator .rightMask {
  left: 100%;
}
#mapDetails {
  font-family: monospace;
  white-space: pre;
}
#transitionView {
  overflow-x: scroll;
  white-space: nowrap;
  min-height: 50px;
  max-height: 200px;
  padding: 50px 0 0 0;
  margin-top: -25px;
  width: 100%;
}
.map {
  width: 20px;
  height: 20px;
  display: inline-block;
  border-radius: 50%;
  background-color: black;
  border: 4px solid white;
  font-size: 10px;
  text-align: center;
  line-height: 18px;
  color: white;
  vertical-align: top;
  margin-top: -13px;
  /* raise z-index */
  position: relative;
  z-index: 2;
  cursor: pointer;
}
.map.selected {
  border-color: black;
}
.transitions {
  display: inline-block;
  margin-left: -15px;
}
.transition {
  min-height: 55px;
  margin: 0 0 -2px 2px;
}
/* gray out deprecated transitions */
.deprecated > .transitionEdge,
.deprecated > .map {
  opacity: 0.5;
}
.deprecated > .transition {
  border-color: rgba(0, 0, 0, 0.5);
}
/* Show a border for all but the first transition */
.transition:nth-of-type(2),
.transition:nth-last-of-type(n+2) {
  border-left: 2px solid;
  margin-left: 0px;
}
/* special case for 2 transitions */
.transition:nth-last-of-type(1) {
  border-left: none;
}
/* topmost transitions are not related */
#transitionView > .transition {
  border-left: none;
}
/* topmost transition edge needs initial offset to be aligned */
#transitionView > .transition  > .transitionEdge {
  margin-left: 13px;
}
.transitionEdge {
  height: 2px;
  width: 80px;
  display: inline-block;
  margin: 0 0 2px 0;
  background-color: black;
  vertical-align: top;
  padding-left: 15px;
}
.transitionLabel {
  color: black;
  transform: rotate(-15deg);
  transform-origin: top left;
  margin-top: -10px;
  font-size: 10px;
  white-space: normal;
  word-break: break-all;
  background-color: rgba(255,255,255,0.5);
}
.black{
  background-color: black;
}
.red {
  background-color: red;
}
.green {
  background-color: green;
}
.yellow {
  background-color: yellow;
  color: black;
}
.blue {
  background-color: blue;
}
.orange {
  background-color: orange;
}
.violet {
  background-color: violet;
  color: black;
}
.showSubtransitions {
  width: 0;
  height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 10px solid black;
  cursor: zoom-in;
  margin: 4px 0 0 4px;
}
.showSubtransitions.opened {
  border-top: none;
  border-bottom: 10px solid black;
  cursor: zoom-out;
}
#tooltip {
  position: absolute;
  width: 10px;
  height: 10px;
  background-color: red;
  pointer-events: none;
  z-index: 100;
  display: none;
}
#searchBarInput {
  width: 200px;
}

</style>
<script src="./splaytree.js"></script>
<script src="./codemap.js"></script>
<script src="./csvparser.js"></script>
<script src="./consarray.js"></script>
<script src="./profile.js"></script>
<script src="./profile_view.js"></script>
<script src="./logreader.js"></script>
<script src="./SourceMap.js"></script>
<script src="./arguments.js"></script>
<script src="./map-processor.js"></script>
<script>
"use strict"
// =========================================================================
const kChunkHeight = 250;
const kChunkWidth = 10;

class State {
  constructor() {
    this._nofChunks = 400;
    this._map = undefined;
    this._timeline = undefined;
    this._chunks = undefined;
    this._view = new View(this);
    this._navigation = new Navigation(this, this.view);
  }
  get timeline() { return this._timeline }
  set timeline(value) {
    this._timeline = value;
    this.updateChunks();
    this.view.updateTimeline();
    this.view.updateStats();
  }
  get chunks() { return this._chunks }
  get nofChunks() { return this._nofChunks }
  set nofChunks(count) {
    this._nofChunks = count;
    this.updateChunks();
    this.view.updateTimeline();
  }
  get view() { return this._view }
  get navigation() { return this._navigation }
  get map() { return this._map }
  set map(value) {
    this._map = value;
    this._navigation.updateUrl();
    this.view.updateMapDetails();
    this.view.redraw();
  }
  updateChunks() {
    this._chunks = this._timeline.chunks(this._nofChunks);
  }
  get entries() {
    if (!this.map) return {};
    return {
      map: this.map.id,
      time: this.map.time
    }
  }
}

// =========================================================================
// DOM Helper
function $(id) {
  return document.getElementById(id)
}

function removeAllChildren(node) {
  while (node.lastChild) {
    node.removeChild(node.lastChild);
  }
}

function selectOption(select, match) {
  let options = select.options;
  for (let i = 0; i < options.length; i++) {
    if (match(i, options[i])) {
      select.selectedIndex = i;
      return;
    }
  }
}

function div(classes) {
  let node = document.createElement('div');
  if (classes !== void 0) {
    if (typeof classes === "string") {
      node.classList.add(classes);
    } else {
      classes.forEach(cls => node.classList.add(cls));
    }
  }
  return node;
}

function table(className) {
  let node = document.createElement("table")
  if (className) node.classList.add(className)
  return node;
}

function td(textOrNode) {
  let node = document.createElement("td");
  if (typeof textOrNode === "object") {
    node.appendChild(textOrNode);
  } else {
    node.innerText = textOrNode;
  }
  return node;
}


function tr() {
  return document.createElement("tr");
}

define(Array.prototype, "histogram", function(mapFn) {
  let histogram = [];
  for (let i = 0; i < this.length; i++) {
    let value = this[i];
    let index = Math.round(mapFn(value))
    let bucket = histogram[index];
    if (bucket !== undefined) {
      bucket.push(value);
    } else {
      histogram[index] = [value];
    }
  }
  for (let i = 0; i < histogram.length; i++) {
    histogram[i] = histogram[i] || [];
  }
  return histogram;
});


// =========================================================================
// EventHandlers
function handleSearchBar(){
  let searchBar = $('searchBarInput');
  let searchBarInput = searchBar.value;
  let selectedMap = V8Map.get(searchBarInput);
  //removeAllChildren($('mapIdList'));
  if(selectedMap){
    let map = selectedMap;
    document.state.map = map;
    searchBar.className = "green";
  } else {
    searchBar.className = "red";
  }
}

function handleBodyLoad() {
  let upload = $('fileReader');
  upload.onclick = (e) => $("file").click();
  upload.ondragover = (e) => e.preventDefault();
  upload.ondrop = (e) => handleLoadFile(e);
  $('file').onchange = (e) => handleLoadFile(e);
  upload.onkeydown = (e) =>  {
    if (event.key == "Enter") $("file").click();
  };
  upload.focus();

  document.state = new State();
  $("transitionView").addEventListener("mousemove", e => {
    let tooltip = $("tooltip");
    tooltip.style.left = e.pageX + "px";
    tooltip.style.top = e.pageY + "px";
    let map = e.target.map;
    if (map) {
      $("tooltipContents").innerText = map.description;
    }
  });

  function handleLoadFile(event) {
    // Used for drop and file change.
    event.preventDefault();
    let host = event.dataTransfer ? event.dataTransfer : event.target;
    let file = host.files[0];
    let reader = new FileReader();
    document.body.className = 'loading';
    reader.onload = function(evt) {
      try {
        handleLoadText(this.result);
       document.body.className = 'success';
      } catch(e) {
       document.body.className = 'failure';
       console.error(e);
      }
    }
    // Defer the reading to allow spinner CSS animation.
    setTimeout(() => reader.readAsText(file), 0);
  }
}


function handleLoadText(text) {
  let mapProcessor = new MapProcessor();
  document.state.timeline = mapProcessor.processString(text);
}

function handleKeyDown(event) {
  let nav = document.state.navigation;
  switch(event.key) {
    case "ArrowUp":
      event.preventDefault();
      if (event.shiftKey) {
        nav.selectPrevEdge();
      } else {
        nav.moveInChunk(-1);
      }
      return false;
    case "ArrowDown":
      event.preventDefault();
      if (event.shiftKey) {
        nav.selectNextEdge();
      } else {
        nav.moveInChunk(1);
      }
      return false;
    case "ArrowLeft":
      nav.moveInChunks(false);
      break;
    case "ArrowRight":
      nav.moveInChunks(true);
      break;
    case "+":
      nav.increaseTimelineResolution();
      break;
    case "-":
      nav.decreaseTimelineResolution();
      break;
  }
};
document.onkeydown = handleKeyDown;

function handleTimelineIndicatorMove(event) {
  if (event.buttons == 0) return;
  let timelineTotalWidth = $("timelineCanvas").offsetWidth;
  let factor = $("timelineOverview").offsetWidth / timelineTotalWidth;
  $("timeline").scrollLeft += event.movementX / factor;
}

// =========================================================================

Object.defineProperty(Edge.prototype, 'getColor', { value:function() {
  return transitionTypeToColor(this.type);
}});

class Navigation {
  constructor(state, view) {
    this.state = state;
    this.view = view;
  }
  get map() { return this.state.map }
  set map(value) { this.state.map = value }
  get chunks() { return this.state.chunks }

  increaseTimelineResolution() {
    this.state.nofChunks *= 1.5;
  }

  decreaseTimelineResolution() {
    this.state.nofChunks /= 1.5;
  }

  selectNextEdge() {
    if (!this.map) return;
    if (this.map.children.length != 1) return;
    this.map = this.map.children[0].to;
  }

  selectPrevEdge() {
    if (!this.map) return;
    if (!this.map.parent()) return;
    this.map = this.map.parent();
  }

  selectDefaultMap() {
      this.map = this.chunks[0].at(0);
  }
  moveInChunks(next) {
    if (!this.map) return this.selectDefaultMap();
    let chunkIndex = this.map.chunkIndex(this.chunks);
    let chunk = this.chunks[chunkIndex];
    let index = chunk.indexOf(this.map);
    if (next) {
      chunk = chunk.next(this.chunks);
    } else {
      chunk = chunk.prev(this.chunks);
    }
    if (!chunk) return;
    index = Math.min(index, chunk.size()-1);
    this.map = chunk.at(index);
  }

  moveInChunk(delta) {
    if (!this.map) return this.selectDefaultMap();
    let chunkIndex = this.map.chunkIndex(this.chunks)
    let chunk = this.chunks[chunkIndex];
    let index = chunk.indexOf(this.map) + delta;
    let map;
    if (index < 0) {
      map = chunk.prev(this.chunks).last();
    } else if (index >= chunk.size()) {
      map = chunk.next(this.chunks).first()
    } else {
      map = chunk.at(index);
    }
    this.map = map;
  }

  updateUrl() {
    let entries = this.state.entries;
    let params = new URLSearchParams(entries);
    window.history.pushState(entries, "", "?" + params.toString());
  }
}

class View {
  constructor(state) {
    this.state = state;
    setInterval(this.updateOverviewWindow, 50);
    this.backgroundCanvas = document.createElement("canvas");
    this.transitionView = new TransitionView(state, $("transitionView"));
    this.statsView = new StatsView(state, $("stats"));
    this.isLocked = false;
  }
  get chunks() { return this.state.chunks }
  get timeline() { return this.state.timeline }
  get map() { return this.state.map }

  updateStats() {
    this.statsView.update();
  }

  updateMapDetails() {
    let details = "";
    if (this.map) {
      details += "ID: " + this.map.id;
      details += "\nSource location: " + this.map.filePosition;
      details += "\n" + this.map.description;
    }
    $("mapDetails").innerText = details;
    this.transitionView.showMap(this.map);
  }

  updateTimeline() {
    let chunksNode = $("timelineChunks");
    removeAllChildren(chunksNode);
    let chunks = this.chunks;
    let max = chunks.max(each => each.size());
    let start = this.timeline.startTime;
    let end = this.timeline.endTime;
    let duration = end - start;
    const timeToPixel = chunks.length * kChunkWidth / duration;
    let addTimestamp = (time, name) => {
      let timeNode = div("timestamp");
      timeNode.innerText = name;
      timeNode.style.left = ((time-start) * timeToPixel) + "px";
      chunksNode.appendChild(timeNode);
    };
    let backgroundTodo = [];
    for (let i = 0; i < chunks.length; i++) {
      let chunk = chunks[i];
      let height = (chunk.size() / max * kChunkHeight);
      chunk.height = height;
      if (chunk.isEmpty()) continue;
      let node = div();
      node.className = "chunk";
      node.style.left = (i * kChunkWidth) + "px";
      node.style.height = height + "px";
      node.chunk = chunk;
      node.addEventListener("mousemove", e => this.handleChunkMouseMove(e));
      node.addEventListener("click", e => this.handleChunkClick(e));
      node.addEventListener("dblclick", e => this.handleChunkDoubleClick(e));
      backgroundTodo.push([chunk, node])
      chunksNode.appendChild(node);
      chunk.markers.forEach(marker => addTimestamp(marker.time, marker.name));
    }

    this.asyncSetTimelineChunkBackground(backgroundTodo)

    // Put a time marker roughly every 20 chunks.
    let expected  = duration / chunks.length * 20;
    let interval = (10 ** Math.floor(Math.log10(expected)));
    let correction = Math.log10(expected / interval);
    correction = (correction < 0.33) ? 1 : (correction < 0.75) ? 2.5 : 5;
    interval *= correction;

    let time = start;
    while (time < end) {
      addTimestamp(time, ((time-start) / 1000) + " ms");
      time += interval;
    }
    this.drawOverview();
    this.redraw();
  }

  handleChunkMouseMove(event) {
    if (this.isLocked) return false;
    let chunk = event.target.chunk;
    if (!chunk) return;
    // topmost map (at chunk.height) == map #0.
    let relativeIndex =
        Math.round(event.layerY / event.target.offsetHeight * chunk.size());
    let map = chunk.at(relativeIndex);
    this.state.map = map;
  }

  handleChunkClick(event) {
    this.isLocked = !this.isLocked;
  }

  handleChunkDoubleClick(event) {
    this.isLocked = true;
    let chunk = event.target.chunk;
    if (!chunk) return;
    this.transitionView.showMaps(chunk.getUniqueTransitions());
  }

  asyncSetTimelineChunkBackground(backgroundTodo) {
    const kIncrement = 100;
    let start = 0;
    let delay = 1;
    while (start < backgroundTodo.length) {
      let end = Math.min(start+kIncrement, backgroundTodo.length);
      setTimeout((from, to) => {
        for (let i = from; i < to; i++) {
          let [chunk, node] = backgroundTodo[i];
          this.setTimelineChunkBackground(chunk, node);
        }
      }, delay++, start, end);
      start = end;
    }
  }

  setTimelineChunkBackground(chunk, node) {
    // Render the types of transitions as bar charts
    const kHeight = chunk.height;
    const kWidth = 1;
    this.backgroundCanvas.width = kWidth;
    this.backgroundCanvas.height = kHeight;
    let ctx = this.backgroundCanvas.getContext("2d");
    ctx.clearRect(0, 0, kWidth, kHeight);
    let y = 0;
    let total = chunk.size();
    let type, count;
    if (true) {
       chunk.getTransitionBreakdown().forEach(([type, count]) => {
          ctx.fillStyle = transitionTypeToColor(type);
          let height = count / total * kHeight;
          ctx.fillRect(0, y, kWidth, y + height);
          y += height;
      });
    } else {
      chunk.items.forEach(map => {
        ctx.fillStyle = transitionTypeToColor(map.getType());
        let y = chunk.yOffset(map);
        ctx.fillRect(0, y, kWidth, y + 1);
      });
    }

    let imageData = this.backgroundCanvas.toDataURL("image/webp", 0.2);
    node.style.backgroundImage = "url(" + imageData + ")";
  }

  updateOverviewWindow() {
    let indicator = $("timelineOverviewIndicator");
    let totalIndicatorWidth = $("timelineOverview").offsetWidth;
    let div = $("timeline");
    let timelineTotalWidth = $("timelineCanvas").offsetWidth;
    let factor = $("timelineOverview").offsetWidth / timelineTotalWidth;
    let width = div.offsetWidth * factor;
    let left = div.scrollLeft * factor;
    indicator.style.width = width + "px";
    indicator.style.left = left + "px";
  }

  drawOverview() {
    const height = 50;
    const kFactor = 2;
    let canvas =  this.backgroundCanvas;
    canvas.height = height;
    canvas.width = window.innerWidth;
    let ctx = canvas.getContext("2d");

    let chunks = this.state.timeline.chunkSizes(canvas.width * kFactor);
    let max = chunks.max();

    ctx.clearRect(0, 0, canvas.width, height);
    ctx.strokeStyle = "black";
    ctx.fillStyle = "black";
    ctx.beginPath();
    ctx.moveTo(0,height);
    for (let i = 0; i < chunks.length; i++) {
      ctx.lineTo(i/kFactor, height - chunks[i]/max * height);
    }
    ctx.lineTo(chunks.length, height);
    ctx.stroke();
    ctx.closePath();
    ctx.fill();
    let imageData = canvas.toDataURL("image/webp", 0.2);
    $("timelineOverview").style.backgroundImage = "url(" + imageData + ")";
  }

  redraw() {
    let canvas= $("timelineCanvas");
    canvas.width = (this.chunks.length+1) * kChunkWidth;
    canvas.height = kChunkHeight;
    let ctx = canvas.getContext("2d");
    ctx.clearRect(0, 0, canvas.width, kChunkHeight);
    if (!this.state.map) return;
    this.drawEdges(ctx);
  }

  setMapStyle(map, ctx) {
    ctx.fillStyle = map.edge && map.edge.from  ? "black" : "green";
  }

  setEdgeStyle(edge, ctx) {
    let color = edge.getColor();
    ctx.strokeStyle = color;
    ctx.fillStyle = color;
  }

  markMap(ctx, map) {
    let [x, y] = map.position(this.state.chunks);
    ctx.beginPath();
    this.setMapStyle(map, ctx);
    ctx.arc(x, y, 3, 0, 2 * Math.PI);
    ctx.fill();
    ctx.beginPath();
    ctx.fillStyle = "white";
    ctx.arc(x, y, 2, 0, 2 * Math.PI);
    ctx.fill();
  }

  markSelectedMap(ctx, map) {
    let [x, y] = map.position(this.state.chunks);
    ctx.beginPath();
    this.setMapStyle(map, ctx);
    ctx.arc(x, y, 6, 0, 2 * Math.PI);
    ctx.stroke();
  }

  drawEdges(ctx) {
    // Draw the trace of maps in reverse order to make sure the outgoing
    // transitions of previous maps aren't drawn over.
    const kMaxOutgoingEdges = 100;
    let nofEdges = 0;
    let stack = [];
    let current = this.state.map;
    while (current && nofEdges < kMaxOutgoingEdges) {
      nofEdges += current.children.length;
      stack.push(current);
      current = current.parent();
    }
    ctx.save();
    this.drawOutgoingEdges(ctx, this.state.map, 3);
    ctx.restore();

    let labelOffset = 15;
    let xPrev = 0;
    while (current = stack.pop()) {
      if (current.edge) {
        this.setEdgeStyle(current.edge, ctx);
        let [xTo, yTo] = this.drawEdge(ctx, current.edge, true, labelOffset);
        if (xTo == xPrev) {
          labelOffset += 8;
        } else {
          labelOffset = 15
        }
        xPrev = xTo;
      }
      this.markMap(ctx, current);
      current = current.parent();
      ctx.save();
      // this.drawOutgoingEdges(ctx, current, 1);
      ctx.restore();
    }
    // Mark selected map
    this.markSelectedMap(ctx, this.state.map);
  }

  drawEdge(ctx, edge, showLabel=true, labelOffset=20) {
    if (!edge.from || !edge.to) return [-1, -1];
    let [xFrom, yFrom] = edge.from.position(this.chunks);
    let [xTo, yTo] = edge.to.position(this.chunks);
    let sameChunk = xTo == xFrom;
    if (sameChunk) labelOffset += 8;

    ctx.beginPath();
    ctx.moveTo(xFrom, yFrom);
    let offsetX = 20;
    let offsetY = 20;
    let midX = xFrom + (xTo- xFrom) / 2;
    let midY = (yFrom + yTo) / 2 - 100;
    if (!sameChunk) {
      ctx.quadraticCurveTo(midX, midY, xTo, yTo);
    } else {
      ctx.lineTo(xTo, yTo);
    }
    if (!showLabel) {
      ctx.stroke();
    } else {
      let centerX, centerY;
      if (!sameChunk) {
      centerX = (xFrom/2 + midX + xTo/2)/2;
      centerY = (yFrom/2 + midY + yTo/2)/2;
      } else {
        centerX = xTo;
        centerY = yTo;
      }
      ctx.moveTo(centerX, centerY);
      ctx.lineTo(centerX + offsetX, centerY - labelOffset);
      ctx.stroke();
      ctx.textAlign = "left";
      ctx.fillText(edge.toString(), centerX + offsetX + 2, centerY - labelOffset)
    }
    return [xTo, yTo];
  }

  drawOutgoingEdges(ctx, map, max=10, depth=0) {
    if (!map) return;
    if (depth >= max) return;
    ctx.globalAlpha = 0.5 - depth * (0.3/max);
    ctx.strokeStyle = "#666";

    const limit = Math.min(map.children.length, 100)
    for (let i = 0; i < limit; i++) {
      let edge = map.children[i];
      this.drawEdge(ctx, edge, true);
      this.drawOutgoingEdges(ctx, edge.to, max, depth+1);
    }
  }
}


class TransitionView {
  constructor(state, node) {
    this.state = state;
    this.container = node;
    this.currentNode = node;
    this.currentMap = undefined;
  }

  selectMap(map) {
    this.currentMap = map;
    this.state.map = map;
  }

  showMap(map) {
    if (this.currentMap === map) return;
    this.currentMap = map;
    this._showMaps([map]);
  }

  showMaps(list, name) {
    this.state.view.isLocked = true;
    this._showMaps(list);
  }

 _showMaps(list, name) {
    // Hide the container to avoid any layouts.
    this.container.style.display = "none";
    removeAllChildren(this.container);
    list.forEach(map => this.addMapAndParentTransitions(map));
    this.container.style.display = ""
  }

  addMapAndParentTransitions(map) {
    if (map === void 0) return;
    this.currentNode = this.container;
    let parents = map.getParents();
    if (parents.length > 0) {
      this.addTransitionTo(parents.pop());
      parents.reverse().forEach(each => this.addTransitionTo(each));
    }
    let mapNode = this.addSubtransitions(map);
    // Mark and show the selected map.
    mapNode.classList.add("selected");
    if (this.selectedMap == map) {
      setTimeout(() => mapNode.scrollIntoView({
        behavior: "smooth", block: "nearest", inline: "nearest"
      }), 1);
    }
  }

  addMapNode(map) {
    let node = div("map");
    if (map.edge) node.classList.add(map.edge.getColor());
    node.map = map;
    node.addEventListener("click", () => this.selectMap(map));
    if (map.children.length > 1) {
      node.innerText = map.children.length;
      let showSubtree = div("showSubtransitions");
      showSubtree.addEventListener("click", (e) => this.toggleSubtree(e, node));
      node.appendChild(showSubtree);
    } else if (map.children.length == 0) {
      node.innerHTML = "&#x25CF;"
    }
    this.currentNode.appendChild(node);
    return node;
  }

  addSubtransitions(map) {
    let mapNode = this.addTransitionTo(map);
    // Draw outgoing linear transition line.
    let current = map;
    while (current.children.length == 1) {
      current = current.children[0].to;
      this.addTransitionTo(current);
    }
    return mapNode;
  }

 addTransitionEdge(map) {
    let classes = ["transitionEdge", map.edge.getColor()];
    let edge = div(classes);
    let labelNode = div("transitionLabel");
    labelNode.innerText = map.edge.toString();
    edge.appendChild(labelNode);
    return edge;
  }

  addTransitionTo(map) {
    // transition[ transitions[ transition[...], transition[...], ...]];

    let transition = div("transition");
    if (map.isDeprecated()) transition.classList.add("deprecated");
    if (map.edge) {
      transition.appendChild(this.addTransitionEdge(map));
    }
    let mapNode = this.addMapNode(map);
    transition.appendChild(mapNode);

    let subtree = div("transitions");
    transition.appendChild(subtree);

    this.currentNode.appendChild(transition);
    this.currentNode = subtree;

    return mapNode;

  }

  toggleSubtree(event, node) {
    let map = node.map;
    event.target.classList.toggle("opened");
    let transitionsNode = node.parentElement.querySelector(".transitions");
    let subtransitionNodes  =  transitionsNode.children;
    if (subtransitionNodes.length <= 1) {
      // Add subtransitions excepth the one that's already shown.
      let visibleTransitionMap = subtransitionNodes.length == 1 ?
            transitionsNode.querySelector(".map").map : void 0;
      map.children.forEach(edge => {
        if (edge.to != visibleTransitionMap) {
          this.currentNode = transitionsNode;
          this.addSubtransitions(edge.to);
        }
      });
    } else {
      // remove all but the first (currently selected) subtransition
      for (let i = subtransitionNodes.length-1; i > 0; i--) {
        transitionsNode.removeChild(subtransitionNodes[i]);
      }
    }
  }
}

class StatsView {
  constructor(state, node) {
    this.state = state;
    this.node = node;
  }
  get timeline() { return this.state.timeline }
  get transitionView() { return this.state.view.transitionView; }
  update() {
    removeAllChildren(this.node);
    this.updateGeneralStats();
    this.updateNamedTransitionsStats();
  }
  updateGeneralStats() {
    let pairs = [
      ["Total", null, e => true],
      ["Transitions", 'black', e => e.edge && e.edge.isTransition()],
      ["Fast to Slow", 'violet', e => e.edge && e.edge.isFastToSlow()],
      ["Slow to Fast", 'orange', e => e.edge && e.edge.isSlowToFast()],
      ["Initial Map", 'yellow', e => e.edge && e.edge.isInitial()],
      ["Replace Descriptors", 'red', e => e.edge && e.edge.isReplaceDescriptors()],
      ["Copy as Prototype", 'red', e => e.edge && e.edge.isCopyAsPrototype()],
      ["Optimize as Prototype", null, e => e.edge && e.edge.isOptimizeAsPrototype()],
      ["Deprecated", null, e => e.isDeprecated()],
      ["Bootstrapped", 'green', e => e.isBootstrapped()],
    ];

    let text = "";
    let tableNode = table("transitionType");
    tableNode.innerHTML = "<thead><tr><td>Color</td><td>Type</td><td>Count</td><td>Percent</td></tr></thead>";
    let name, filter;
    let total = this.timeline.size();
    pairs.forEach(([name, color, filter]) => {
      let row = tr();
      if (color !== null) {
        row.appendChild(td(div(['colorbox', color])));
      } else {
        row.appendChild(td(""));
      }
      row.onclick = (e) => {
        // lazily compute the stats
        let node = e.target.parentNode;
        if (node.maps == undefined) {
          node.maps = this.timeline.filterUniqueTransitions(filter);
        }
        this.transitionView.showMaps(node.maps);
      }
      row.appendChild(td(name));
      let count = this.timeline.count(filter);
      row.appendChild(td(count));
      let percent = Math.round(count / total * 1000) / 10;
      row.appendChild(td(percent.toFixed(1) + "%"));
      tableNode.appendChild(row);
    });
    this.node.appendChild(tableNode);
  };
  updateNamedTransitionsStats() {
    let tableNode = table("transitionTable");
    let nameMapPairs = Array.from(this.timeline.transitions.entries());
    tableNode.innerHTML = "<thead><tr><td>Propery Name</td><td>#</td></tr></thead>";
    nameMapPairs
      .sort((a,b) => b[1].length - a[1].length)
      .forEach(([name, maps]) => {
        let row = tr();
        row.maps = maps;
        row.addEventListener("click",
            e => this.transitionView.showMaps(
                e.target.parentNode.maps.map(map => map.to)));
        row.appendChild(td(name));
        row.appendChild(td(maps.length));
        tableNode.appendChild(row);
    });
    this.node.appendChild(tableNode);
  }
}

// =========================================================================

function transitionTypeToColor(type) {
  switch(type) {
    case "new": return "green";
    case "Normalize": return "violet";
    case "SlowToFast": return "orange";
    case "InitialMap": return "yellow";
    case "Transition": return "black";
    case "ReplaceDescriptors": return "red";
  }
  return "black";
}

// ShadowDom elements =========================================================

</script>
</head>
<body onload="handleBodyLoad(event)" onkeypress="handleKeyDown(event)">
  <h1>V8 Map Explorer</h1>
  <section>
    <div id="fileReader" tabindex=1 >
      <span id="label">
        Drag and drop a v8.log file into this area, or click to choose from disk.
      </span>
      <input id="file" type="file" name="files">
    </div>
    <div id="loader">
      <div id="spinner"></div>
    </div>
  </section>

  <div id="content">
    <h2>Stats</h2>
    <section id="stats"></section>

    <h2>Timeline</h2>
    <div id="timeline">
      <div id="timelineLabel">Frequency</div>
      <div id="timelineChunks"></div>
      <canvas id="timelineCanvas"></canvas>
    </div>
    <div id="timelineOverview"
        onmousemove="handleTimelineIndicatorMove(event)" >
      <div id="timelineOverviewIndicator">
        <div class="leftMask"></div>
        <div class="rightMask"></div>
      </div>
    </div>

    <h2>Transitions</h2>
    <section id="transitionView"></section>
    <br/>


    <h2>Search Map by Address</h2>
    <section id="searchBar"></section>
    <input type="search" id="searchBarInput" placeholder="Search maps by address..">
    <button onclick="handleSearchBar()">Search</button>
    <ul id="mapIdList" title="Map Id List">
    </ul>


    <h2>Selected Map</h2>
    <section id="mapDetails"></section>
  </div>

  <section>
    <h2>Instructions</h2>
    <p>Visualize Map trees that have been gathered using <code>path/to/d8 $FILE --trace-maps</code>.</p>
    <p>You can inspect the transition tree in DevTools by looking at <code>document.state.timeline.values</code>.
    <h3>Keyboard Shortcuts</h3>
    <dl>
      <dt><kbd>SHIFT</kbd> + <kbd>Arrow Up</kbd></dt>
      <dd>Follow Map transition forward (first child)</dd>

      <dt><kbd>SHIFT</kbd> + <kbd>Arrow Down</kbd></dt>
      <dd>Follow Map transition backwards</dd>

      <dt><kbd>Arrow Up</kbd></dt>
      <dd>Go to previous Map chunk</dd>

      <dt><kbd>Arrow Down</kbd></dt>
      <dd>Go to next Map in chunk</dd>

      <dt><kbd>Arrow Left</kbd></dt>
      <dd>Go to previous chunk</dd>

      <dt><kbd>Arrow Right</kbd></dt>
      <dd>Go to next chunk</dd>

      <dt><kbd>+</kbd></dt>
      <dd>Timeline zoom in</dd>

      <dt><kbd>-</kbd></dt>
      <dd>Timeline zoom out</dd>
    </dl>
  </section>

  <div id="tooltip">
    <div id="tooltipContents"></div>
  </div>
</body>
</html>