import Animator from '../common/animator';
import Easing from '../common/easing';
import Utils from '../common/utils';
import Layer from '../core/layer';
import Tool from '../core/tool';
import { AreaMap, Centers, ColorInfo, ColorMap, ColorSumItem } from '../common/interfaces';

import FillArea from '../common/fillarea';
import { FillTask } from '../common/filltask';
import OrderSelectTool from './order-select-tool';
import OrderPlayTool from './order-play-tool';
import Rect from '../core/rect';
import Repeater from '../common/repeater';
import { generateOrderAuto, validateOrder } from '../common/color-order';


const TAG = 'OrderEditLayer';

export default class OrderEditLayer extends Layer {
  private colorMap: ColorMap = {};
  colorOrder: number[] = [];

  private colorSum: ColorSumItem[] = [];
  public ordered: ColorSumItem[] = [];
  public unordered: ColorSumItem[] = [];

  page: HTMLImageElement;
  private output: HTMLCanvasElement;
  private outputCtx: CanvasRenderingContext2D;

  private play: HTMLCanvasElement;
  private playCtx: CanvasRenderingContext2D;
  private playIdx: number;

  private areaPixels: ImageData;
  private mapData: ImageData;
  private map: HTMLImageElement;
  private scale: number;
  private mapPixels: Uint32Array;
  private undoList: any[] = [];
  private redoList: any[] = [];
  private cacheList: any[] = [];
  private areaMap: AreaMap = {};
  private areaList: any[];

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

  raw: HTMLImageElement;


  override get defaultToolKey(): string { return 'order-edit-select'; }
  
  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);
  }


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

    this.colorMap = colorMap || {};
    this.colorOrder = colorOrder || [];
    this.orderAuto = orderAuto || [];
    this.centers = centers;
    this.map = map;
    this.page = page;
    this.raw = raw;

    this.init();

    this.color = '#aa0000';

    //add tools
    this.addTool(new OrderSelectTool(this));
    this.addTool(new OrderPlayTool(this))
  }

  init() {

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

    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.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.setMap(map);

    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.validateColorMap();
    this.drawToOutput();

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

    // 尚未生成中心点, 启动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;
        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);
    }
  }

  update(page?: HTMLImageElement, map?: HTMLImageElement, colorMap?: ColorMap, colorOrder?: number[], raw?: HTMLImageElement) {
    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;
    if (map) {  // 如果map图有变更, 那么重新计算中心点
      this.centers = null;
    }
    //re initialization
    this.init();
    this.editUpdate();
  }

  editUpdate() {
    this.trigger('edit-update');
  }


  drawToOutput() {
    console.time('drawToOutput');
    this.outputCtx.fillStyle = 'white';
    this.outputCtx.fillRect(0, 0, this.width, this.height);
    Object.keys(this.colorMap).forEach(key => {
      if (!this.areaMap[key]) {
        console.warn(`area ${key} not exists!`)
        delete this.colorMap[key];
      }
    })

    //删除colorOrder中不存在的颜色值
    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--; // 避免漏掉
      }
    }


    //根据填色顺序，获取已经上色部分的colorMap
    let coloredColorMap: ColorMap = {};
    Object.keys(this.colorMap).forEach(areaIndex => {
      if (this.colorOrder.indexOf(this.colorMap[areaIndex].color) >= 0) {
        coloredColorMap[areaIndex] = this.colorMap[areaIndex];
      }
    })


    //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 (coloredColorMap[areaIndex]) {
        outPixels[i] = this.colorMap[areaIndex].color;
      }
    }
    this.outputCtx.putImageData(outData, 0, 0);
    console.timeEnd('drawToOutput');
    this.invalidate();
  }

  validateColorMap() {
    Object.keys(this.colorMap).forEach(key => {
      if (!this.areaMap[key]) {
        console.warn(`area ${key} not exists!`)
        delete this.colorMap[key];
      }
    })
    //删除colorOrder中不存在的颜色值
    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--; // 避免漏掉
      }
    }
  }

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

  /**
   * redraw filler layer.
   */
  override invalidate() {
    super.invalidate();
    this.updateOutputList();
  }

  //更新对外展示的信息
  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;
  }


  /**
   * Create area map from map pixels.
   */
  createAreaMap() {
    let width = this.width;
    let height = this.height;
    let floodArr = this.mapPixels;
    let areaMap: 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 = 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): FillArea {
    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(areaIndex): FillArea {
    return this.areaMap[areaIndex];
  }



  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.drawToOutput();
    this.invalidate();
  }




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




  /**
   * 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,
      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.colorOrder = JSON.parse(data.colorOrder);
    this.editUpdate();
    this.invalidate();
  }

  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,
      colorOrder: JSON.stringify(this.colorOrder),
    }
    this.undoList.push(data);
    data = this.redoList.shift();
    this.output = data.output;
    this.outputCtx = this.output.getContext('2d');
    this.colorOrder = JSON.parse(data.colorOrder);
    this.editUpdate();
    this.invalidate();
  }

  /**
   * 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 colorOrder = JSON.stringify(this.colorOrder);
    this.undoList.push({
      output: canvas,
      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;

    this.drawOnCtx(ctx, this.width, this.height);
    // draw original
    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);
    }

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

    if (this.lastToolKey == 'order-edit-play') {
      ctx.drawImage(this.play, 0, 0, this.play.width, this.play.height, 0, 0, width, height);
    } else {
      ctx.drawImage(this.output, 0, 0, this.output.width, this.output.height, 0,
        0, width, height);
    }
  
    ctx.restore();
  }


  getColorSum() {
    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]
        }
      }
    })
    return Object.keys(sumHash).map(key => {
      return sumHash[key];
    })
  }

  /**
   * 已排序元素拖动调整顺序
   * @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.resetPlay();
    this.editUpdate();
    this.invalidate();
  }

  /**
   * 移除已排序的元素，下降到未排序列表
   * @param item 
   * @returns 
   */
  removeOrderedItem(item: ColorSumItem) {
    if (!item) return;
    console.log(item);
    let idx = this.colorOrder.findIndex(elem => elem == item.color);
    if (idx < 0) return;

    this.backupOutput();
    this.colorOrder.splice(idx, 1);
    this.editUpdate();
    this.drawToOutput();
    this.resetPlay();
    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);
    }
    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 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, cssColor);
  }

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

  blinkCanvas: HTMLCanvasElement;
  blinkRepeater: Repeater;
  blinkByAreas(areas: FillArea[], cssColor: any) : Rect {

    let colorInt = Utils.getColorInteger(cssColor);

    //Stop the previous running repeater.
    if (this.blinkRepeater) this.blinkRepeater.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] = this.colorOrder.findIndex(elem => elem == colorInt) >= 0 ? 0xffffffff : colorInt;
        }
      }
    })
    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.blinkRepeater = new Repeater(() => {
      this.blinkCanvas = this.blinkCanvas ? null : _blinkCanvas;
      this.invalidate();
    }, () => {
      this.blinkCanvas = null;
      this.blinkRepeater = null;
      this.invalidate();
    }, 5, 300).start();

    return rect;
  }


  /**
   * 重新排序，把所有颜色都划归到未排序去
   */
  reset() {
    this.backupOutput();
    this.colorOrder = [];  // 清空
    this.editUpdate();
    this.invalidate();
    this.outputCtx.fillStyle = 'white';
    this.outputCtx.fillRect(0, 0, this.width, this.height);
  }

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

    if (!validateOrder(this.colorMap, this.orderAuto)) {
      console.log("order not valid, generate order auto");
      this.orderAuto = generateOrderAuto(this.colorMap, this.centers);
    }

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

  }

}

