
import Matrix from './matrix';
import Rect from './rect';
import PanTool from './pantool';
import ColorPickerTool from './color-picker-tool';
import EventEmitter from './eventemitter';
import Utils from './utils';
import Tool from './tool';
import Layer from './layer';
import { Padding, Point } from './interface';
import EditorConfig from './editor-config';
import Animator from '../common/animator';
import Easing from '../common/easing';

type MouseEventHandler = (evt: MouseEvent) => void;
type EventHandler = (evt: EventHandler) => void;
type KeyboardEventHandler = (evt: KeyboardEvent) => void;
type WheelEventHandler = (evt: WheelEvent) => void;
type ResizeEventHandler = () => void;

const TAG = 'Editor';

export default class Editor extends EventEmitter {

  canvas: HTMLCanvasElement;
  ctx: CanvasRenderingContext2D | null;


  private _matrix: Matrix = Matrix.identical;


  backgroundLayer: Layer | null;
  bgColor: string;


  private invalidateHandler: Function;

  private lastPoint: Point | null = null;
  private isDragging: boolean = false;

  wheelDy: number = 0;
  STEP: number = 0;

  private tools: Tool[] = [];
  private currentTool: Tool | null;


  private needDraw: boolean = false;
  private theMarkRect: Rect | null = null;

  public readonly ready: Promise<void>;

  //用于计算bestFitMatrix
  padding: Padding = { left: 250, top: 80, right: 266, bottom: 80 };
  //bestFitStyle
  bestFitStyle: 'canavsCenter' | 'viewportCenter' = 'canavsCenter';


  /**
   * Event handlers.
   * Keep them for removing, prevent memory leaking.
   */
  private resizeHandler: ResizeEventHandler;
  private clickHander: MouseEventHandler;
  private dblclickHandler: MouseEventHandler;
  private mousemoveHandler: MouseEventHandler;
  private mousedownHandler: MouseEventHandler;
  private mouseupHandler: MouseEventHandler;
  private keydownHandler: KeyboardEventHandler;
  private wheelHandler: WheelEventHandler;

  opts: EditorConfig = {
    minScale : 0.05,
    maxScale : 5,
  }


  constructor(canvas: HTMLCanvasElement, _opts?: EditorConfig, _padding?: any) {
    super();
    if (_opts) {
      Object.assign(this.opts, _opts);
    }
    if (_padding) {
      Object.assign(this.padding, _padding);
    }
    this.canvas = canvas;
    var self = this;
    this.ctx = canvas.getContext('2d');

    this.matrix = Matrix.identical;
    this.currentTool = null;
    this.backgroundLayer = null;
    this.bgColor = "#efefef";

    //this.invalidateFunc = function () { self.invalidate(); }
    this.invalidateHandler = this.invalidate.bind(this);

    this.resizeHandler = this.onresize.bind(this);
    window.addEventListener('resize', this.resizeHandler);

    this.clickHander = this.onclick.bind(this);
    canvas.addEventListener('click', this.clickHander);

    this.dblclickHandler = this.ondblclick.bind(this);
    canvas.addEventListener('dblclick', this.dblclickHandler);

    this.mousemoveHandler = this.onmousemove.bind(this);
    document.addEventListener('mousemove', this.mousemoveHandler);

    this.mousedownHandler = this.onmousedown.bind(this);
    canvas.addEventListener('mousedown', this.mousedownHandler);

    this.mouseupHandler = this.onmouseup.bind(this);
    canvas.addEventListener('mouseup', this.mouseupHandler);

    this.keydownHandler = this.onkeydown.bind(this);
    document.addEventListener('keydown', this.keydownHandler);

    //鼠标滚轮
    this.wheelHandler = this.onwheel.bind(this);
    canvas.addEventListener('wheel', this.wheelHandler);


    //init tools
    this.addTool(new PanTool());
    //this.addTool(new ColorPickerTool());
    this.setToolByKey(new PanTool().key);

    //Editor是否初始化完成
    this.ready = new Promise((done) => {
      //启动定时器，检查页面是否渲染完成, canvas获得实际尺寸
      let checkInterval = setInterval(() => {
        console.log(this.canvas.offsetHeight, this.canvas.offsetWidth);
        if (this.canvas.offsetHeight != 0 && this.canvas.offsetWidth != 0) {
          clearInterval(checkInterval);
          this.updateCanvas();
          done();
        }
      }, 10);
    })
  }


