import { Canvas, CanvasRenderingContext2D, createCanvas, Image, loadImage } from "canvas";
import { decompressFrames, parseGIF } from "./gifuct-js";
import { QRCodeModel, QRErrorCorrectLevel, QRUtil } from "./qrcode";
import GIFEncoder from "./gif.js/GIFEncoder";

const defaultScale = 0.4;

export type ComponentOptions = {
  /**
   * Component options for data/ECC.
   */
  data?: {
    /**
     * Scale factor for data/ECC dots.
     * @default 1
     */
    scale?: number;
  };

  /**
   * Component options for timing patterns.
   */
  timing?: {
    /**
     * Scale factor for timing patterns.
     * @default 1
     */
    scale?: number;

    /**
     * Protector for timing patterns.
     * @default false
     */
    protectors?: boolean;
  };

  /**
   * Component options for alignment patterns.
   */
  alignment?: {
    /**
     * Scale factor for alignment patterns.
     * @default 1
     */
    scale?: number;

    /**
     * Protector for alignment patterns.
     * @default false
     */
    protectors?: boolean;
  };

  /**
   * Component options for alignment pattern on the bottom-right corner.
   */
  cornerAlignment?: {
    /**
     * Scale factor for alignment pattern on the bottom-right corner.
     * @default 1
     */
    scale?: number;

    /**
     * Protector for alignment pattern on the bottom-right corner.
     * @default true
     */
    protectors?: boolean;
  };
};

export type Options = {
  /**
   * Text to be encoded in the QR code.
   */
  text: string;

  /**
   * Size of the QR code in pixel.
   *
   * @defaultValue 400
   */
  size?: number;

  /**
   * Size of margins around the QR code body in pixel.
   *
   * @defaultValue 20
   */
  margin?: number;

  /**
   * Error correction level of the QR code.
   *
   * Accepts a value provided by _QRErrorCorrectLevel_.
   *
   * For more information, please refer to [https://www.qrcode.com/en/about/error_correction.html](https://www.qrcode.com/en/about/error_correction.html).
   *
   * @defaultValue 0
   */
  correctLevel?: number;

  /**
   * **This is an advanced option.**
   *
   * Specify the mask pattern to be used in QR code encoding.
   *
   * Accepts a value provided by _QRMaskPattern_.
   *
   * To find out all eight mask patterns, please refer to [https://en.wikipedia.org/wiki/File:QR_Code_Mask_Patterns.svg](https://en.wikipedia.org/wiki/File:QR_Code_Mask_Patterns.svg)
   *
   * For more information, please refer to [https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders#Masking](https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders#Masking).
   */
  maskPattern?: number;

  /**
   * **This is an advanced option.**
   *
   * Specify the version to be used in QR code encoding.
   *
   * Accepts an integer in range [1, 40].
   *
   * For more information, please refer to [https://www.qrcode.com/en/about/version.html](https://www.qrcode.com/en/about/version.html).
   */
  version?: number;

  /**
   * Options to control components in the QR code.
   *
   * @deafultValue undefined
   */
  components?: ComponentOptions;

  /**
   * Color of the blocks on the QR code.
   *
   * Accepts a CSS &lt;color&gt;.
   *
   * For more information about CSS &lt;color&gt;, please refer to [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
   *
   * @defaultValue "#000000"
   */
  colorDark?: string;

  /**
   * Color of the empty areas on the QR code.
   *
   * Accepts a CSS &lt;color&gt;.
   *
   * For more information about CSS &lt;color&gt;, please refer to [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
   *
   * @defaultValue "#ffffff"
   */
  colorLight?: string;

  /**
   * Automatically calculate the _colorLight_ value from the QR code's background.
   *
   * @defaultValue true
   */
  autoColor?: boolean;

  /**
   * Background image to be used in the QR code.
   *
   * Accepts a `data:` string in web browsers or a Buffer in Node.js.
   *
   * @defaultValue undefined
   */
  backgroundImage?: string | Buffer;

  /**
   * Color of the dimming mask above the background image.
   *
   * Accepts a CSS &lt;color&gt;.
   *
   * For more information about CSS &lt;color&gt;, please refer to [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
   *
   * @defaultValue "rgba(0, 0, 0, 0)"
   */
  backgroundDimming?: string;

  /**
   * GIF background image to be used in the QR code.
   *
   * @defaultValue undefined
   */
  gifBackground?: ArrayBuffer;

  /**
   * Use a white margin instead of a transparent one which reveals the background of the QR code on margins.
   *
   * @defaultValue true
   */
  whiteMargin?: boolean;

  /**
   * Logo image to be displayed at the center of the QR code.
   *
   * Accepts a `data:` string in web browsers or a Buffer in Node.js.
   *
   * When set to `undefined` or `null`, the logo is disabled.
   *
   * @defaultValue undefined
   */
  logoImage?: string | Buffer;

  /**
   * Ratio of the logo size to the QR code size.
   *
   * @defaultValue 0.2
   */
  logoScale?: number;

  /**
   * Size of margins around the logo image in pixels.
   *
   * @defaultValue 6
   */
  logoMargin?: number;

  /**
   * Corner radius of the logo image in pixels.
   *
   * @defaultValue 8
   */
  logoCornerRadius?: number;

  /**
   * @deprecated
   *
   * Ratio of the real size to the full size of the blocks.
   *
   * This can be helpful when you want to make more parts of the background visible.
   *
   * @deafultValue 0.4
   */
  dotScale?: number;
};

