var EXPORTED_SYMBOLS = ["ReftestFissionChild"];

class ReftestFissionChild extends JSWindowActorChild {

  forwardAfterPaintEventToParent(rects, originalTargetUri, dispatchToSelfAsWell) {
    if (dispatchToSelfAsWell) {
      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.
        Cu.reportError(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;
        }

        // 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);
        let 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 (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) {
                  Cu.reportError(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;
      }

    }
  }
}
