/* 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";

const {
  AutoRefreshHighlighter,
} = require("resource://devtools/server/actors/highlighters/auto-refresh.js");
const { apply } = require("resource://devtools/shared/layout/dom-matrix-2d.js");
const {
  CANVAS_SIZE,
  DEFAULT_COLOR,
  clearRect,
  drawLine,
  drawRect,
  getCurrentMatrix,
  updateCanvasElement,
  updateCanvasPosition,
} = require("resource://devtools/server/actors/highlighters/utils/canvas.js");
const {
  CanvasFrameAnonymousContentHelper,
  getComputedStyle,
} = require("resource://devtools/server/actors/highlighters/utils/markup.js");
const {
  getAbsoluteScrollOffsetsForNode,
  getCurrentZoom,
  getDisplayPixelRatio,
  getUntransformedQuad,
  getWindowDimensions,
  setIgnoreLayoutChanges,
} = require("resource://devtools/shared/layout/utils.js");

const FLEXBOX_LINES_PROPERTIES = {
  edge: {
    lineDash: [5, 3],
  },
  item: {
    lineDash: [0, 0],
  },
  alignItems: {
    lineDash: [0, 0],
  },
};

const FLEXBOX_CONTAINER_PATTERN_LINE_DASH = [5, 3]; // px
const FLEXBOX_CONTAINER_PATTERN_WIDTH = 14; // px
const FLEXBOX_CONTAINER_PATTERN_HEIGHT = 14; // px
const FLEXBOX_JUSTIFY_CONTENT_PATTERN_WIDTH = 7; // px
const FLEXBOX_JUSTIFY_CONTENT_PATTERN_HEIGHT = 7; // px

/**
 * Cached used by `FlexboxHighlighter.getFlexContainerPattern`.
 */
const gCachedFlexboxPattern = new Map();

const FLEXBOX = "flexbox";
const JUSTIFY_CONTENT = "justify-content";

/**
 * The FlexboxHighlighter is the class that overlays a visual canvas on top of
 * display: [inline-]flex elements.
 *
 * @param {String} options.color
 *        The color that should be used to draw the highlighter for this flexbox.
 * Structure:
 * <div class="highlighter-container">
 *   <div id="flexbox-root" class="flexbox-root">
 *     <canvas id="flexbox-canvas"
 *             class="flexbox-canvas"
 *             width="4096"
 *             height="4096"
 *             hidden="true">
 *     </canvas>
 *   </div>
 * </div>
 */
class FlexboxHighlighter extends AutoRefreshHighlighter {
  constructor(highlighterEnv) {
    super(highlighterEnv);

    this.ID_CLASS_PREFIX = "flexbox-";

    this.markup = new CanvasFrameAnonymousContentHelper(
      this.highlighterEnv,
      this._buildMarkup.bind(this)
    );
    this.isReady = this.markup.initialize();

    this.onPageHide = this.onPageHide.bind(this);
    this.onWillNavigate = this.onWillNavigate.bind(this);

    this.highlighterEnv.on("will-navigate", this.onWillNavigate);

    const { pageListenerTarget } = highlighterEnv;
    pageListenerTarget.addEventListener("pagehide", this.onPageHide);

    // Initialize the <canvas> position to the top left corner of the page
    this._canvasPosition = {
      x: 0,
      y: 0,
    };

    this._ignoreZoom = true;

    // Calling `updateCanvasPosition` anyway since the highlighter could be initialized
    // on a page that has scrolled already.
    updateCanvasPosition(
      this._canvasPosition,
      this._scroll,
      this.win,
      this._winDimensions
    );
  }

