import Animator from '../common/animator';
import Easing from '../common/easing';
import Repeater from '../common/repeater';
import Utils from '../common/utils';
import ColorPickerTool from '../core/color-picker-tool';
import Layer from '../core/layer';
import Rect from '../core/rect';
import Tool from '../core/tool';
import BucketTool from './number-bucket-tool';
import EraseTool from './number-erase-tool';

import FillArea from '../common/fillarea';
import { FillTask, FILL_TYPE } from '../common/filltask';
import { Centers, ColorInfo, ColorMap, ColorSumItem } from '../common/interfaces';
import NumberEditTool from './number-edit-tool';
import { Lab, sRGB } from '../color/color';
import { generateOrderAuto, getColorOrder2, validateOrder } from '../common/color-order';
import NumberPlayTool from './number-play-tool';
import NumberSelectTool from './number-select-tool';
import { isThisISOWeek } from 'date-fns';
import { ThisReceiver } from '@angular/compiler';
import { NzMessageService } from 'ng-zorro-antd/message';

interface ToolHash {
  [toolKey: string]: NumberEditTool;
}

//interface NumberEditLayerConfig {
//  color?: string, //current color
//}

//const _defaults: NumberEditLayerConfig = {
//  color: '#006600'
//}

const TAG = 'NumberEditLayer';

export default class NumberEditLayer extends Layer {
  message: NzMessageService;
  output: HTMLCanvasElement;
  outputCtx: CanvasRenderingContext2D;
  areaOutput: HTMLCanvasElement;
  areaOutputCtx: CanvasRenderingContext2D;
  areaMask: HTMLCanvasElement;
  areaMaskCtx: CanvasRenderingContext2D;
  fastfillOutput: HTMLCanvasElement;
  fastfillCtx: CanvasRenderingContext2D;
  fasteraseOutput: HTMLCanvasElement;
  fasteraseCtx: CanvasRenderingContext2D;
  areaPixels: ImageData;
  animator: Animator;
  aniMask: HTMLCanvasElement;
  aniMaskCtx: CanvasRenderingContext2D;
  aniMaskImage: HTMLImageElement;
  aniMaskCanvas: HTMLCanvasElement;
  play: HTMLCanvasElement;
  playCtx: CanvasRenderingContext2D;
  playIdx: number;
  unorderMask: HTMLCanvasElement;
  unorderMaskCtx: CanvasRenderingContext2D;
  // 编排模式下，不展示未排序颜色
  private _showUnorderMask: boolean = false;
  public get showUnorderMask(): boolean {
    return this._showUnorderMask;
  }
  public set showUnorderMask(value: boolean) {
    this._showUnorderMask = value;
  }
  mapData: ImageData;
  map: HTMLImageElement;
  scale: number;
  mapPixels: Uint32Array;
  undoList: any[] = [];
  redoList: any[] = [];
  cacheList: any[] = [];
  areaMap: any = [];
  areaList: any[];
  aniTask: any;
  aniRadius: number;
  fastfillTask: FillTask;
  fastfillAreas: {};
  fasteraseTask: FillTask;
  fasteraseAreas: {};
  brushTask: any;
  brushPendingPoints: any;
  brushBrush: any;
  brushLastPoint: any;
  brushDist: number;
  label: HTMLImageElement;

  //tools: Tool[] = []; //layer支持的所有工具列表

  //public config: NumberEditLayerConfig = { ..._defaults }
  colorMap: ColorMap;
  colorOrder: number[] = [];
  centers: Centers; // 中心点
  page: HTMLImageElement;
  raw: HTMLImageElement;

  uncoloredAreas: FillArea[];
  colorSum: ColorSumItem[] = [];
  ordered: ColorSumItem[] = [];
  unordered: ColorSumItem[] = [];
  progress: any = {};

  private _canAuto: boolean = false;  // 是否可以展示自动排序按钮==>取决于中心点是否预备完毕
  public get canAuto(): boolean {
    return this._canAuto;
  }
  public set canAuto(value: boolean) {
    this._canAuto = value;
  }


  override get defaultToolKey(): string { return 'pan'; }

  private _color: string;
  private _colorInt: number;

  public get colorInt(): number { return this._colorInt; }
  public set colorInt(value: number) {
    this._colorInt = value;
    this._color = Utils.getColorFromInteger(this._colorInt);
  }

  public get color(): string { return this._color; }
  public set color(value: string) {
    this._color = value;
    this._colorInt = Utils.getColorInteger(this._color);
  }

  private _hideLine = false;
  public get hideLine() { return this._hideLine; }
  public set hideLine(value) { this._hideLine = value; this.invalidate() }

  private _mystery = false;
  public get mystery() { return this._mystery; }
  public set mystery(value) { this._mystery = value; this.invalidate() }

  /**
   * @param page original image.
   * @param map map image.
   * @param output  output image.
   */
  constructor(message: NzMessageService, page: HTMLImageElement, map: HTMLImageElement, colorMap: any,
    colorOrder: number[], centers: Centers, raw?: HTMLImageElement) {
    super(page.width, page.height);

    this.message = message;

    this.colorMap = colorMap || {};
    this.colorOrder = colorOrder?.filter((item, index, arr) => arr.indexOf(item, 0) == index);  // 去重

    this.centers = centers;
    this.page = page;
    this.map = map;
    this.raw = raw;

    this.init();

    this.color = '#aa0000';

    this.addTool(new NumberSelectTool(this));
    this.addTool(new NumberPlayTool(this));
    this.addTool(new ColorPickerTool());
    this.addTool(new BucketTool(this));
    this.addTool(new EraseTool(this));

  }

