import { Lab } from "../color/color";
import FillArea from "./fillarea";
import { AreaMap } from "./interfaces";
import Utils from "./utils";

interface ColorInfo {
  color: number;
  lab: Lab;
}

/**
 * 根据上色图生成map图
 */
export default class ColoredMap {
  page: HTMLImageElement;
  map: HTMLImageElement;
  colored: HTMLImageElement;

  mapData: ImageData;
  mapPixels: Uint32Array;
  pageData: ImageData;
  pagePixels: Uint32Array;
  coloredData: ImageData;
  coloredPixels: Uint32Array;

  newMapData: ImageData;
  newMapPixels: Uint32Array;

  /**
   * 
   * @param page 线稿图
   * @param map 原map图
   * @param colored 上色图
   */
  constructor(page: HTMLImageElement, map: HTMLImageElement, colored: HTMLImageElement) {
    this.page = page;
    this.map = map;
    this.colored = colored;

    this.mapData = Utils.getImageData(this.map);
    this.mapPixels = new Uint32Array(this.mapData.data.buffer);
    this.pageData = Utils.getImageData(this.page);
    this.pagePixels = new Uint32Array(this.pageData.data.buffer);
    this.coloredData = Utils.getImageData(this.colored);
    this.coloredPixels = new Uint32Array(this.coloredData.data.buffer);

    this.newMapData = Utils.getImageData(this.map);
    this.newMapPixels = new Uint32Array(this.newMapData.data.buffer);

    //makesure colored no alpha
    let pixels = new Uint8Array(this.coloredPixels.buffer);
    let count = 0;
    for (let i = 0; i < this.coloredPixels.length; i++) {
      if (pixels[i * 4 + 3] != 0xff) {
        console.log(this.coloredPixels[i].toString(16) + " " + pixels[i * 4 + 3]);
        count++;
      }
      pixels[i * 4 + 3] = 0xff;
    }
    console.log("alpha count: " + count);

    this.process();
  }

  get width() { return this.page.width }
  get height() { return this.page.height }

  /**
   * 根据上色图解析重新生成map
   * 用于线稿图线条不完整（未囊括所有区块，如过渡部分的阴影并没有画线条），将根据上色图重新生成map，补充遗留区块
   * 算法：
   * 1. 拷贝出一张新的map图, 先将线条的部分置为0（线条部分暂不参与子区块的floodfill运算）
   * 2. 针对map的每一个区块，遍历找到新的颜色点，对该颜色点做floodfill操作（注：跟该点色值相同的周边颜色，即是floodfill的条件，但实测发现周边色值十分复杂，存在大量微小变化，因此这里实际是取一个色值范围）
   * 3. 接着对做完floodfill的新的map图做mergearea操作, 合并较小区块 
   * 4. 对子区块（原属同一个大区块里的）根据颜色值做merge操作（意思是两个子区块如果代表颜色十分接近甚至相同，应该合并。实测发现很有必要）
   */
  process() {
    let floodColorDist = 8;  // 重要！允许flood的最大色差范围。调小了区块连不成片，在后续merge操作可能就被吃掉了；调大了有可能肉眼可见两个色差不大的区块直接flood就连在一起了
    let areaMap = this.createAreaMap(this.newMapData);
    let areas: FillArea[] = Object.values(areaMap);
    console.log("area count: " + areas.length);
    let ai, len, aw, i, x, y;
    let floodCount, totalCount = 0;

    console.time("process");

    //cut the new map with page.
    for (let i = 0; i < this.newMapPixels.length; i++) {
      if (this.pagePixels[i] != 0) this.newMapPixels[i] = 0;
    }

    // 统计各区块的颜色， 先做一轮净化
    console.time("purify colored");
    areas.forEach(area => {
      let colorList = this.selectAreaColors(area);
      this.purifyColoredArea(area, colorList);
    })
    console.timeEnd("purify colored");

    console.time("area floodfill");
    areas.forEach(area => {
      console.log(area);
      floodCount = 0;
      aw = area.width();
      len = area.width() * area.height();
      for (ai = 0; ai < len; ai++) {
        x = area.left + ai % aw;
        y = area.top + Math.floor(ai / aw);
        i = y * this.width + x;
        if (this.newMapPixels[i] == area.color && this.newMapPixels[i] != 0) {
          // find a new diffrent color, need to floodfill
          let srcColor = this.coloredPixels[i];
          let dstColor = this.randomColor();
          this.floodFill(area, i, srcColor, dstColor);
          floodCount++;
          totalCount++;
        }
      }
      console.log("area: " + area.color + " " + area.count + " floodfill done! floodCount=" + floodCount);
    })
    console.log("map floodfill finish. areas count: " + areas.length + " total flood count: " + totalCount);
    console.timeEnd("area floodfill");

    console.time("mergeAreas");
    this.mergeAreas(this.newMapData);
    console.timeEnd("mergeAreas");

    // merge一轮过后，消灭10个点以内的小区块，这些区块一般都是没有邻居的孤立小点
    console.time("removeSmallArea");
    this.removeSmallArea(this.newMapData, 10);
    console.timeEnd("removeSmallArea");

    // fill alpha gap
    console.time('fillAlphaGap');
    this.fillAlphaGap(this.pageData, this.newMapData, 50);
    console.timeEnd('fillAlphaGap');

    console.time("mergeAreas");
    this.mergeAreas(this.newMapData);
    console.timeEnd("mergeAreas");

    console.time("fillEmptyGap");
    this.fillEmptyGap(this.newMapData, 10);
    console.timeEnd("fillEmptyGap");

    // // do merge again after fillEmptyGap
    // console.time("mergeAreas again");
    // this.mergeAreas(this.newMapData);
    // console.timeEnd("mergeAreas again");


    console.timeEnd("process");

  }