  _buildMarkup() {
    const container = this.markup.createNode({
      attributes: {
        class: "highlighter-container",
      },
    });

    const root = this.markup.createNode({
      parent: container,
      attributes: {
        id: "root",
        class: "root",
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    // We use a <canvas> element because there is an arbitrary number of items and texts
    // to draw which wouldn't be possible with HTML or SVG without having to insert and
    // remove the whole markup on every update.
    this.markup.createNode({
      parent: root,
      nodeType: "canvas",
      attributes: {
        id: "canvas",
        class: "canvas",
        hidden: "true",
        width: CANVAS_SIZE,
        height: CANVAS_SIZE,
      },
      prefix: this.ID_CLASS_PREFIX,
    });

    return container;
  }

  clearCache() {
    gCachedFlexboxPattern.clear();
  }

  destroy() {
    const { highlighterEnv } = this;
    highlighterEnv.off("will-navigate", this.onWillNavigate);

    const { pageListenerTarget } = highlighterEnv;

    if (pageListenerTarget) {
      pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
    }

    this.markup.destroy();

    // Clear the pattern cache to avoid dead object exceptions (Bug 1342051).
    this.clearCache();

    this.axes = null;
    this.crossAxisDirection = null;
    this.flexData = null;
    this.mainAxisDirection = null;
    this.transform = null;

    AutoRefreshHighlighter.prototype.destroy.call(this);
  }

  /**
   * Draw the justify content for a given flex item (left, top, right, bottom) position.
   */
  drawJustifyContent(left, top, right, bottom) {
    const { devicePixelRatio } = this.win;
    this.ctx.fillStyle = this.getJustifyContentPattern(devicePixelRatio);
    drawRect(this.ctx, left, top, right, bottom, this.currentMatrix);
    this.ctx.fill();
  }

  get canvas() {
    return this.getElement("canvas");
  }

  get color() {
    return this.options.color || DEFAULT_COLOR;
  }

  get container() {
    return this.currentNode;
  }

  get ctx() {
    return this.canvas.getCanvasContext("2d");
  }

  getElement(id) {
    return this.markup.getElement(this.ID_CLASS_PREFIX + id);
  }

  /**
   * Gets the flexbox container pattern used to render the container regions.
   *
   * @param  {Number} devicePixelRatio
   *         The device pixel ratio we want the pattern for.
   * @return {CanvasPattern} flex container pattern.
   */
  getFlexContainerPattern(devicePixelRatio) {
    let flexboxPatternMap = null;

    if (gCachedFlexboxPattern.has(devicePixelRatio)) {
      flexboxPatternMap = gCachedFlexboxPattern.get(devicePixelRatio);
    } else {
      flexboxPatternMap = new Map();
    }

    if (gCachedFlexboxPattern.has(FLEXBOX)) {
      return gCachedFlexboxPattern.get(FLEXBOX);
    }

    // Create the diagonal lines pattern for the rendering the flexbox gaps.
    const canvas = this.markup.createNode({ nodeType: "canvas" });
    const width = (canvas.width =
      FLEXBOX_CONTAINER_PATTERN_WIDTH * devicePixelRatio);
    const height = (canvas.height =
      FLEXBOX_CONTAINER_PATTERN_HEIGHT * devicePixelRatio);

    const ctx = canvas.getContext("2d");
    ctx.save();
    ctx.setLineDash(FLEXBOX_CONTAINER_PATTERN_LINE_DASH);
    ctx.beginPath();
    ctx.translate(0.5, 0.5);

    ctx.moveTo(0, 0);
    ctx.lineTo(width, height);

    ctx.strokeStyle = this.color;
    ctx.stroke();
    ctx.restore();

    const pattern = ctx.createPattern(canvas, "repeat");
    flexboxPatternMap.set(FLEXBOX, pattern);
    gCachedFlexboxPattern.set(devicePixelRatio, flexboxPatternMap);

    return pattern;
  }

  /**
   * Gets the flexbox justify content pattern used to render the justify content regions.
   *
   * @param  {Number} devicePixelRatio
   *         The device pixel ratio we want the pattern for.
   * @return {CanvasPattern} flex justify content pattern.
   */
  getJustifyContentPattern(devicePixelRatio) {
    let flexboxPatternMap = null;

    if (gCachedFlexboxPattern.has(devicePixelRatio)) {
      flexboxPatternMap = gCachedFlexboxPattern.get(devicePixelRatio);
    } else {
      flexboxPatternMap = new Map();
    }

    if (flexboxPatternMap.has(JUSTIFY_CONTENT)) {
      return flexboxPatternMap.get(JUSTIFY_CONTENT);
    }

    // Create the inversed diagonal lines pattern
    // for the rendering the justify content gaps.
    const canvas = this.markup.createNode({ nodeType: "canvas" });
    const zoom = getCurrentZoom(this.win);
    const width = (canvas.width =
      FLEXBOX_JUSTIFY_CONTENT_PATTERN_WIDTH * devicePixelRatio * zoom);
    const height = (canvas.height =
      FLEXBOX_JUSTIFY_CONTENT_PATTERN_HEIGHT * devicePixelRatio * zoom);

    const ctx = canvas.getContext("2d");
    ctx.save();
    ctx.setLineDash(FLEXBOX_CONTAINER_PATTERN_LINE_DASH);
    ctx.beginPath();
    ctx.translate(0.5, 0.5);

    ctx.moveTo(0, height);
    ctx.lineTo(width, 0);

    ctx.strokeStyle = this.color;
    ctx.stroke();
    ctx.restore();

    const pattern = ctx.createPattern(canvas, "repeat");
    flexboxPatternMap.set(JUSTIFY_CONTENT, pattern);
    gCachedFlexboxPattern.set(devicePixelRatio, flexboxPatternMap);

    return pattern;
  }

  getNode(id) {
    return this.markup.content.root.getElementById(this.ID_CLASS_PREFIX + id);
  }

  /**
   * The AutoRefreshHighlighter's _hasMoved method returns true only if the
   * element's quads have changed. Override it so it also returns true if the
   * flex container and its flex items have changed.
   */
  _hasMoved() {
    const hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this);

    if (!this.computedStyle) {
      this.computedStyle = getComputedStyle(this.container);
    }

    const flex = this.container.getAsFlexContainer();

    const oldCrossAxisDirection = this.crossAxisDirection;
    this.crossAxisDirection = flex ? flex.crossAxisDirection : null;
    const newCrossAxisDirection = this.crossAxisDirection;

    const oldMainAxisDirection = this.mainAxisDirection;
    this.mainAxisDirection = flex ? flex.mainAxisDirection : null;
    const newMainAxisDirection = this.mainAxisDirection;

    // Concatenate the axes to simplify conditionals.
    this.axes = `${this.mainAxisDirection} ${this.crossAxisDirection}`;

    const oldFlexData = this.flexData;
    this.flexData = getFlexData(this.container);
    const hasFlexDataChanged = compareFlexData(oldFlexData, this.flexData);

    const oldAlignItems = this.alignItemsValue;
    this.alignItemsValue = this.computedStyle.alignItems;
    const newAlignItems = this.alignItemsValue;

    const oldFlexDirection = this.flexDirection;
    this.flexDirection = this.computedStyle.flexDirection;
    const newFlexDirection = this.flexDirection;

    const oldFlexWrap = this.flexWrap;
    this.flexWrap = this.computedStyle.flexWrap;
    const newFlexWrap = this.flexWrap;

    const oldJustifyContent = this.justifyContentValue;
    this.justifyContentValue = this.computedStyle.justifyContent;
    const newJustifyContent = this.justifyContentValue;

    const oldTransform = this.transformValue;
    this.transformValue = this.computedStyle.transform;
    const newTransform = this.transformValue;

    return (
      hasMoved ||
      hasFlexDataChanged ||
      oldAlignItems !== newAlignItems ||
      oldFlexDirection !== newFlexDirection ||
      oldFlexWrap !== newFlexWrap ||
      oldJustifyContent !== newJustifyContent ||
      oldCrossAxisDirection !== newCrossAxisDirection ||
      oldMainAxisDirection !== newMainAxisDirection ||
      oldTransform !== newTransform
    );
  }

  _hide() {
    this.alignItemsValue = null;
    this.computedStyle = null;
    this.flexData = null;
    this.flexDirection = null;
    this.flexWrap = null;
    this.justifyContentValue = null;

    setIgnoreLayoutChanges(true);
    this._hideFlexbox();
    setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
  }

  _hideFlexbox() {
    this.getElement("canvas").setAttribute("hidden", "true");
  }

  /**
   * The <canvas>'s position needs to be updated if the page scrolls too much, in order
   * to give the illusion that it always covers the viewport.
   */
  _scrollUpdate() {
    const hasUpdated = updateCanvasPosition(
      this._canvasPosition,
      this._scroll,
      this.win,
      this._winDimensions
    );

    if (hasUpdated) {
      this._update();
    }
  }

  _show() {
    this._hide();
    return this._update();
  }

  _showFlexbox() {
    this.getElement("canvas").removeAttribute("hidden");
  }

  /**
   * If a page hide event is triggered for current window's highlighter, hide the
   * highlighter.
   */
  onPageHide({ target }) {
    if (target.defaultView === this.win) {
      this.hide();
    }
  }

  /**
   * Called when the page will-navigate. Used to hide the flexbox highlighter and clear
   * the cached gap patterns and avoid using DeadWrapper obejcts as gap patterns the
   * next time.
   */
  onWillNavigate({ isTopLevel }) {
    this.clearCache();

    if (isTopLevel) {
      this.hide();
    }
  }

  renderFlexContainer() {
    if (!this.currentQuads.content || !this.currentQuads.content[0]) {
      return;
    }

    const { devicePixelRatio } = this.win;
    const containerQuad = getUntransformedQuad(this.container, "content");
    const { width, height } = containerQuad.getBounds();

    this.setupCanvas({
      lineDash: FLEXBOX_LINES_PROPERTIES.alignItems.lineDash,
      lineWidthMultiplier: 2,
    });

    this.ctx.fillStyle = this.getFlexContainerPattern(devicePixelRatio);

    drawRect(this.ctx, 0, 0, width, height, this.currentMatrix);

    // Find current angle of outer flex element by measuring the angle of two arbitrary
    // points, then rotate canvas, so the hash pattern stays 45deg to the boundary.
    const p1 = apply(this.currentMatrix, [0, 0]);
    const p2 = apply(this.currentMatrix, [1, 0]);
    const angleRad = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]);
    this.ctx.rotate(angleRad);

    this.ctx.fill();
    this.ctx.stroke();
    this.ctx.restore();
  }