  init() {

    this.output = Utils.createCanvas(this.width, this.height);
    this.outputCtx = this.output.getContext('2d');
    this.outputCtx.fillStyle = 'white';
    this.outputCtx.fillRect(0, 0, this.width, this.height);

    this.scale = this.map.width / this.page.width;
    this.mapData = Utils.getImageData(this.map);
    this.mapPixels = new Uint32Array(this.mapData.data.buffer);
    this.createAreaMap();

    this.drawToOutput();

    // undo redo support.
    this.undoList = [];
    this.redoList = [];
    this.cacheList = [];

    console.log('FillerLayer@width=', this.width, ' height=', this.height,
      ' scale=', this.scale);


    this.areaOutput = Utils.createCanvas(this.width, this.height);
    this.areaOutputCtx = this.areaOutput.getContext('2d');
    this.areaMask = Utils.createCanvas(this.width, this.height);
    this.areaMaskCtx = this.areaMask.getContext('2d');

    this.fastfillOutput = Utils.createCanvas(this.width, this.height);
    this.fastfillCtx = this.fastfillOutput.getContext('2d');

    this.fasteraseOutput = Utils.createCanvas(this.width, this.height);
    this.fasteraseCtx = this.fasteraseOutput.getContext("2d");

    this.play = Utils.createCanvas(this.width, this.height);
    this.playCtx = this.play.getContext('2d');
    this.playCtx.fillStyle = 'white';
    this.playCtx.fillRect(0, 0, this.width, this.height);
    this.playIdx = 0;

    this.unorderMask = Utils.createCanvas(this.width, this.height);
    this.unorderMaskCtx = this.unorderMask.getContext('2d');
    // this.showUnorderMask = false;

    this.areaPixels =
      this.areaOutputCtx.createImageData(this.width, this.height);


    // For animation
    this.animator = new Animator(0, 1);
    this.animator.setEasing(Easing.easeInQuad);
    this.animator.setDuration(1000);

    this.animator.on('animationUpdate', animator => {
      this.onAnimationUpdate(animator)
    });

    this.aniMask = Utils.createCanvas(this.width, this.height);
    this.aniMaskCtx = this.aniMask.getContext('2d');

    // map图修改后可能导致colorMap发生变化, 在此重新刷新colorMap
    let colorMapChange = this.validateColorMap();

    // 没有colorOrder，算一个默认的colorOrder出来
    if (!this.colorOrder) {
      let orderAuto = generateOrderAuto(this.colorMap, this.centers);
      this.colorOrder = getColorOrder2(this.colorMap, this.centers, this.colorOrder, orderAuto);
      if (!this.colorOrder) { // 获取不到可用order，可能是没有生成中心点，这个时候根据colorMap出现的颜色顺序定个order
        this.colorOrder = Array.from(new Set(Object.values(this.colorMap).map((ci: ColorInfo) => ci.color)));
      }
    }
    let colorOrderChange = this.updateColorOrder();

    this.uncoloredAreas = this.areaList.filter(area => !this.colorMap[area.color])
      .sort((a: FillArea, b: FillArea) => {
        return a.count - b.count;
      });
    this.colorSum = this.getColorSum();
    this.updateOutputList();
    this.updateUnorderMask(true);
    this.progress = this.getProgress();

    if (colorMapChange || colorOrderChange) {
      console.log("number-edit-layer init colorMap or colorOrder change!");
      this.editUpdate();
    }

    // 尚未生成中心点, 启动web worker计算中心点
    if (!this.centers || Object.keys(this.centers).length <= 0) {
      this.canAuto = false;
      this.startWorker();
    } else {
      this.canAuto = true;
    }
  }

  /**
   * 启动webworker执行计算中心点的耗时任务
   */
  private startWorker() {
    if (typeof Worker === 'undefined') return;
    try {
      const worker = new Worker(new URL('../common/centers.worker.ts', import.meta.url));
      console.log("create centers webworker!!!");
      worker.onmessage = (result) => {
        console.log("get worker result:" + JSON.stringify(result.data));
        this.centers = result.data;
        this.canAuto = true;
        this.editUpdate(); // 更新下中心点
        worker.terminate();
      };
      worker.onerror = function (event) {
        console.error("webworker error:" + event.message);
        worker.terminate();
      };
      let pagePixels = new Uint32Array(Utils.getImageData(this.page).data.buffer);
      let mapPixels = new Uint32Array(Utils.getImageData(this.map).data.buffer);
      worker.postMessage({ pagePixels: pagePixels, mapPixels: mapPixels, width: this.width, height: this.height });
    } catch (e) {
      console.log(e.message);
    }
  }

  /**
   * 更新map
   * @param map 
   * @param triggerUpdate 初始化加载为false，如果是修改map， 则为true
   */
  setMap(map: HTMLImageElement) {
    this.map = map;
    this.editUpdate();
    this.invalidate();
  }

  /**
   * 
   * @param page 
   * @param map 
   * @param colorMap 
   * @param raw 
   */
  update(page?: HTMLImageElement, map?: HTMLImageElement, colorMap?: any,
    colorOrder?: number[], raw?: HTMLImageElement, centers?: Centers) {
    this.page = page || this.page;
    this.map = map || this.map;
    this.colorMap = colorMap || this.colorMap;
    this.colorOrder = colorOrder || this.colorOrder;
    this.raw = raw || this.raw;
    this.centers = centers || this.centers;
    if (map && !centers) { // map图有变更， 重新计算中心点
      this.centers = null;
    }
    this.init();

    this.editUpdate();
    this.invalidate();
  }

  updateColorMap(colorMap) {
    this.backupOutput();
    this.colorMap = colorMap;
    // todo : ajust colorOrder after color merge
    this.updateColorOrder()
    this.drawToOutput();
    this.updateUnorderMask();
    this.editUpdate();
    this.invalidate();
  }

  drawToOutput() {
    console.time('drawToOutput');
    this.outputCtx.fillStyle = 'white';
    this.outputCtx.fillRect(0, 0, this.width, this.height);

    //fast create output by map
    let outData = this.outputCtx.getImageData(0, 0, this.output.width, this.output.height)
    let outPixels = new Uint32Array(outData.data.buffer);
    let areaIndex;
    for (var i = 0; i < outPixels.length; i++) {
      areaIndex = this.mapPixels[i];
      if (this.colorMap[areaIndex]) {
        outPixels[i] = this.colorMap[areaIndex].color;
      }
    }
    this.outputCtx.putImageData(outData, 0, 0);
    console.timeEnd('drawToOutput');
  }


