import { CanvasTexture, LinearFilter, ClampToEdgeWrapping } from 'three';
import { rgbToHex, to1D } from '../../common/convert';

/**
 * Draws a rounded rectangle using the current state of the canvas.
 * If you omit the last three params, it will draw a rectangle
 * outline with a 5 pixel border radius
 *
 * https://stackoverflow.com/a/3368118
 *
 * @param {CanvasRenderingContext2D} ctx
 * @param {Number} x The top left x coordinate
 * @param {Number} y The top left y coordinate
 * @param {Number} width The width of the rectangle
 * @param {Number} height The height of the rectangle
 * @param {Number} [radius = 5] The corner radius; It can also be an object
 *                 to specify different radii for corners
 * @param {Number} [radius.tl = 0] Top left
 * @param {Number} [radius.tr = 0] Top right
 * @param {Number} [radius.br = 0] Bottom right
 * @param {Number} [radius.bl = 0] Bottom left
 * @param {Boolean} [fill = false] Whether to fill the rectangle.
 * @param {Boolean} [stroke = true] Whether to stroke the rectangle.
 */
export function roundRect(
  ctx: CanvasRenderingContext2D,
  x: number,
  y: number,
  width: number,
  height: number,
  radius?: number | { tl: number; tr: number; br: number; bl: number },
  fill?: boolean,
  stroke?: boolean,
) {
  if (typeof stroke === 'undefined') {
    stroke = true;
  }
  if (typeof radius === 'undefined') {
    radius = 5;
  }
  if (typeof radius === 'number') {
    radius = { tl: radius, tr: radius, br: radius, bl: radius };
  } else {
    var defaultRadius = { tl: 0, tr: 0, br: 0, bl: 0 };
    for (var side in defaultRadius) {
      radius[side] = radius[side] || defaultRadius[side];
    }
  }
  ctx.beginPath();
  ctx.moveTo(x + radius.tl, y);
  ctx.lineTo(x + width - radius.tr, y);
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
  ctx.lineTo(x + width, y + height - radius.br);
  ctx.quadraticCurveTo(
    x + width,
    y + height,
    x + width - radius.br,
    y + height,
  );
  ctx.lineTo(x + radius.bl, y + height);
  ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
  ctx.lineTo(x, y + radius.tl);
  ctx.quadraticCurveTo(x, y, x + radius.tl, y);
  ctx.closePath();
  if (fill) {
    ctx.fill();
  }
  if (stroke) {
    ctx.stroke();
  }
}

export interface CanvasUtilsOptions {
  fill?: boolean;
  backgroundColor?: string;
  foregroundColor?: string;
  rounded?: boolean;
  roundedRadius?: number;
  textBorderColor?: string;
  textBorderSize?: number;
  textShadowBlur?: number;
}

export class CanvasUtils {
  constructor(private options?: CanvasUtilsOptions) {}

  public createTextCanvas(
    text: string | string[],
    bold = true,
    fontSize = 16,
    padding = 4,
  ): { texture: CanvasTexture; width: number; height: number } {
    const ctx = document.createElement('canvas').getContext('2d');
    const font = `${fontSize}px${bold ? ' bold' : ''} sans`;

    const lines = Array.isArray(text) ? text : [text];
    const lineWidths = [];
    let longestLine = 0;

    // Measure the text bounds
    ctx.font = font;
    lines.forEach((line) => {
      const lineWidth = ctx.measureText(line).width;
      if (longestLine < lineWidth) {
        longestLine = lineWidth;
      }
      lineWidths.push(lineWidth);
    });

    const width = longestLine + padding * 2;
    const textHeight = fontSize * lines.length;
    const height = textHeight + padding * 2;

    // Resize canvas
    ctx.canvas.width = width;
    ctx.canvas.height = height;

    // Set text parameters
    ctx.font = font;
    ctx.textAlign = 'center';

    // Draw background
    if (this.options?.fill ?? true) {
      ctx.fillStyle = this.options?.backgroundColor || '#fff';
      if (this.options?.rounded) {
        roundRect(
          ctx,
          0,
          0,
          width,
          height,
          this.options?.roundedRadius || 4,
          true,
          false,
        );
      } else {
        ctx.fillRect(0, 0, width, height);
      }
    }

    // Scale the text to fit within the canvas
    const scaleFactor = Math.min(1, width / longestLine);
    ctx.translate(
      Math.floor(width / 2 - padding) + 0.5,
      Math.floor(padding + fontSize / 2) + 0.5,
    );
    ctx.scale(scaleFactor, 1);

    // Draw the text

    if (this.options?.textShadowBlur !== undefined) {
      ctx.shadowColor = this.options?.textBorderColor || '#000';
      ctx.shadowBlur = this.options?.textShadowBlur;
      if (this.options?.textBorderSize !== undefined) {
        ctx.lineWidth = this.options?.textBorderSize;
        lines.forEach((line, i) => {
          ctx.strokeText(line, padding, i * fontSize + padding);
        });
      }
      ctx.shadowBlur = 0;
    }

    ctx.fillStyle = this.options?.foregroundColor || '#000';
    lines.forEach((line, i) => {
      ctx.fillText(line, padding, i * fontSize + padding);
    });

    // Create texture with appropriate flags
    const texture = new CanvasTexture(ctx.canvas);
    texture.minFilter = LinearFilter;
    texture.wrapS = ClampToEdgeWrapping;
    texture.wrapT = ClampToEdgeWrapping;

    return { texture, width, height };
  }

  public readPixelDataRGB(image: HTMLImageElement): number[] {
    const array = new Array(image.width * image.height);
    const ctx = document.createElement('canvas').getContext('2d');
    ctx.canvas.width = image.width;
    ctx.canvas.height = image.height;
    ctx.drawImage(image, 0, 0, image.width, image.height);

    // pixel data
    const data = ctx.getImageData(0, 0, image.width, image.height);
    for (let x = 0; x < image.width; x++) {
      for (let y = 0; y < image.height; y++) {
        const index = to1D(x, y, image.width);
        array[index] = rgbToHex(
          data.data[index * 4],
          data.data[index * 4 + 1],
          data.data[index * 4 + 2],
        );
      }
    }

    return array;
  }

  public readPixelDataRScaled(
    image: HTMLImageElement,
    scale: number,
  ): number[] {
    const array = new Array(image.width * image.height);
    const ctx = document.createElement('canvas').getContext('2d');
    ctx.canvas.width = image.width;
    ctx.canvas.height = image.height;
    ctx.drawImage(image, 0, 0, image.width, image.height);

    // pixel data
    const data = ctx.getImageData(0, 0, image.width, image.height);
    for (let x = 0; x < image.width; x++) {
      for (let y = 0; y < image.height; y++) {
        const index = to1D(x, y, image.width);
        array[index] = (data.data[index * 4] * scale) / 255;
      }
    }

    return array;
  }
}