export class AwesomeQR {
  private canvas: Canvas;
  private canvasContext: CanvasRenderingContext2D;
  private qrCode?: QRCodeModel;
  private options: Options;

  static CorrectLevel = QRErrorCorrectLevel;

  private static defaultComponentOptions: ComponentOptions = {
    data: {
      scale: 1,
    },
    timing: {
      scale: 1,
      protectors: false,
    },
    alignment: {
      scale: 1,
      protectors: false,
    },
    cornerAlignment: {
      scale: 1,
      protectors: true,
    },
  };

  static defaultOptions: Options = {
    text: "",
    size: 400,
    margin: 20,
    colorDark: "#000000",
    colorLight: "#ffffff",
    correctLevel: QRErrorCorrectLevel.M,
    backgroundImage: undefined,
    backgroundDimming: "rgba(0,0,0,0)",
    logoImage: undefined,
    logoScale: 0.2,
    logoMargin: 4,
    logoCornerRadius: 8,
    whiteMargin: true,
    components: AwesomeQR.defaultComponentOptions,
    autoColor: true,
  };

  constructor(options: Partial<Options>) {
    const _options = Object.assign({}, options);

    (Object.keys(AwesomeQR.defaultOptions) as (keyof Options)[]).forEach((k) => {
      if (!(k in _options)) {
        Object.defineProperty(_options, k, { value: AwesomeQR.defaultOptions[k], enumerable: true, writable: true });
      }
    });

    if (!_options.components) {
      _options.components = AwesomeQR.defaultComponentOptions;
    } else if (typeof _options.components === "object") {
      (Object.keys(AwesomeQR.defaultComponentOptions) as (keyof ComponentOptions)[]).forEach((k) => {
        if (!(k in _options.components!)) {
          Object.defineProperty(_options.components!, k, {
            value: AwesomeQR.defaultComponentOptions[k],
            enumerable: true,
            writable: true,
          });
        } else {
          Object.defineProperty(_options.components!, k, {
            value: { ...AwesomeQR.defaultComponentOptions[k], ..._options.components![k] },
            enumerable: true,
            writable: true,
          });
        }
      });
    }

    if (_options.dotScale !== null && _options.dotScale !== undefined) {
      if (_options.dotScale! <= 0 || _options.dotScale! > 1) {
        throw new Error("dotScale should be in range (0, 1].");
      }
      _options.components.data!.scale = _options.dotScale;
      _options.components.timing!.scale = _options.dotScale;
      _options.components.alignment!.scale = _options.dotScale;
    }

    this.options = _options as Options;
    this.canvas = createCanvas(options.size!, options.size!);
    this.canvasContext = this.canvas.getContext("2d")!;
    this.qrCode = new QRCodeModel(-1, this.options.correctLevel!);
    if (Number.isInteger(this.options.maskPattern)) {
      this.qrCode.maskPattern = this.options.maskPattern!;
    }
    if (Number.isInteger(this.options.version)) {
      this.qrCode.typeNumber = this.options.version!;
    }
    this.qrCode.addData(this.options.text);
    this.qrCode.make();
  }

  draw(): Promise<Buffer | ArrayBuffer | string | undefined> {
    return new Promise((resolve) => this._draw().then(resolve));
  }

  private _clear() {
    this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
  }

