/*
!!! THIS FILE IS COPIED FROM https://github.com/fitzgen/servo-trace-dump !!!
Make sure to upstream changes, or they will get lost!
*/
/* 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";

(function (exports, window) {

  /*** State ******************************************************************/

  const COLORS = exports.COLORS = [
    "#0088cc",
    "#5b5fff",
    "#b82ee5",
    "#ed2655",
    "#f13c00",
    "#d97e00",
    "#2cbb0f",
    "#0072ab",
  ];

  // A class containing the cleaned up trace state.
  const State = exports.State = (function () {
    return class State {
      constructor(rawTraces, windowWidth) {
        // The traces themselves.
        this.traces = null;

        // Only display traces that take at least this long. Default is .1 ms.
        this.minimumTraceTime = 100000;

        // Maximimum and minimum times seen in traces. These get normalized to be
        // relative to 0, so after initialization minTime is always 0.
        this.minTime = Infinity;
        this.maxTime = 0;

        // The current start and end of the viewport selection.
        this.startSelection = 0;
        this.endSelection = 0;

        // The current width of the window.
        this.windowWidth = windowWidth;

        // Whether the user is actively grabbing the left or right grabby, or the
        // viewport slider.
        this.grabbingLeft = false;
        this.grabbingRight = false;
        this.grabbingSlider = false;

        // Maps category labels to a persistent color so that they are always
        // rendered the same color.
        this.colorIndex = 0;
        this.categoryToColor = Object.create(null);

        this.initialize(rawTraces);
      }

      // Clean up and massage the trace data.
      initialize(rawTraces) {
        this.traces = rawTraces.filter(t => t.endTime - t.startTime >= this.minimumTraceTime);

        this.traces.sort((t1, t2) => {
          let cmp = t1.startTime - t2.startTime;
          if (cmp !== 0) {
            return cmp;
          }

          return t1.endTime - t2.endTime;
        });

        this.findMinTime();
        this.normalizeTimes();
        this.removeIdleTime();
        this.findMaxTime();

        this.startSelection = 3 * this.maxTime / 8;
        this.endSelection = 5 * this.maxTime / 8;
      }

      // Find the minimum timestamp.
      findMinTime() {
        this.minTime = this.traces.reduce((min, t) => Math.min(min, t.startTime),
                                          Infinity);
      }

      // Find the maximum timestamp.
      findMaxTime() {
        this.maxTime = this.traces.reduce((max, t) => Math.max(max, t.endTime),
                                          0);
      }

      // Normalize all times to be relative to the minTime and then reset the
      // minTime to 0.
      normalizeTimes() {
        for (let i = 0; i < this.traces.length; i++) {
          let trace = this.traces[i];
          trace.startTime -= this.minTime;
          trace.endTime -= this.minTime;
        }
        this.minTime = 0;
      }

      // Remove idle time between traces. It isn't useful to see and makes
      // visualizing the data more difficult.
      removeIdleTime() {
        let totalIdleTime = 0;
        let lastEndTime = null;

        for (let i = 0; i < this.traces.length; i++) {
          let trace = this.traces[i];

          if (lastEndTime !== null && trace.startTime > lastEndTime) {
            totalIdleTime += trace.startTime - lastEndTime;
          }

          lastEndTime = trace.endTime;

          trace.startTime -= totalIdleTime;
          trace.endTime -= totalIdleTime;
        }
      }

      // Get the color for the given category, or assign one if no such color
      // exists yet.
      getColorForCategory(category) {
        let result = this.categoryToColor[category];
        if (!result) {
          result = COLORS[this.colorIndex++ % COLORS.length];
          this.categoryToColor[category] = result;
        }
        return result;
      }

      // Translate pixels into nanoseconds.
      pxToNs(px) {
        return px / this.windowWidth * this.maxTime;
      }

      // Translate nanoseconds into pixels.
      nsToPx(ns) {
        return ns / this.maxTime * this.windowWidth
      }

      // Translate nanoseconds into pixels in the zoomed viewport region.
      nsToSelectionPx(ns) {
        return ns / (this.endSelection - this.startSelection) * this.windowWidth;
      }

      // Update the start selection to the given position's time.
      updateStartSelection(position) {
        this.startSelection = clamp(this.pxToNs(position),
                                    this.minTime,
                                    this.endSelection);
      }

      // Update the end selection to the given position's time.
      updateEndSelection(position) {
        this.endSelection = clamp(this.pxToNs(position),
                                  this.startSelection,
                                  this.maxTime);
      }

      // Move the start and end selection by the given delta movement.
      moveSelection(movement) {
        let delta = clamp(this.pxToNs(movement),
                          -this.startSelection,
                          this.maxTime - this.endSelection);

        this.startSelection += delta;
        this.endSelection += delta;
      }

      // Widen or narrow the selection based on the given zoom.
      zoomSelection(zoom) {
        const increment = this.maxTime / 1000;

        this.startSelection = clamp(this.startSelection - zoom * increment,
                                    this.minTime,
                                    this.endSelection);

        this.endSelection = clamp(this.endSelection + zoom * increment,
                                  this.startSelection,
                                  this.maxTime);
      }

      // Get the set of traces that overlap the current selection.
      getTracesInSelection() {
        const tracesInSelection = [];

        for (let i = 0; i < state.traces.length; i++) {
          let trace = state.traces[i];

          if (trace.endTime < state.startSelection) {
            continue;
          }

          if (trace.startTime > state.endSelection) {
            break;
          }

          tracesInSelection.push(trace);
        }

        return tracesInSelection;
      }
    };
  }());

  /*** Utilities **************************************************************/

  // Ensure that min <= value <= max
  const clamp = exports.clamp = (value, min, max) => {
    return Math.max(Math.min(value, max), min);
  };

  // Get the closest power of ten to the given number.
  const closestPowerOfTen = exports.closestPowerOfTen = n => {
    let powerOfTen = 1;
    let diff = Math.abs(n - powerOfTen);

    while (true) {
      let nextPowerOfTen = powerOfTen * 10;
      let nextDiff = Math.abs(n - nextPowerOfTen);

      if (nextDiff > diff) {
        return powerOfTen;
      }

      diff = nextDiff;
      powerOfTen = nextPowerOfTen;
    }
  };

  // Select the tick increment for the given range size and maximum number of
  // ticks to show for that range.
  const selectIncrement = exports.selectIncrement = (range, maxTicks) => {
    let increment = closestPowerOfTen(range / 10);
    while (range / increment > maxTicks) {
      increment *= 2;
    }
    return increment;
  };

  // Get the category name for the given trace.
  const traceCategory = exports.traceCategory = trace => {
    return Object.keys(trace.category)[0];
  };

  /*** Window Specific Code ***************************************************/

  if (!window) {
    return;
  }

  // XXX: Everything below here relies on the presence of `window`! Try to
  // minimize this code and factor out the parts that don't explicitly need
  // `window` or `document` as much as possible. We can't easily test code that
  // relies upon `window`.

  const state = exports.state = new State(window.TRACES, window.innerWidth);

  /*** Initial Persistent Element Creation ************************************/

  window.document.body.innerHTML = "";

  const sliderContainer = window.document.createElement("div");
  sliderContainer.id = "slider";
  window.document.body.appendChild(sliderContainer);

  const leftGrabby = window.document.createElement("span");
  leftGrabby.className = "grabby";
  sliderContainer.appendChild(leftGrabby);

  const sliderViewport = window.document.createElement("span");
  sliderViewport.id = "slider-viewport";
  sliderContainer.appendChild(sliderViewport);

  const rightGrabby = window.document.createElement("span");
  rightGrabby.className = "grabby";
  sliderContainer.appendChild(rightGrabby);

  const tracesContainer = window.document.createElement("div");
  tracesContainer.id = "traces";
  window.document.body.appendChild(tracesContainer);

  /*** Listeners *************************************************************/

  // Run the given function and render afterwards.
  const withRender = fn => function () {
    fn.apply(null, arguments);
    render();
  };

  window.addEventListener("resize", withRender(() => {
    state.windowWidth = window.innerWidth;
  }));

  window.addEventListener("mouseup", () => {
    state.grabbingSlider = state.grabbingLeft = state.grabbingRight = false;
  });

  leftGrabby.addEventListener("mousedown", () => {
    state.grabbingLeft = true;
  });

  rightGrabby.addEventListener("mousedown", () => {
    state.grabbingRight = true;
  });

  sliderViewport.addEventListener("mousedown", () => {
    state.grabbingSlider = true;
  });

  window.addEventListener("mousemove", event => {
    if (state.grabbingSlider) {
      state.moveSelection(event.movementX);
      event.preventDefault();
      render();
    } else if (state.grabbingLeft) {
      state.updateStartSelection(event.clientX);
      event.preventDefault();
      render();
    } else if (state.grabbingRight) {
      state.updateEndSelection(event.clientX);
      event.preventDefault();
      render();
    }
  });

  sliderContainer.addEventListener("wheel", withRender(event => {
    state.zoomSelection(event.deltaY);
  }));

  /*** Rendering **************************************************************/

  // Create a function that calls the given function `fn` only once per animation
  // frame.
  const oncePerAnimationFrame = fn => {
    let animationId = null;
    return () => {
      if (animationId !== null) {
        return;
      }

      animationId = window.requestAnimationFrame(() => {
        fn();
        animationId = null;
      });
    };
  };

  // Only call the given function once per window width resize.
  const oncePerWindowWidth = fn => {
    let lastWidth = null;
    return () => {
      if (state.windowWidth !== lastWidth) {
        fn();
        lastWidth = state.windowWidth;
      }
    };
  };

  // Top level entry point for rendering. Renders the current `window.state`.
  const render = oncePerAnimationFrame(() => {
    renderSlider();
    renderTraces();
  });

  // Render the slider at the top of the screen.
  const renderSlider = () => {
    let selectionDelta = state.endSelection - state.startSelection;

    // -6px because of the 3px width of each grabby.
    sliderViewport.style.width = state.nsToPx(selectionDelta) - 6 + "px";
    leftGrabby.style.marginLeft = state.nsToPx(state.startSelection) + "px";
    rightGrabby.style.rightMargin = state.nsToPx(state.maxTime - state.endSelection) + "px";

    renderSliderTicks();
  };

  // Render the ticks along the slider overview.
  const renderSliderTicks = oncePerWindowWidth(() => {
    let oldTicks = Array.from(window.document.querySelectorAll(".slider-tick"));
    for (let tick of oldTicks) {
      tick.remove();
    }

    let increment = selectIncrement(state.maxTime, 20);
    let px = state.nsToPx(increment);
    let ms = 0;
    for (let i = 0; i < state.windowWidth; i += px) {
      let tick = window.document.createElement("div");
      tick.className = "slider-tick";
      tick.textContent = ms + " ms";
      tick.style.left = i + "px";
      window.document.body.appendChild(tick);
      ms += increment / 1000000;
    }
  });

  // Render the individual traces.
  const renderTraces = () => {
    renderTracesTicks();

    let tracesToRender = state.getTracesInSelection();

    // Ensure that we have exactly enough trace row elements. If we have more
    // elements than traces we are going to render, then remove some. If we have
    // fewer elements than traces we are going to render, then add some.
    let rows = Array.from(tracesContainer.querySelectorAll(".outer"));
    while (rows.length > tracesToRender.length) {
      rows.pop().remove();
    }
    while (rows.length < tracesToRender.length) {
      let elem = makeTraceTemplate();
      tracesContainer.appendChild(elem);
      rows.push(elem);
    }

    for (let i = 0; i < tracesToRender.length; i++) {
      renderTrace(tracesToRender[i], rows[i]);
    }
  };

  // Render the ticks behind the traces.
  const renderTracesTicks = () => {
    let oldTicks = Array.from(tracesContainer.querySelectorAll(".traces-tick"));
    for (let tick of oldTicks) {
      tick.remove();
    }

    let selectionDelta = state.endSelection - state.startSelection;
    let increment = selectIncrement(selectionDelta, 10);
    let px = state.nsToPx(increment);
    let offset = state.startSelection % increment;
    let time = state.startSelection - offset + increment;

    while (time < state.endSelection) {
      let tick = document.createElement("div");
      tick.className = "traces-tick";
      tick.textContent = Math.round(time / 1000000) + " ms";
      tick.style.left = state.nsToSelectionPx(time - state.startSelection) + "px";
      tracesContainer.appendChild(tick);

      time += increment;
    }
  };

  // Create the DOM structure for an individual trace.
  const makeTraceTemplate = () => {
    let outer = window.document.createElement("div");
    outer.className = "outer";

    let inner = window.document.createElement("div");
    inner.className = "inner";

    let tooltip = window.document.createElement("div");
    tooltip.className = "tooltip";

    let header = window.document.createElement("h3");
    header.className = "header";
    tooltip.appendChild(header);

    let duration = window.document.createElement("h4");
    duration.className = "duration";
    tooltip.appendChild(duration);

    let pairs = window.document.createElement("dl");

    let timeStartLabel = window.document.createElement("dt");
    timeStartLabel.textContent = "Start:"
    pairs.appendChild(timeStartLabel);

    let timeStartValue = window.document.createElement("dd");
    timeStartValue.className = "start";
    pairs.appendChild(timeStartValue);

    let timeEndLabel = window.document.createElement("dt");
    timeEndLabel.textContent = "End:"
    pairs.appendChild(timeEndLabel);

    let timeEndValue = window.document.createElement("dd");
    timeEndValue.className = "end";
    pairs.appendChild(timeEndValue);

    let urlLabel = window.document.createElement("dt");
    urlLabel.textContent = "URL:";
    pairs.appendChild(urlLabel);

    let urlValue = window.document.createElement("dd");
    urlValue.className = "url";
    pairs.appendChild(urlValue);

    let iframeLabel = window.document.createElement("dt");
    iframeLabel.textContent = "iframe?";
    pairs.appendChild(iframeLabel);

    let iframeValue = window.document.createElement("dd");
    iframeValue.className = "iframe";
    pairs.appendChild(iframeValue);

    let incrementalLabel = window.document.createElement("dt");
    incrementalLabel.textContent = "Incremental?";
    pairs.appendChild(incrementalLabel);

    let incrementalValue = window.document.createElement("dd");
    incrementalValue.className = "incremental";
    pairs.appendChild(incrementalValue);

    tooltip.appendChild(pairs);
    outer.appendChild(tooltip);
    outer.appendChild(inner);
    return outer;
  };

  // Render `trace` into the given `elem`. We reuse the trace elements and modify
  // them with the new trace that will populate this particular `elem` rather than
  // clearing the DOM out and rebuilding it from scratch. Its a bit of a
  // performance win when there are a lot of traces being rendered. Funnily
  // enough, iterating over the complete set of traces hasn't been a performance
  // problem at all and the bottleneck seems to be purely rendering the subset of
  // traces we wish to show.
  const renderTrace = (trace, elem) => {
    let inner = elem.querySelector(".inner");
    inner.style.width = state.nsToSelectionPx(trace.endTime - trace.startTime) + "px";
    inner.style.marginLeft = state.nsToSelectionPx(trace.startTime - state.startSelection) + "px";

    let category = traceCategory(trace);
    inner.textContent = category;
    inner.style.backgroundColor = state.getColorForCategory(category);

    let header = elem.querySelector(".header");
    header.textContent = category;

    let duration = elem.querySelector(".duration");
    duration.textContent = (trace.endTime - trace.startTime) / 1000000 + " ms";

    let timeStartValue = elem.querySelector(".start");
    timeStartValue.textContent = trace.startTime / 1000000 + " ms";

    let timeEndValue = elem.querySelector(".end");
    timeEndValue.textContent = trace.endTime / 1000000 + " ms";

    if (trace.metadata) {
      let urlValue = elem.querySelector(".url");
      urlValue.textContent = trace.metadata.url;
      urlValue.removeAttribute("hidden");

      let iframeValue = elem.querySelector(".iframe");
      iframeValue.textContent = trace.metadata.iframe.RootWindow ? "No" : "Yes";
      iframeValue.removeAttribute("hidden");

      let incrementalValue = elem.querySelector(".incremental");
      incrementalValue.textContent = trace.metadata.incremental.Incremental ? "Yes" : "No";
      incrementalValue.removeAttribute("hidden");
    } else {
      elem.querySelector(".url").setAttribute("hidden", "");
      elem.querySelector(".iframe").setAttribute("hidden", "");
      elem.querySelector(".incremental").setAttribute("hidden", "");
    }
  };

  render();

}(typeof exports === "object" ? exports : window,
  typeof window === "object" ? window : null));
