import Utils from '../common/utils';
import FillArea from '../common/fillarea';
import Rect from '../core/rect';

var _colors = {};

export default class MyFloodFill {

  private inputImage: HTMLImageElement;
  private canvas: HTMLCanvasElement;
  private ctx: CanvasRenderingContext2D;
  private inputImageData: ImageData;
  private floodFilled: ImageData;

  constructor(image: HTMLImageElement, scale: number = 1, step: number = 0) {
    this.inputImage = image;
    this.canvas = createCanvas(image.width, image.height);
    this.ctx = this.canvas.getContext('2d');
    Utils.setImageSmoothing(this.ctx, false);

    let _canvas = createCanvas(image.width * scale, image.height * scale);
    let _ctx = _canvas.getContext('2d');
    Utils.setImageSmoothing(_ctx, false);
    _ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, _canvas.width, _canvas.height);
    this.inputImageData = _ctx.getImageData(0, 0, _canvas.width, _canvas.height);
    this.sumColor(this.inputImageData);
    this.floodFilled = createFloodFillImage(this.inputImageData, scale, step);
    // img data to image.
    _ctx.putImageData(this.floodFilled, 0, 0);

    this.ctx.drawImage(_canvas, 0, 0, _canvas.width, _canvas.height, 0, 0, this.canvas.width, this.canvas.height);
  }

  sumColor(imgData: ImageData) {
    let hash = {};

    /*
    let _pixels = new Uint8Array(imgData.data.buffer);
    for (var i = 0; i < _pixels.length / 4; i++) {
      if (_pixels[i * 4 + 3] < 128) {
        _pixels[i * 4] = 0;
        _pixels[i * 4 + 1] = 0;
        _pixels[i * 4 + 2] = 0;
        _pixels[i * 4 + 3] = 0;
      }
    }
    */

    let pixels = new Uint32Array(imgData.data.buffer);
    for (var i = 0; i < pixels.length; i++) {
      if (hash[pixels[i]]) hash[pixels[i]]++;
      else hash[pixels[i]] = 1;
    }

    let res = Object.keys(hash).map((colorStr: string) => {
      let color = parseInt(colorStr);
      let count = hash[colorStr];
      return {
        //color: color.toString(16),
        color: Utils.getColorFromInteger(color),
        count
      };
    }).sort((a, b) => {
      return b.count - a.count
    })
    console.log('colorSum', res);
  }

  getAreaCount(): number {
    let hash: any = {};
    let pixels = new Uint32Array(this.floodFilled.data.buffer);
    for (var i = 0; i < pixels.length; i++) {
      hash[pixels[i]] = 1;
    }
    return Object.keys(hash).length;
  }

  /**
   * 输出图片
   * @returns 
   */
  toImage(): Promise<HTMLImageElement> {
    return new Promise((done, reject) => {
      var img = new Image();
      img.onload = function () { done(img); };
      img.onerror = reject;
      img.src = this.canvas.toDataURL('image/png');
    });
  }

  /**
   * 输出文件
   * @returns 
   */
  toBlob(): Promise<Blob> {
    return new Promise((done, reject) => {
      this.canvas.toBlob((blob: Blob) => done(blob), 'image/png');
    });
  }





  /**
   * 创建map, 输出文件
   * @param image 
   * @returns 
   */
  static createMapBlob(image: HTMLImageElement): Promise<Blob> {
    return new MyFloodFill(image).toBlob();
  }


  /**
   * 创建MAP， 输出图片
   * @param image 线稿图片
   * @returns 
   */
  static createMapImage(image: HTMLImageElement, step: number = 0): Promise<HTMLImageElement> {
    return new MyFloodFill(image, 1, step).toImage();
  }


  /**
   * 线条闭合检测
   * @param image 
   */
  static lineCloseCheck(image: HTMLImageElement): Rect[] {
    let _canvas = createCanvas(image.width, image.height);
    let _ctx = _canvas.getContext('2d');
    Utils.setImageSmoothing(_ctx, false);
    _ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, _canvas.width, _canvas.height);
    let original = _ctx.getImageData(0, 0, _canvas.width, _canvas.height);
    let floodFilled = copyImage(_ctx, original);

    // 将线条打薄，即透明度<128的置为空白
    setLowAlphaToEmpty(original, floodFilled);

    // 开始检测断点。 算法：寻找只有一个邻居的线条点
    let result: {x: number, y: number}[] = [];
    let floodArr = new Uint32Array(floodFilled.data.buffer);
    let width = original.width;
    let height = original.height;
    let neighbours;
    let count;
    let x, y, index, idx, p, px, py, i, j, expand;
    for (x = 2; x < width - 2; x++) {  // 略过2px的边缘
      for (y = 2; y < height - 2; y++) {
        index = y * width + x;
        if (floodArr[index] != 0) {
          neighbours = getNearestNeighbours(1);
          count = 0;
          for (j = 0; j < neighbours.length; j++) {
            p = neighbours[j];
            px = x + p.x;
            py = y + p.y;
            idx = py * width + px;
            if (px > 0 && px < width && py > 0 && py < height && floodArr[idx] != 0) {
              count++;
            }
          }
          if (count <= 1) {  // 只有一个邻居，则判别为未闭合
            result.push({x, y});
          }
        }
      }
    }

    console.log("疑似断点数：" + result.length);

    // 对结果进行进一步的筛选。算法：将找到的断点扩大为5*5的小矩形，判断有没有跟它相交的矩形，如果有则是有效结果，没有则剔除之
    let smallRects: Rect[] = [];
    let middleResult: {x: number, y: number}[] = [];
    expand = 2;
    for (i = 0; i < result.length; i++) {
      let p = result[i];
      let left = Math.max(0, p.x - expand);
      let right = Math.min(width - 1, p.x + expand);
      let top = Math.max(0, p.y - expand);
      let bottom = Math.min(height - 1, p.y + expand);
      let rect = new Rect(left, top, right, bottom);
      smallRects.push(rect);
    }
    for (i = 0; i < smallRects.length; i++) {
      let rect = smallRects[i];
      for (j = i+1; j < smallRects.length; j++) {
        if (checkIntersect(rect, smallRects[j])) {
          break;
        }
      }
      if (j < smallRects.length) { // 有相交，说明是有效的断点
        middleResult.push(result[i]);
      }
    }
    console.log("初筛后断点数：" + middleResult.length);

    // 对外呈现的结果：对result结果进行进一步的整理，扩大为矩形框，同时去掉相交的矩形
    let finalResult: Rect[] = [];
    expand = 10;
    for (i = 0; i < middleResult.length; i++) {
      let p = middleResult[i];
      let left = Math.max(0, p.x - expand);
      let right = Math.min(width - 1, p.x + expand);
      let top = Math.max(0, p.y - expand);
      let bottom = Math.min(height - 1, p.y + expand);
      let rect = new Rect(left, top, right, bottom);
      for (j = 0; j < finalResult.length; j++) {
        if (checkIntersect(finalResult[j], rect)) {
          break;
        }
      }
      if (j >= finalResult.length) { // 没有相交
        finalResult.push(rect);
      }
    }

    console.log("最终未闭合线条检测结果：" + finalResult.length);
    return finalResult;
  }


}