  private static _prepareRoundedCornerClip(
    canvasContext: CanvasRenderingContext2D,
    x: number,
    y: number,
    w: number,
    h: number,
    r: number
  ) {
    canvasContext.beginPath();
    canvasContext.moveTo(x, y);
    canvasContext.arcTo(x + w, y, x + w, y + h, r);
    canvasContext.arcTo(x + w, y + h, x, y + h, r);
    canvasContext.arcTo(x, y + h, x, y, r);
    canvasContext.arcTo(x, y, x + w, y, r);
    canvasContext.closePath();
  }

  private static _getAverageRGB(image: Image) {
    const blockSize = 5;
    const defaultRGB = {
      r: 0,
      g: 0,
      b: 0,
    };
    let width, height;
    let i = -4;
    const rgb = {
      r: 0,
      g: 0,
      b: 0,
    };
    let count = 0;

    height = image.naturalHeight || image.height;
    width = image.naturalWidth || image.width;

    const canvas = createCanvas(width, height);
    const context = canvas.getContext("2d");

    if (!context) {
      return defaultRGB;
    }

    context.drawImage(image, 0, 0);

    let data;
    try {
      data = context.getImageData(0, 0, width, height);
    } catch (e) {
      return defaultRGB;
    }

    while ((i += blockSize * 4) < data.data.length) {
      if (data.data[i] > 200 || data.data[i + 1] > 200 || data.data[i + 2] > 200) continue;
      ++count;
      rgb.r += data.data[i];
      rgb.g += data.data[i + 1];
      rgb.b += data.data[i + 2];
    }

    rgb.r = ~~(rgb.r / count);
    rgb.g = ~~(rgb.g / count);
    rgb.b = ~~(rgb.b / count);

    return rgb;
  }

  private static _drawDot(
    canvasContext: CanvasRenderingContext2D,
    centerX: number,
    centerY: number,
    nSize: number,
    xyOffset: number = 0,
    dotScale: number = 1
  ) {
    canvasContext.fillRect(
      (centerX + xyOffset) * nSize,
      (centerY + xyOffset) * nSize,
      dotScale * nSize,
      dotScale * nSize
    );
  }

  private static _drawAlignProtector(
    canvasContext: CanvasRenderingContext2D,
    centerX: number,
    centerY: number,
    nSize: number
  ) {
    canvasContext.clearRect((centerX - 2) * nSize, (centerY - 2) * nSize, 5 * nSize, 5 * nSize);
    canvasContext.fillRect((centerX - 2) * nSize, (centerY - 2) * nSize, 5 * nSize, 5 * nSize);
  }

  private static _drawAlign(
    canvasContext: CanvasRenderingContext2D,
    centerX: number,
    centerY: number,
    nSize: number,
    xyOffset: number = 0,
    dotScale: number = 1,
    colorDark: string,
    hasProtector: boolean
  ) {
    const oldFillStyle = canvasContext.fillStyle;
    canvasContext.fillStyle = colorDark;
    new Array(4).fill(0).map((_, i) => {
      AwesomeQR._drawDot(canvasContext, centerX - 2 + i, centerY - 2, nSize, xyOffset, dotScale);
      AwesomeQR._drawDot(canvasContext, centerX + 2, centerY - 2 + i, nSize, xyOffset, dotScale);
      AwesomeQR._drawDot(canvasContext, centerX + 2 - i, centerY + 2, nSize, xyOffset, dotScale);
      AwesomeQR._drawDot(canvasContext, centerX - 2, centerY + 2 - i, nSize, xyOffset, dotScale);
    });
    AwesomeQR._drawDot(canvasContext, centerX, centerY, nSize, xyOffset, dotScale);
    if (!hasProtector) {
      canvasContext.fillStyle = "rgba(255, 255, 255, 0.6)";
      new Array(2).fill(0).map((_, i) => {
        AwesomeQR._drawDot(canvasContext, centerX - 1 + i, centerY - 1, nSize, xyOffset, dotScale);
        AwesomeQR._drawDot(canvasContext, centerX + 1, centerY - 1 + i, nSize, xyOffset, dotScale);
        AwesomeQR._drawDot(canvasContext, centerX + 1 - i, centerY + 1, nSize, xyOffset, dotScale);
        AwesomeQR._drawDot(canvasContext, centerX - 1, centerY + 1 - i, nSize, xyOffset, dotScale);
      });
    }
    canvasContext.fillStyle = oldFillStyle;
  }

