/*
 * This module defines the filters for use with image buffers.
 *
 * This module is basically a collection of functions stored in an object
 * as opposed to modules. The functions are destructive, modifying
 * the passed in canvas rather than creating a copy.
 *
 * Generally speaking users of this module will use the Filters.apply method
 * on a canvas to create an effect.
 *
 * A number of functions are borrowed/adapted from
 * http://www.html5rocks.com/en/tutorials/canvas/imagefilters/
 * or the java processing implementation.
 *
 * @private
 */

const Filters = {

  /*
   * Helper functions
   */

  /**
   * Returns the pixel buffer for a canvas.
   *
   * @private
   *
   * @param  {Canvas|ImageData} canvas the canvas to get pixels from
   * @return {Uint8ClampedArray}       a one-dimensional array containing
   *                                   the data in the RGBA order, with integer
   *                                   values between 0 and 255.
   */
  _toPixels(canvas) {
    // Return pixel data if 'canvas' is an ImageData object.
    if (canvas instanceof ImageData) {
      return canvas.data;
    } else {
      // Check 2D context support.
      if (canvas.getContext('2d')) {
        // Retrieve pixel data.
        return canvas
          .getContext('2d')
          .getImageData(0, 0, canvas.width, canvas.height).data;
      } else if (canvas.getContext('webgl')) { //Check WebGL context support
        const gl = canvas.getContext('webgl');
        // Calculate the size of pixel data
        // (4 bytes per pixel - one byte for each RGBA channel).
        const len = gl.drawingBufferWidth * gl.drawingBufferHeight * 4;
        const data = new Uint8Array(len);
        // Use gl.readPixels to fetch pixel data from the WebGL
        // canvas, storing it in the data array as UNSIGNED_BYTE integers.
        gl.readPixels(
          0,
          0,
          canvas.width,
          canvas.height,
          gl.RGBA,
          gl.UNSIGNED_BYTE,
          data
        );
        return data;
      }
    }
  },

  /**
   * Returns a 32-bit number containing ARGB data at the ith pixel in the
   * 1D array containing pixels data.
   *
   * @private
   *
   * @param  {Uint8ClampedArray} data array returned by _toPixels()
   * @param  {Integer}           i    index of a 1D Image Array
   * @return {Integer}                32-bit integer value representing
   *                                  ARGB value.
   */
  _getARGB(data, i) {
    // Determine the starting position in the 'data' array for the 'i'-th pixel.
    const offset = i * 4;
    return (
      // Combining the extracted components using bitwise OR operations to form the final ARGB value.
      ((data[offset + 3] << 24) & 0xff000000) | //Extract alpha component
      ((data[offset] << 16) & 0x00ff0000) | //Extract Red component
      ((data[offset + 1] << 8) & 0x0000ff00) | //Extract green component
      (data[offset + 2] & 0x000000ff) //Extract blue component
    );
  },

  /**
   * Modifies pixels RGBA values to values contained in the data object.
   *
   * @private
   *
   * @param {Uint8ClampedArray} pixels array returned by _toPixels()
   * @param {Int32Array}        data   source 1D array where each value
   *                                   represents ARGB values
   */
  _setPixels(pixels, data) {
    let offset = 0;
    for (let i = 0, al = pixels.length; i < al; i++) {
      offset = i * 4;
      pixels[offset + 0] = (data[i] & 0x00ff0000) >>> 16;
      pixels[offset + 1] = (data[i] & 0x0000ff00) >>> 8;
      pixels[offset + 2] = data[i] & 0x000000ff;
      pixels[offset + 3] = (data[i] & 0xff000000) >>> 24;
    }
  },


  /**
   * Returns the ImageData object for a canvas.
   * https://developer.mozilla.org/en-US/docs/Web/API/ImageData
   *
   * @private
   *
   * @param  {Canvas|ImageData} canvas canvas to get image data from
   * @return {ImageData}               Holder of pixel data (and width and
   *                                   height) for a canvas
   */
  _toImageData(canvas) {
    if (canvas instanceof ImageData) {
      return canvas;
    } else {
      return canvas
        .getContext('2d')
        .getImageData(0, 0, canvas.width, canvas.height);
    }
  },


  /**
   * Returns a blank ImageData object.
   *
   * @private
   *
   * @param  {Integer} width
   * @param  {Integer} height
   * @return {ImageData}
   */
  _createImageData(width, height) {
    Filters._tmpCanvas = document.createElement('canvas');
    Filters._tmpCtx = Filters._tmpCanvas.getContext('2d');
    return this._tmpCtx.createImageData(width, height);
  },

  /**
   * Applys a filter function to a canvas.
   *
   * The difference between this and the actual filter functions defined below
   * is that the filter functions generally modify the pixel buffer but do
   * not actually put that data back to the canvas (where it would actually
   * update what is visible). By contrast this method does make the changes
   * actually visible in the canvas.
   *
   * The apply method is the method that callers of this module would generally
   * use. It has been separated from the actual filters to support an advanced
   * use case of creating a filter chain that executes without actually updating
   * the canvas in between everystep.
   *
   * @private
   * @param  {HTMLCanvasElement} canvas The input canvas to apply the filter on.
   * @param  {function(ImageData,Object)} func The filter function to apply to the canvas's pixel data.
   * @param  {Object} filterParam An optional parameter to pass to the filter function.
   */
  apply(canvas, func, filterParam) {
    const pixelsState = canvas.getContext('2d');
    const imageData = pixelsState.getImageData(
      0, 0, canvas.width, canvas.height);

    //Filters can either return a new ImageData object, or just modify
    //the one they received.
    const newImageData = func(imageData, filterParam);
    //If new ImageData is returned, replace the canvas's pixel data with it.
    if (newImageData instanceof ImageData) {
      pixelsState.putImageData(
        newImageData,
        0,
        0,
        0,
        0,
        canvas.width,
        canvas.height
      );
    } else {  //Restore the original pixel.
      pixelsState.putImageData(
        imageData,
        0,
        0,
        0,
        0,
        canvas.width,
        canvas.height
      );
    }
  },

  /*
   * Filters
   */

  /**
   * Converts the image to black and white pixels depending if they are above or
   * below the threshold defined by the level parameter. The parameter must be
   * between 0.0 (black) and 1.0 (white). If no level is specified, 0.5 is used.
   *
   * Borrowed from http://www.html5rocks.com/en/tutorials/canvas/imagefilters/
   *
   * @private
   * @param  {Canvas} canvas Canvas to apply thershold filter on.
   * @param  {Float} level Threshold level (0-1).
   */
  threshold(canvas, level = 0.5) {
    const pixels = Filters._toPixels(canvas);

    // Calculate threshold value on a (0-255) scale.
    const thresh = Math.floor(level * 255);

    for (let i = 0; i < pixels.length; i += 4) {
      const r = pixels[i];
      const g = pixels[i + 1];
      const b = pixels[i + 2];
      // CIE luminance for RGB
      const gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;
      let val;
      if (gray >= thresh) {
        val = 255;
      } else {
        val = 0;
      }
      pixels[i] = pixels[i + 1] = pixels[i + 2] = val; //set pixel to val.
    }
  },

  /**
   * Converts any colors in the image to grayscale equivalents.
   * No parameter is used.
   *
   * Borrowed from http://www.html5rocks.com/en/tutorials/canvas/imagefilters/
   *
   * @private
   * @param {Canvas} canvas Canvas to apply gray filter on.
   */
  gray(canvas) {
    const pixels = Filters._toPixels(canvas);

    for (let i = 0; i < pixels.length; i += 4) {
      const r = pixels[i];
      const g = pixels[i + 1];
      const b = pixels[i + 2];

      // CIE luminance for RGB
      const gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;
      pixels[i] = pixels[i + 1] = pixels[i + 2] = gray; // set pixel to gray.
    }
  },

  /**
   * Sets the alpha channel to entirely opaque. No parameter is used.
   *
   * @private
   * @param {Canvas} canvas
   */
  opaque(canvas) {
    const pixels = Filters._toPixels(canvas);

    for (let i = 0; i < pixels.length; i += 4) {
      pixels[i + 3] = 255;
    }

    return pixels;
  },

  /**
   * Sets each pixel to its inverse value. No parameter is used.
   * @private
   * @param  {Canvas} canvas
   */
  invert(canvas) {
    const pixels = Filters._toPixels(canvas);

    for (let i = 0; i < pixels.length; i += 4) {
      pixels[i] = 255 - pixels[i];
      pixels[i + 1] = 255 - pixels[i + 1];
      pixels[i + 2] = 255 - pixels[i + 2];
    }
  },

  /**
   * Limits each channel of the image to the number of colors specified as
   * the parameter. The parameter can be set to values between 2 and 255, but
   * results are most noticeable in the lower ranges.
   *
   * Adapted from java based processing implementation
   *
   * @private
   * @param  {Canvas} canvas
   * @param  {Integer} level
   */
  posterize(canvas, level = 4) {
    const pixels = Filters._toPixels(canvas);
    if (level < 2 || level > 255) {
      throw new Error(
        'Level must be greater than 2 and less than 255 for posterize'
      );
    }

    const levels1 = level - 1;
    for (let i = 0; i < pixels.length; i += 4) {
      const rlevel = pixels[i];
      const glevel = pixels[i + 1];
      const blevel = pixels[i + 2];

      // New pixel value by posterizing each color.
      pixels[i] = ((rlevel * level) >> 8) * 255 / levels1;
      pixels[i + 1] = ((glevel * level) >> 8) * 255 / levels1;
      pixels[i + 2] = ((blevel * level) >> 8) * 255 / levels1;
    }
  },

  /**
   * Increases the bright areas in an image.
   * @private
   * @param  {Canvas} canvas
   */
  dilate(canvas) {
    const pixels = Filters._toPixels(canvas);
    let currIdx = 0;
    const maxIdx = pixels.length ? pixels.length / 4 : 0;
    const out = new Int32Array(maxIdx);
    let currRowIdx, maxRowIdx, colOrig, colOut, currLum;

    let idxRight, idxLeft, idxUp, idxDown;
    let colRight, colLeft, colUp, colDown;
    let lumRight, lumLeft, lumUp, lumDown;
    // Iterates through rows of pixels.
    while (currIdx < maxIdx) {
      currRowIdx = currIdx;
      maxRowIdx = currIdx + canvas.width;
      // Iterates through pixels within the current row.
      while (currIdx < maxRowIdx) {
        // Get original color of current pixel.
        colOrig = colOut = Filters._getARGB(pixels, currIdx);
        idxLeft = currIdx - 1;
        idxRight = currIdx + 1;
        idxUp = currIdx - canvas.width;
        idxDown = currIdx + canvas.width;

        // Adjust the indices to avoid going out of bounds.
        if (idxLeft < currRowIdx) {
          idxLeft = currIdx;
        }
        if (idxRight >= maxRowIdx) {
          idxRight = currIdx;
        }
        if (idxUp < 0) {
          idxUp = 0;
        }
        if (idxDown >= maxIdx) {
          idxDown = currIdx;
        }
        colUp = Filters._getARGB(pixels, idxUp);
        colLeft = Filters._getARGB(pixels, idxLeft);
        colDown = Filters._getARGB(pixels, idxDown);
        colRight = Filters._getARGB(pixels, idxRight);

        // Compute luminance
        currLum =
          77 * ((colOrig >> 16) & 0xff) +
          151 * ((colOrig >> 8) & 0xff) +
          28 * (colOrig & 0xff);
        lumLeft =
          77 * ((colLeft >> 16) & 0xff) +
          151 * ((colLeft >> 8) & 0xff) +
          28 * (colLeft & 0xff);
        lumRight =
          77 * ((colRight >> 16) & 0xff) +
          151 * ((colRight >> 8) & 0xff) +
          28 * (colRight & 0xff);
        lumUp =
          77 * ((colUp >> 16) & 0xff) +
          151 * ((colUp >> 8) & 0xff) +
          28 * (colUp & 0xff);
        lumDown =
          77 * ((colDown >> 16) & 0xff) +
          151 * ((colDown >> 8) & 0xff) +
          28 * (colDown & 0xff);

        // Update the output color based on the highest luminance value
        if (lumLeft > currLum) {
          colOut = colLeft;
          currLum = lumLeft;
        }
        if (lumRight > currLum) {
          colOut = colRight;
          currLum = lumRight;
        }
        if (lumUp > currLum) {
          colOut = colUp;
          currLum = lumUp;
        }
        if (lumDown > currLum) {
          colOut = colDown;
          currLum = lumDown;
        }
        // Store the updated color.
        out[currIdx++] = colOut;
      }
    }
    Filters._setPixels(pixels, out);
  },

  /**
   * Reduces the bright areas in an image.
   * Similar to `dilate()`, but updates the output color based on the lowest luminance value.
   * @private
   * @param  {Canvas} canvas
   */
  erode(canvas) {
    const pixels = Filters._toPixels(canvas);
    let currIdx = 0;
    const maxIdx = pixels.length ? pixels.length / 4 : 0;
    const out = new Int32Array(maxIdx);
    let currRowIdx, maxRowIdx, colOrig, colOut, currLum;
    let idxRight, idxLeft, idxUp, idxDown;
    let colRight, colLeft, colUp, colDown;
    let lumRight, lumLeft, lumUp, lumDown;

    while (currIdx < maxIdx) {
      currRowIdx = currIdx;
      maxRowIdx = currIdx + canvas.width;
      while (currIdx < maxRowIdx) {
        colOrig = colOut = Filters._getARGB(pixels, currIdx);
        idxLeft = currIdx - 1;
        idxRight = currIdx + 1;
        idxUp = currIdx - canvas.width;
        idxDown = currIdx + canvas.width;

        if (idxLeft < currRowIdx) {
          idxLeft = currIdx;
        }
        if (idxRight >= maxRowIdx) {
          idxRight = currIdx;
        }
        if (idxUp < 0) {
          idxUp = 0;
        }
        if (idxDown >= maxIdx) {
          idxDown = currIdx;
        }
        colUp = Filters._getARGB(pixels, idxUp);
        colLeft = Filters._getARGB(pixels, idxLeft);
        colDown = Filters._getARGB(pixels, idxDown);
        colRight = Filters._getARGB(pixels, idxRight);

        //compute luminance
        currLum =
          77 * ((colOrig >> 16) & 0xff) +
          151 * ((colOrig >> 8) & 0xff) +
          28 * (colOrig & 0xff);
        lumLeft =
          77 * ((colLeft >> 16) & 0xff) +
          151 * ((colLeft >> 8) & 0xff) +
          28 * (colLeft & 0xff);
        lumRight =
          77 * ((colRight >> 16) & 0xff) +
          151 * ((colRight >> 8) & 0xff) +
          28 * (colRight & 0xff);
        lumUp =
          77 * ((colUp >> 16) & 0xff) +
          151 * ((colUp >> 8) & 0xff) +
          28 * (colUp & 0xff);
        lumDown =
          77 * ((colDown >> 16) & 0xff) +
          151 * ((colDown >> 8) & 0xff) +
          28 * (colDown & 0xff);

        if (lumLeft < currLum) {
          colOut = colLeft;
          currLum = lumLeft;
        }
        if (lumRight < currLum) {
          colOut = colRight;
          currLum = lumRight;
        }
        if (lumUp < currLum) {
          colOut = colUp;
          currLum = lumUp;
        }
        if (lumDown < currLum) {
          colOut = colDown;
          currLum = lumDown;
        }
        // Store the updated color.
        out[currIdx++] = colOut;
      }
    }
    Filters._setPixels(pixels, out);
  },

  blur(canvas, radius) {
    blurARGB(canvas, radius);
  }
};