// 判断某点是否边缘点
function isEdge(x, y, w, h): boolean {
  if (x < 2 || x >= w - 2 || y < 2 || y >= h - 2 ) return true;
  else return false;
}

/**
 * Create a fill map for image
 * @param imgData image data.
 */
function createFloodFillImage(imgData, scale: number = 1, step: number = 0) {
  var ctx = createContext(imgData.width, imgData.height);
  var width = imgData.width;
  var height = imgData.height;

  var original = imgData;
  var floodFilled = copyImage(ctx, original);


  console.time('createFloodFillImage');

  if (step >= 1) {

    // guoziyun: 改动的地方：在floodfill之前先对alpha线条点做处理，将其置为0。
    // 目的是为了避免线条像素化后两个线条之间如果间隙过窄，会形成隔断。本应属于同一个区块的地方变成两个区块
    // 实测发现这样会把线条打薄，导致很多区块失去线条约束都连成了一片了，不可取
    setLowAlphaToEmpty(original, floodFilled);
  }

  if (step >= 2) {
    // flood fill， 先对page图做一次floodfill
    console.time('floodFillAll');
    floodFillAll(floodFilled);
    console.timeEnd('floodFillAll');
  }

  if (step >= 3) {
    // remove original pixels in dest，将线条点置为透明（值非0的点）
    setHightAlphaToEmpty(original, floodFilled);
    // var srcArr = new Uint32Array(original.data.buffer);
    // var destArr = new Uint32Array(floodFilled.data.buffer);
    // for (var i = 0; i < srcArr.length; i++) {
    //   if (srcArr[i] != 0) {
    //     destArr[i] = 0;
    //   }
    // }
  }

  if (step >= 4) {
    // 在这里先merge一轮小区块
    console.time('mergeAreas');
    mergeAreas(floodFilled);
    console.timeEnd('mergeAreas');
  }


  if (step >= 5) {
    // fill alpha gap 将原apha线条点(0 < alpha位 < 255)的点涂成与临近的区块颜色
    console.time('fillAlphaGap');
    fillAlphaGap(original, floodFilled, 50 * scale);
    console.timeEnd('fillAlphaGap');
  }

  if (step >= 6) {
    // 合并小区块
    console.time('mergeAreas');
    mergeAreas(floodFilled);
    console.timeEnd('mergeAreas');
  }
  
  if (step >= 7 ) {
    // fill empty gap 最后将之前置为0的纯黑线条点涂为临近的区块颜色
    console.time('fillEmptyGap');
    fillEmptyGap(floodFilled, 10 * scale);
    console.timeEnd('fillEmptyGap');
  }
  
  console.timeEnd('createFloodFillImage');
  return floodFilled;
}

