import sanitize from './sanitize';
import hasUnicode from './has-unicode';
import cache from '../../core/base/cache';

/**
 * Determines if a given text node is an icon ligature
 *
 * @method isIconLigature
 * @memberof axe.commons.text
 * @instance
 * @param {VirtualNode} textVNode The virtual text node
 * @param {Number} occurrenceThreshold Number of times the font is encountered before auto-assigning the font as a ligature or not
 * @param {Number} differenceThreshold Percent of differences in pixel data or pixel width needed to determine if a font is a ligature font
 * @return {Boolean}
 */
export default function isIconLigature(
  textVNode,
  differenceThreshold = 0.15,
  occurrenceThreshold = 3
) {
  /**
   * Determine if the visible text is a ligature by comparing the
   * first letters image data to the entire strings image data.
   * If the two images are significantly different (typical set to 5%
   * statistical significance, but we'll be using a slightly higher value
   * of 15% to help keep the size of the canvas down) then we know the text
   * has been replaced by a ligature.
   *
   * Example:
   * If a text node was the string "File", looking at just the first
   * letter "F" would produce the following image:
   *
   * ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
   * │ │ │█│█│█│█│█│█│█│█│█│█│█│ │ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │ │█│█│█│█│█│█│█│█│█│█│█│ │ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │ │█│█│ │ │ │ │ │ │ │ │ │ │ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │ │█│█│ │ │ │ │ │ │ │ │ │ │ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │ │█│█│█│█│█│█│█│ │ │ │ │ │ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │ │█│█│█│█│█│█│█│ │ │ │ │ │ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │ │█│█│ │ │ │ │ │ │ │ │ │ │ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │ │█│█│ │ │ │ │ │ │ │ │ │ │ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │ │█│█│ │ │ │ │ │ │ │ │ │ │ │
   * └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
   *
   * But if the entire string "File" produced an image which had at least
   * a 15% difference in pixels, we would know that the string was replaced
   * by a ligature:
   *
   * ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
   * │ │█│█│█│█│█│█│█│█│█│█│ │ │ │ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │█│ │ │ │ │ │ │ │ │█│█│ │ │ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │█│ │█│█│█│█│█│█│ │█│ │█│ │ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │█│ │ │ │ │ │ │ │ │█│█│█│█│ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │█│ │█│█│█│█│█│█│ │ │ │ │█│ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │█│ │ │ │ │ │ │ │ │ │ │ │█│ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │█│ │█│█│█│█│█│█│█│█│█│ │█│ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │█│ │ │ │ │ │ │ │ │ │ │ │█│ │
   * ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
   * │ │█│█│█│█│█│█│█│█│█│█│█│█│█│ │
   * └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
   */
  const nodeValue = textVNode.actualNode.nodeValue.trim();

  // text with unicode or non-bmp letters cannot be ligature icons
  if (
    !sanitize(nodeValue) ||
    hasUnicode(nodeValue, { emoji: true, nonBmp: true })
  ) {
    return false;
  }

  const canvasContext = cache.get('canvasContext', () =>
    document
      .createElement('canvas')
      .getContext('2d', { willReadFrequently: true })
  );
  const canvas = canvasContext.canvas;

  // keep track of each font encountered and the number of times it shows up
  // as a ligature.
  const fonts = cache.get('fonts', () => ({}));
  const style = window.getComputedStyle(textVNode.parent.actualNode);
  const fontFamily = style.getPropertyValue('font-family');

  if (!fonts[fontFamily]) {
    fonts[fontFamily] = {
      occurrences: 0,
      numLigatures: 0
    };
  }
  const font = fonts[fontFamily];

  // improve the performance by only comparing the image data of a font a certain number of times
  // NOTE: This MIGHT cause an issue if someone uses an icon font to render actual text.
  //   We're leaving this as-is, unless someone reports a false positive over it.
  if (font.occurrences >= occurrenceThreshold) {
    // if the font has always been a ligature assume it's a ligature font
    if (font.numLigatures / font.occurrences === 1) {
      return true;
    }
    // inversely, if it's never been a ligature assume it's not a ligature font
    else if (font.numLigatures === 0) {
      return false;
    }

    // we could theoretically get into an odd middle ground scenario in which
    // the font family is being used as normal text sometimes and as icons
    // other times. in these cases we would need to always check the text
    // to know if it's an icon or not
  }
  font.occurrences++;

  // 30px was chosen to account for common ligatures in normal fonts
  // such as fi, ff, ffi. If a ligature would add a single column of
  // pixels to a 30x30 grid, it would not meet the statistical significance
  // threshold of 15% (30x30 = 900; 30/900 = 3.333%). this also allows for
  // more than 1 column differences (60/900 = 6.666%) and things like
  // extending the top of the f in the fi ligature.
  let fontSize = 30;
  let fontStyle = `${fontSize}px ${fontFamily}`;

  // set the size of the canvas to the width of the first letter
  canvasContext.font = fontStyle;
  const firstChar = nodeValue.charAt(0);
  let width = canvasContext.measureText(firstChar).width;

  // we already checked for typical zero-width unicode formatting characters further up,
  // so we assume that any remaining zero-width characters are part of an icon ligature
  // @see https://github.com/dequelabs/axe-core/issues/3918
  if (width === 0) {
    font.numLigatures++;
    return true;
  }

  // ensure font meets the 30px width requirement (30px font-size doesn't
  // necessarily mean its 30px wide when drawn)
  if (width < 30) {
    const diff = 30 / width;
    width *= diff;
    fontSize *= diff;
    fontStyle = `${fontSize}px ${fontFamily}`;
  }
  canvas.width = width;
  canvas.height = fontSize;

  // changing the dimensions of a canvas resets all properties (include font)
  // and clears it
  canvasContext.font = fontStyle;
  canvasContext.textAlign = 'left';
  canvasContext.textBaseline = 'top';
  canvasContext.fillText(firstChar, 0, 0);
  const compareData = new Uint32Array(
    canvasContext.getImageData(0, 0, width, fontSize).data.buffer
  );

  // if the font doesn't even have character data for a single char then
  // it has to be an icon ligature (e.g. Material Icon)
  if (!compareData.some(pixel => pixel)) {
    font.numLigatures++;
    return true;
  }

  canvasContext.clearRect(0, 0, width, fontSize);
  canvasContext.fillText(nodeValue, 0, 0);
  const compareWith = new Uint32Array(
    canvasContext.getImageData(0, 0, width, fontSize).data.buffer
  );

  // calculate the number of differences between the first letter and the
  // entire string, ignoring color differences
  const differences = compareData.reduce((diff, pixel, i) => {
    if (pixel === 0 && compareWith[i] === 0) {
      return diff;
    }
    if (pixel !== 0 && compareWith[i] !== 0) {
      return diff;
    }
    return ++diff;
  }, 0);

  // calculate the difference between the width of each character and the
  // combined with of all characters
  const expectedWidth = nodeValue.split('').reduce((totalWidth, char) => {
    return totalWidth + canvasContext.measureText(char).width;
  }, 0);
  const actualWidth = canvasContext.measureText(nodeValue).width;

  const pixelDifference = differences / compareData.length;
  const sizeDifference = 1 - actualWidth / expectedWidth;

  if (
    pixelDifference >= differenceThreshold &&
    sizeDifference >= differenceThreshold
  ) {
    font.numLigatures++;
    return true;
  }

  return false;
}