  /**
   * 获取canvas的宽高
   */
  get width(): number { return this.canvas.width }
  get height(): number { return this.canvas.height }

  /**
   * 获取缩放参数
   * 默认使用 window.devicePixelRatio， 建议使用默认值
   */
  get scaleX(): number { return this.opts.scale || window.devicePixelRatio }
  get scaleY(): number { return this.opts.scale || window.devicePixelRatio }

  /**
   * 获取要展示的内容的宽高
   */
  get contentWidth(): number { return this.backgroundLayer && this.backgroundLayer.getWidth() || 200 }
  get contentHeight(): number { return this.backgroundLayer && this.backgroundLayer.getHeight() || 200 }


  /**
   * 根据当前 devicePixelRatio 以及 canvas的实际尺寸重新设置 canvas的宽高
   * devicePixelRatio, 参考： https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio 
   * 这里要注意canvas宽高和canvas的实际尺寸是两回事。 canvas的实际尺寸是由其css属性来决定的，
   * 其大小可能由于屏幕缩放，窗口调整而发生变化。 而canvas的宽高并不会自动变化。 所以我们需要
   * 随时检测canvas的实际尺寸，并根据实际尺寸来调整宽高， 以保证其宽高的一致性, 否则可能会变形。 
   */
  updateCanvas() {
    let _width = this.canvas.offsetWidth;
    let _height = this.canvas.offsetHeight;
    this.canvas.width = Math.floor(_width * this.scaleX);
    this.canvas.height = Math.floor(_height * this.scaleY);
    this.invalidate();
    this.debugCanvas();
  }

  set matrix(matrix: Matrix) {
    this._matrix = matrix;
    this.invalidate();
  }

  get matrix() {
    return this._matrix;
  }


  debugCanvas() {
    let canvas = this.canvas;
    console.log('canvas dimention:', canvas.width, canvas.height);
    console.log('canvas dimention2:', canvas.offsetWidth, canvas.offsetHeight);
  }

  /**
   * 计算能将内容最佳展示出来的matrix
   * TODO : 目前是简单计算了一下， 还需优化。 
   */
  get bestFitMatrix(): Matrix {
    let scaleX = (this.width - this.padding.left * this.scaleX - this.padding.right * this.scaleX) / this.contentWidth;
    let scaleY = (this.height - this.padding.top * this.scaleY - this.padding.bottom * this.scaleY) / this.contentHeight;
    let scale = Math.min(scaleX, scaleY);
    let translateX = (this.width - this.contentWidth * scale) / 2;
    let translateY = (this.height - this.contentHeight * scale) / 2;
    return new Matrix([scale, 0, 0, scale, translateX, translateY]);
  }


  onresize() {
    console.log('onresize()..');
    this.updateCanvas();
  }

  /**
   * 鼠标事件预处理
   * 转换为相对于canvas的坐标
   * @param evt 
   */
  preprocessMouseEvent(evt: MouseEvent) {
    let self = this;
    let t = evt.target as HTMLElement;
    //console.log(TAG, 'target=', evt.type, evt.button, evt.target == this.canvas, t.tagName);
    //let x = evt.pageX - this.canvas.offsetLeft;
    //let y = evt.pageY - this.canvas.offsetTop;

    //console.log(`(${this.canvas.offsetLeft}, ${this.canvas.offsetTop})`)
    //console.log(`(${evt.offsetX}, ${evt.offsetY}) (${evt.pageX}, ${evt.pageY}) (${x},${y})`);

    //fill canvas xy
    if (evt.target == this.canvas) {
      evt._canvasPoint = {
        x: evt.offsetX * self.scaleX,
        y: evt.offsetY * self.scaleY,
      }
    } else {
      //'mousemove' event是挂在document下的，需要单独处理,先简单处理一下
      evt._canvasPoint = {
        x: (evt.pageX - this.canvas.offsetLeft) * self.scaleX,
        y: (evt.pageY - this.canvas.offsetTop) * self.scaleY,
      }
    }

    //content xy
    evt._contentPoint = this.matrix.invMapPoint(evt._canvasPoint.x, evt._canvasPoint.y);
  }