/**
 * 将线条像素化后alpha < 128 的过度点置为空
 * @param srcImage source ImageData  ojbect.
 * @param floodFilledImage flood filled ImageData object.
 */
function  setLowAlphaToEmpty(srcImage, floodFilledImage) {
  var srcBuf = srcImage.data; // to get the alpha directly
  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
  var width = srcImage.width;
  var height = srcImage.height;
  var j, x, y, index;

  // set all alpha points to empty
  for (x = 0; x < width; x++) {
    for (y = 0; y < height; y++) {
      if (isEdge(x, y, width, height)) continue;  // 如果是边缘点不做此处理，主要是为了解决边缘虚化区块容易无法很好分隔的问题 
      index = y * width + x;
      j = index * 4 + 3; // alpha byte index
      if (srcBuf[j] < 128) { // 颜色比较浅的才认为是空白，比较深的还当作线条
        floodArr[index] = 0;
      }
    }
  }
}

/**
 * 将线条像素化后alpha >= 128 的点置为空
 * @param srcImage source ImageData  ojbect.
 * @param floodFilledImage flood filled ImageData object.
 */
 function setHightAlphaToEmpty(srcImage, floodFilledImage) {
  var srcBuf = srcImage.data; // to get the alpha directly
  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
  var width = srcImage.width;
  var height = srcImage.height;
  var j, x, y, index;

  // set all alpha points to empty
  for (x = 0; x < width; x++) {
    for (y = 0; y < height; y++) {
      index = y * width + x;
      j = index * 4 + 3; // alpha byte index
      if (srcBuf[j] >= 128 || (srcBuf[j] > 0 && isEdge(x, y, width, height)) ) {  //别忘了边缘点
        floodArr[index] = 0;
      }
    }
  }
}

/**
 * Flood Fill image with random Color
 */
