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

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
});

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "canvasMaxArea",
  "gfx.canvas.max-area"
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "canvasMaxSize",
  "gfx.canvas.max-size"
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "compositorReadback",
  "remote.screenshot.use_readback",
  false
);

const CONTEXT_2D = "2d";
const BG_COLOUR = "rgb(255,255,255)";
const XHTML_NS = "http://www.w3.org/1999/xhtml";

/**
 * Provides primitives to capture screenshots.
 *
 * @namespace
 */
export const capture = {};

capture.Format = {
  Base64: 0,
  Hash: 1,
};

/**
 * Draw a rectangle off the framebuffer.
 *
 * @param {DOMWindow} win
 *     The DOM window used for the framebuffer, and providing the interfaces
 *     for creating an HTMLCanvasElement.
 * @param {BrowsingContext} browsingContext
 *     The BrowsingContext from which the snapshot should be taken.
 * @param {number} left
 *     The left, X axis offset of the rectangle.
 * @param {number} top
 *     The top, Y axis offset of the rectangle.
 * @param {number} width
 *     The width dimension of the rectangle to paint.
 * @param {number} height
 *     The height dimension of the rectangle to paint.
 * @param {object=} options
 * @param {HTMLCanvasElement=} options.canvas
 *     Optional canvas to reuse for the screenshot.
 * @param {number=} options.flags
 *     Optional integer representing flags to pass to drawWindow; these
 *     are defined on CanvasRenderingContext2D.
 * @param {number=} options.dX
 *     Horizontal offset between the browser window and content area. Defaults to 0.
 * @param {number=} options.dY
 *     Vertical offset between the browser window and content area. Defaults to 0.
 * @param {boolean=} options.readback
 *     If true, read back a snapshot of the pixel data currently in the
 *     compositor/window. Defaults to false, unless the
 *     `remote.screenshot.use_readback` preference is set.
 * @param {boolean=} options.drawView
 *     If true, the rectangle is relative to the visible viewport rather than to
 *     the page, and view level rendering such as the root scrollbars is
 *     included. Ignored when reading back. Defaults to false.
 *
 * @returns {HTMLCanvasElement}
 *     The canvas on which the selection from the window's framebuffer
 *     has been painted on.
 */
capture.canvas = async function (
  win,
  browsingContext,
  left,
  top,
  width,
  height,
  {
    canvas = null,
    flags = null,
    dX = 0,
    dY = 0,
    readback = false,
    drawView = false,
  } = {}
) {
  if (lazy.compositorReadback && !readback) {
    // Readback can only return the content area composited on screen, and its
    // coordinates are relative to the chrome window rather than the document,
    // so any requested region degrades to the whole content area.
    const browser = browsingContext.top.embedderElement;
    if (browser) {
      readback = true;
      ({ left, top, width, height } = browser.getBoundingClientRect());
    }
  }

  // FIXME(bug 1761032): This looks a bit sketchy, overrideDPPX doesn't
  // influence rendering...
  const scale = browsingContext.overrideDPPX || win.devicePixelRatio;

  const canvasHeight = height * scale;
  const canvasWidth = width * scale;
  const canvasArea = canvasWidth * canvasHeight;

  if (canvasWidth > lazy.canvasMaxSize) {
    throw new lazy.error.UnsupportedOperationError(
      `${canvasWidth} exceeds the maximum allowed screenshot width of ${lazy.canvasMaxSize} pixels`
    );
  }

  if (canvasHeight > lazy.canvasMaxSize) {
    throw new lazy.error.UnsupportedOperationError(
      `${canvasHeight} exceeds the maximum allowed screenshot height of ${lazy.canvasMaxSize} pixels`
    );
  }

  if (canvasArea > lazy.canvasMaxArea) {
    throw new lazy.error.UnsupportedOperationError(
      `${canvasArea} exceeds the maximum allowed screenshot area of ${lazy.canvasMaxArea} square pixels`
    );
  }

  try {
    if (canvas === null) {
      canvas = win.document.createElementNS(XHTML_NS, "canvas");
      canvas.width = canvasWidth;
      canvas.height = canvasHeight;
    }

    const ctx = canvas.getContext(CONTEXT_2D);

    if (readback) {
      if (flags === null) {
        flags =
          ctx.DRAWWINDOW_DRAW_CARET |
          ctx.DRAWWINDOW_DRAW_VIEW |
          ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
      }

      // drawWindow doesn't take scaling into account.
      ctx.scale(scale, scale);
      ctx.drawWindow(win, left + dX, top + dY, width, height, BG_COLOUR, flags);
    } else {
      let rect = new DOMRect(left, top, width, height);
      let snapshot = await browsingContext.currentWindowGlobal.drawSnapshot(
        rect,
        scale,
        BG_COLOUR,
        { drawView }
      );

      ctx.drawImage(snapshot, 0, 0);

      // Bug 1574935 - Huge dimensions can trigger an OOM because multiple copies
      // of the bitmap will exist in memory. Force the removal of the snapshot
      // because it is no longer needed.
      snapshot.close();
    }
  } catch (error) {
    throw new lazy.error.UnknownError(
      `Unable to capture screenshot: ${error.message}`
    );
  }
  return canvas;
};

/**
 * Encode the contents of an HTMLCanvasElement to a Base64 encoded string.
 *
 * @param {HTMLCanvasElement} canvas
 *     The canvas to encode.
 * @param {string} type
 *     The image format to output such as `image/png`.
 * @param {number} encoderOptions
 *     A number between 0 and 1 representing the image quality. Defaults to `1.0`.
 *
 * @returns {string}
 *     A Base64 encoded string.
 */
capture.toBase64 = function (canvas, type, encoderOptions = 1.0) {
  let u = canvas.toDataURL(type, encoderOptions);
  return u.substring(u.indexOf(",") + 1);
};

/**
 * Hash the contents of an HTMLCanvasElement to a SHA-256 hex digest.
 *
 * @param {HTMLCanvasElement} canvas
 *     The canvas to encode.
 *
 * @returns {string}
 *     A hex digest of the SHA-256 hash of the base64 encoded string.
 */
capture.toHash = function (canvas) {
  let u = capture.toBase64(canvas, "image/png");
  let buffer = new TextEncoder().encode(u);
  return crypto.subtle.digest("SHA-256", buffer).then(hash => hex(hash));
};

/**
 * Convert buffer into to hex.
 *
 * @param {ArrayBuffer} buffer
 *     The buffer containing the data to convert to hex.
 *
 * @returns {string}
 *     A hex digest of the input buffer.
 */
function hex(buffer) {
  let hexCodes = [];
  let view = new DataView(buffer);
  for (let i = 0; i < view.byteLength; i += 4) {
    let value = view.getUint32(i);
    let stringValue = value.toString(16);
    let padding = "00000000";
    let paddedValue = (padding + stringValue).slice(-padding.length);
    hexCodes.push(paddedValue);
  }
  return hexCodes.join("");
}
