export class ReftestFissionChild extends JSWindowActorChild {
  forwardAfterPaintEventToParent(
    rects,
    originalTargetUri,
    dispatchToSelfAsWell
  ) {
    if (dispatchToSelfAsWell && this.contentWindow) {
      let event = new this.contentWindow.CustomEvent(
        "Reftest:MozAfterPaintFromChild",
        { bubbles: true, detail: { rects, originalTargetUri } }
      );
      this.contentWindow.dispatchEvent(event);
    }

    let parentContext = this.browsingContext.parent;
    if (parentContext) {
      try {
        this.sendAsyncMessage("ForwardAfterPaintEvent", {
          toBrowsingContext: parentContext,
          fromBrowsingContext: this.browsingContext,
          rects,
          originalTargetUri,
        });
      } catch (e) {
        // |this| can be destroyed here and unable to send messages, which is
        // not a problem, the reftest harness probably torn down the page and
        // moved on to the next test.
        console.error(e);
      }
    }
  }

  handleEvent(evt) {
    switch (evt.type) {
      case "MozAfterPaint":
        // We want to forward any after paint events to our parent document so that
        // that it reaches the root content document where the main reftest harness
        // code (reftest-content.js) will process it and update the canvas.
        var rects = [];
        for (let r of evt.clientRects) {
          rects.push({
            left: r.left,
            top: r.top,
            right: r.right,
            bottom: r.bottom,
          });
        }
        this.forwardAfterPaintEventToParent(
          rects,
          this.document.documentURI,
          /* dispatchToSelfAsWell */ false
        );
        break;
    }
  }

  transformRect(transform, rect) {
    let p1 = transform.transformPoint({ x: rect.left, y: rect.top });
    let p2 = transform.transformPoint({ x: rect.right, y: rect.top });
    let p3 = transform.transformPoint({ x: rect.left, y: rect.bottom });
    let p4 = transform.transformPoint({ x: rect.right, y: rect.bottom });
    let quad = new DOMQuad(p1, p2, p3, p4);
    return quad.getBounds();
  }

  SetupDisplayportRoot() {
    let returnStrings = { infoStrings: [], errorStrings: [] };

    let contentRootElement = this.contentWindow.document.documentElement;
    if (!contentRootElement) {
      return Promise.resolve(returnStrings);
    }

    // If we don't have the reftest-async-scroll attribute we only look at
    // the root element for potential display ports to set.
    if (!contentRootElement.hasAttribute("reftest-async-scroll")) {
      let winUtils = this.contentWindow.windowUtils;
      this.setupDisplayportForElement(
        contentRootElement,
        winUtils,
        returnStrings
      );
      return Promise.resolve(returnStrings);
    }

    // Send a msg to the parent side to get the parent side to tell all
    // process roots to do the displayport setting.
    let browsingContext = this.browsingContext;
    let promise = this.sendQuery("TellChildrenToSetupDisplayport", {
      browsingContext,
    });
    return promise.then(
      function (result) {
        for (let errorString of result.errorStrings) {
          returnStrings.errorStrings.push(errorString);
        }
        for (let infoString of result.infoStrings) {
          returnStrings.infoStrings.push(infoString);
        }
        return returnStrings;
      },
      function (reason) {
        returnStrings.errorStrings.push(
          "SetupDisplayport SendQuery to parent promise rejected: " + reason
        );
        return returnStrings;
      }
    );
  }

  attrOrDefault(element, attr, def) {
    return element.hasAttribute(attr)
      ? Number(element.getAttribute(attr))
      : def;
  }

  setupDisplayportForElement(element, winUtils, returnStrings) {
    var dpw = this.attrOrDefault(element, "reftest-displayport-w", 0);
    var dph = this.attrOrDefault(element, "reftest-displayport-h", 0);
    var dpx = this.attrOrDefault(element, "reftest-displayport-x", 0);
    var dpy = this.attrOrDefault(element, "reftest-displayport-y", 0);
    if (dpw !== 0 || dph !== 0 || dpx != 0 || dpy != 0) {
      returnStrings.infoStrings.push(
        "Setting displayport to <x=" +
          dpx +
          ", y=" +
          dpy +
          ", w=" +
          dpw +
          ", h=" +
          dph +
          ">"
      );
      winUtils.setDisplayPortForElement(dpx, dpy, dpw, dph, element, 1);
    }
  }