  /**
   * Internal events.
   */
  onDragStart(evt: MouseEvent) {
    console.log('onDragStart()');
    this.currentTool && this.currentTool.onDragStart(evt);
  }

  onDragMove(evt: MouseEvent) {
    this.currentTool && this.currentTool.onDragMove(evt);
  }

  onDragEnd(evt: MouseEvent) {
    var self = this;
    console.log('onDragEnd()');
    self.currentTool && self.currentTool.onDragEnd(evt);
  }

  /**
   * 'mousedown' listener
   */
  onmousedown(evt: MouseEvent) {
    this.preprocessMouseEvent(evt);
    this.isDragging = false;
    this.lastPoint = null;
    // If left mouse is down, track the point.
    if (evt.button == 0) {
      this.lastPoint = evt._canvasPoint;
    }
    this.currentTool && this.currentTool.onmousedown(evt);
  }

  /**
   * 'mousemove' listener
   */
  onmousemove(evt: MouseEvent) {
    this.preprocessMouseEvent(evt);
    // For drag tracking.
    if (evt.button == 0 && this.lastPoint) {
      var p = evt._canvasPoint;
      evt._dx = p.x - this.lastPoint.x;
      evt._dy = p.y - this.lastPoint.y;
      if (this.isDragging) {
        this.lastPoint = p;
        this.onDragMove(evt);
      } else {
        var dist = Math.sqrt(Math.pow(evt._dx, 2) + Math.pow(evt._dy, 2));
        if (dist > 10) {
          this.isDragging = true;
          this.lastPoint = p;
          this.onDragStart(evt);
        }
      }
    }

    this.currentTool && this.currentTool.onmousemove(evt);
  }

  /**
   * 'mouseup' listener
   */
  onmouseup(evt: MouseEvent) {
    this.preprocessMouseEvent(evt);
    var self = this;
    self.lastPoint = null;
    if (self.isDragging) {
      self.onDragEnd(evt);
    }
    self.currentTool && self.currentTool.onmouseup(evt);
  }

  /**
   * onclick
   */
  onclick(evt: MouseEvent) {
    this.preprocessMouseEvent(evt);
    var self = this;
    self.currentTool && self.currentTool.onclick(evt);
  }

  /**
   * ondblclick
   */
  ondblclick(evt: MouseEvent) {
    this.preprocessMouseEvent(evt);
    var self = this;
    self.currentTool && self.currentTool.ondblclick(evt);
  }

  /**
   * keydown listener
   */
  onkeydown(e: KeyboardEvent) {
    var self = this;
    //console.log('keyCode', e.key,);
    if (e.key.toLocaleLowerCase() == 'escape') { // esc
      self.currentTool && self.currentTool.oncancel();
    }
  }


  /**
   * 鼠标滚轮/Trackpad
   * Trackpad双指可以上下左右移动，也可以放到缩小
   * 鼠标滚利则只能上下移动, 放大缩小需要按住ctrl键
   * @param evt 
   */
  onwheel(evt: WheelEvent) {
    this.preprocessMouseEvent(evt);
    evt.preventDefault();
    //console.log('evt', evt, evt.ctrlKey, evt.deltaX, evt.deltaY, evt.detail)
    if (evt.ctrlKey) { //Trackpad双指, 考虑deltaY即可
      //todo 统一使用getXY()
      let pivotX = evt.offsetX * this.scaleX;
      let pivotY = evt.offsetY * this.scaleY;
      let scaleBy = 1;
      if (evt.deltaY > 0) { //缩小
        scaleBy = 1 / (1 + Math.abs(evt.deltaY / 60)); //别问为什么是60，问就是不知道
      } else { //放大
        scaleBy = 1 + Math.abs(evt.deltaY / 60);
      }
      this.scaleBy(scaleBy, scaleBy, pivotX, pivotY);
    } else {
      this.pan(-evt.deltaX, -evt.deltaY);
    }
  }