  lastUnorderColorMap: ColorMap = {};
  /**
   * 刷新unorderMask，注意性能
   * 经过优化，可放心调用
   * @param force: 强制全局刷新
   */
  updateUnorderMask(force: boolean = false) {
    console.time('updateUnorderMask');

    //获取未排序部分的colorMap
    let curUorderColorMap: ColorMap = {};
    Object.keys(this.colorMap).forEach(areaIndex => {
      if (this.colorOrder.indexOf(this.colorMap[areaIndex].color) < 0) {
        curUorderColorMap[areaIndex] = this.colorMap[areaIndex];
      }
    })

    // 比较当前curUorderColorMap和lastUnorderColorMap的差异
    let areaIdxSet = new Set(Object.keys(curUorderColorMap).concat(Object.keys(this.lastUnorderColorMap)));
    let diffColorMap: ColorMap = {};
    areaIdxSet.forEach(idx => {
      if (!curUorderColorMap[idx]) {
        diffColorMap[idx] = this.lastUnorderColorMap[idx];
      }
      if (!this.lastUnorderColorMap[idx]) {
        diffColorMap[idx] = curUorderColorMap[idx];
      }
    })
    let colors = new Set(Object.values(diffColorMap).map(e => e.color));  // 发生变化的颜色
    let updatemode = 2;

    if (Object.keys(diffColorMap).length <= 0) {
      console.log("nothing change, no need to update");
      updatemode = 0;
    } else {
      if (colors.size <= 1) {
        console.log("only one color change, part update by area! area count: " + Object.keys(diffColorMap).length);
        updatemode = 1;
      } else {
        console.log("fast update by map");
        updatemode = 2;
      }
    }

    if (force) {
      updatemode = 2;
    }

    if (updatemode == 1) {
      let areas = Object.keys(diffColorMap).map(idx => this.areaMap[idx]);
      let ai, len, aw, i, x, y;
      let areaIndex;
      let outData = this.unorderMaskCtx.getImageData(0, 0, this.unorderMask.width, this.unorderMask.height)
      let outPixels = new Uint32Array(outData.data.buffer);
      areas.forEach(area => {
        if (!area) return;  //加上这一句，否则可能会挂掉。在mapEdit做了区块合并后，area可能不存在了 
        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;
          areaIndex = this.mapPixels[i];
          if (areaIndex == area.color) {
            if (curUorderColorMap[areaIndex]) {
              outPixels[i] = this.colorMap[areaIndex].color;
            } else {
              outPixels[i] = 0;
            }
          }
        }
      })
      this.unorderMaskCtx.putImageData(outData, 0, 0);

    } else if (updatemode == 2) {  //fast create unordermask by map
      this.unorderMaskCtx.clearRect(0, 0, this.unorderMask.width, this.unorderMask.height);
      let outData = this.unorderMaskCtx.getImageData(0, 0, this.unorderMask.width, this.unorderMask.height)
      let outPixels = new Uint32Array(outData.data.buffer);
      let areaIndex;
      for (var i = 0; i < outPixels.length; i++) {
        areaIndex = this.mapPixels[i];
        if (curUorderColorMap[areaIndex]) {
          outPixels[i] = this.colorMap[areaIndex].color;
        }
      }
      this.unorderMaskCtx.putImageData(outData, 0, 0);
    }

    this.lastUnorderColorMap = curUorderColorMap;