  setupDisplayportForElementSubtree(element, winUtils, returnStrings) {
    this.setupDisplayportForElement(element, winUtils, returnStrings);
    for (let c = element.firstElementChild; c; c = c.nextElementSibling) {
      this.setupDisplayportForElementSubtree(c, winUtils, returnStrings);
    }
    if (
      typeof element.contentDocument !== "undefined" &&
      element.contentDocument
    ) {
      returnStrings.infoStrings.push(
        "setupDisplayportForElementSubtree descending into subdocument"
      );
      this.setupDisplayportForElementSubtree(
        element.contentDocument.documentElement,
        element.contentWindow.windowUtils,
        returnStrings
      );
    }
  }

  setupAsyncScrollOffsetsForElement(
    element,
    winUtils,
    allowFailure,
    returnStrings
  ) {
    let sx = this.attrOrDefault(element, "reftest-async-scroll-x", 0);
    let sy = this.attrOrDefault(element, "reftest-async-scroll-y", 0);
    if (sx != 0 || sy != 0) {
      try {
        // This might fail when called from RecordResult since layers
        // may not have been constructed yet
        winUtils.setAsyncScrollOffset(element, sx, sy);
        return true;
      } catch (e) {
        if (allowFailure) {
          returnStrings.infoStrings.push(
            "setupAsyncScrollOffsetsForElement error calling setAsyncScrollOffset: " +
              e
          );
        } else {
          returnStrings.errorStrings.push(
            "setupAsyncScrollOffsetsForElement error calling setAsyncScrollOffset: " +
              e
          );
        }
      }
    }
    return false;
  }

  setupAsyncScrollOffsetsForElementSubtree(
    element,
    winUtils,
    allowFailure,
    returnStrings
  ) {
    let updatedAny = this.setupAsyncScrollOffsetsForElement(
      element,
      winUtils,
      returnStrings
    );
    for (let c = element.firstElementChild; c; c = c.nextElementSibling) {
      if (
        this.setupAsyncScrollOffsetsForElementSubtree(
          c,
          winUtils,
          allowFailure,
          returnStrings
        )
      ) {
        updatedAny = true;
      }
    }
    if (
      typeof element.contentDocument !== "undefined" &&
      element.contentDocument
    ) {
      returnStrings.infoStrings.push(
        "setupAsyncScrollOffsetsForElementSubtree Descending into subdocument"
      );
      if (
        this.setupAsyncScrollOffsetsForElementSubtree(
          element.contentDocument.documentElement,
          element.contentWindow.windowUtils,
          allowFailure,
          returnStrings
        )
      ) {
        updatedAny = true;
      }
    }
    return updatedAny;
  }

