/* 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 XHTML_NS = "http://www.w3.org/1999/xhtml";
const FONT_PREVIEW_TEXT = "Abc";
const FONT_PREVIEW_FONT_SIZE = 40;
const FONT_PREVIEW_FILLSTYLE = "black";
const FONT_PREVIEW_FONT_FALLBACK = "serif";
// Offset (in px) to avoid cutting off text edges of italic fonts.
const FONT_PREVIEW_OFFSET = 4;
// Factor used to resize the canvas in order to get better text quality.
const FONT_PREVIEW_OVERSAMPLING_FACTOR = 2;
const FONT_NEED_WRAPPING_QUOTES_REGEX = /^[^'"].* /;

/**
 * Helper function for getting an image preview of the given font.
 *
 * @param font {string}
 *        Name of font to preview
 * @param doc {Document}
 *        Document to use to render font
 * @param options {object}
 *        Object with options 'previewText' and 'previewFontSize'
 *
 * @return {object} An object with the following properties:
 *         - dataUrl {string}: The data URI of the font preview image
 *         - size {Number}: The optimal width of preview image
 *         - ctx {CanvasRenderingContext2D}: The canvas context (returned for tests)
 */
function getFontPreviewData(font, doc, options) {
  options = options || {};
  const previewText = options.previewText || FONT_PREVIEW_TEXT;
  const previewTextLines = previewText.split("\n");
  const previewFontSize = options.previewFontSize || FONT_PREVIEW_FONT_SIZE;
  const fillStyle = options.fillStyle || FONT_PREVIEW_FILLSTYLE;
  const fontStyle = options.fontStyle || "";
  const fontWeight = options.fontWeight || "";

  const canvas = doc.createElementNS(XHTML_NS, "canvas");
  const ctx = canvas.getContext("2d");

  // We want to wrap some font in quotes so font family like `Font Awesome 5 Brands` are
  // properly applied, but we don't want to wrap all fonts, otherwise generic family names
  // (e.g. `monospace`) wouldn't work.
  // It should be safe to only add the quotes when the font has some spaces (generic family
  // names don't have spaces, https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-family#generic-name)
  // We also don't want to add quotes if there are already some
  // `font` is the declaration value, so it can have multiple parts,
  // e.g: `"Menlo", MonoLisa, monospace`
  const fontParts = [];
  // We could use the parser to properly handle complex values, for example css variable,
  // but ideally this function would only receive computed values (see Bug 1952821).
  // If we'd get `var(--x)` here, we'd have to resolve it somehow, so it'd be simpler to
  // get the computed value directly.
  for (let f of font.split(",")) {
    if (FONT_NEED_WRAPPING_QUOTES_REGEX.test(f.trim())) {
      f = `"${f}"`;
    }
    fontParts.push(f);
  }
  // Add a fallback value
  fontParts.push(FONT_PREVIEW_FONT_FALLBACK);

  // Apply individual font properties to the canvas element so we can easily compute the
  // canvas context font
  // First, we need to start with a default shorthand to make it work
  canvas.style.font = `${FONT_PREVIEW_FONT_SIZE}px ${FONT_PREVIEW_FONT_FALLBACK}`;
  // Then we can set the different properties
  canvas.style.fontFamily = fontParts.join(", ");
  canvas.style.fontSize = `${previewFontSize}px`;
  if (fontWeight) {
    canvas.style.fontWeight = fontWeight;
  }
  if (fontStyle) {
    canvas.style.fontStyle = fontStyle;
  }

  const fontValue = canvas.style.font;

  // Get the correct preview text measurements and set the canvas dimensions
  ctx.font = fontValue;
  ctx.fillStyle = fillStyle;
  const previewTextLinesWidths = previewTextLines.map(
    previewTextLine => ctx.measureText(previewTextLine).width
  );
  const textWidth = Math.round(Math.max(...previewTextLinesWidths));

  // The canvas width is calculated as the width of the longest line plus
  // an offset at the left and right of it.
  // The canvas height is calculated as the font size multiplied by the
  // number of lines plus an offset at the top and bottom.
  //
  // In order to get better text quality, we oversample the canvas.
  // That means, after the width and height are calculated, we increase
  // both sizes by some factor.
  const simpleCanvasWidth = textWidth + FONT_PREVIEW_OFFSET * 2;
  canvas.width = simpleCanvasWidth * FONT_PREVIEW_OVERSAMPLING_FACTOR;
  canvas.height =
    (previewFontSize * previewTextLines.length + FONT_PREVIEW_OFFSET * 2) *
    FONT_PREVIEW_OVERSAMPLING_FACTOR;

  // we have to reset these after changing the canvas size
  ctx.font = fontValue;
  ctx.fillStyle = fillStyle;

  // Oversample the canvas for better text quality
  ctx.scale(FONT_PREVIEW_OVERSAMPLING_FACTOR, FONT_PREVIEW_OVERSAMPLING_FACTOR);

  ctx.textBaseline = "top";
  ctx.textAlign = "center";
  const horizontalTextPosition = simpleCanvasWidth / 2;
  let verticalTextPosition = FONT_PREVIEW_OFFSET;
  for (let i = 0; i < previewTextLines.length; i++) {
    ctx.fillText(
      previewTextLines[i],
      horizontalTextPosition,
      verticalTextPosition
    );

    // Move vertical text position one line down
    verticalTextPosition += previewFontSize;
  }

  const dataURL = canvas.toDataURL("image/png");

  return {
    dataURL,
    size: textWidth + FONT_PREVIEW_OFFSET * 2,
    ctx,
  };
}

exports.getFontPreviewData = getFontPreviewData;

/**
 * Get the text content of a rule given some CSS text, a line and a column
 * Consider the following example:
 *
 * ```css
 * body {
 *  color: red;
 * }
 * p {
 *  line-height: 2em;
 *  color: blue;
 * }
 * ```
 *
 * Calling the function with the whole text above and `line=4` and `column=1` would
 * return `line-height: 2em; color: blue;`
 *
 * @param {string} initialText
 * @param {number} line (1-indexed)
 * @param {number} column (1-indexed)
 * @return {object} An object of the form {offset: number, text: string}
 *                  The offset is the index into the input string where
 *                  the rule text started.  The text is the content of
 *                  the rule.
 */
function getRuleText(initialText, line, column) {
  if (typeof line === "undefined" || typeof column === "undefined") {
    throw new Error("Location information is missing");
  }

  const { text } = getTextAtLineColumn(initialText, line, column);
  const res = InspectorUtils.getRuleBodyText(text);
  if (res === null || typeof res === "undefined") {
    throw new Error("Couldn't find rule");
  }
  return res;
}

exports.getRuleText = getRuleText;

/**
 * Return the offset and substring of |text| that starts at the given
 * line and column.
 *
 * @param {string} text
 * @param {number} line (1-indexed)
 * @param {number} column (1-indexed)
 * @return {object} An object of the form {offset: number, text: string},
 *                  where the offset is the offset into the input string
 *                  where the text starts, and where text is the text.
 */
function getTextAtLineColumn(text, line, column) {
  let offset;
  if (line > 1) {
    const rx = new RegExp(
      "(?:[^\\r\\n\\f]*(?:\\r\\n|\\n|\\r|\\f)){" + (line - 1) + "}"
    );
    offset = rx.exec(text)[0].length;
  } else {
    offset = 0;
  }
  offset += column - 1;
  return { offset, text: text.substr(offset) };
}

exports.getTextAtLineColumn = getTextAtLineColumn;