  getAreaCount(): number {
    let hash: any = {};
    for (let i = 0; i < this.newMapPixels.length; i++) {
      hash[this.newMapPixels[i]] = 1;
    }
    return Object.keys(hash).length;
  }

  /**
* 输出图片
* @returns 
*/
  toImage(): Promise<HTMLImageElement> {
    let canvas: HTMLCanvasElement = Utils.createCanvas(this.width, this.height);
    let ctx: CanvasRenderingContext2D = canvas.getContext('2d');
    ctx.putImageData(this.newMapData, 0, 0);
    return new Promise((done, reject) => {
      let img = new Image();
      img.onload = function () { done(img); };
      img.onerror = reject;
      img.src = canvas.toDataURL('image/png');
    });
  }

  /**
   * 输出文件
   * @returns 
   */
  toBlob(): Promise<Blob> {
    let canvas: HTMLCanvasElement = Utils.createCanvas(this.width, this.height);
    let ctx: CanvasRenderingContext2D = canvas.getContext('2d');
    ctx.putImageData(this.newMapData, 0, 0);
    return new Promise((done, reject) => {
      canvas.toBlob((blob: Blob) => done(blob), 'image/png');
    });
  }


  /**
   * 创建map, 输出文件
   * @param page 线稿图
   * @param map 区块图
   * @param colored 上色图
   * @returns 
   */
  static createMapBlob(page: HTMLImageElement, map: HTMLImageElement, colored: HTMLImageElement): Promise<Blob> {
    return new ColoredMap(page, map, colored).toBlob();
  }

  /**
   * 创建MAP， 输出图片
   * @param page 线稿图
   * @param map 区块图
   * @param colored 上色图
   * @returns 
   */
  static createMapImage(page: HTMLImageElement, map: HTMLImageElement, colored: HTMLImageElement): Promise<HTMLImageElement> {
    return new ColoredMap(page, map, colored).toImage();
  }