    console.timeEnd('updateUnorderMask');
  }

  validateColorMap() {
    let change = false;
    Object.keys(this.colorMap).forEach(key => {
      if (!this.areaMap[key]) {
        console.warn(`area ${key} not exists!`)
        delete this.colorMap[key];
        change = true;
      }
    })
    return change;
  }

  // 刷新colorOrder
  updateColorOrder(): boolean {
    let ret = false;

    this.colorOrder = this.colorOrder?.filter((item, index, arr) => arr.indexOf(item, 0) == index);  // 去重

    // 剔除不存在的颜色值
    let colorSet = new Set(Object.values(this.colorMap).map((ci: ColorInfo) => ci.color));
    for (let i = 0; i < this.colorOrder.length; i++) {
      if (!colorSet.has(this.colorOrder[i])) {
        this.colorOrder.splice(i, 1);
        i--; // 避免漏掉
        ret = true;
      }
    }
    return ret;
  }


  /**
   * Translate point.
   * Because our cordinate system is based on map, so
   * we don't need to translate.
   */
  translate(p) {
    return p;
    /*
    let pp = {};
    pp.x = p.x * this.scale;
    pp.y = p.y * this.scale;
    return pp;
    */
  }

  /**
   * redraw filler layer.
   */
  override invalidate() {
    this.trigger('invalidate');
  }

  //更新对外展示的信息
  updateOutputList() {
    let colorSum = this.getColorSum();

    this.ordered = this.colorOrder.map(colorInt => {
      let index = colorSum.findIndex(sumItem => sumItem.color == colorInt);
      let [item] = colorSum.splice(index, 1);
      return item;
    });
    this.unordered = colorSum;
  }


  editUpdate() {
    this.uncoloredAreas = this.areaList.filter(area => !this.colorMap[area.color])
      .sort((a: FillArea, b: FillArea) => {
        return a.count - b.count;
      });
    this.colorSum = this.getColorSum();
    this.updateOutputList();
    this.progress = this.getProgress();
    //等待动画执行完成
    setTimeout(() => {
      this.trigger('edit-update');
    }, 1000);
  }

  /**
   * Create area map from map pixels.
   */
  createAreaMap() {
    let width = this.width;
    let height = this.height;
    let floodArr = this.mapPixels;
    let areaMap = {};
    let i: number, x: number, y: number, color: number;
    for (i = 0; i < floodArr.length; i++) {
      color = floodArr[i];
      if (!color) continue;
      x = i % width;
      //y = parseInt(i / width);
      y = Math.floor(i / width);
      if (areaMap[color]) {
        areaMap[color].addPoint(x, y);
      } else {
        areaMap[color] = new FillArea(color, x, y);
      }
    }
    this.areaMap = areaMap;
    this.areaList = Object.keys(areaMap).map(key => {
      return areaMap[key];
    });
  }

  /**
   * Get area by x, y
   */
  getArea(x, y) {
    x = parseInt(x);
    y = parseInt(y);
    if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
      return null;
    }
    let color = this.mapPixels[y * this.width + x];
    return this.areaMap[color];
  }

  /**
   * Get area by color(areaIndex)
   */
  getAreaByColor(color) {
    return this.areaMap[color];
  }

  /**
   * Preapare area for filling.
   */
  prepareArea(area, animation) {
    //console.log('FillerLayer@prepareArea#area', area);
    this.areaOutputCtx.clearRect(0, 0, this.width, this.height);
    this.areaMaskCtx.clearRect(0, 0, this.width, this.height);
    // TODO reuse area data.
    let areaData =
      this.areaMaskCtx.createImageData(area.width(), area.height());
    let areaPixels = new Uint32Array(areaData.data.buffer);
    let ai, ax, ay, aw, i, x, y;
    aw = area.width();
    for (ai = 0; ai < areaPixels.length; ai++) {
      x = area.left + ai % aw;
      y = area.top + Math.floor(ai / aw);
      i = y * this.width + x;
      if (this.mapPixels[i] == area.color) {
        areaPixels[ai] = this.mapPixels[i];
      } else {
        areaPixels[ai] = 0;
      }
    }
    this.areaMaskCtx.putImageData(areaData, area.left, area.top);

    if (animation) {
      this.aniMaskCtx.save();
      this.aniMaskCtx.fillStyle = 'white';
      this.aniMaskCtx.fillRect(0, 0, this.aniMask.width, this.aniMask.height);
      this.aniMaskCtx.drawImage(this.output, area.left, area.top, area.width(),
        area.height(), area.left, area.top,
        area.width(), area.height());
      this.aniMaskCtx.globalCompositeOperation = 'destination-in';
      this.aniMaskCtx.drawImage(this.areaMask, area.left, area.top,
        area.width(), area.height(), area.left,
        area.top, area.width(), area.height());
      this.aniMaskCtx.restore();
    }
  }

  pickColor(p) {
    let area = this.getArea(p.x, p.y);
    if (!area) return null;
    let colorInfo = this.colorMap[area.color];
    if (!colorInfo) return null;
    console.log('fillerlayer.pickColor: ', colorInfo);
    return colorInfo.cssColor;
  }

  /**
   * Fill task.
   */
  fill(task) {
    console.log('FillerLayer@task', task);
    if (!task) return;

    // check 是否有颜色增加
    let colorInt = Utils.getColorInteger(task.color);
    let item = this.colorSum.find(e => e.color == colorInt);
    if (!item) { // 有颜色增加, 是否超过199个颜色
      if (this.height <= 2000 && this.colorSum.length >= 199) {
        this.message.warning($localize`颜色数超过199, 无法新增新颜色!`);
        return;
      }
      if (this.height > 2000 && this.colorSum.length >= 299) {
        this.message.warning($localize`颜色数超过299, 无法新增新颜色!`);
        return;
      }
    } else { // 无颜色增加，单个颜色是否超过50
      if (item.total >= 50) {
        this.message.warning($localize`单个颜色超过50!`);
        return;
      }
    }

    task.area = this.getArea(task.x, task.y);
    console.log('FillerLayer@task.area', task.area);
    if (!task.area)
      return;

    this.backupOutput();

    task.areaIndex = task.area.color;
    this._bucketFill(task, true);
    let area = task.area;
    let al = area.left;
    let at = area.top;
    let aw = area.width();
    let ah = area.height();
    // Put area output to output
    this.outputCtx.drawImage(this.areaOutput, al, at, aw, ah, al, at, aw, ah);
    this.invalidate();
  }

  erase(task) {
    console.log('FillerLayer@task', task);
    if (!task)
      return;
    task.area = this.getArea(task.x, task.y);
    console.log('FillerLayer@task.area', task.area);
    if (!task.area)
      return;

    if (!this.colorMap[task.area.color]) {
      //area not colored.
      return;
    }

    this.backupOutput();

    //set area not colored.
    //console.log('colorMap', this.colorMap);
    delete this.colorMap[task.area.color];
    this.updateColorOrder();
    this.updateUnorderMask();
    //console.log('colorMap', this.colorMap);

    task.areaIndex = task.area.color;

    this.prepareArea(task.area, false);

    let area = task.area;
    let al = area.left;
    let at = area.top;
    let aw = area.width();
    let ah = area.height();

    this.outputCtx.save();
    this.outputCtx.globalCompositeOperation = 'destination-out'
    this.outputCtx.drawImage(this.areaMask, al, at, aw, ah, al, at, aw, ah);
    this.outputCtx.restore();

    this.invalidate();
    this.editUpdate();
  }


  /**
   * 擦出所选区域的颜色
   * @param {FillArea} area 
   */
  eraseArea(area) {
    if (!area) return;

    if (!this.colorMap[area.color]) {
      //area not colored.
      return;
    }

    //set area not colored.
    // console.log('colorMap', this.colorMap);
    delete this.colorMap[area.color];

    this.prepareArea(area, false);

    let al = area.left;
    let at = area.top;
    let aw = area.width();
    let ah = area.height();

    this.outputCtx.save();
    this.outputCtx.globalCompositeOperation = 'destination-out'
    this.outputCtx.drawImage(this.areaMask, al, at, aw, ah, al, at, aw, ah);
    this.outputCtx.restore();
  }


  /**
   * 根据css颜色获取所选区域
   */
  getAreasByColor(cssColor) {
    let colorInt = Utils.getColorInteger(cssColor);
    let areas = Object.keys(this.colorMap).map(index => {
      let colorInfo = this.colorMap[index];
      if (colorInfo.color == colorInt) {
        return index
      } else {
        return null
      }
    }).filter(index => {
      return index;
    }).map(index => {
      return this.areaMap[index];
    }).filter(area => {
      return area;
    });
    return areas;
  }

  /**
   * 擦除所选颜色
   * @param {String} cssColor 
   */
  eraseByColor(cssColor) {
    let areas = this.getAreasByColor(cssColor);
    console.log('erase areas count: ', areas.length);
    this.backupOutput();
    areas.forEach(area => {
      this.eraseArea(area);
    })
    this.updateColorOrder();
    this.updateUnorderMask();
    this.invalidate();
    this.editUpdate();
  }


  // 重新上色清空所有
  eraseAll() {
    this.backupOutput();
    this.colorMap = {};
    this.colorOrder = [];
    this.outputCtx.clearRect(0, 0, this.output.width, this.output.height);
    this.unorderMaskCtx.clearRect(0, 0, this.unorderMask.width, this.unorderMask.height);
    this.invalidate();
    this.editUpdate();
  }


  /**
   * Bucket fill implementation.
   */
  _bucketFill(task, animation) {
    console.log('animation:', animation);

    this.prepareArea(task.area, animation);

    //console.log('bucket fill:', task)
    let colorIndex = Utils.getColorInteger(task.color);
    task.area.setColored(colorIndex, task.color);
    this.colorMap[task.area.color] = {
      color: colorIndex,
      cssColor: task.color,
    };
    // 可能有颜色增加
    if (!this.colorOrder.includes(colorIndex)) {
      this.colorOrder.push(colorIndex);
    }
    // 也可能有颜色减少
    this.updateColorOrder();
    this.updateUnorderMask();

    //console.log('colorMap:', this.colorMap);


    switch (task.type) {
      default:
        this.fillSolid(task);
    }

    // Cut area output with mask.
    this.areaOutputCtx.save();
    this.areaOutputCtx.globalCompositeOperation = 'destination-in';
    this.areaOutputCtx.drawImage(this.areaMask, 0, 0);
    this.areaOutputCtx.restore();

    if (this.animator && this.animator.isRunning()) {
      this.animator.cancel();
      this.aniTask = null;
    }

    if (this.animator) {
      this.aniTask = task;
      this.aniRadius = this.getAniRadius(task.area, task.x, task.y);
      let duration = Math.max(task.area.width(), task.area.height()) / 3;
      console.log('duration', duration);
      if (duration > 300)
        duration = 300;
      if (duration < 150)
        duration = 150;
      this.animator.set(0.1, 1).setDuration(duration).start();
    }

    this.editUpdate();

  }

  /**
   * Fill solid
   */
  fillSolid(task) {
    let ctx = this.areaOutputCtx;
    let area = task.area;
    ctx.save();
    if (task.textureImage) {
      ctx.fillStyle = ctx.createPattern(task.textureImage, 'repeat');
      ctx.fillRect(area.left, area.top, area.width(), area.height());
      ctx.globalCompositeOperation = 'overlay';
      ctx.fillStyle = task.color;
      ctx.fillRect(area.left, area.top, area.width(), area.height());
    } else {
      ctx.fillStyle = task.color;
      ctx.fillRect(area.left, area.top, area.width(), area.height());
    }
    ctx.restore();
  }



  getAniRadius(area, x, y) {
    return Math.max(
      Math.sqrt(Math.pow(area.left - x, 2) + Math.pow(area.top - y, 2)),
      Math.sqrt(Math.pow(area.right - x, 2) + Math.pow(area.top - y, 2)),
      Math.sqrt(Math.pow(area.left - x, 2) + Math.pow(area.bottom - y, 2)),
      Math.sqrt(Math.pow(area.right - x, 2) + Math.pow(area.bottom - y, 2)));
  }

  /**
   * Update animation mask by progress.
   */
  updateAniMask(progress) {
    if (!this.aniTask)
      return;
    let task = this.aniTask;
    let area = task.area;
    let radius = this.aniRadius * progress;
    let ctx = this.aniMaskCtx;
    ctx.save();
    ctx.globalCompositeOperation = 'destination-out';

    // if (Utils.isLoaded(this.aniMaskImage)) {
    if (this.aniMaskCanvas) {
      let l = task.x - radius;
      let t = task.y - radius;
      ctx.drawImage(this.aniMaskCanvas, 0, 0, this.aniMaskCanvas.width,
        this.aniMaskCanvas.height, l, t, radius * 2, radius * 2);

    } else {
      ctx.beginPath();
      ctx.arc(task.x, task.y, radius, 0, 2 * Math.PI, false);
      ctx.fillStyle = 'black';
      ctx.fill();
    }

    ctx.restore();
  }

  /**
   * Animator callback function
   */
  onAnimationUpdate(animator: Animator) {
    //console.log('onAnimationUpdate@value', animator.getValue());
    this.updateAniMask(animator.getValue());
    this.invalidate();
  }

  /**
   * Fast fill
   */

  fastfillStart(task) {

    this.backupOutput();

    // generate random unique id.
    // task.fastfill = Math.round(Math.random() * 100000);
    this.fastfillTask = task;
    this.fastfillAreas = {};
    // clear fastfill canvas.
    this.fastfillCtx.clearRect(0, 0, this.fastfillOutput.width,
      this.fastfillOutput.height);
    this.fastfillAddPoint(task.x, task.y);

  }

  /**
   * Fast fill add point
   */
  fastfillAddPoint(x, y) {
    if (!this.fastfillTask)
      return;
    let area = this.getArea(x, y);
    if (!area)
      return;
    if (this.fastfillAreas[area.color])
      return;

    let task = Object.assign({}, this.fastfillTask);
    task.area = area;
    task.x = x;
    task.y = y;
    task.areaIndex = area.color;
    this._bucketFill(task, true);
    this.fastfillCtx.drawImage(this.areaOutput, 0, 0);
    this.fastfillAreas[area.color] = 1;
    this.invalidate();
  }

  /**
   * Fast fill end.
   */

  fastfillEnd() {
    if (!this.fastfillTask)
      return;
    this.outputCtx.drawImage(this.fastfillOutput, 0, 0);
    this.fastfillTask = null;
    this.fastfillAreas = null;
    this.invalidate();
  }

  /**
   * Cancel the fastfill.
   */
  fastfillCancel() {
    if (!this.fastfillTask)
      return;
    if (Object.keys(this.fastfillAreas).length > 1) {
      this.fastfillEnd();
    } else {
      this.fastfillTask = null;
      this.fastfillAreas = null;
    }
    this.invalidate();
  }


  fasteraseStart(task) {

    this.backupOutput();

    // generate random unique id.
    this.fasteraseTask = task;
    this.fasteraseAreas = {};
    // clear fasterase canvas.
    this.fasteraseCtx.clearRect(0, 0, this.fasteraseOutput.width, this.fasteraseOutput.height);
    this.fasteraseAddPoint(task.x, task.y);

  }

  /**
   * Fast erase add point
   */
  fasteraseAddPoint(x, y) {
    if (!this.fasteraseTask)
      return;
    let area = this.getArea(x, y);
    if (!area)
      return;
    if (this.fasteraseAreas[area.color])
      return;
    if (!this.colorMap[area.color]) { //area not colored.
      return;
    }

    delete this.colorMap[area.color];
    this.updateColorOrder();
    this.updateUnorderMask();

    this.prepareArea(area, false);

    let al = area.left;
    let at = area.top;
    let aw = area.width();
    let ah = area.height();

    this.fasteraseCtx.drawImage(this.areaMask, al, at, aw, ah, al, at, aw, ah);
    this.fasteraseAreas[area.color] = 1;
    this.invalidate();
    this.editUpdate();
  }

  /**
   * Fast erase end.
   */

  fasteraseEnd() {
    if (!this.fasteraseTask)
      return;
    this.outputCtx.save();
    this.outputCtx.globalCompositeOperation = 'destination-out'
    this.outputCtx.drawImage(this.fasteraseOutput, 0, 0);
    this.outputCtx.restore();
    this.fasteraseTask = null;
    this.fasteraseAreas = null;
    this.invalidate();
  }

  /**
   * Cancel the fasterase.
   */
  fasteraseCancel() {
    if (!this.fasteraseTask)
      return;
    if (Object.keys(this.fasteraseAreas).length > 1) {
      this.fasteraseEnd();
    } else {
      this.fasteraseTask = null;
      this.fasteraseAreas = null;
    }
    this.invalidate();
  }


  /**
   * Draw pending points to area output.
   */
  drawPendingPoints() {
    if (!this.brushTask)
      return;
    if (this.brushPendingPoints.length <= 0)
      return;
    let p, brushImage, l, t, size;
    size = this.brushTask.size;

    while (this.brushPendingPoints.length > 0) {
      p = this.brushPendingPoints.shift();
      this.brushTask.points.push(p);
      brushImage = this.brushBrush.getBrush();
      l = p.x - size / 2;
      t = p.y - size / 2;
      this.areaOutputCtx.drawImage(brushImage, 0, 0, brushImage.width,
        brushImage.height, l, t, size, size);
    }

    // cut
    this.areaOutputCtx.save();
    this.areaOutputCtx.globalCompositeOperation = 'destination-in';
    this.areaOutputCtx.drawImage(this.areaMask, 0, 0);
    this.areaOutputCtx.restore();

    this.invalidate();
  }




  /**
   * Check if undo avaliable
   */
  hasUndo() {
    return this.undoList.length > 0;
  }

  /**
   * Check if redo avaliable
   */
  hasRedo() {
    return this.redoList.length > 0;
  }

  /**
   * Undo.
   */
  override undo() {
    console.log('FillerLayer@undo#size', this.undoList.length);
    if (this.undoList.length <= 0)
      return;
    let data = {
      output: this.output,
      colorMap: JSON.stringify(this.colorMap),
      colorOrder: JSON.stringify(this.colorOrder)
    }
    // put the current output to first of redolist
    this.redoList.unshift(data);
    // the last of undolist as output
    data = this.undoList.pop();
    this.output = data.output;
    this.outputCtx = this.output.getContext('2d');
    this.colorMap = JSON.parse(data.colorMap);
    this.colorOrder = JSON.parse(data.colorOrder);
    this.updateUnorderMask();
    this.invalidate();
    this.editUpdate();
  }

  override undoSize() { return this.undoList.length }
  override redoSize() { return this.redoList.length }

  /**
   * Redo
   */
  override redo() {
    if (this.redoList.length <= 0)
      return;
    let data = {
      output: this.output,
      colorMap: JSON.stringify(this.colorMap),
      colorOrder: JSON.stringify(this.colorOrder),
    }
    this.undoList.push(data);
    data = this.redoList.shift();
    this.output = data.output;
    this.outputCtx = this.output.getContext('2d');
    this.colorMap = JSON.parse(data.colorMap);
    this.colorOrder = JSON.parse(data.colorOrder);
    this.updateUnorderMask();
    this.invalidate();
    this.editUpdate();
  }

  /**
   * Backup current output to undo list.
   */
  backupOutput() {
    console.log('backup output....')
    // Clear redo list, put all redos to cache for reuse.
    let canvasList = this.redoList.map(d => {
      return d.output;
    })
    this.cacheList = this.cacheList.concat(canvasList);
    this.redoList = [];
    let canvas = this.getCanvas();
    let ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(this.output, 0, 0);
    let colorMap = JSON.stringify(this.colorMap);
    let colorOrder = JSON.stringify(this.colorOrder);
    this.undoList.push({
      output: canvas,
      colorMap: colorMap,
      colorOrder: colorOrder,

    });
    console.log('undoList', this.undoList);
  }


  /**
   * Get canvas for undo/redo
   */
  getCanvas() {
    if (this.cacheList.length > 0) {
      console.log('getCanvsa()@from cache');
      return this.cacheList.pop();
    } else if (this.undoList.length > 20) {
      // Limit undo list size
      console.log('getCanvsa()@shift undo');
      return this.undoList.shift().output;
    } else {
      console.log('getCanvsa()@create new');
      return Utils.createCanvas(this.width, this.height);
    }
  }

  /**
   * Draw this layer.
   * @Override Layer#draw(ctx)
   */
  override draw(ctx) {
    ctx.imageSmoothingEnabled = this.smoothing;

    //Utils.setImageSmoothing(ctx, false);

    this.drawOnCtx(ctx, this.width, this.height);



    // draw original
    if (!this.hideLine) {
      if (this.showSvg && this.raw) {
        ctx.drawImage(this.raw, 0, 0, this.page.width,
          this.page.height, 0, 0, this.width, this.height);
      } else {
        ctx.drawImage(this.page, 0, 0, this.page.width,
          this.page.height, 0, 0, this.width, this.height);
      }
    }

    if (this.label) {
      ctx.drawImage(this.label, 0, 0, this.label.width, this.label.height,
        0, 0, this.width, this.height);
    }

    //draw blink hint
    if (this.blinkCanvas) {
      ctx.drawImage(this.blinkCanvas, 0, 0, this.blinkCanvas.width, this.blinkCanvas.height,
        0, 0, this.width, this.height);
    }


  }

  drawOnCtx(ctx, width, height) {
    ctx.save();
    ctx.clearRect(0, 0, width, height);


    ctx.fillStyle = "white";
    ctx.fillRect(0, 0, this.width, this.height);

    // 如果当前工具是播放，则只画play canvas
    if (this.lastToolKey == 'number-edit-play') {
      ctx.drawImage(this.play, 0, 0, this.play.width, this.play.height, 0, 0, width, height);
      ctx.restore();
      return;
    }

    /*
    ctx.drawImage(this.map, 0, 0, this.map.width, this.map.height, 0, 0, width,
                  height);

    */
    ctx.drawImage(this.output, 0, 0, this.output.width, this.output.height, 0, 0, width, height);

    // 编排模式下，需要掩蔽未排序区块
    if (this.showUnorderMask) {
      ctx.save();
      ctx.globalCompositeOperation = 'destination-out'
      ctx.drawImage(this.unorderMask, 0, 0, this.unorderMask.width, this.unorderMask.height, 0, 0, width, height);
      ctx.restore();
    }

    // draw fastfill
    if (this.fastfillTask) {
      ctx.drawImage(this.fastfillOutput, 0, 0, this.fastfillOutput.width,
        this.fastfillOutput.height, 0, 0, width, height);
    }

    // draw fasterase
    if (this.fasteraseTask) {
      ctx.save();
      ctx.globalCompositeOperation = 'destination-out'
      ctx.drawImage(this.fasteraseOutput, 0, 0, this.fasteraseOutput.width,
        this.fasteraseOutput.height, 0, 0, width, height);
      ctx.restore();
    }

    // brush
    if (this.brushTask) {
      ctx.drawImage(this.areaOutput, 0, 0, this.areaOutput.width,
        this.areaOutput.height, 0, 0, width, height);
    }

    // draw animation
    if (this.animator && this.animator.isRunning()) {
      ctx.drawImage(this.aniMask, 0, 0, this.aniMask.width, this.aniMask.height,
        0, 0, width, height);
    }

    /*
    ctx.strokeStyle = "black";
    ctx.strokeRect(0, 0, width, height);
    */

    // ctx.drawImage(this.original, 0, 0, this.original.width,
    // this.original.height,
    //  0, 0, width, height);

    ctx.restore();
  }


  getOutput() {
    return new Promise((done, reject) => {
      this.output.toBlob(b => {
        done(b)
      })
    });
  }

  getWorkBlob(): Promise<Blob> {
    let canvas = Utils.createCanvas(this.width, this.height);
    let ctx = canvas.getContext('2d');
    ctx.drawImage(this.output, 0, 0, this.output.width, this.output.height, 0, 0, this.width, this.height);
    if (!this.mystery) {
      ctx.drawImage(this.page, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
    }
    return new Promise((done, reject) => { canvas.toBlob(b => { done(b) }) });
  }


  getProgress() {
    let total = this.areaList.length;
    let colored = Object.keys(this.colorMap).length;
    return { total, colored }
  }

  getColorMap() {
    return this.colorMap;
  }

  /**
   * 统计填色情况
   */
  getColorSum(): ColorSumItem[] {
    let sumHash = {};
    Object.keys(this.colorMap).forEach(_areaIndex => {
      let areaIndex = parseInt(_areaIndex);
      let { color, cssColor } = this.colorMap[areaIndex];
      if (sumHash[color]) {
        sumHash[color].total++;
        sumHash[color].areas.push(areaIndex);
      } else {
        sumHash[color] = {
          total: 1,
          cssColor,
          color,
          areas: [areaIndex]
        }
      }
    })
    let labWhite = sRGB.white().toXYZ().toLab();
    let sum: ColorSumItem[] = Object.keys(sumHash).map(key => {
      let item: ColorSumItem = sumHash[key];
      item.lab = Lab.fromRGBA(item.color);
      item.distanceFromWhite = item.lab.deltaE(labWhite);
      return item;
    }).sort((a, b) => a.distanceFromWhite - b.distanceFromWhite);

    return sum;
  }

  blinkCanvas: HTMLCanvasElement;
  repeater: Repeater;


  blinkByAreas(areas: FillArea[]): Rect {

    //Stop the previous running repeater.
    if (this.repeater) this.repeater.cancel();
    if (!areas || areas.length <= 0) return new Rect();

    //没有考虑scale
    let _blinkCanvas = Utils.createCanvas(this.width, this.height);
    let ctx = _blinkCanvas.getContext('2d');
    ctx.clearRect(0, 0, this.width, this.height);

    let imgData = ctx.getImageData(0, 0, _blinkCanvas.width, _blinkCanvas.height);
    let pixels = new Uint32Array(imgData.data.buffer);
    //console.log(TAG, 'pixels=', pixels);

    areas.forEach((area: FillArea) => {
      let ai, aw, i, x, y;
      let areaPixelCount = area.width() * area.height();
      aw = area.width();
      for (ai = 0; ai < areaPixelCount; ai++) {
        x = area.left + ai % aw;
        y = area.top + Math.floor(ai / aw);
        i = y * this.width + x;
        if (this.mapPixels[i] == area.color) {
          pixels[i] = 0xffffffff;
        }
      }
    })
    ctx.putImageData(imgData, 0, 0);

    let rect = new Rect(
      Math.min.call(null, ...areas.map(a => a.left)),
      Math.min.call(null, ...areas.map(a => a.top)),
      Math.max.call(null, ...areas.map(a => a.right)),
      Math.max.call(null, ...areas.map(a => a.bottom))
    );

    ctx.strokeStyle = '#dd0000';
    //ctx.strokeStyle = '#efefef';
    ctx.lineWidth = 2;
    let insetRect = rect.inset(ctx.lineWidth, ctx.lineWidth, ctx.lineWidth, ctx.lineWidth);
    ctx.strokeRect(insetRect.left, insetRect.top, insetRect.width(), insetRect.height());

    this.blinkCanvas = _blinkCanvas;
    this.invalidate();

    this.repeater = new Repeater(() => {
      this.blinkCanvas = this.blinkCanvas ? null : _blinkCanvas;
      this.invalidate();
    }, () => {
      this.blinkCanvas = null;
      this.repeater = null;
      this.invalidate();
    }, 5, 300).start();

    return rect;
  }

  /**
   * 闪动提示
   * 
   * 
   * @param cssColor 
   * @returns 
   */
  blinkByColor(cssColor: any): Rect {

    //throw new Error('Method not implemented.');
    console.log(TAG, 'blinkByColor#', cssColor);
    let areas = this.getAreasByColor(cssColor);
    console.log(TAG, 'blinkByColor#', areas);
    return this.blinkByAreas(areas);
  }


  /**
   * 批量修改区域的颜色 -直接修改colorMap
   * @param areaIndexes 
   * @param destColorCss 
   */
  changeAreasColor(areaIndexes: number[], destColorCss: string) {

    //this.backupOutput();
    let oldColor = this.colorMap[areaIndexes[0]].color;
    let color = Utils.getColorInteger(destColorCss);
    let cssColor = Utils.getColorFromInteger(color);
    areaIndexes.forEach(areaIndex => {
      this.colorMap[areaIndex] = { color, cssColor };
    })
    // colorOrder也需要同步修改
    let index = this.colorOrder.indexOf(oldColor);
    if (index >= 0) {
      this.colorOrder[index] = color;
    }
    this.drawToOutput();
    this.updateUnorderMask();
    this.invalidate();
    //this.editUpdate();

  }



  /**
   * 已排序元素拖动调整顺序
   * @param from 
   * @param to 
   * @returns 
   */
  moveOrderedItem(from: number, to: number) {
    if (from < 0 || from >= this.colorOrder.length
      || to < 0 || to >= this.colorOrder.length
      || to == from) {
      return;
    }

    this.backupOutput();

    const target = this.colorOrder[from];
    const delta = to < from ? -1 : 1;
    let array = this.colorOrder;

    for (let i = from; i !== to; i += delta) {
      array[i] = array[i + delta];
    }

    array[to] = target;

    this.editUpdate();
    this.invalidate();
  }

  /**
   * 如果是已排序则下降到未排序，反之如果未排序则回归到已排序
   * @param color 
   */
  upOrDownColor(color: number) {
    if (!color) return;
    console.log("upOrDownColor: " + color);
    let idx = this.colorOrder.indexOf(color);

    this.backupOutput();

    if (idx >= 0) { // 在已排序中，下降到未排序
      this.colorOrder.splice(idx, 1);
    } else { // 在未排序中，回到已排序末尾
      this.colorOrder.push(color);
    }
    this.updateUnorderMask();
    this.editUpdate();
    this.invalidate();
  }

  /**
   * 切换到播放按钮初始化
   */
  resetPlay() {
    console.log("resetPlay: " + this.lastToolKey);
    this.playCtx.fillStyle = 'white';
    this.playCtx.fillRect(0, 0, this.width, this.height);
    this.playIdx = 0;
  }

  /**
   * 单步播放
   */
  playStep() {
    if (this.playIdx >= this.colorOrder.length) {
      this.playIdx = 0;
      this.playCtx.fillStyle = 'white';
      this.playCtx.fillRect(0, 0, this.width, this.height);
      super.invalidate();
      return;
    }
    let item = this.ordered[this.playIdx];
    for (let i = 0; i < item.areas.length; i++) {
      let area = this.areaMap[item.areas[i]];
      console.log(area);
      let areaData = this.playCtx.getImageData(area.left, area.top, area.width(), area.height());
      let areaPixels = new Uint32Array(areaData.data.buffer);
      let ai, aw, n, xx, yy;
      aw = area.width();
      for (ai = 0; ai < areaPixels.length; ai++) {
        xx = area.left + ai % aw;
        yy = area.top + Math.floor(ai / aw);
        n = yy * this.width + xx;
        if (this.mapPixels[n] == area.color) {
          areaPixels[ai] = item.color;
        }
      }
      this.playCtx.putImageData(areaData, area.left, area.top);
    }
    // this.colorInt = this.ordered[this.playIdx].color;
    this.getToolByKey(this.lastToolKey).editor.trigger('pick-color', this.ordered[this.playIdx].cssColor);  // 这样才能自动滚动起来
    super.invalidate();

    this.playIdx++;
  }


  playInterval: any;
  /**
   * 自动播放
   */
  playAuto() {
    this.playStop();
    this.playInterval = setInterval(() => this.playStep(), 300);
  }

  playStop() {
    if (this.playInterval) {
      clearInterval(this.playInterval);
      this.playInterval = null;
    }
  }

  /**
   * 指定播放位置
   * @param idx
   */
  playPos(idx: number) {
    if (idx < 0) return;
    this.playStop();
    this.playIdx = 0;
    this.playCtx.fillStyle = 'white';
    this.playCtx.fillRect(0, 0, this.width, this.height);
    super.invalidate();
    for (let i = 0; i <= idx; i++) {
      this.playStep();
    }
  }

  /**
 * 重新排序，把所有颜色都划归到未排序去
 */
  resetOrder() {
    this.backupOutput();
    this.colorOrder = [];  // 清空
    this.updateUnorderMask();
    this.editUpdate();
    this.invalidate();
  }

  // 自动排序
  autoOrder() {
    if (!this.canAuto) return;

    let orderAuto = generateOrderAuto(this.colorMap, this.centers);
    if (!orderAuto) return;

    this.backupOutput();
    this.colorOrder = orderAuto;
    this.updateUnorderMask();
    this.editUpdate();
    this.invalidate();

  }


  orderFill(x, y) {
    let area: FillArea = this.getArea(x, y);
    if (!area) {
      console.warn(TAG, 'orderFill#', `Area not found@(${x}, ${y})`);
      return;
    };
    let colorInfo: ColorInfo = this.colorMap[area.color];
    if (!colorInfo) {
      console.warn(TAG, 'orderFill#', 'Area not colored!');
      return;
    }
    if (this.colorOrder.indexOf(colorInfo.color) >= 0) {
      console.warn(TAG, 'orderFill#', 'This color already colored');
      return;
    }

    this.backupOutput();

    this.colorOrder.push(colorInfo.color);
    this.editUpdate();

    console.log(TAG, 'orderFill#colorOrder', this.colorOrder)
    this.updateUnorderMask();
    this.invalidate();
  }

}