// BLUR

// Internal kernel stuff for the gaussian blur filter.
let blurRadius;
let blurKernelSize;
let blurKernel;
let blurMult;

/*
 * Port of https://github.com/processing/processing/blob/
 * main/core/src/processing/core/PImage.java#L1250
 *
 * Optimized code for building the blur kernel.
 * further optimized blur code (approx. 15% for radius=20)
 * bigger speed gains for larger radii (~30%)
 * added support for various image types (ALPHA, RGB, ARGB)
 * [toxi 050728]
 */
function buildBlurKernel(r) {
  let radius = (r * 3.5) | 0;
  radius = radius < 1 ? 1 : radius < 248 ? radius : 248;

  if (blurRadius !== radius) {
    blurRadius = radius;
    // Calculating the size of the blur kernel
    blurKernelSize = (1 + blurRadius) << 1;
    blurKernel = new Int32Array(blurKernelSize);
    blurMult = new Array(blurKernelSize);
    for (let l = 0; l < blurKernelSize; l++) {
      blurMult[l] = new Int32Array(256);
    }

    let bk, bki;
    let bm, bmi;
    // Generating blur kernel values.
    for (let i = 1, radiusi = radius - 1; i < radius; i++) {
      blurKernel[radius + i] = blurKernel[radiusi] = bki = radiusi * radiusi;
      bm = blurMult[radius + i];
      bmi = blurMult[radiusi--];
      for (let j = 0; j < 256; j++) {
        bm[j] = bmi[j] = bki * j;
      }
    }
    bk = blurKernel[radius] = radius * radius;
    bm = blurMult[radius];

    for (let k = 0; k < 256; k++) {
      bm[k] = bk * k;
    }
  }
}