  /**
   * 选取区块代表性颜色
   * @param area 
   * @returns 
   */
  selectAreaColors(area: FillArea) {
    let minCount = 40;
    let percent = 0.1;
    let minColorDist = 4;
    let pixels = this.newMapPixels;
    let width = this.width;
    let ai, aw, i, j, x, y;
    let sum = {};
    aw = area.width();
    let count = area.width() * area.height();
    for (ai = 0; ai < count; ai++) {
      x = area.left + ai % aw;
      y = area.top + Math.floor(ai / aw);
      i = y * width + x;
      if (pixels[i] == area.color) {
        let color = this.coloredPixels[i];
        if (!sum[color]) sum[color] = 1;
        else sum[color] = sum[color] + 1;
      }
    }
    let colors = Object.keys(sum).sort((a, b) => { return sum[b] - sum[a] });
    let colorList;
    let firstColor = colors[0];
    // 初筛一轮
    colors = colors.filter((c, i) => {
      if (i == 0) return true; // always return the first one
      return (sum[c] > minCount || (sum[c] / area.count) > percent || (sum[c] / sum[firstColor] > 0.2))
    })
    // 再筛一轮，去掉比较接近的颜色
    colorList = colors.map(c => {
      let cInt = parseInt(c);
      return { color: cInt, lab: Lab.fromRGBA(cInt) };
    })
    for (i = 0; i < colorList.length; i++) {
      for (j = i + 1; j < colorList.length; j++) {
        if (colorList[i].lab.deltaE(colorList[j].lab) < minColorDist) {
          colorList.splice(j, 1);
          j--;
        }
      }
    }

    console.log("area: " + area.color, + " " + area.count + " total colors:" + Object.keys(sum).length + " select colors:" + colorList.length);
    return colorList;
  }

  /**
   * 对colored图指定区块范围按给定颜色进行净化，其他杂色都汇聚到主流颜色去
   * @param area 
   * @param colors 
   */
  purifyColoredArea(area: FillArea, colorList: ColorInfo[]) {
    let pixels = this.newMapPixels;
    let coloredPixels = this.coloredPixels;
    let width = this.width;
    let ai, aw, i, x, y;
    aw = area.width();
    let count = area.width() * area.height();
    for (ai = 0; ai < count; ai++) {
      x = area.left + ai % aw;
      y = area.top + Math.floor(ai / aw);
      i = y * width + x;
      if (pixels[i] == area.color) {
        let color = coloredPixels[i];
        coloredPixels[i] = this.getCloserColor(color, colorList);
      }
    }
  }

  /**
   * 从colorList列表中获取与color最为接近的颜色
   * @param color 
   * @param colorList 
   * @returns 
   */
  getCloserColor(color: number, colorList: ColorInfo[]) {
    if (colorList.length == 1) return colorList[0].color;
    for (let i = 0; i < colorList.length; i++) {
      if (color == colorList[i].color) {
        return color;
      }
    }

    let close = colorList[0].color;
    let dist, minDist = 99999;

    let colorLab = Lab.fromRGBA(color);
    colorList.forEach(c => {
      dist = colorLab.deltaE(c.lab);
      if (dist < minDist) {
        close = c.color;
        minDist = dist;
      }
    })

    return close;
  }

  /**
   * 对指定区块的指定color做floodFill操作
   * @param area 区块area
   * @param index 起始下标
   * @param srcColor 对应在colored上色图中的颜色 
   * @param dstColor 涂到新的mapPixels中的颜色
   * @param maxColorDist
   * @returns 
   */
  floodFill(area: FillArea, index: number, srcColor: number, dstColor: number) {
    let width = this.width;
    let height = this.height;
    let mapPixels = this.newMapPixels;
    let coloredPixels = this.coloredPixels;

    let stack = new Array();
    let maxStack = 0;

    if (coloredPixels[index] == srcColor) {
      stack.push(index);
    }

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

      // if p is colored, continue
      if (mapPixels[p] == dstColor)
        continue;

      mapPixels[p] = dstColor;
      // left
      if (x > 0 && mapPixels[p - 1] == area.color
        && coloredPixels[p - 1] == srcColor) {
        stack.push(p - 1);
      }
      // top
      if (p > (width - 1) && mapPixels[p - width] == area.color
        && coloredPixels[p - width] == srcColor) {
        stack.push(p - width);
      }
      // right
      if (x < (width - 1) && mapPixels[p + 1] == area.color
        && coloredPixels[p + 1] == srcColor) {
        stack.push(p + 1);
      }
      // bottom
      if (p < ((height - 1) * width) && mapPixels[p + width] == area.color
        && coloredPixels[p + width] == srcColor) {
        stack.push(p + width);
      }

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

    return maxStack;
  }