function floodFillAll(imgData) {
  var width = imgData.width;
  var height = imgData.height;
  var pixels = new Uint32Array(imgData.data.buffer);
  var x, y;
  // var oldColor = new Uint8Array([0, 0, 0, 0]);
  var oldColor = 0;
  var newColor;
  var fillFunc = floodFill;
  // if(Math.random() < 0.5) fillFunc = floodFillEdge;
  var maxStack = 0;
  var stackSize;

  // console.log('fillFunc: ' + fillFunc.name);
  for (var i = 0; i < pixels.length; i++) {
    if (pixels[i] == 0) {
      x = i % width;
      y = Math.floor(i / width);
      newColor = randomColor();
      // newColor = randomUniqueColor();
      stackSize = fillFunc(imgData, x, y, oldColor, newColor);
      if (stackSize > maxStack)
        maxStack = stackSize;
      // floodFill(imgData, x, y, oldColor, newColor);
      // floodFillEdge(imgData, x, y, oldColor, newColor);
    }
  }
  console.log('floodFillAll@maxStackSize', maxStack * 100 / (width * height));
}

/**
 *
 * Flood fill x,y.
 * @param imgData ctx.getImageData()
 * @param x
 * @param y
 * @param oldColor Uint8Array [r, g, b, a]
 * @param oldColor Uint8Array [r, g, b, a]
 * @param oldColor int
 * @param newColor int
 *
 */
function floodFill(imgData, x, y, oldColor, newColor) {
  var width = imgData.width;
  var height = imgData.height;
  var stack = new Array();
  var index = y * width + x;
  var pixels = new Uint32Array(imgData.data.buffer);
  // var oc = new Uint32Array(oldColor.buffer);
  var oc = oldColor;
  // var nc = new Uint32Array(newColor.buffer);
  var nc = newColor;
  // console.log('start flood fill: ', x, y, width, height, oldColor, newColor);
  var maxStack = 0;

  if (pixels[index] == oc) {
    stack.push(index);
  }

  var p;
  while (stack.length > 0) {
    if (stack.length > maxStack)
      maxStack = stack.length;
    p = stack.pop();
    x = p % width;

    // if p is colored, continue
    if (pixels[p] == nc)
      continue;

    pixels[p] = nc;
    // left
    if (x > 0 && pixels[p - 1] == oc) {
      stack.push(p - 1);
    }
    // top
    if (p > (width - 1) && pixels[p - width] == oc) {
      stack.push(p - width);
    }
    // right
    if (x < (width - 1) && pixels[p + 1] == oc) {
      stack.push(p + 1);
    }
    // bottom
    if (p < ((height - 1) * width) && pixels[p + width] == oc) {
      stack.push(p + width);
    }

    // // left top
    // if (x > 0 && p > (width -1) && pixels[p - width -1] == oc) {
    //   stack.push(p - width -1);
    // }
    // // right top
    // if (x < (width -1) && p > (width -1) && pixels[p - width + 1] == oc) {
    //   stack.push(p - width + 1);
    // }
    // // left bottom
    // if (x > 0 && p < ((height - 1) * width) && pixels[p + width -1] == oc) {
    //   stack.push(p + width - 1);
    // }
    // // rigth bottom
    // if (x < (width - 1) && p < ((height - 1) * width) && pixels[p + width + 1] == oc) {
    //   stack.push(p + width + 1);
    // }
  }
  // console.log('floodFill@maxStack', maxStack / (width*height));
  return maxStack;
}

/**
 *
 * Flood fill x,y.
 * @param imgData ctx.getImageData()
 * @param x
 * @param y
 * @param oldColor Uint8Array [r, g, b, a]
 * @param newColor Uint8Array [r, g, b, a]
 *
 */
