/* 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/. */

function toFixed(num, fixed) {
  fixed = fixed || 0;
  fixed = Math.pow(10, fixed);
  return Math.floor(num * fixed) / fixed;
}
function createElement(name, props) {
  var el = document.createElement(name);

  for (var key in props) {
    if (key === "style") {
      for (var styleName in props.style) {
        el.style[styleName] = props.style[styleName];
      }
    } else {
      el[key] = props[key];
    }
  }

  return el;
}

function parseDisplayList(lines) {
  var root = {
    line: "DisplayListRoot 0",
    name: "DisplayListRoot",
    address: "0x0",
    frame: "Root",
    children: [],
  };

  var objectAtIndentation = {
    "-1": root,
  };

  for (var i = 0; i < lines.length; i++) {
    var line = lines[i];

    var layerObject = {
      line,
      children: [],
    };
    if (!root) {
      root = layerObject;
    }

    var matches = line.match(
      "(\\s*)(\\w+)\\sp=(\\w+)\\sf=(.*?)\\((.*?)\\)\\s(z=(\\w+)\\s)?(.*?)?( layer=(\\w+))?$"
    );
    if (!matches) {
      dump("Failed to match: " + line + "\n");
      continue;
    }

    var indentation = Math.floor(matches[1].length / 2);
    objectAtIndentation[indentation] = layerObject;
    var parent = objectAtIndentation[indentation - 1];
    if (parent) {
      parent.children.push(layerObject);
    }

    layerObject.name = matches[2];
    layerObject.address = matches[3]; // Use 0x prefix to be consistent with layer dump
    layerObject.frame = matches[4];
    layerObject.contentDescriptor = matches[5];
    layerObject.z = matches[7];
    var rest = matches[8];
    if (matches[10]) {
      // WrapList don't provide a layer
      layerObject.layer = matches[10];
    }
    layerObject.rest = rest;

    // the content node name doesn't have a prefix, this makes the parsing easier
    rest = "content" + rest;

    var nesting = 0;
    var startIndex;
    var lastSpace = -1;
    for (var j = 0; j < rest.length; j++) {
      if (rest.charAt(j) == "(") {
        nesting++;
        if (nesting == 1) {
          startIndex = j;
        }
      } else if (rest.charAt(j) == ")") {
        nesting--;
        if (nesting == 0) {
          var name = rest.substring(lastSpace + 1, startIndex);
          var value = rest.substring(startIndex + 1, j);

          var rectMatches = value.match("^(.*?),(.*?),(.*?),(.*?)$");
          if (rectMatches) {
            layerObject[name] = [
              parseFloat(rectMatches[1]),
              parseFloat(rectMatches[2]),
              parseFloat(rectMatches[3]),
              parseFloat(rectMatches[4]),
            ];
          } else {
            layerObject[name] = value;
          }
        }
      } else if (nesting == 0 && rest.charAt(j) == " ") {
        lastSpace = j;
      }
    }
    // dump("FIELDS: " + JSON.stringify(fields) + "\n");
  }
  return root;
}

function trim(s) {
  return (s || "").replace(/^\s+|\s+$/g, "");
}

function getDataURI(str) {
  if (str.indexOf("data:image/png;base64,") == 0) {
    return str;
  }

  var matches = str.match(
    "data:image/lz4bgra;base64,([0-9]+),([0-9]+),([0-9]+),(.*)"
  );
  if (!matches) {
    return null;
  }

  var canvas = document.createElement("canvas");
  var w = parseInt(matches[1]);
  var stride = parseInt(matches[2]);
  var h = parseInt(matches[3]);
  canvas.width = w;
  canvas.height = h;

  // TODO handle stride

  var binary_string = window.atob(matches[4]);
  var len = binary_string.length;
  var bytes = new Uint8Array(len);
  var decoded = new Uint8Array(stride * h);
  for (var i = 0; i < len; i++) {
    var ascii = binary_string.charCodeAt(i);
    bytes[i] = ascii;
  }

  var ctxt = canvas.getContext("2d");
  var out = ctxt.createImageData(w, h);
  // This is actually undefined throughout the tree and it isn't clear what it
  // should be. Since this is only development code, leave it alone for now.
  // eslint-disable-next-line no-undef
  LZ4_uncompressChunk(bytes, decoded);

  for (var x = 0; x < w; x++) {
    for (var y = 0; y < h; y++) {
      out.data[4 * x + 4 * y * w + 0] = decoded[4 * x + y * stride + 2];
      out.data[4 * x + 4 * y * w + 1] = decoded[4 * x + y * stride + 1];
      out.data[4 * x + 4 * y * w + 2] = decoded[4 * x + y * stride + 0];
      out.data[4 * x + 4 * y * w + 3] = decoded[4 * x + y * stride + 3];
    }
  }

  ctxt.putImageData(out, 0, 0);
  return canvas.toDataURL();
}