  /**
   * Invoke a pending redraw.
   * The redraw will not happend immediatly, it will wait to the
   * next animation frame, during the waiting, other redraw request
   * will be ignored.
   */
  invalidate() {
    var self = this;
    if (self.needDraw) {
      return;
    }
    self.needDraw = true;
    requestAnimationFrame(function () { self.draw(); });
  }


  private drawDummy() {
    this.ctx.fillStyle = "red";
    this.ctx.fillRect(0, 0, 200, 200);
  }

  /**
   * Draw all the shapes and controls.
   */
  private draw() {
    this.needDraw = false;

    if (!this.backgroundLayer) {
      return;
    }

    let self = this;
    let ctx = self.ctx;

    let rect = new Rect(0, 0, this.contentWidth, this.contentHeight);
    rect = self.matrix.mapRect(rect);

    // background.
    ctx.clearRect(0, 0, this.width, this.height);
    ctx.fillStyle = self.opts.bgColor || self.bgColor || 'blue';
    //console.log('ctx.fillStyle: ', ctx.fillStyle)
    ctx.fillRect(0, 0, this.width, this.height);

    // draw shadow
    if (this.opts.shadow) {
      ctx.save();
      ctx.fillStyle = 'white';
      ctx.shadowColor = this.opts.shadowColor || "#ccc";
      ctx.shadowBlur = 30;
      ctx.fillRect(rect.left, rect.top, rect.width(), rect.height());
      ctx.restore();
    }

    ctx.save();
    // apply matrix.
    ctx.transform.apply(ctx, self.matrix.toArray());

    //this.drawDummy();

    // draw background layers after transform.
    if (self.backgroundLayer) {
      ctx.save();
      self.backgroundLayer.draw(ctx);
      ctx.restore();
    }


    ctx.restore();


    // draw border
    if (this.opts.border || true) {
      ctx.strokeStyle = this.opts.borderStyle || "#999";
      ctx.lineWidth = window.devicePixelRatio * 1;
      ctx.strokeRect(rect.left, rect.top, rect.width(), rect.height());
    }

    //draw mark rect.
    if (this.theMarkRect) {
      let r = this.theMarkRect;
      console.log('markRect: ', r);
      ctx.save();
      ctx.strokeStyle = 'white';
      ctx.lineWidth = 3;
      ctx.strokeRect(r.left, r.top, r.width(), r.height());
      ctx.restore();
      //only draw once
      this.theMarkRect = null;
    }


    //draw viewport.
    if (false) {
      ctx.save();
      let vp = this.viewport;
      ctx.strokeStyle = 'green';
      ctx.lineWidth = 2;
      ctx.strokeRect(vp.left, vp.top, vp.width(), vp.height());
      ctx.restore();
    }


    //draw center cross 
    if (false) {
      ctx.save();
      ctx.strokeStyle = 'red';
      ctx.lineWidth = 2;
      ctx.moveTo(0, this.height / 2);
      ctx.lineTo(this.width, this.height / 2);
      ctx.moveTo(this.width / 2, 0);
      ctx.lineTo(this.width / 2, this.height);
      ctx.stroke();
      ctx.restore();
    }

  }




  /**
   * Add background layer
   * TODO reset matrix when layer changes.
   */
  setBackgroundLayer(layer: Layer, bestFit: boolean = false) {
    var self = this;
    if (self.backgroundLayer != layer) {
      if (layer) {
        layer.trigger('load');
        layer.on('invalidate', this.invalidateHandler);
      }
      if (self.backgroundLayer) {
        self.backgroundLayer.trigger('unload');
        self.backgroundLayer.un('invalidate', this.invalidateHandler);
      }

      self.backgroundLayer = layer;
      if (self.backgroundLayer) {
        self.backgroundLayer.lastToolKey && this.setToolByKey(self.backgroundLayer.lastToolKey);
        bestFit && (this.matrix = this.bestFitMatrix);
      }
    }


    this.invalidate();

  }