  /**
   * 对指定区块的指定color做floodFill操作, 返回被floodfill的点个数
   * @param area 区块area
   * @param index 起始下标
   * @param srcColor 对应在colored上色图中的颜色 
   * @param dstColor 涂到新的mapPixels中的颜色
   * @param maxColorDist
   * @returns 
   */
  areaFloodFill(area: FillArea, index: number, srcColor: number, dstColor: number, maxColorDist?: number) {
    let width = this.width;
    let height = this.height;
    let mapPixels = this.newMapPixels;
    let coloredPixels = this.coloredPixels;
    maxColorDist = maxColorDist || 8;  // 此数值至关重要

    let stack = new Array();
    let maxStack = 0;

    if (coloredPixels[index] == srcColor) {
      stack.push(index);
    }

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

      // if p is colored, continue
      if (mapPixels[p] == dstColor)
        continue;

      mapPixels[p] = dstColor;
      // left
      if (x > 0 && mapPixels[p - 1] == area.color
        && this.colorDistance(coloredPixels[p - 1], srcColor) < maxColorDist) {
        stack.push(p - 1);
      }
      // top
      if (p > (width - 1) && mapPixels[p - width] == area.color
        && this.colorDistance(coloredPixels[p - width], srcColor) < maxColorDist) {
        stack.push(p - width);
      }
      // right
      if (x < (width - 1) && mapPixels[p + 1] == area.color
        && this.colorDistance(coloredPixels[p + 1], srcColor) < maxColorDist) {
        stack.push(p + 1);
      }
      // bottom
      if (p < ((height - 1) * width) && mapPixels[p + width] == area.color
        && this.colorDistance(coloredPixels[p + width], srcColor) < maxColorDist) {
        stack.push(p + width);
      }

      // left top
      if (x > 0 && p > (width - 1) && mapPixels[p - width - 1] == area.color
        && this.colorDistance(coloredPixels[p - width - 1], srcColor) < maxColorDist) {
        stack.push(p - width - 1);
      }
      // right top
      if (x < (width - 1) && p > (width - 1) && mapPixels[p - width + 1] == area.color
        && this.colorDistance(coloredPixels[p - width + 1], srcColor) < maxColorDist) {
        stack.push(p - width + 1);
      }
      // left bottom
      if (x > 0 && p < ((height - 1) * width) && mapPixels[p + width - 1] == area.color
        && this.colorDistance(coloredPixels[p + width - 1], srcColor) < maxColorDist) {
        stack.push(p + width - 1);
      }
      // rigth bottom
      if (x < (width - 1) && p < ((height - 1) * width) && mapPixels[p + width + 1] == area.color
        && this.colorDistance(coloredPixels[p + width + 1], srcColor) < maxColorDist) {
        stack.push(p + width + 1);
      }
    }
    // console.log('floodFill@maxStack', maxStack / (width*height));