  renderFlexItems() {
    if (
      !this.flexData ||
      !this.currentQuads.content ||
      !this.currentQuads.content[0]
    ) {
      return;
    }

    this.setupCanvas({
      lineDash: FLEXBOX_LINES_PROPERTIES.item.lineDash,
    });

    for (const flexLine of this.flexData.lines) {
      for (const flexItem of flexLine.items) {
        const { left, top, right, bottom } = flexItem.rect;

        clearRect(this.ctx, left, top, right, bottom, this.currentMatrix);
        drawRect(this.ctx, left, top, right, bottom, this.currentMatrix);
        this.ctx.stroke();
      }
    }

    this.ctx.restore();
  }

  renderFlexLines() {
    if (
      !this.flexData ||
      !this.currentQuads.content ||
      !this.currentQuads.content[0]
    ) {
      return;
    }

    const lineWidth = getDisplayPixelRatio(this.win);
    const options = { matrix: this.currentMatrix };
    const { width: containerWidth, height: containerHeight } =
      getUntransformedQuad(this.container, "content").getBounds();

    this.setupCanvas({
      useContainerScrollOffsets: true,
    });

    for (const flexLine of this.flexData.lines) {
      const { crossStart, crossSize } = flexLine;

      switch (this.axes) {
        case "horizontal-lr vertical-tb":
        case "horizontal-lr vertical-bt":
        case "horizontal-rl vertical-tb":
        case "horizontal-rl vertical-bt":
          clearRect(
            this.ctx,
            0,
            crossStart,
            containerWidth,
            crossStart + crossSize,
            this.currentMatrix
          );

          // Avoid drawing the start flex line when they overlap with the flex container.
          if (crossStart != 0) {
            drawLine(
              this.ctx,
              0,
              crossStart,
              containerWidth,
              crossStart,
              options
            );
            this.ctx.stroke();
          }

          // Avoid drawing the end flex line when they overlap with the flex container.
          if (crossStart + crossSize < containerHeight - lineWidth * 2) {
            drawLine(
              this.ctx,
              0,
              crossStart + crossSize,
              containerWidth,
              crossStart + crossSize,
              options
            );
            this.ctx.stroke();
          }
          break;
        case "vertical-tb horizontal-lr":
        case "vertical-bt horizontal-rl":
          clearRect(
            this.ctx,
            crossStart,
            0,
            crossStart + crossSize,
            containerHeight,
            this.currentMatrix
          );

          // Avoid drawing the start flex line when they overlap with the flex container.
          if (crossStart != 0) {
            drawLine(
              this.ctx,
              crossStart,
              0,
              crossStart,
              containerHeight,
              options
            );
            this.ctx.stroke();
          }

          // Avoid drawing the end flex line when they overlap with the flex container.
          if (crossStart + crossSize < containerWidth - lineWidth * 2) {
            drawLine(
              this.ctx,
              crossStart + crossSize,
              0,
              crossStart + crossSize,
              containerHeight,
              options
            );
            this.ctx.stroke();
          }
          break;
        case "vertical-bt horizontal-lr":
        case "vertical-tb horizontal-rl":
          clearRect(
            this.ctx,
            containerWidth - crossStart,
            0,
            containerWidth - crossStart - crossSize,
            containerHeight,
            this.currentMatrix
          );

          // Avoid drawing the start flex line when they overlap with the flex container.
          if (crossStart != 0) {
            drawLine(
              this.ctx,
              containerWidth - crossStart,
              0,
              containerWidth - crossStart,
              containerHeight,
              options
            );
            this.ctx.stroke();
          }

          // Avoid drawing the end flex line when they overlap with the flex container.
          if (crossStart + crossSize < containerWidth - lineWidth * 2) {
            drawLine(
              this.ctx,
              containerWidth - crossStart - crossSize,
              0,
              containerWidth - crossStart - crossSize,
              containerHeight,
              options
            );
            this.ctx.stroke();
          }
          break;
      }
    }

    this.ctx.restore();
  }