  async receiveMessage(msg) {
    switch (msg.name) {
      case "ForwardAfterPaintEventToSelfAndParent": {
        // The embedderElement can be null if the child we got this from was removed.
        // Not much we can do to transform the rects, but it doesn't matter, the rects
        // won't reach reftest-content.js.
        if (msg.data.fromBrowsingContext.embedderElement == null) {
          this.forwardAfterPaintEventToParent(
            msg.data.rects,
            msg.data.originalTargetUri,
            /* dispatchToSelfAsWell */ true
          );
          return undefined;
        }

        let translate = new DOMMatrixReadOnly().translate(0, 0);
        if (this.contentWindow) {
          // Transform the rects from fromBrowsingContext to us.
          // We first translate from the content rect to the border rect of the iframe.
          let style = this.contentWindow.getComputedStyle(
            msg.data.fromBrowsingContext.embedderElement
          );
          translate = new DOMMatrixReadOnly().translate(
            parseFloat(style.paddingLeft) + parseFloat(style.borderLeftWidth),
            parseFloat(style.paddingTop) + parseFloat(style.borderTopWidth)
          );
        }

        // Then we transform from the iframe to our root frame.
        // We are guaranteed to be the process with the embedderElement for fromBrowsingContext.
        let transform =
          msg.data.fromBrowsingContext.embedderElement.getTransformToViewport();
        let combined = translate.multiply(transform);

        let newrects = msg.data.rects.map(r => this.transformRect(combined, r));

        this.forwardAfterPaintEventToParent(
          newrects,
          msg.data.originalTargetUri,
          /* dispatchToSelfAsWell */ true
        );
        break;
      }

      case "EmptyMessage":
        return undefined;
      case "UpdateLayerTree": {
        let errorStrings = [];
        try {
          if (this.manager.isProcessRoot) {
            this.contentWindow.windowUtils.updateLayerTree();
          }
        } catch (e) {
          errorStrings.push("updateLayerTree failed: " + e);
        }
        return { errorStrings };
      }
      case "FlushRendering": {
        let errorStrings = [];
        let warningStrings = [];
        let infoStrings = [];

        try {
          let { ignoreThrottledAnimations, needsAnimationFrame } = msg.data;

          if (this.manager.isProcessRoot) {
            var anyPendingPaintsGeneratedInDescendants = false;

            if (this.contentWindow && needsAnimationFrame) {
              await new Promise(resolve =>
                this.contentWindow.requestAnimationFrame(resolve)
              );
            }

            function flushWindow(win) {
              var utils = win.windowUtils;
              var afterPaintWasPending = utils.isMozAfterPaintPending;

              var root = win.document.documentElement;
              if (root && !root.classList.contains("reftest-no-flush")) {
                try {
                  if (ignoreThrottledAnimations) {
                    utils.flushLayoutWithoutThrottledAnimations();
                  } else {
                    root.getBoundingClientRect();
                  }
                } catch (e) {
                  warningStrings.push("flushWindow failed: " + e + "\n");
                }
              }

              if (!afterPaintWasPending && utils.isMozAfterPaintPending) {
                infoStrings.push(
                  "FlushRendering generated paint for window " +
                    win.location.href
                );
                anyPendingPaintsGeneratedInDescendants = true;
              }

              for (let i = 0; i < win.frames.length; ++i) {
                try {
                  if (!Cu.isRemoteProxy(win.frames[i])) {
                    flushWindow(win.frames[i]);
                  }
                } catch (e) {
                  console.error(e);
                }
              }
            }

            // `contentWindow` will be null if the inner window for this actor
            // has been navigated away from.
            if (this.contentWindow) {
              flushWindow(this.contentWindow);
            }

            if (
              anyPendingPaintsGeneratedInDescendants &&
              !this.contentWindow.windowUtils.isMozAfterPaintPending
            ) {
              warningStrings.push(
                "Internal error: descendant frame generated a MozAfterPaint event, but the root document doesn't have one!"
              );
            }
          }
        } catch (e) {
          errorStrings.push("flushWindow failed: " + e);
        }
        return { errorStrings, warningStrings, infoStrings };
      }

      case "SetupDisplayport": {
        let contentRootElement = this.document.documentElement;
        let winUtils = this.contentWindow.windowUtils;
        let returnStrings = { infoStrings: [], errorStrings: [] };
        if (contentRootElement) {
          this.setupDisplayportForElementSubtree(
            contentRootElement,
            winUtils,
            returnStrings
          );
        }
        return returnStrings;
      }

      case "SetupAsyncScrollOffsets": {
        let returns = { infoStrings: [], errorStrings: [], updatedAny: false };
        let contentRootElement = this.document.documentElement;

        if (!contentRootElement) {
          return returns;
        }

        let winUtils = this.contentWindow.windowUtils;

        returns.updatedAny = this.setupAsyncScrollOffsetsForElementSubtree(
          contentRootElement,
          winUtils,
          msg.data.allowFailure,
          returns
        );
        return returns;
      }
    }
    return undefined;
  }
}