function floodFillEdge(imgData, x, y, oldColor, newColor) {
  var width = imgData.width;
  var height = imgData.height;
  var stack = new Array();
  var index = y * width + x;
  var pixels = new Uint32Array(imgData.data.buffer);
  var oc = new Uint32Array(oldColor.buffer);
  var nc = new Uint32Array(newColor.buffer);
  // console.log('start flood fill: ', x, y, width, height, oldColor, newColor);

  // edges
  var estack = new Array();

  if (pixels[index] == oc[0]) {
    stack.push(index);
  }

  var p;
  while (stack.length > 0) {
    p = stack.pop();
    x = p % width;

    // if p is colored, continue
    if (pixels[p] == nc[0])
      continue;

    pixels[p] = nc[0];

    // left
    if (x > 0 && pixels[p - 1] == oc[0]) {
      stack.push(p - 1);
    } else if (x > 0 && pixels[p - 1] != oc[0]) {
      estack.push(p - 1);
    }

    // top
    if (p > (width - 1) && pixels[p - width] == oc[0]) {
      stack.push(p - width);
    } else if (p > (width - 1) && pixels[p - width] != oc[0]) {
      estack.push(p - width);
    }

    // right
    if (x < (width - 1) && pixels[p + 1] == oc[0]) {
      stack.push(p + 1);
    } else if (x < (width - 1) && pixels[p + 1] != oc[0]) {
      estack.push(p + 1);
    }

    // bottom
    if (p < ((height - 1) * width) && pixels[p + width] == oc[0]) {
      stack.push(p + width);
    } else if (p < ((height - 1) * width) && pixels[p + width] != oc[0]) {
      estack.push(p + width);
    }
  }

  var r, g, b, a, t;
  var curPixel = new Uint8Array(4);
  var curPixel32 = new Uint32Array(curPixel.buffer);

  // fill edges.
  while (estack.length > 0) {
    p = estack.pop();
    // if p is alread colored.
    if (pixels[p] == nc[0])
      continue;
    // if p is oldColor, enter a new fill area, continue
    if (pixels[p] == oc[0])
      continue;
    // calculate p's distance witha oldColor
    curPixel32[0] = pixels[p];
    t = Math.sqrt(Math.pow(oldColor[0] - curPixel[0], 2) +
      Math.pow(oldColor[1] - curPixel[1], 2) +
      Math.pow(oldColor[2] - curPixel[2], 2) +
      Math.pow(oldColor[3] - curPixel[3], 2));

    if (t > 128)
      continue;
    pixels[p] = nc[0];

    // left
    if (x > 0) {
      estack.push(p - 1);
    }

    // top
    if (p > (width - 1)) {
      estack.push(p - width);
    }

    // right
    if (x < (width - 1)) {
      estack.push(p + 1);
    }

    if (p < ((height - 1) * width)) {
      estack.push(p + width);
    }
  }
}

/**
 *  Build neighbourhood for areas.
 *  @param floodArr
 *  @param areaMap
 *  @param area which need to build it's neighbours
 */
/*
function buildNeighbour(floodArr, areaMap, area) { var areaMap = {}; }
*/

function createFillAreaMap(floodImg) {
  var width = floodImg.width;
  var height = floodImg.height;
  var floodArr = new Uint32Array(floodImg.data.buffer);
  var areaMap = {};
  for (var i = 0; i < floodArr.length; i++) {
    var color = floodArr[i];
    var x = i % width;
    var y = Math.floor(i / width);
    if (areaMap[color]) {
      areaMap[color].addPoint(x, y);
    } else {
      areaMap[color] = new FillArea(color, x, y);
    }
  }
  return areaMap;
}