  /**
   * Clear the whole alignment container along the main axis for each flex item.
   */
  // eslint-disable-next-line complexity
  renderJustifyContent() {
    if (
      !this.flexData ||
      !this.currentQuads.content ||
      !this.currentQuads.content[0]
    ) {
      return;
    }

    const { width: containerWidth, height: containerHeight } =
      getUntransformedQuad(this.container, "content").getBounds();

    this.setupCanvas({
      lineDash: FLEXBOX_LINES_PROPERTIES.alignItems.lineDash,
      offset: (getDisplayPixelRatio(this.win) / 2) % 1,
      skipLineAndStroke: true,
      useContainerScrollOffsets: true,
    });

    for (const flexLine of this.flexData.lines) {
      const { crossStart, crossSize } = flexLine;
      let mainStart = 0;

      // In these two situations mainStart goes from right to left so set it's
      // value as appropriate.
      if (
        this.axes === "horizontal-lr vertical-bt" ||
        this.axes === "horizontal-rl vertical-tb"
      ) {
        mainStart = containerWidth;
      }

      for (const flexItem of flexLine.items) {
        const { left, top, right, bottom } = flexItem.rect;

        switch (this.axes) {
          case "horizontal-lr vertical-tb":
          case "horizontal-rl vertical-bt":
            this.drawJustifyContent(
              mainStart,
              crossStart,
              left,
              crossStart + crossSize
            );
            mainStart = right;
            break;
          case "horizontal-lr vertical-bt":
          case "horizontal-rl vertical-tb":
            this.drawJustifyContent(
              right,
              crossStart,
              mainStart,
              crossStart + crossSize
            );
            mainStart = left;
            break;
          case "vertical-tb horizontal-lr":
          case "vertical-bt horizontal-rl":
            this.drawJustifyContent(
              crossStart,
              mainStart,
              crossStart + crossSize,
              top
            );
            mainStart = bottom;
            break;
          case "vertical-bt horizontal-lr":
          case "vertical-tb horizontal-rl":
            this.drawJustifyContent(
              containerWidth - crossStart - crossSize,
              mainStart,
              containerWidth - crossStart,
              top
            );
            mainStart = bottom;
            break;
        }
      }

      // Draw the last justify-content area after the last flex item.
      switch (this.axes) {
        case "horizontal-lr vertical-tb":
        case "horizontal-rl vertical-bt":
          this.drawJustifyContent(
            mainStart,
            crossStart,
            containerWidth,
            crossStart + crossSize
          );
          break;
        case "horizontal-lr vertical-bt":
        case "horizontal-rl vertical-tb":
          this.drawJustifyContent(
            0,
            crossStart,
            mainStart,
            crossStart + crossSize
          );
          break;
        case "vertical-tb horizontal-lr":
        case "vertical-bt horizontal-rl":
          this.drawJustifyContent(
            crossStart,
            mainStart,
            crossStart + crossSize,
            containerHeight
          );
          break;
        case "vertical-bt horizontal-lr":
        case "vertical-tb horizontal-rl":
          this.drawJustifyContent(
            containerWidth - crossStart - crossSize,
            mainStart,
            containerWidth - crossStart,
            containerHeight
          );
          break;
      }
    }

    this.ctx.restore();
  }