  getBackgroundLayer() {
    return this.backgroundLayer;
  }


  /**
   * Get event x,y
   * @param map Map the point into innner xy.
   */
  /*
  getXY(evt: MouseEvent, map: boolean = false) {
    var self = this;
    var p = {
      x: (evt.pageX - $(self.canvas).offset().left) * self.scaleX,
      y: (evt.pageY - $(self.canvas).offset().top) * self.scaleY,
    };
    evt.canvasPoint = p;
    if (!map)
      return p;
    return self.matrix.invMapPoint(p.x, p.y);
  }
  */

  /**
   * For pan tool
   */
  pan(dx, dy) {
    if (this.animator) this.animator.cancel();
    this.matrix.postTranslate(dx, dy);
    this.invalidate();
  }


  /**
   * 围绕指定的中心点进行缩放
   * @param dScaleX 
   * @param dScaleY 
   * @param pivotX   
   * @param pivotY 
   */
  scaleBy(dScaleX: number, dScaleY: number, pivotX: number, pivotY: number) {
    if (this.animator) this.animator.cancel();
    let dScale = Math.max(this.opts.minScale / this.matrix.getScale().x, dScaleX);
    this.matrix.postScaleBy(dScale, dScale, pivotX, pivotY);
    this.invalidate();
  }

  /**
   * Reset scale.
   */
  bestFit() {
    //this.matrix = this.bestFitMatrix;
    this.animationTo(this.bestFitMatrix);
  }

  /**
   * Reset scale.
   */
  reset100() {
    let translateX = (this.width - this.contentWidth) / 2;
    let translateY = (this.height - this.contentHeight) / 2;
    this.animationTo(new Matrix([1, 0, 0, 1, translateX, translateY]));
  }

  getScale() {
    return this.matrix.getScale();
  }

  setBgColor(color: string) {
    this.bgColor = color;
    this.invalidate();
  }


  addTool(tool: Tool) {
    console.log('addTool:', tool)
    this.tools.push(tool);
  }

  /**
   * get Tool hash for quick search.
   */
  get toolHash() {
    return this.tools.reduce((last: any, cur: Tool) => {
      last[cur.key] = cur;
      return last;
    }, {});
  }


  /**
   * Set the current active tool.
   * @param tool Tool or null
   * @returns 
   */
  setTool(tool: Tool) {
    if (this.currentTool == tool) {
      tool.onSelect();
      return;
    }
      
    tool && tool.setEditor(this);
    this.currentTool && this.currentTool.onUnSelect();
    this.currentTool && this.currentTool.setEditor(null);
    this.currentTool = tool;
    console.log('Select tool: ', this.currentTool);
    this.updateCursor();
    if (tool && this.backgroundLayer) this.backgroundLayer.lastToolKey = tool.key;
    tool && tool.onSelect();
  }

  /**
   * Set the current tool of the editor.
   */
  setToolByKey(toolKey: string) {
    console.log('all tools:', this.tools)
    let tool = this.toolHash[toolKey];
    if (!tool && this.backgroundLayer) {
      tool = this.backgroundLayer.getToolByKey(toolKey);
    }
    this.setTool(tool);
  }


  updateCursor() {
    if (this.currentTool) {
      this.canvas.style.cursor = this.currentTool.cursor;
    }
  }


  /**
   * Get all tool status
   * including edit's tool and current layer's tools
   * @returns 
   */
  getToolsStatus() {
    let tools = this.tools.slice();
    if (this.backgroundLayer) {
      tools = tools.concat(this.backgroundLayer.getTools());
    }
    return tools.map((tool: Tool) => {
      return { key: tool.key, active: tool.isActive() }
    })
  }