function mergeAreas(floodFilledImage, threshold?) {
  const kk = (floodFilledImage.width / 1000) * 20;
  threshold = threshold || kk;
  var areaMap = createFillAreaMap(floodFilledImage);
  var keys = Object.keys(areaMap);
  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
  var width = floodFilledImage.width;
  var height = floodFilledImage.height;
  var areas = keys.map(function (key) { return areaMap[key]; });
  var smallAreas = areas.filter(function (a) { return a.count < threshold; });

  console.log('mergeAreas@areaSize', areas.length, smallAreas.length);
  var i, j, x, y, index, p, px, py;
  var neighbours = getNearestNeighbours(1);
  smallAreas.forEach(function (area) {
    // area.neighbours = [];
    let neighbourSet = new Set();
    for (x = area.left; x <= area.right; x++) {
      for (y = area.top; y <= area.bottom; y++) {
        index = y * width + x;
        if (floodArr[index] == area.color) {
          // check pixe's neighbour
          for (j = 0; j < neighbours.length; j++) {
            p = neighbours[j];
            px = x + p.x;
            py = y + p.y;
            index = py * width + px;
            if (px > 0 && px < width && py > 0 && py < height &&
              floodArr[index] != 0 && floodArr[index] != area.color) {
              // area.neighbours.push(areaMap[floodArr[index]]);
              neighbourSet.add(areaMap[floodArr[index]]);
            }
          }
        }
      }
    }
    area.neighbours = Array.from(neighbourSet);
  });

  // remove the area has no neighbours.
  smallAreas =
    smallAreas.filter(function (area) { return area.neighbours.length > 0; });
  // console.log('mergeAreas@areaSize', areas.length, smallAreas.length);

  // for each samll area.
  // get it's direct neighbour area list
  // select the biggest one, and merge with it.
  // if no neighbours mark it can't merge.

  /**
   * merge another area
   */
  function mergeToMe(me, other) {
    // if i am a small area.
    // merge other's neighbours
    if (me.neighbours) {
      me.neighbours =
        me.neighbours.concat(other.neighbours)
          .filter(function (a) { return a != me && a != other; });
    }

    // set other merged
    other.merged = true;

    for (x = other.left; x <= other.right; x++) {
      for (y = other.top; y <= other.bottom; y++) {
        index = y * width + x;
        if (floodArr[index] == other.color) {
          floodArr[index] = me.color;
          me.addPoint(x, y);
        }
      }
    }
  }

  var loop = 0;
  do {
    loop++;

    smallAreas.forEach(function (area) {
      if (area.merged)
        return;
      var neighbour;
      for (var i = 0; i < area.neighbours.length; i++) {
        // merge other small areas.
        neighbour = area.neighbours[i];
        // if neighbour is merged or neighbour is a large one
        if (neighbour.merged || !neighbour.neighbours) {
          continue;
        }
        mergeToMe(area, neighbour);
      }
      if (area.neighbours.length > 0) {
        // merge me to the first large neighbour
        mergeToMe(area.neighbours[0], area);
      }
    });

    smallAreas = smallAreas.filter(function (
      area) { return !area.merged && area.neighbours.length > 0; });

    console.log('mergeAreas@loop:' + loop, smallAreas.length);

    if (smallAreas.length == 0)
      break;

  } while (loop < 4);
}

/**
 * Fill blank pixels with color.
 */
function fillEmptyGap(floodFilledImage, maxDistance) {
  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
  var width = floodFilledImage.width;
  var height = floodFilledImage.height;
  var emptyPoints = []; // x,y,x,y,x,y......
  var unFilledPoints = [];
  var taskArr = []; // index, color, index, color
  var i, j, x, y, index, color, p, px, py;

  var neighbours = getNearestNeighbours(1);

  // get all empty points
  for (x = 0; x < width; x++) {
    for (y = 0; y < height; y++) {
      index = y * width + x;
      if (floodArr[index] == 0) {
        emptyPoints.push(x, y);
      }
    }
  }

  console.log('FillEmptyGap#emptyPoints@length', emptyPoints.length / 2);
  var loop = 0;

  do {
    loop++;

    // loop all alpha points and try to fill.
    var found;
    for (i = 0; i < emptyPoints.length; i += 2) {
      x = emptyPoints[i];
      y = emptyPoints[i + 1];
      found = false;
      for (j = 0; j < neighbours.length; j++) {
        p = neighbours[j];
        px = x + p.x;
        py = y + p.y;
        index = py * width + px;
        if (px > 0 && px < width && py > 0 && py < height &&
          floodArr[index] != 0) {
          taskArr.push(y * width + x, floodArr[index]);
          found = true;
          break;
        }
      }
      if (!found)
        unFilledPoints.push(x, y);
    }

    console.log('fillEmptyGap@loop:' + loop, emptyPoints.length,
      taskArr.length / 2, unFilledPoints.length / 2);

    // no more alpha pixels can find it's neighbours.
    if (taskArr.length == 0)
      break;

    // do the task
    for (i = 0; i < taskArr.length; i += 2) {
      index = taskArr[i];
      color = taskArr[i + 1];
      floodArr[index] = color;
    }

    taskArr = [];
    emptyPoints = unFilledPoints;
    unFilledPoints = [];

  } while (loop <= maxDistance);
}