// Port of https://github.com/processing/processing/blob/
// main/core/src/processing/core/PImage.java#L1433
function blurARGB(canvas, radius) {
  // Get pixel data.
  const pixels = Filters._toPixels(canvas);
  const width = canvas.width;
  const height = canvas.height;
  const numPackedPixels = width * height;
  const argb = new Int32Array(numPackedPixels);
  for (let j = 0; j < numPackedPixels; j++) {
    argb[j] = Filters._getARGB(pixels, j);
  }
  let sum, cr, cg, cb, ca;
  let read, ri, ym, ymi, bk0;
  const a2 = new Int32Array(numPackedPixels);
  const r2 = new Int32Array(numPackedPixels);
  const g2 = new Int32Array(numPackedPixels);
  const b2 = new Int32Array(numPackedPixels);
  let yi = 0;
  buildBlurKernel(radius);
  let x, y, i;
  let bm;
  // Horizontal pass.
  for (y = 0; y < height; y++) {
    for (x = 0; x < width; x++) {
      cb = cg = cr = ca = sum = 0;
      read = x - blurRadius;
      // Handle edge cases.
      if (read < 0) {
        bk0 = -read;
        read = 0;
      } else {
        if (read >= width) {
          break;
        }
        bk0 = 0;
      }
      for (i = bk0; i < blurKernelSize; i++) {
        if (read >= width) {
          break;
        }
        const c = argb[read + yi];
        bm = blurMult[i];
        ca += bm[(c & -16777216) >>> 24];
        cr += bm[(c & 16711680) >> 16];
        cg += bm[(c & 65280) >> 8];
        cb += bm[c & 255];
        sum += blurKernel[i];
        read++;
      }
      ri = yi + x;
      a2[ri] = ca / sum;
      r2[ri] = cr / sum;
      g2[ri] = cg / sum;
      b2[ri] = cb / sum;
    }
    yi += width;
  }
  yi = 0;
  ym = -blurRadius;
  ymi = ym * width;
  //  Vertical pass.
  for (y = 0; y < height; y++) {
    for (x = 0; x < width; x++) {
      cb = cg = cr = ca = sum = 0;
      // Handle edge cases.
      if (ym < 0) {
        bk0 = ri = -ym;
        read = x;
      } else {
        if (ym >= height) {
          break;
        }
        bk0 = 0;
        ri = ym;
        read = x + ymi;
      }
      for (i = bk0; i < blurKernelSize; i++) {
        if (ri >= height) {
          break;
        }
        bm = blurMult[i];
        ca += bm[a2[read]];
        cr += bm[r2[read]];
        cg += bm[g2[read]];
        cb += bm[b2[read]];
        sum += blurKernel[i];
        ri++;
        read += width;
      }
      // Set final ARGB value
      argb[x + yi] =
        ((ca / sum) << 24) |
        ((cr / sum) << 16) |
        ((cg / sum) << 8) |
        (cb / sum);
    }
    yi += width;
    ymi += width;
    ym++;
  }
  Filters._setPixels(pixels, argb);
}

export { Filters as default };