  /**
   * Set up the canvas with the given options prior to drawing.
   *
   * @param {String} [options.lineDash = null]
   *        An Array of numbers that specify distances to alternately draw a
   *        line and a gap (in coordinate space units). If the number of
   *        elements in the array is odd, the elements of the array get copied
   *        and concatenated. For example, [5, 15, 25] will become
   *        [5, 15, 25, 5, 15, 25]. If the array is empty, the line dash list is
   *        cleared and line strokes return to being solid.
   *
   *        We use the following constants here:
   *          FLEXBOX_LINES_PROPERTIES.edge.lineDash,
   *          FLEXBOX_LINES_PROPERTIES.item.lineDash
   *          FLEXBOX_LINES_PROPERTIES.alignItems.lineDash
   * @param {Number} [options.lineWidthMultiplier = 1]
   *        The width of the line.
   * @param {Number} [options.offset = `(displayPixelRatio / 2) % 1`]
   *        The single line width used to obtain a crisp line.
   * @param {Boolean} [options.skipLineAndStroke = false]
   *        Skip the setting of lineWidth and strokeStyle.
   * @param {Boolean} [options.useContainerScrollOffsets = false]
   *        Take the flexbox container's scroll and zoom offsets into account.
   *        This is needed for drawing flex lines and justify content when the
   *        flexbox container itself is display:scroll.
   */
  setupCanvas({
    lineDash = null,
    lineWidthMultiplier = 1,
    offset = (getDisplayPixelRatio(this.win) / 2) % 1,
    skipLineAndStroke = false,
    useContainerScrollOffsets = false,
  }) {
    const { devicePixelRatio } = this.win;
    const lineWidth = getDisplayPixelRatio(this.win);
    const zoom = getCurrentZoom(this.win);
    const style = getComputedStyle(this.container);
    const position = style.position;
    let offsetX = this._canvasPosition.x;
    let offsetY = this._canvasPosition.y;

    if (useContainerScrollOffsets) {
      offsetX += this.container.scrollLeft / zoom;
      offsetY += this.container.scrollTop / zoom;
    }

    // If the flexbox container is position:fixed we need to subtract the scroll
    // positions of all ancestral elements.
    if (position === "fixed") {
      const { scrollLeft, scrollTop } = getAbsoluteScrollOffsetsForNode(
        this.container
      );
      offsetX -= scrollLeft / zoom;
      offsetY -= scrollTop / zoom;
    }

    const canvasX = Math.round(offsetX * devicePixelRatio * zoom);
    const canvasY = Math.round(offsetY * devicePixelRatio * zoom);

    this.ctx.save();
    this.ctx.translate(offset - canvasX, offset - canvasY);

    if (lineDash) {
      this.ctx.setLineDash(lineDash);
    }

    if (!skipLineAndStroke) {
      this.ctx.lineWidth = lineWidth * lineWidthMultiplier;
      this.ctx.strokeStyle = this.color;
    }
  }