/**
 * Fill pixels in srcImage with alpha<255
 * with the nearest filled color
 * @param srcImage source ImageData  ojbect.
 * @param floodFilledImage flood filled ImageData object.
 * @param maxDistance
 */
function fillAlphaGap(srcImage, floodFilledImage, maxDistance) {
  maxDistance = maxDistance || 2;
  var srcArr = new Uint32Array(srcImage.data.buffer);
  var srcBuf = srcImage.data; // to get the alpha directly
  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
  var width = srcImage.width;
  var height = srcImage.height;
  var alphaPoints = []; // x,y,x,y,x,y......
  var unFilledPoints = [];
  var taskArr = []; // index, color, index, color
  var i, j, x, y, index, color, p, px, py;

  var neighbours = getNearestNeighbours(1);

  // get all alpha points
  for (x = 0; x < width; x++) {
    for (y = 0; y < height; y++) {
      index = y * width + x;
      j = index * 4 + 3; // alpha byte index
      if (floodArr[index] == 0 && srcBuf[j] < 255 
        && (srcBuf[j] >= 128 || (srcBuf[j] > 0 && isEdge(x, y, width, height)))) {  // < 128 的前面已经处理过了
        alphaPoints.push(x, y);
      }
    }
  }

  // console.log('neighbours:', neighbours);
  // console.log('alphaPoints@length', alphaPoints.length / 2);
  var loop = 0;

  do {
    loop++;

    // loop all alpha points and try to fill.
    var found;
    for (i = 0; i < alphaPoints.length; i += 2) {
      x = alphaPoints[i];
      y = alphaPoints[i + 1];
      found = false;
      for (j = 0; j < neighbours.length; j++) {
        p = neighbours[j];
        px = x + p.x;
        py = y + p.y;
        index = py * width + px;
        if (px > 0 && px < width && py > 0 && py < height &&
          floodArr[index] != 0) {
          taskArr.push(y * width + x, floodArr[index]);
          found = true;
          break;
        }
      }
      if (!found)
        unFilledPoints.push(x, y);
    }

    console.log('fillAlphaGap@loop:' + loop, alphaPoints.length,
      taskArr.length / 2, unFilledPoints.length / 2);

    // no more alpha pixels can find it's neighbours.
    if (taskArr.length == 0)
      break;

    // do the task
    for (i = 0; i < taskArr.length; i += 2) {
      index = taskArr[i];
      color = taskArr[i + 1];
      floodArr[index] = color;
    }

    taskArr = [];
    alphaPoints = unFilledPoints;
    unFilledPoints = [];

  } while (loop <= maxDistance);
}

/**
 * Get the nearest N neighbours
 * and sort by distance.
 * @param n integer >=1
 */
function getNearestNeighbours(n) {
  var distArr = [];
  for (var i = (-1) * n; i <= n; i++) {
    for (var j = (-1) * n; j <= n; j++) {
      if (i == 0 && j == 0) {
        // exclued self
        continue;
      }
      var distance = Math.pow(i, 2) + Math.pow(j, 2);
      distArr.push({ x: i, y: j, dist: distance });
    }
  }

  distArr.sort(function (a, b) { return a.dist - b.dist; });

  return distArr;
}