  markRect(left: number, top: number, right: number, bottom: number) {
    let rect = new Rect(left, top, right, bottom);
    let canvasRect = this.matrix.mapRect(rect);
    console.log('rect: ', rect, canvasRect);
    this.theMarkRect = canvasRect;
    this.invalidate();
  }

  clearMarkRect() {
    this.theMarkRect = null;
    this.invalidate();
  }

  centerToRect(left: number, top: number, right: number, bottom: number) {
    let rect = new Rect(left, top, right, bottom);
    let cr = this.matrix.mapRect(rect);
    let cx = this.canvas.width / 2;
    let cy = this.canvas.height / 2;
    let srcCenter = cr.center();
    let dx = cx - srcCenter.x;
    let dy = cy - srcCenter.y;

    let scale = this.canvas.width / 3 / cr.width();

    this.pan(dx, dy);
    this.matrix.postScaleBy(scale, scale, cx, cy);
    this.markRect(left, top, right, bottom)
  }


  /**
   * 获取canvas指定点的颜色
   * @param point 
   * @param layerPoint 
   * @returns rgba(xxx)
   */
  pickColor(point: Point, layerPoint: Point): string | null {
    console.log('pickColor point:', point, layerPoint)
    let x = Math.floor(point.x);
    let y = Math.floor(point.y);
    if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
      return null;
    }
    let imgData = this.ctx.getImageData(x, y, 1, 1);
    let color = Utils.makeColor(imgData.data);
    console.log('color: ', color);
    this.trigger('pick-color', color);
    return color;
  }


  /**
   * 获取编辑器可视区域
   */
  get viewport(): Rect {
    return new Rect(
      this.padding.left * this.scaleX,
      this.padding.top * this.scaleY,
      this.width - this.padding.right * this.scaleX,
      this.height - this.padding.bottom * this.scaleY
    );
  }


  /**
   * bestfit the content rect to the viewport.
   * @param rect 
   */
  focusToRect(rect: Rect) {
    let center = rect.center();
    let m = this.matrix.clone();
    let p = m.mapPoint(center.x, center.y);
    let viewport = this.viewport;

    let vc = viewport.center();
    let dx = vc.x - p.x;
    let dy = vc.y - p.y;
    m.postTranslate(dx, dy); //rect中心点移动到屏幕可见区域中心

    let maxScale = Math.min(viewport.width() / rect.width(), viewport.height() / rect.height());
    if (maxScale > 20) maxScale = 20;
    let scaleBy = maxScale / m.getScale().x;
    m.postScaleBy(scaleBy, scaleBy, vc.x, vc.y);


    //this.matrix = m;
    this.animationTo(m);
  }

  animator: Animator;
  animationTo(endMatrix: Matrix) {
    if (this.animator) this.animator.cancel();
    let animator = new Animator(this.matrix.m, endMatrix.m)
      .setDuration(400)
      //.setEasing(Easing.easeInOutQuadBack2);
      .setEasing(Easing.easeInOutQuad);
    animator.on('animationUpdate', ((a, values) => {
      this.matrix = new Matrix(values);
    }))
    animator.start();
    this.animator = animator;
  }



  /**
   * 销毁editor对象，防止内存泄漏
   */
  override destroy(): void {
    super.destroy(); //event emitter
    this.setBackgroundLayer(null); //remove layer
    this.setTool(null); //remove tool

    //remove event handlers
    window.removeEventListener('resize', this.resizeHandler);
    this.canvas.removeEventListener('click', this.clickHander);
    this.canvas.removeEventListener('dblclick', this.dblclickHandler);
    this.canvas.removeEventListener('mousedown', this.mousedownHandler);
    this.canvas.removeEventListener('mouseup', this.mouseupHandler);
    this.canvas.removeEventListener('wheel', this.wheelHandler);
    this.canvas.removeEventListener('wheel', this.wheelHandler);
    document.removeEventListener('mousemove', this.mousemoveHandler);
    document.removeEventListener('keydown', this.keydownHandler);

  }



}