function parseLayers(layersDumpLines) {
  function parseMatrix2x3(str) {
    str = trim(str);

    // Something like '[ 1 0; 0 1; 0 158; ]'
    var matches = str.match("^\\[ (.*?) (.*?); (.*?) (.*?); (.*?) (.*?); \\]$");
    if (!matches) {
      return null;
    }

    var matrix = [
      [parseFloat(matches[1]), parseFloat(matches[2])],
      [parseFloat(matches[3]), parseFloat(matches[4])],
      [parseFloat(matches[5]), parseFloat(matches[6])],
    ];

    return matrix;
  }
  function parseColor(str) {
    str = trim(str);

    // Something like 'rgba(0, 0, 0, 0)'
    var colorMatches = str.match("^rgba\\((.*), (.*), (.*), (.*)\\)$");
    if (!colorMatches) {
      return null;
    }

    var color = {
      r: colorMatches[1],
      g: colorMatches[2],
      b: colorMatches[3],
      a: colorMatches[4],
    };
    return color;
  }
  function parseFloat_cleo(str) {
    str = trim(str);

    // Something like 2.000
    if (parseFloat(str) == str) {
      return parseFloat(str);
    }

    return null;
  }
  function parseRect2D(str) {
    str = trim(str);

    // Something like '(x=0, y=0, w=2842, h=158)'
    var rectMatches = str.match("^\\(x=(.*?), y=(.*?), w=(.*?), h=(.*?)\\)$");
    if (!rectMatches) {
      return null;
    }

    var rect = [
      parseFloat(rectMatches[1]),
      parseFloat(rectMatches[2]),
      parseFloat(rectMatches[3]),
      parseFloat(rectMatches[4]),
    ];
    return rect;
  }
  function parseRegion(str) {
    str = trim(str);

    // Something like '< (x=0, y=0, w=2842, h=158); (x=0, y=1718, w=2842, h=500); >'
    if (str.charAt(0) != "<" || str.charAt(str.length - 1) != ">") {
      return null;
    }

    var region = [];
    str = trim(str.substring(1, str.length - 1));
    while (str != "") {
      var rectMatches = str.match(
        "^\\(x=(.*?), y=(.*?), w=(.*?), h=(.*?)\\);(.*)$"
      );
      if (!rectMatches) {
        return null;
      }

      var rect = [
        parseFloat(rectMatches[1]),
        parseFloat(rectMatches[2]),
        parseFloat(rectMatches[3]),
        parseFloat(rectMatches[4]),
      ];
      str = trim(rectMatches[5]);
      region.push(rect);
    }
    return region;
  }

  var LAYERS_LINE_REGEX = "(\\s*)(\\w+)\\s\\((\\w+)\\)(.*)";

  var root;
  var objectAtIndentation = [];
  for (var i = 0; i < layersDumpLines.length; i++) {
    // Something like 'ThebesLayerComposite (0x12104cc00) [shadow-visible=< (x=0, y=0, w=1920, h=158); >] [visible=< (x=0, y=0, w=1920, h=158); >] [opaqueContent] [valid=< (x=0, y=0, w=1920, h=2218); >]'
    var line = layersDumpLines[i].name || layersDumpLines[i];

    var tileMatches = line.match("(\\s*)Tile \\(x=(.*), y=(.*)\\): (.*)");
    if (tileMatches) {
      let indentation = Math.floor(matches[1].length / 2);
      var x = tileMatches[2];
      var y = tileMatches[3];
      var dataUri = tileMatches[4];
      let parent = objectAtIndentation[indentation - 1];
      var tiles = parent.tiles || {};

      tiles[x] = tiles[x] || {};
      tiles[x][y] = dataUri;

      parent.tiles = tiles;

      continue;
    }

    var surfaceMatches = line.match("(\\s*)Surface: (.*)");
    if (surfaceMatches) {
      let indentation = Math.floor(matches[1].length / 2);
      let parent =
        objectAtIndentation[indentation - 1] ||
        objectAtIndentation[indentation - 2];

      var surfaceURI = surfaceMatches[2];
      if (parent.surfaceURI != null) {
        console.log(
          "error: surfaceURI already set for this layer " + parent.line
        );
      }
      parent.surfaceURI = surfaceURI;

      // Look for the buffer-rect offset
      var contentHostLine =
        layersDumpLines[i - 2].name || layersDumpLines[i - 2];
      let matches = contentHostLine.match(LAYERS_LINE_REGEX);
      if (matches) {
        var contentHostRest = matches[4];
        parent.contentHostProp = {};
        parseProperties(contentHostRest, parent.contentHostProp);
      }

      continue;
    }

    var layerObject = {
      line,
      children: [],
    };
    if (!root) {
      root = layerObject;
    }

    let matches = line.match(LAYERS_LINE_REGEX);
    if (!matches) {
      continue; // Something like a texturehost dump. Safe to ignore
    }

    if (
      matches[2].includes("TiledContentHost") ||
      matches[2].includes("ContentHost") ||
      matches[2].includes("ContentClient") ||
      matches[2].includes("MemoryTextureHost") ||
      matches[2].includes("ImageHost")
    ) {
      continue; // We're already pretty good at visualizing these
    }

    var indentation = Math.floor(matches[1].length / 2);
    objectAtIndentation[indentation] = layerObject;
    for (var c = indentation + 1; c < objectAtIndentation.length; c++) {
      objectAtIndentation[c] = null;
    }
    if (indentation > 0) {
      var parent = objectAtIndentation[indentation - 1];
      while (!parent) {
        indentation--;
        parent = objectAtIndentation[indentation - 1];
      }

      parent.children.push(layerObject);
    }

    layerObject.name = matches[2];
    layerObject.address = matches[3];

    var rest = matches[4];

    function parseProperties(rest, layerObject) {
      var fields = [];
      var nesting = 0;
      var startIndex;
      for (let j = 0; j < rest.length; j++) {
        if (rest.charAt(j) == "[") {
          nesting++;
          if (nesting == 1) {
            startIndex = j;
          }
        } else if (rest.charAt(j) == "]") {
          nesting--;
          if (nesting == 0) {
            fields.push(rest.substring(startIndex + 1, j));
          }
        }
      }

      for (let j = 0; j < fields.length; j++) {
        // Something like 'valid=< (x=0, y=0, w=1920, h=2218); >' or 'opaqueContent'
        var field = fields[j];
        // dump("FIELD: " + field + "\n");
        var parts = field.split("=", 2);
        var fieldName = parts[0];
        rest = field.substring(fieldName.length + 1);
        if (parts.length == 1) {
          layerObject[fieldName] = "true";
          layerObject[fieldName].type = "bool";
          continue;
        }
        var float = parseFloat_cleo(rest);
        if (float) {
          layerObject[fieldName] = float;
          layerObject[fieldName].type = "float";
          continue;
        }
        var region = parseRegion(rest);
        if (region) {
          layerObject[fieldName] = region;
          layerObject[fieldName].type = "region";
          continue;
        }
        var rect = parseRect2D(rest);
        if (rect) {
          layerObject[fieldName] = rect;
          layerObject[fieldName].type = "rect2d";
          continue;
        }
        var matrix = parseMatrix2x3(rest);
        if (matrix) {
          layerObject[fieldName] = matrix;
          layerObject[fieldName].type = "matrix2x3";
          continue;
        }
        var color = parseColor(rest);
        if (color) {
          layerObject[fieldName] = color;
          layerObject[fieldName].type = "color";
          continue;
        }
        if (rest[0] == "{" && rest[rest.length - 1] == "}") {
          var object = {};
          parseProperties(rest.substring(1, rest.length - 2).trim(), object);
          layerObject[fieldName] = object;
          layerObject[fieldName].type = "object";
          continue;
        }
        fieldName = fieldName.split(" ")[0];
        layerObject[fieldName] = rest[0];
        layerObject[fieldName].type = "string";
      }
    }
    parseProperties(rest, layerObject);

    if (!layerObject["shadow-transform"]) {
      // No shadow transform = identify
      layerObject["shadow-transform"] = [
        [1, 0],
        [0, 1],
        [0, 0],
      ];
    }

    // Compute screenTransformX/screenTransformY
    // TODO Fully support transforms
    if (layerObject["shadow-transform"] && layerObject.transform) {
      layerObject["screen-transform"] = [
        layerObject["shadow-transform"][2][0],
        layerObject["shadow-transform"][2][1],
      ];
      var currIndentation = indentation - 1;
      while (currIndentation >= 0) {
        var transform =
          objectAtIndentation[currIndentation]["shadow-transform"] ||
          objectAtIndentation[currIndentation].transform;
        if (transform) {
          layerObject["screen-transform"][0] += transform[2][0];
          layerObject["screen-transform"][1] += transform[2][1];
        }
        currIndentation--;
      }
    }

    // dump("Fields: " + JSON.stringify(fields) + "\n");
  }
  root.compositeTime = layersDumpLines.compositeTime;
  // dump("OBJECTS: " + JSON.stringify(root) + "\n");
  return root;
}
function populateLayers(
  root,
  displayList,
  pane,
  previewParent,
  hasSeenRoot,
  contentScale,
  rootPreviewParent
) {
  contentScale = contentScale || 1;
  rootPreviewParent = rootPreviewParent || previewParent;

  function getDisplayItemForLayer(displayList) {
    var items = [];
    if (!displayList) {
      return items;
    }
    if (displayList.layer == root.address) {
      items.push(displayList);
    }
    for (var i = 0; i < displayList.children.length; i++) {
      var subDisplayItems = getDisplayItemForLayer(displayList.children[i]);
      for (let j = 0; j < subDisplayItems.length; j++) {
        items.push(subDisplayItems[j]);
      }
    }
    return items;
  }
  var elem = createElement("div", {
    className: "layerObjectDescription",
    textContent: root.line,
    style: {
      whiteSpace: "pre",
    },
    onmouseover() {
      if (this.layerViewport) {
        this.layerViewport.classList.add("layerHover");
      }
    },
    onmouseout() {
      if (this.layerViewport) {
        this.layerViewport.classList.remove("layerHover");
      }
    },
  });
  var icon = createElement("img", {
    src: "show.png",
    style: {
      width: "12px",
      height: "12px",
      marginLeft: "4px",
      marginRight: "4px",
      cursor: "pointer",
    },
    onclick() {
      if (this.layerViewport) {
        if (this.layerViewport.style.visibility == "hidden") {
          this.layerViewport.style.visibility = "";
          this.src = "show.png";
        } else {
          this.layerViewport.style.visibility = "hidden";
          this.src = "hide.png";
        }
      }
    },
  });
  elem.insertBefore(icon, elem.firstChild);
  pane.appendChild(elem);

  if (root["shadow-visible"] || root.visible) {
    var visibleRegion = root["shadow-visible"] || root.visible;
    var layerViewport = createElement("div", {
      id: root.address + "_viewport",
      style: {
        position: "absolute",
        pointerEvents: "none",
      },
    });
    elem.layerViewport = layerViewport;
    icon.layerViewport = layerViewport;
    var layerViewportMatrix = [1, 0, 0, 1, 0, 0];
    if (root["shadow-clip"] || root.clip) {
      var clip = root["shadow-clip"] || root.clip;
      var clipElem = createElement("div", {
        id: root.address + "_clip",
        style: {
          left: clip[0] + "px",
          top: clip[1] + "px",
          width: clip[2] + "px",
          height: clip[3] + "px",
          position: "absolute",
          overflow: "hidden",
          pointerEvents: "none",
        },
      });
      layerViewportMatrix[4] += -clip[0];
      layerViewportMatrix[5] += -clip[1];
      layerViewport.style.transform =
        "translate(-" + clip[0] + "px, -" + clip[1] + "px)";
    }
    if (root["shadow-transform"] || root.transform) {
      var matrix = root["shadow-transform"] || root.transform;
      layerViewportMatrix[0] = matrix[0][0];
      layerViewportMatrix[1] = matrix[0][1];
      layerViewportMatrix[2] = matrix[1][0];
      layerViewportMatrix[3] = matrix[1][1];
      layerViewportMatrix[4] += matrix[2][0];
      layerViewportMatrix[5] += matrix[2][1];
    }
    layerViewport.style.transform =
      "matrix(" +
      layerViewportMatrix[0] +
      "," +
      layerViewportMatrix[1] +
      "," +
      layerViewportMatrix[2] +
      "," +
      layerViewportMatrix[3] +
      "," +
      layerViewportMatrix[4] +
      "," +
      layerViewportMatrix[5] +
      ")";
    if (!hasSeenRoot) {
      hasSeenRoot = true;
      layerViewport.style.transform =
        "scale(" + 1 / contentScale + "," + 1 / contentScale + ")";
    }
    if (clipElem) {
      previewParent.appendChild(clipElem);
      clipElem.appendChild(layerViewport);
    } else {
      previewParent.appendChild(layerViewport);
    }
    previewParent = layerViewport;
    for (let i = 0; i < visibleRegion.length; i++) {
      let rect2d = visibleRegion[i];
      var layerPreview = createElement("div", {
        id: root.address + "_visible_part" + i + "-" + visibleRegion.length,
        className: "layerPreview",
        style: {
          position: "absolute",
          left: rect2d[0] + "px",
          top: rect2d[1] + "px",
          width: rect2d[2] + "px",
          height: rect2d[3] + "px",
          overflow: "hidden",
          border: "solid 1px black",
          background:
            'url("noise.png"), linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.2))',
        },
      });
      layerViewport.appendChild(layerPreview);

      function isInside(rect1, rect2) {
        if (
          rect1[0] + rect1[2] < rect2[0] &&
          rect2[0] + rect2[2] < rect1[0] &&
          rect1[1] + rect1[3] < rect2[1] &&
          rect2[1] + rect2[3] < rect1[1]
        ) {
          return true;
        }
        return true;
      }

      var hasImg = false;
      // Add tile img objects for this part
      var previewOffset = rect2d;

      if (root.tiles) {
        hasImg = true;
        for (var x in root.tiles) {
          for (var y in root.tiles[x]) {
            if (isInside(rect2d, [x, y, 512, 512])) {
              var tileImgElem = createElement("img", {
                src: getDataURI(root.tiles[x][y]),
                style: {
                  position: "absolute",
                  left: x - previewOffset[0] + "px",
                  top: y - previewOffset[1] + "px",
                  pointerEvents: "auto",
                },
              });
              layerPreview.appendChild(tileImgElem);
            }
          }
        }
        layerPreview.style.background = "";
      } else if (root.surfaceURI) {
        hasImg = true;
        var offsetX = 0;
        var offsetY = 0;
        if (root.contentHostProp && root.contentHostProp["buffer-rect"]) {
          offsetX = root.contentHostProp["buffer-rect"][0];
          offsetY = root.contentHostProp["buffer-rect"][1];
        }
        var surfaceImgElem = createElement("img", {
          src: getDataURI(root.surfaceURI),
          style: {
            position: "absolute",
            left: offsetX - previewOffset[0] + "px",
            top: offsetY - previewOffset[1] + "px",
            pointerEvents: "auto",
          },
        });
        layerPreview.appendChild(surfaceImgElem);
        layerPreview.style.background = "";
      } else if (root.color) {
        hasImg = true;
        layerPreview.style.background =
          "rgba(" +
          root.color.r +
          ", " +
          root.color.g +
          ", " +
          root.color.b +
          ", " +
          root.color.a +
          ")";
      }

      if (hasImg || true) {
        layerPreview.mouseoverElem = elem;
        layerPreview.onmouseenter = function () {
          this.mouseoverElem.onmouseover();
        };
        layerPreview.onmouseout = function () {
          this.mouseoverElem.onmouseout();
        };
      }
    }

    var layerDisplayItems = getDisplayItemForLayer(displayList);
    for (let i = 0; i < layerDisplayItems.length; i++) {
      var displayItem = layerDisplayItems[i];
      var displayElem = createElement("div", {
        className: "layerObjectDescription",
        textContent: "            " + trim(displayItem.line),
        style: {
          whiteSpace: "pre",
        },
        displayItem,
        layerViewport,
        onmouseover() {
          if (this.diPreview) {
            this.diPreview.classList.add("displayHover");

            var description = "";
            if (this.displayItem.contentDescriptor) {
              description += "Content: " + this.displayItem.contentDescriptor;
            } else {
              description += "Content: Unknown";
            }
            description +=
              "<br>Item: " +
              this.displayItem.name +
              " (" +
              this.displayItem.address +
              ")";
            description +=
              "<br>Layer: " + root.name + " (" + root.address + ")";
            if (this.displayItem.frame) {
              description += "<br>Frame: " + this.displayItem.frame;
            }
            if (this.displayItem.layerBounds) {
              description +=
                "<br>Bounds: [" +
                toFixed(this.displayItem.layerBounds[0] / 60, 2) +
                ", " +
                toFixed(this.displayItem.layerBounds[1] / 60, 2) +
                ", " +
                toFixed(this.displayItem.layerBounds[2] / 60, 2) +
                ", " +
                toFixed(this.displayItem.layerBounds[3] / 60, 2) +
                "] (CSS Pixels)";
            }
            if (this.displayItem.z) {
              description += "<br>Z: " + this.displayItem.z;
            }
            // At the end
            if (this.displayItem.rest) {
              description += "<br>" + this.displayItem.rest;
            }

            var box = this.diPreview.getBoundingClientRect();
            this.diPreview.tooltip = createElement("div", {
              className: "csstooltip",
              innerHTML: description,
              style: {
                top:
                  Math.min(
                    box.bottom,
                    document.documentElement.clientHeight - 150
                  ) + "px",
                left: box.left + "px",
              },
            });

            document.body.appendChild(this.diPreview.tooltip);
          }
        },
        onmouseout() {
          if (this.diPreview) {
            this.diPreview.classList.remove("displayHover");
            document.body.removeChild(this.diPreview.tooltip);
          }
        },
      });

      icon = createElement("img", {
        style: {
          width: "12px",
          height: "12px",
          marginLeft: "4px",
          marginRight: "4px",
        },
      });
      displayElem.insertBefore(icon, displayElem.firstChild);
      pane.appendChild(displayElem);
      // bounds doesn't adjust for within the layer. It's not a bad fallback but
      // will have the wrong offset
      let rect2d = displayItem.layerBounds || displayItem.bounds;
      if (rect2d) {
        // This doesn't place them corectly
        var appUnitsToPixels = 60 / contentScale;
        let diPreview = createElement("div", {
          id: "displayitem_" + displayItem.content + "_" + displayItem.address,
          className: "layerPreview",
          style: {
            position: "absolute",
            left: rect2d[0] / appUnitsToPixels + "px",
            top: rect2d[1] / appUnitsToPixels + "px",
            width: rect2d[2] / appUnitsToPixels + "px",
            height: rect2d[3] / appUnitsToPixels + "px",
            border: "solid 1px gray",
            pointerEvents: "auto",
          },
          displayElem,
          onmouseover() {
            this.displayElem.onmouseover();
          },
          onmouseout() {
            this.displayElem.onmouseout();
          },
        });

        layerViewport.appendChild(diPreview);
        displayElem.diPreview = diPreview;
      }
    }
  }

  for (var i = 0; i < root.children.length; i++) {
    populateLayers(
      root.children[i],
      displayList,
      pane,
      previewParent,
      hasSeenRoot,
      contentScale,
      rootPreviewParent
    );
  }
}