function fillGapOld(ctx, floodFilled) {
  // create gap bitmap
  var gapImg = ctx.createImageData(floodFilled);
  var width = floodFilled.width;
  var height = floodFilled.height;
  var maxDistance = 2;
  var floodArr = new Uint32Array(floodFilled.data.buffer);
  var gapArr = new Uint32Array(gapImg.data.buffer);

  // Build a nearest points array based on (0, 0)
  // and sort it by it's distance to (0,0)
  var distArr = [];
  for (var i = (-1) * maxDistance; i <= maxDistance; i++) {
    for (var j = (-1) * maxDistance; j <= maxDistance; j++) {
      if (i != 0 && j != 0) {
        var distance = Math.pow(i, 2) + Math.pow(j, 2);
        distArr.push({ x: i, y: j, dist: distance });
      }
    }
  }

  distArr.sort(function (a, b) { return a.dist - b.dist; });

  // for every gap pixels in flood image, find the nearest colored pixel
  // and set as it's color.
  var i: number, j: number, x: number, y: number, px: number, py: number;
  for (i = 0; i < floodArr.length; i++) {
    if (floodArr[i] == 0) {
      x = i % width;
      y = Math.floor(i / width);
      /*
      var arr = distArr.map(function(obj){
        return {
          x : obj.x + x,
          y : obj.y + y
        }
      });
      */
      for (j = 0; j < distArr.length; j++) {
        var p = distArr[j];
        px = x + p.x;
        py = y + p.y;

        // if(p.x > 0 && p.x < width && p.y > 0 && p.y < height &&
        // floodArr[p.y*width+p.x] !=0 ) {
        if (px > 0 && px < width && py > 0 && py < height &&
          floodArr[py * width + px] != 0) {
          gapArr[y * width + x] = floodArr[py * width + px];
          break;
        }
      }
      // this is the gap
      // find nearest filled pixel
    }
  }

  // merge the gap bitmap with the flood bitmap
  for (var i = 0; i < floodArr.length; i++) {
    if (floodArr[i] == 0) {
      floodArr[i] = gapArr[i];
    }
  }
}

/**
 *  Copy image data from src
 *  @param ctx  canvas context
 *  @param src  source image data.
 */
function copyImage(ctx, src) {
  var dest = ctx.createImageData(src);
  var srcArr = new Uint32Array(src.data.buffer);
  var destArr = new Uint32Array(dest.data.buffer);
  for (var i = 0; i < srcArr.length; i++) {
    destArr[i] = srcArr[i];
  }
  return dest;
}

/**
 * Create canvas context by specified dimension.
 */
function createContext(width, height) {
  return createCanvas(width, height).getContext('2d');
}

/**
 * Create canvas
 * @param width
 * @param height
 */
function createCanvas(width, height) {
  var canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  return canvas;
}

function randomUniqueColor() {
  do {
    var color = randomColor();
    if (!_colors[color]) {
      _colors[color] = color;
      return color;
    }
  } while (1)
  return randomColor();
}

function randomColor() {
  var color = new Uint8Array([0, 0, 0, 255]);
  for (var i = 0; i < 3; i++) {
    color[i] = Math.floor(Math.random() * 256);
  }
  var u32 = new Uint32Array(color.buffer);
  return u32[0];
}

function emptyColor() {
  var color = new Uint8Array([255, 255, 255, 255]);
  var a = new Uint32Array(color.buffer);
  return a[0];
}


// 判断两个矩形是否相交
function checkIntersect(rectA: Rect, rectB: Rect): boolean {
  let nonIntersect = 
    (rectB.right < rectA.left) || 
    (rectB.left > rectA.right) ||
    (rectB.bottom < rectA.top) ||
    (rectB.top > rectA.bottom);
  
  let intersect = !nonIntersect;
  return intersect;
}