  _update() {
    setIgnoreLayoutChanges(true);

    this._winDimensions = getWindowDimensions(this.win);
    const { width, height } = this._winDimensions;

    // Updates the <canvas> element's position and size.
    // It also clear the <canvas>'s drawing context.
    updateCanvasElement(
      this.canvas,
      this._canvasPosition,
      this.win.devicePixelRatio,
      {
        zoomWindow: this.win,
      }
    );

    // Update the current matrix used in our canvas' rendering
    const { currentMatrix, hasNodeTransformations } = getCurrentMatrix(
      this.container,
      this.win,
      {
        ignoreWritingModeAndTextDirection: true,
      }
    );
    this.currentMatrix = currentMatrix;
    this.hasNodeTransformations = hasNodeTransformations;

    if (this.prevColor != this.color) {
      this.clearCache();
    }
    this.renderFlexContainer();
    this.renderFlexLines();
    this.renderJustifyContent();
    this.renderFlexItems();
    this._showFlexbox();
    this.prevColor = this.color;

    const root = this.getNode("root");
    root.style.setProperty("width", `${width}px`);
    root.style.setProperty("height", `${height}px`);

    setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
    return true;
  }
}

/**
 * Returns an object representation of the Flex data object and its array of FlexLine
 * and FlexItem objects along with the DOMRects of the flex items.
 *
 * @param  {DOMNode} container
 *         The flex container.
 * @return {Object|null} representation of the Flex data object.
 */