    return maxStack;
  }




  /**
   * 合并小区快
   * @param imgdata 
   * @param threshold 
   */
  mergeAreas(imgdata: ImageData, threshold?: number) {
    const kk = (this.width / 1000) * 20;
    threshold = threshold || kk;
    let areaMap = this.createAreaMap(imgdata);
    let keys = Object.keys(areaMap);
    let floodArr = new Uint32Array(imgdata.data.buffer);
    let width = imgdata.width;
    let height = imgdata.height;
    let areas = keys.map(function (key) { return areaMap[key]; });
    let smallAreas = areas.filter(function (a) { return a.count < threshold; });

    console.log('mergeAreas@areaSize', areas.length, smallAreas.length);
    let i, j, x, y, index, p, px, py;
    let neighbours = this.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);
          }
        }
      }
    }

    let loop = 0;
    do {
      loop++;

      smallAreas.forEach(function (area) {
        if (area.merged)
          return;
        let neighbour;
        for (let 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 small area with alpha
   * @param imgdata 
   * @param threshold 
   */
  removeSmallArea(imgdata: ImageData, threshold?: number) {
    threshold = threshold || 8;
    let width = imgdata.width;
    let height = imgdata.height;
    let floodArr = new Uint32Array(imgdata.data.buffer);
    let areaMap = this.createAreaMap(imgdata);
    let keys = Object.keys(areaMap);
    let areas = keys.map(function (key) { return areaMap[key]; });
    let smallAreas = areas.filter(function (a) { return a.count < threshold; });
    let i, j, x, y, index, p, px, py;
    let neighbours = this.getNearestNeighbours(1);

    console.log("total areas count: " + areas.length + " smallAreas count:" + smallAreas.length);

    smallAreas.forEach(function (area) {
      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) {
            floodArr[index] = 0;  // 直接置为0，等同线条处理, 也可以考虑结合以下注释掉的代码，只remove没有neighbour的孤立区块
            // 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) {
            //     neighbourSet.add(areaMap[floodArr[index]]);
            //   }
            // }
          }
        }
      }
      // if (neighbourSet.size <= 0) { // 没有邻居，孤立小区块
      //   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) {
      //         floodArr[index] = 0;  // 直接置为0，等同线条处理, 也可以考虑结合以下注释掉的代码，只remove没有neighbour的孤立区块
      //       }
      //     }
      //   }
      // }
    });

  }

  /**
   * 按照中心点半径合并区块
   * @param imgdata 
   * @param threshold 
   */
  mergeAreasByRudias(imgdata: ImageData, threshold?: number) {
    threshold = threshold || 4;
    let areaMap = this.createAreaMap(imgdata);
    let keys = Object.keys(areaMap);
    let floodArr = new Uint32Array(imgdata.data.buffer);
    let width = imgdata.width;
    let height = imgdata.height;
    let areas = keys.map(function (key) { return areaMap[key]; });
    // 计算每个区块的中心点半径
    areas.forEach(area => {
      area.radius = this.findAreaCenter(area).radius;
    })
    let smallAreas = areas.filter(function (a) { return a.radius < threshold; });

    console.log('mergeAreasByRudias@areaSize', areas.length, smallAreas.length);

    let i, j, x, y, index, p, px, py;
    let neighbours = this.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 &&
                areaMap[floodArr[index]] &&
                floodArr[index] != 0 && floodArr[index] != area.color) {
                // area.neighbours.push(subAreaMap[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; });

    /**
     * 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);
          }
        }
      }
    }

    let loop = 0;
    do {
      loop++;

      smallAreas.forEach(function (area) {
        if (area.merged)
          return;
        let neighbour;
        for (let 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);
        }
        // area.neighbours = area.neighbours.sort((a, b) => {return a.count - b.count});
        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('mergeAreasByRudias@loop:' + loop, smallAreas.length);

      if (smallAreas.length == 0)
        break;

    } while (loop < 4);

  }

  /**
     * 合并原区块中代表颜色是一致的相邻区块
     * @param imgdata 
     */
  mergeAreasByColor(imgdata: ImageData, maxColorDist?: number) {
    maxColorDist = maxColorDist || 4;
    let originMapPixels = new Uint32Array(Utils.getImageData(this.map).data.buffer);
    let areaMap = this.createAreaMap(imgdata);
    let keys = Object.keys(areaMap);
    let floodArr = new Uint32Array(imgdata.data.buffer);
    let width = imgdata.width;
    let height = imgdata.height;
    let areas = keys.map(function (key) { return areaMap[key]; });

    let neighbours = this.getNearestNeighbours(1);
    let j, x, y, index, p, px, py;

    areas.forEach(area => {
      area.mainColor = this.getColorFromColored(area);
    })

    areas.forEach(area => {
      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) {
            let originColor = originMapPixels[index];
            // check pixels 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
                // && this.pagePixels[index] == 0 // 同属原来的大区块
                && originColor == originMapPixels[index]) {
                // area.neighbours.push(areaMap[floodArr[index]]);
                neighbourSet.add(areaMap[floodArr[index]]);
              }
            }
          }
        }
      }
      area.neighbours = Array.from(neighbourSet).filter((a: any) => { return this.colorDistance(a.mainColor, area.mainColor) < maxColorDist });
    })

    let smallAreas = areas.filter(function (a) { return a.neighbours.length > 0; });


    console.log('mergeAreasByColor@areaSize', areas.length, smallAreas.length);

    /**
     * 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);
          }
        }
      }
    }

    let loop = 0;
    do {
      loop++;

      smallAreas.forEach(function (area) {
        if (area.merged)
          return;
        let neighbour;
        for (let 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('mergeAreasByColor@loop:' + loop, smallAreas.length);

      if (smallAreas.length == 0)
        break;

    } while (loop < 4);
  }


  /**
   * 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
   */
  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 = this.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 ) {
          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);
  }


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

    let neighbours = this.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);
    let loop = 0;

    do {
      loop++;

      // loop all alpha points and try to fill.
      let 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);
  }


  /**
  * Create area map from map pixels.
  */
  createAreaMap(imgdata: ImageData): AreaMap {
    let width = imgdata.width;
    let floodArr = new Uint32Array(imgdata.data.buffer);
    let areaMap: AreaMap = {};
    let i, x, y, color;
    for (i = 0; i < floodArr.length; i++) {
      color = floodArr[i];
      // if (color == 0) continue;
      x = i % width;
      y = Math.floor(i / width);
      if (areaMap[color]) {
        areaMap[color].addPoint(x, y);
      } else {
        areaMap[color] = new FillArea(color, x, y);
      }
    }
    return areaMap;
  }

  /**
  * Create subarea map from area.
  */
  createSubAreaMap(area: FillArea): AreaMap {
    let orignalMapPixels = new Uint32Array(Utils.getImageData(this.map).data.buffer);
    let width = this.width;
    let floodArr = this.newMapPixels;
    let areaMap: AreaMap = {};
    let i, x, y, color;
    for (i = 0; i < floodArr.length; i++) {
      if (orignalMapPixels[i] != area.color) continue;
      color = floodArr[i];
      x = i % width;
      y = Math.floor(i / width);
      if (areaMap[color]) {
        areaMap[color].addPoint(x, y);
      } else {
        areaMap[color] = new FillArea(color, x, y);
      }
    }
    return areaMap;
  }

  /**
   * Finder center of area.
   * @param {FillArea} area 
   */
  findAreaCenter(area) {

    console.log('area', area);

    let ai, ax, ay, aw, ah, i, x, y;
    aw = area.width();
    ah = area.height();

    let areaPixels = new Uint32Array(aw * ah);

    for (ai = 0; ai < areaPixels.length; ai++) {
      ax = ai % aw;
      ay = (ai - ax) / aw;
      x = area.left + ax;
      y = area.top + ay;
      i = y * this.width + x;
      if (this.newMapPixels[i] == area.color) {
        areaPixels[ai] = this.newMapPixels[i];
      } else {
        areaPixels[ai] = 0;
      }
    }


    //find contours
    let flag, pixel;
    let contours = [];
    for (ax = 0; ax < aw; ax++) {
      flag = 0;
      for (ay = 0; ay < ah; ay++) {
        ai = ay * aw + ax;
        pixel = areaPixels[ai];
        if (!flag && pixel) {
          flag = 1;
          contours.push(ai);
        } else if (flag && !pixel) {
          flag = 0;
          contours.push(ai - aw);
        } else if (flag && pixel) continue;
        else if (!flag && !pixel) continue;
      }
      if (flag) {
        //last point
        contours.push(ai);
      }
    }

    for (ay = 0; ay < ah; ay++) {
      flag = 0;
      for (ax = 0; ax < aw; ax++) {
        ai = ay * aw + ax;
        pixel = areaPixels[ai];
        if (!flag && pixel) {
          flag = 1;
          contours.push(ai);
        } else if (flag && !pixel) {
          flag = 0;
          contours.push(ai - 1);
        } else if (flag && pixel) continue;
        else if (!flag && !pixel) continue;
      }
      if (flag) {
        //last point
        contours.push(ai);
      }
    }



    let contoursHash = {}
    let contoursNew = [];
    for (i = 0; i < contours.length; i++) {
      ai = contours[i];
      if (contoursHash[ai]) continue;
      else {
        contoursNew.push(ai);
        contoursHash[ai] = true;
      }
    }
    console.log('contours size:', contours.length, contoursNew.length);
    contoursHash = null;
    contours = contoursNew;

    let contoursX = [];
    let contoursY = [];
    for (i = 0; i < contours.length; i++) {
      ai = contours[i];
      x = ai % aw;
      y = (ai - x) / aw;
      contoursX[i] = x;
      contoursY[i] = y;
    }


    //console.log('contoursXY', contoursX, contoursY);

    let minDist, maxDist, maxAi, dist, x1, y1, x2, y2, dx, dy, radius;
    maxDist = 0;
    for (ai = 0; ai < areaPixels.length; ai++) {
      pixel = areaPixels[ai];
      minDist = Number.MAX_VALUE;
      x1 = ai % aw;
      y1 = (ai - x1) / aw;
      if (pixel) {
        for (i = 0; i < contours.length; i++) {
          dx = x1 - contoursX[i];
          dy = y1 - contoursY[i];
          dist = dx * dx + dy * dy;
          if (dist < minDist) minDist = dist;
          //性能优化
          if (minDist < maxDist) break;
        }
        if (minDist > maxDist) {
          maxDist = minDist;
          maxAi = ai;
        }
      }

    }

    x = maxAi % aw;
    y = (maxAi - x) / aw;
    radius = Math.sqrt(maxDist)
    console.log('maxAi: ', maxAi, x, y, maxDist, radius);


    let gx = x + area.left;
    let gy = y + area.top;

    return {
      x: gx,
      y: gy,
      radius
    }

  }

  /**
   * 统计这个区域的所有像素的颜色，选出最具代表的颜色
   * @param area 
   * @returns 
   */
  getColorFromColored(area: FillArea): number {
    let ai, aw, i, x, y;
    let sum = {};
    aw = area.width();
    let count = area.width() * area.height();
    for (ai = 0; ai < count; ai++) {
      x = area.left + ai % aw;
      y = area.top + Math.floor(ai / aw);
      i = y * this.width + x;
      if (this.newMapPixels[i] == area.color && this.pagePixels[i] == 0) { // 排除线条点
        let color = this.coloredPixels[i];
        if (!sum[color]) sum[color] = 1;
        else sum[color] = sum[color] + 1;
      }
    }
    let maxColor: string;
    let maxCount = 0;
    Object.keys(sum).forEach((color: string) => {
      if (sum[color] > maxCount) {
        maxCount = sum[color];
        maxColor = color;
      }
    });
    //console.log('sum:', sum);
    let colorInt = parseInt(maxColor);

    return colorInt;
  }


  /**
 * Get the nearest N neighbours
 * and sort by distance.
 * @param n integer >=1
 */
  getNearestNeighbours(n) {
    let distArr = [];
    for (let i = (-1) * n; i <= n; i++) {
      for (let j = (-1) * n; j <= n; j++) {
        if (i == 0 && j == 0) {
          // exclued self
          continue;
        }
        let 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;
  }



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

  colorDistance(color1: number, color2: number) {
    let lab1 = Lab.fromRGBA(color1);
    let lab2 = Lab.fromRGBA(color2);
    let distance = Math.abs(lab1.deltaE(lab2));
    return distance;
  }


  /**
 * 按照大小(像素点个数)合并子区块（已废弃）
 * @param area 
 * @param threshold 
 */
  mergeSubAreasBySize(area, threshold?) {
    let width = this.width;
    let height = this.height;
    const kk = (this.width / 1000) * 20;
    threshold = threshold || kk;
    let floodArr = this.newMapPixels;
    let subAreaMap = this.createSubAreaMap(area);
    let keys = Object.keys(subAreaMap);
    let subAreas = Object.values(subAreaMap);
    let smallSubAreas = subAreas.filter(a => { return a.count < threshold; });
    console.log('mergeSubAreasBySize@areaSize', subAreas.length, smallSubAreas.length);

    let i, j, x, y, index, p, px, py;
    let neighbours = this.getNearestNeighbours(1);
    smallSubAreas.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 &&
                subAreaMap[floodArr[index]] &&
                floodArr[index] != 0 && floodArr[index] != area.color) {
                // area.neighbours.push(subAreaMap[floodArr[index]]);
                neighbourSet.add(subAreaMap[floodArr[index]]);
              }
            }
          }
        }
      }
      area.neighbours = Array.from(neighbourSet);
    });

    // remove the area has no neighbours.
    smallSubAreas = smallSubAreas.filter(function (area) { return area.neighbours.length > 0; });

    /**
     * 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);
          }
        }
      }
    }

    let loop = 0;
    do {
      loop++;

      smallSubAreas.forEach(function (area) {
        if (area.merged)
          return;
        let neighbour;
        for (let 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);
        }
        // area.neighbours = area.neighbours.sort((a, b) => {return a.count - b.count});
        if (area.neighbours.length > 0) {
          // merge me to the first large neighbour
          mergeToMe(area.neighbours[0], area);
        }
      });

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

      console.log('mergeSubAreasBySize@loop:' + loop, smallSubAreas.length);

      if (smallSubAreas.length == 0)
        break;

    } while (loop < 4);

  }

  /**
   * 按照中心点半径合并子区块（已废弃）
   * @param area 
   */
  mergeSubAreasByRadius(area, threshold?) {
    let width = this.width;
    let height = this.height;
    threshold = threshold || 4;
    let floodArr = this.newMapPixels;
    let subAreaMap = this.createSubAreaMap(area);
    let subAreas = Object.values(subAreaMap);
    // 计算每个区块的中心点半径
    subAreas.forEach(area => {
      area.radius = this.findAreaCenter(area).radius;
    })
    let smallSubAreas = subAreas.filter(a => { return a.radius < threshold });
    console.log('mergeSubAreasByRadius@areaSize', subAreas.length, smallSubAreas.length);

    let i, j, x, y, index, p, px, py;
    let neighbours = this.getNearestNeighbours(1);
    smallSubAreas.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 &&
                subAreaMap[floodArr[index]] &&
                floodArr[index] != 0 && floodArr[index] != area.color) {
                // area.neighbours.push(subAreaMap[floodArr[index]]);
                neighbourSet.add(subAreaMap[floodArr[index]]);
              }
            }
          }
        }
      }
      area.neighbours = Array.from(neighbourSet);
    });

    // remove the area has no neighbours.
    smallSubAreas = smallSubAreas.filter(function (area) { return area.neighbours.length > 0; });

    /**
     * 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);
          }
        }
      }
    }

    let loop = 0;
    do {
      loop++;

      smallSubAreas.forEach(function (area) {
        if (area.merged)
          return;
        let neighbour;
        for (let 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);
        }
        // area.neighbours = area.neighbours.sort((a, b) => {return a.count - b.count});
        if (area.neighbours.length > 0) {
          // merge me to the first large neighbour
          mergeToMe(area.neighbours[0], area);
        }
      });

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

      console.log('mergeSubAreasByRudias@loop:' + loop, smallSubAreas.length);

      if (smallSubAreas.length == 0)
        break;

    } while (loop < 4);

  }

  /**
   * just for test
   * @returns 
   */
  createColored2(): Promise<Blob> {
    let canvas: HTMLCanvasElement = Utils.createCanvas(this.width, this.height);
    let ctx: CanvasRenderingContext2D = canvas.getContext('2d');
    ctx.putImageData(this.coloredData, 0, 0);
    return new Promise((done, reject) => {
      canvas.toBlob((blob: Blob) => done(blob), 'image/png');
    });
  }

}