// This function takes a stdout snippet and finds the frames
function parseMultiLineDump(log) {
  var container = createElement("div", {
    style: {
      height: "100%",
      position: "relative",
    },
  });

  var layerManagerFirstLine = "[a-zA-Z]*LayerManager \\(.*$\n";
  var nextLineStartWithSpace = "([ \\t].*$\n)*";
  var layersRegex = "(" + layerManagerFirstLine + nextLineStartWithSpace + ")";

  var startLine = "Painting --- after optimization:\n";
  var endLine = "Painting --- layer tree:";
  var displayListRegex = "(" + startLine + "(.*\n)*?" + endLine + ")";

  var regex = new RegExp(layersRegex + "|" + displayListRegex, "gm");
  var matches = log.match(regex);
  console.log(matches);
  window.matches = matches;

  var matchList = createElement("span", {
    style: {
      height: "95%",
      width: "10%",
      position: "relative",
      border: "solid black 2px",
      display: "inline-block",
      float: "left",
      overflow: "auto",
    },
  });
  container.appendChild(matchList);
  var contents = createElement("span", {
    style: {
      height: "95%",
      width: "88%",
      display: "inline-block",
    },
    textContent: "Click on a frame on the left to view the layer tree",
  });
  container.appendChild(contents);

  var lastDisplayList = null;
  var frameID = 1;
  for (let i = 0; i < matches.length; i++) {
    var currMatch = matches[i];

    if (currMatch.indexOf(startLine) == 0) {
      // Display list match
      var matchLines = matches[i].split("\n");
      lastDisplayList = parseDisplayList(matchLines);
    } else {
      // Layer tree match:
      let displayList = lastDisplayList;
      lastDisplayList = null;
      var currFrameDiv = createElement("a", {
        style: {
          padding: "3px",
          display: "block",
        },
        href: "#",
        textContent: "LayerTree " + frameID++,
        onclick() {
          contents.innerHTML = "";
          var matchLines = matches[i].split("\n");
          var dumpDiv = parseDump(matchLines, displayList);
          contents.appendChild(dumpDiv);
        },
      });
      matchList.appendChild(currFrameDiv);
    }
  }

  return container;
}