  private async _draw(): Promise<Buffer | Uint8Array | string | undefined> {
    const nCount = this.qrCode?.moduleCount!;
    const rawSize = this.options.size!;
    let rawMargin = this.options.margin!;

    if (rawMargin < 0 || rawMargin * 2 >= rawSize) {
      rawMargin = 0;
    }

    const margin = Math.ceil(rawMargin);
    const rawViewportSize = rawSize - 2 * rawMargin;

    const whiteMargin = this.options.whiteMargin!;
    const backgroundDimming = this.options.backgroundDimming!;
    const nSize = Math.ceil(rawViewportSize / nCount);
    const viewportSize = nSize * nCount;
    const size = viewportSize + 2 * margin;

    const mainCanvas = createCanvas(size, size);
    const mainCanvasContext = mainCanvas.getContext("2d");

    this._clear();

    // Translate to make the top and left margins off the viewport
    mainCanvasContext.save();
    mainCanvasContext.translate(margin, margin);

    const backgroundCanvas = createCanvas(size, size);
    const backgroundCanvasContext = backgroundCanvas.getContext("2d");

    let parsedGIFBackground = null;
    let gifFrames: any[] = [];

    if (!!this.options.gifBackground) {
      const gif = parseGIF(this.options.gifBackground);
      parsedGIFBackground = gif;
      gifFrames = decompressFrames(gif, true);
      if (this.options.autoColor) {
        let r = 0,
          g = 0,
          b = 0;
        let count = 0;
        for (let i = 0; i < gifFrames[0].colorTable.length; i++) {
          const c = gifFrames[0].colorTable[i];
          if (c[0] > 200 || c[1] > 200 || c[2] > 200) continue;
          if (c[0] === 0 && c[1] === 0 && c[2] === 0) continue;
          count++;
          r += c[0];
          g += c[1];
          b += c[2];
        }

        r = ~~(r / count);
        g = ~~(g / count);
        b = ~~(b / count);
        this.options.colorDark = `rgb(${r},${g},${b})`;
      }
    } else if (!!this.options.backgroundImage) {
      const backgroundImage = await loadImage(this.options.backgroundImage!);

      if (this.options.autoColor) {
        const avgRGB = AwesomeQR._getAverageRGB(backgroundImage);
        this.options.colorDark = `rgb(${avgRGB.r},${avgRGB.g},${avgRGB.b})`;
      }

      backgroundCanvasContext.drawImage(
        backgroundImage,
        0,
        0,
        backgroundImage.width,
        backgroundImage.height,
        0,
        0,
        size,
        size
      );
      backgroundCanvasContext.rect(0, 0, size, size);
      backgroundCanvasContext.fillStyle = backgroundDimming;
      backgroundCanvasContext.fill();
    } else {
      backgroundCanvasContext.rect(0, 0, size, size);
      backgroundCanvasContext.fillStyle = this.options.colorLight!;
      backgroundCanvasContext.fill();
    }

    const alignmentPatternCenters = QRUtil.getPatternPosition(this.qrCode!.typeNumber);

    const dataScale = this.options.components?.data?.scale || defaultScale;
    const dataXyOffset = (1 - dataScale) * 0.5;
    for (let row = 0; row < nCount; row++) {
      for (let col = 0; col < nCount; col++) {
        const bIsDark = this.qrCode!.isDark(row, col);
        const isBlkPosCtr = (col < 8 && (row < 8 || row >= nCount - 8)) || (col >= nCount - 8 && row < 8);
        const isTiming = (row == 6 && col >= 8 && col <= nCount - 8) || (col == 6 && row >= 8 && row <= nCount - 8);
        let isProtected = isBlkPosCtr || isTiming;

        for (let i = 1; i < alignmentPatternCenters.length - 1; i++) {
          isProtected =
            isProtected ||
            (row >= alignmentPatternCenters[i] - 2 &&
              row <= alignmentPatternCenters[i] + 2 &&
              col >= alignmentPatternCenters[i] - 2 &&
              col <= alignmentPatternCenters[i] + 2);
        }

        const nLeft = col * nSize + (isProtected ? 0 : dataXyOffset * nSize);
        const nTop = row * nSize + (isProtected ? 0 : dataXyOffset * nSize);
        mainCanvasContext.strokeStyle = bIsDark ? this.options.colorDark! : this.options.colorLight!;
        mainCanvasContext.lineWidth = 0.5;
        mainCanvasContext.fillStyle = bIsDark ? this.options.colorDark! : "rgba(255, 255, 255, 0.6)";
        if (alignmentPatternCenters.length === 0) {
          if (!isProtected) {
            mainCanvasContext.fillRect(
              nLeft,
              nTop,
              (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize,
              (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize
            );
          }
        } else {
          const inAgnRange = col < nCount - 4 && col >= nCount - 4 - 5 && row < nCount - 4 && row >= nCount - 4 - 5;
          if (!isProtected && !inAgnRange) {
            // if align pattern list is empty, then it means that we don't need to leave room for the align patterns
            mainCanvasContext.fillRect(
              nLeft,
              nTop,
              (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize,
              (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize
            );
          }
        }
      }
    }

    const cornerAlignmentCenter = alignmentPatternCenters[alignmentPatternCenters.length - 1];

    // - PROTECTORS
    const protectorStyle = "rgba(255, 255, 255, 0.6)";

    // - FINDER PROTECTORS
    mainCanvasContext.fillStyle = protectorStyle;
    mainCanvasContext.fillRect(0, 0, 8 * nSize, 8 * nSize);
    mainCanvasContext.fillRect(0, (nCount - 8) * nSize, 8 * nSize, 8 * nSize);
    mainCanvasContext.fillRect((nCount - 8) * nSize, 0, 8 * nSize, 8 * nSize);

    // - TIMING PROTECTORS
    if (this.options.components?.timing?.protectors) {
      mainCanvasContext.fillRect(8 * nSize, 6 * nSize, (nCount - 8 - 8) * nSize, nSize);
      mainCanvasContext.fillRect(6 * nSize, 8 * nSize, nSize, (nCount - 8 - 8) * nSize);
    }

    // - CORNER ALIGNMENT PROTECTORS
    if (this.options.components?.cornerAlignment?.protectors) {
      AwesomeQR._drawAlignProtector(mainCanvasContext, cornerAlignmentCenter, cornerAlignmentCenter, nSize);
    }

    // - ALIGNMENT PROTECTORS
    if (this.options.components?.alignment?.protectors) {
      for (let i = 0; i < alignmentPatternCenters.length; i++) {
        for (let j = 0; j < alignmentPatternCenters.length; j++) {
          const agnX = alignmentPatternCenters[j];
          const agnY = alignmentPatternCenters[i];
          if (agnX === 6 && (agnY === 6 || agnY === cornerAlignmentCenter)) {
            continue;
          } else if (agnY === 6 && (agnX === 6 || agnX === cornerAlignmentCenter)) {
            continue;
          } else if (agnX === cornerAlignmentCenter && agnY === cornerAlignmentCenter) {
            continue;
          } else {
            AwesomeQR._drawAlignProtector(mainCanvasContext, agnX, agnY, nSize);
          }
        }
      }
    }

    // - FINDER
    mainCanvasContext.fillStyle = this.options.colorDark!;
    mainCanvasContext.fillRect(0, 0, 7 * nSize, nSize);
    mainCanvasContext.fillRect((nCount - 7) * nSize, 0, 7 * nSize, nSize);
    mainCanvasContext.fillRect(0, 6 * nSize, 7 * nSize, nSize);
    mainCanvasContext.fillRect((nCount - 7) * nSize, 6 * nSize, 7 * nSize, nSize);
    mainCanvasContext.fillRect(0, (nCount - 7) * nSize, 7 * nSize, nSize);
    mainCanvasContext.fillRect(0, (nCount - 7 + 6) * nSize, 7 * nSize, nSize);
    mainCanvasContext.fillRect(0, 0, nSize, 7 * nSize);
    mainCanvasContext.fillRect(6 * nSize, 0, nSize, 7 * nSize);
    mainCanvasContext.fillRect((nCount - 7) * nSize, 0, nSize, 7 * nSize);
    mainCanvasContext.fillRect((nCount - 7 + 6) * nSize, 0, nSize, 7 * nSize);
    mainCanvasContext.fillRect(0, (nCount - 7) * nSize, nSize, 7 * nSize);
    mainCanvasContext.fillRect(6 * nSize, (nCount - 7) * nSize, nSize, 7 * nSize);

    mainCanvasContext.fillRect(2 * nSize, 2 * nSize, 3 * nSize, 3 * nSize);
    mainCanvasContext.fillRect((nCount - 7 + 2) * nSize, 2 * nSize, 3 * nSize, 3 * nSize);
    mainCanvasContext.fillRect(2 * nSize, (nCount - 7 + 2) * nSize, 3 * nSize, 3 * nSize);

    // - TIMING
    const timingScale = this.options.components?.timing?.scale || defaultScale;
    const timingXyOffset = (1 - timingScale) * 0.5;
    for (let i = 0; i < nCount - 8; i += 2) {
      AwesomeQR._drawDot(mainCanvasContext, 8 + i, 6, nSize, timingXyOffset, timingScale);
      AwesomeQR._drawDot(mainCanvasContext, 6, 8 + i, nSize, timingXyOffset, timingScale);
    }

    // - CORNER ALIGNMENT PROTECTORS
    const cornerAlignmentScale = this.options.components?.cornerAlignment?.scale || defaultScale;
    const cornerAlignmentXyOffset = (1 - cornerAlignmentScale) * 0.5;
    AwesomeQR._drawAlign(
      mainCanvasContext,
      cornerAlignmentCenter,
      cornerAlignmentCenter,
      nSize,
      cornerAlignmentXyOffset,
      cornerAlignmentScale,
      this.options.colorDark!,
      this.options.components?.cornerAlignment?.protectors || false
    );

    // - ALIGNEMNT
    const alignmentScale = this.options.components?.alignment?.scale || defaultScale;
    const alignmentXyOffset = (1 - alignmentScale) * 0.5;
    for (let i = 0; i < alignmentPatternCenters.length; i++) {
      for (let j = 0; j < alignmentPatternCenters.length; j++) {
        const agnX = alignmentPatternCenters[j];
        const agnY = alignmentPatternCenters[i];
        if (agnX === 6 && (agnY === 6 || agnY === cornerAlignmentCenter)) {
          continue;
        } else if (agnY === 6 && (agnX === 6 || agnX === cornerAlignmentCenter)) {
          continue;
        } else if (agnX === cornerAlignmentCenter && agnY === cornerAlignmentCenter) {
          continue;
        } else {
          AwesomeQR._drawAlign(
            mainCanvasContext,
            agnX,
            agnY,
            nSize,
            alignmentXyOffset,
            alignmentScale,
            this.options.colorDark!,
            this.options.components?.alignment?.protectors || false
          );
        }
      }
    }

    // Fill the margin
    if (whiteMargin) {
      mainCanvasContext.fillStyle = "#FFFFFF";
      mainCanvasContext.fillRect(-margin, -margin, size, margin);
      mainCanvasContext.fillRect(-margin, viewportSize, size, margin);
      mainCanvasContext.fillRect(viewportSize, -margin, margin, size);
      mainCanvasContext.fillRect(-margin, -margin, margin, size);
    }

    if (!!this.options.logoImage) {
      const logoImage = await loadImage(this.options.logoImage!);

      let logoScale = this.options.logoScale!;
      let logoMargin = this.options.logoMargin!;
      let logoCornerRadius = this.options.logoCornerRadius!;
      if (logoScale <= 0 || logoScale >= 1.0) {
        logoScale = 0.2;
      }
      if (logoMargin < 0) {
        logoMargin = 0;
      }
      if (logoCornerRadius < 0) {
        logoCornerRadius = 0;
      }

      const logoSize = viewportSize * logoScale;
      const x = 0.5 * (size - logoSize);
      const y = x;

      // Restore the canvas
      // After restoring, the top and left margins should be taken into account
      mainCanvasContext.restore();

      // Clean the area that the logo covers (including margins)
      mainCanvasContext.fillStyle = "#FFFFFF";
      mainCanvasContext.save();
      AwesomeQR._prepareRoundedCornerClip(
        mainCanvasContext,
        x - logoMargin,
        y - logoMargin,
        logoSize + 2 * logoMargin,
        logoSize + 2 * logoMargin,
        logoCornerRadius + logoMargin
      );
      mainCanvasContext.clip();
      const oldGlobalCompositeOperation = mainCanvasContext.globalCompositeOperation;
      mainCanvasContext.globalCompositeOperation = "destination-out";
      mainCanvasContext.fill();
      mainCanvasContext.globalCompositeOperation = oldGlobalCompositeOperation;
      mainCanvasContext.restore();

      // Draw logo image
      mainCanvasContext.save();
      AwesomeQR._prepareRoundedCornerClip(mainCanvasContext, x, y, logoSize, logoSize, logoCornerRadius);
      mainCanvasContext.clip();
      mainCanvasContext.drawImage(logoImage, x, y, logoSize, logoSize);
      mainCanvasContext.restore();

      // Re-translate the canvas to translate the top and left margins into invisible area
      mainCanvasContext.save();
      mainCanvasContext.translate(margin, margin);
    }

    if (!!parsedGIFBackground) {
      let gifOutput: any;

      // Reuse in order to apply the patch
      let backgroundCanvas: Canvas;
      let backgroundCanvasContext: CanvasRenderingContext2D;

      let patchCanvas: Canvas;
      let patchCanvasContext: CanvasRenderingContext2D;
      let patchData: any;

      gifFrames.forEach(function (frame: any) {
        if (!gifOutput) {
          gifOutput = new GIFEncoder(rawSize, rawSize);
          gifOutput.setDelay(frame.delay);
          gifOutput.setRepeat(0);
        }

        const { width, height } = frame.dims;
        if (!backgroundCanvas) {
          backgroundCanvas = createCanvas(width, height);
          backgroundCanvasContext = backgroundCanvas.getContext("2d");
          backgroundCanvasContext.rect(0, 0, backgroundCanvas.width, backgroundCanvas.height);
          backgroundCanvasContext.fillStyle = "#ffffff";
          backgroundCanvasContext.fill();
        }

        if (!patchCanvas || !patchData || width !== patchCanvas.width || height !== patchCanvas.height) {
          patchCanvas = createCanvas(width, height);
          patchCanvasContext = patchCanvas.getContext("2d");
          patchData = patchCanvasContext.createImageData(width, height);
        }

        patchData.data.set(frame.patch);
        patchCanvasContext.putImageData(patchData, 0, 0);

        backgroundCanvasContext.drawImage(patchCanvas, frame.dims.left, frame.dims.top);

        const unscaledCanvas = createCanvas(size, size);
        const unscaledCanvasContext = unscaledCanvas.getContext("2d");

        unscaledCanvasContext.drawImage(backgroundCanvas, 0, 0, size, size);
        unscaledCanvasContext.rect(0, 0, size, size);
        unscaledCanvasContext.fillStyle = backgroundDimming;
        unscaledCanvasContext.fill();
        unscaledCanvasContext.drawImage(mainCanvas, 0, 0, size, size);

        // Scale the final image
        const outCanvas = createCanvas(rawSize, rawSize);
        const outCanvasContext = outCanvas.getContext("2d");
        outCanvasContext.drawImage(unscaledCanvas, 0, 0, rawSize, rawSize);
        gifOutput.addFrame(outCanvasContext.getImageData(0, 0, outCanvas.width, outCanvas.height).data);
      });

      if (!gifOutput) {
        throw new Error("No frames.");
      }

      gifOutput.finish();

      if (isElement(this.canvas)) {
        const u8array = gifOutput.stream().toFlattenUint8Array();
        const binary = u8array.reduce((bin: string, u8: number) => bin + String.fromCharCode(u8), "");
        return Promise.resolve(`data:image/gif;base64,${window.btoa(binary)}`);
      }

      return Promise.resolve(Buffer.from(gifOutput.stream().toFlattenUint8Array()));
    } else {
      // Swap and merge the foreground and the background
      backgroundCanvasContext.drawImage(mainCanvas, 0, 0, size, size);
      mainCanvasContext.drawImage(backgroundCanvas, -margin, -margin, size, size);

      // Scale the final image
      const outCanvas = createCanvas(rawSize, rawSize); //document.createElement("canvas");
      const outCanvasContext = outCanvas.getContext("2d");
      outCanvasContext.drawImage(mainCanvas, 0, 0, rawSize, rawSize);
      this.canvas = outCanvas;

      if (isElement(this.canvas)) {
        return Promise.resolve(this.canvas.toDataURL());
      }

      return Promise.resolve(this.canvas.toBuffer());
    }
  }
}

function isElement(obj: any): boolean {
  try {
    //Using W3 DOM2 (works for FF, Opera and Chrome)
    return obj instanceof HTMLElement;
  } catch (e) {
    //Browsers not supporting W3 DOM2 don't have HTMLElement and
    //an exception is thrown and we end up here. Testing some
    //properties that all elements have (works on IE7)
    return (
      typeof obj === "object" &&
      obj.nodeType === 1 &&
      typeof obj.style === "object" &&
      typeof obj.ownerDocument === "object"
    );
  }
}