function getFlexData(container) {
  const flex = container.getAsFlexContainer();

  if (!flex) {
    return null;
  }

  return {
    lines: flex.getLines().map(line => {
      return {
        crossSize: line.crossSize,
        crossStart: line.crossStart,
        firstBaselineOffset: line.firstBaselineOffset,
        growthState: line.growthState,
        lastBaselineOffset: line.lastBaselineOffset,
        items: line.getItems().map(item => {
          return {
            crossMaxSize: item.crossMaxSize,
            crossMinSize: item.crossMinSize,
            mainBaseSize: item.mainBaseSize,
            mainDeltaSize: item.mainDeltaSize,
            mainMaxSize: item.mainMaxSize,
            mainMinSize: item.mainMinSize,
            node: item.node,
            rect: getRectFromFlexItemValues(item, container),
          };
        }),
      };
    }),
  };
}

/**
 * Given a FlexItemValues, return a DOMRect representing the flex item taking
 * into account its flex container's border and padding.
 *
 * @param  {FlexItemValues} item
 *         The FlexItemValues for which we need the DOMRect.
 * @param  {DOMNode}
 *         Flex container containing the flex item.
 * @return {DOMRect} representing the flex item.
 */
function getRectFromFlexItemValues(item, container) {
  const rect = item.frameRect;
  const domRect = new DOMRect(rect.x, rect.y, rect.width, rect.height);
  const win = container.ownerGlobal;
  const style = win.getComputedStyle(container);
  const borderLeftWidth = parseInt(style.borderLeftWidth, 10) || 0;
  const borderTopWidth = parseInt(style.borderTopWidth, 10) || 0;
  const paddingLeft = parseInt(style.paddingLeft, 10) || 0;
  const paddingTop = parseInt(style.paddingTop, 10) || 0;
  const scrollX = container.scrollLeft || 0;
  const scrollY = container.scrollTop || 0;

  domRect.x -= paddingLeft + scrollX;
  domRect.y -= paddingTop + scrollY;

  if (style.overflow === "visible" || style.overflow === "clip") {
    domRect.x -= borderLeftWidth;
    domRect.y -= borderTopWidth;
  }

  return domRect;
}

/**
 * Returns whether or not the flex data has changed.
 *
 * @param  {Flex} oldFlexData
 *         The old Flex data object.
 * @param  {Flex} newFlexData
 *         The new Flex data object.
 * @return {Boolean} true if the flex data has changed and false otherwise.
 */
// eslint-disable-next-line complexity
function compareFlexData(oldFlexData, newFlexData) {
  if (!oldFlexData || !newFlexData) {
    return true;
  }

  const oldLines = oldFlexData.lines;
  const newLines = newFlexData.lines;

  if (oldLines.length !== newLines.length) {
    return true;
  }

  for (let i = 0; i < oldLines.length; i++) {
    const oldLine = oldLines[i];
    const newLine = newLines[i];

    if (
      oldLine.crossSize !== newLine.crossSize ||
      oldLine.crossStart !== newLine.crossStart ||
      oldLine.firstBaselineOffset !== newLine.firstBaselineOffset ||
      oldLine.growthState !== newLine.growthState ||
      oldLine.lastBaselineOffset !== newLine.lastBaselineOffset
    ) {
      return true;
    }

    const oldItems = oldLine.items;
    const newItems = newLine.items;

    if (oldItems.length !== newItems.length) {
      return true;
    }

    for (let j = 0; j < oldItems.length; j++) {
      const oldItem = oldItems[j];
      const newItem = newItems[j];

      if (
        oldItem.crossMaxSize !== newItem.crossMaxSize ||
        oldItem.crossMinSize !== newItem.crossMinSize ||
        oldItem.mainBaseSize !== newItem.mainBaseSize ||
        oldItem.mainDeltaSize !== newItem.mainDeltaSize ||
        oldItem.mainMaxSize !== newItem.mainMaxSize ||
        oldItem.mainMinSize !== newItem.mainMinSize
      ) {
        return true;
      }

      const oldItemRect = oldItem.rect;
      const newItemRect = newItem.rect;

      // We are using DOMRects so we only need to compare x, y, width and
      // height (left, top, right and bottom are calculated from these values).
      if (
        oldItemRect.x !== newItemRect.x ||
        oldItemRect.y !== newItemRect.y ||
        oldItemRect.width !== newItemRect.width ||
        oldItemRect.height !== newItemRect.height
      ) {
        return true;
      }
    }
  }

  return false;
}

exports.FlexboxHighlighter = FlexboxHighlighter;