function parseDump(log, displayList, compositeTitle, compositeTime) {
  compositeTitle |= "";
  compositeTime |= 0;

  var container = createElement("div", {
    style: {
      background: "white",
      height: "100%",
      position: "relative",
    },
  });

  if (compositeTitle == null && compositeTime == null) {
    var titleDiv = createElement("div", {
      className: "treeColumnHeader",
      style: {
        width: "100%",
      },
      textContent:
        compositeTitle +
        (compositeTitle ? " (near " + compositeTime.toFixed(0) + " ms)" : ""),
    });
    container.appendChild(titleDiv);
  }

  var mainDiv = createElement("div", {
    style: {
      position: "absolute",
      top: "16px",
      left: "0px",
      right: "0px",
      bottom: "0px",
    },
  });
  container.appendChild(mainDiv);

  var layerListPane = createElement("div", {
    style: {
      cssFloat: "left",
      height: "100%",
      width: "300px",
      overflowY: "scroll",
    },
  });
  mainDiv.appendChild(layerListPane);

  var previewDiv = createElement("div", {
    style: {
      position: "absolute",
      left: "300px",
      right: "0px",
      top: "0px",
      bottom: "0px",
      overflow: "auto",
    },
  });
  mainDiv.appendChild(previewDiv);

  var root = parseLayers(log);
  populateLayers(root, displayList, layerListPane, previewDiv);
  return container;
}