import { Point } from './interface';
import Rect from './rect';

/**
 *
 * Matrix
 *
 */
export default class Matrix {
  m: Array<number>;
  initial: Array<number>;
  inverse: Matrix;
  maxScale: number  = 20;
  minScale: any;

  constructor(m: Array<number>) {
    if (m) {
      this.m = m.slice();
    } else {
      this.m = [1, 0, 0, 1, 0, 0];
    }
    this.initial = this.m.slice();
    //invert matrix.
    this.inverse = null;
  }

  public static get identical() {
    return new Matrix([1, 0, 0, 1, 0, 0]);
  }

  /**
   * Clone matrix.
   */
  clone() {
    return new Matrix(this.m.slice());
  }

  /**
   * Invert the matrix.
   */
  invert() {
    const d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]);
    const m0 = this.m[3] * d;
    const m1 = -this.m[1] * d;
    const m2 = -this.m[2] * d;
    const m3 = this.m[0] * d;
    const m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]);
    const m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]);

    this.m[0] = m0;
    this.m[1] = m1;
    this.m[2] = m2;
    this.m[3] = m3;
    this.m[4] = m4;
    this.m[5] = m5;
    return this;
  }

  /**
   * Multiply another matrix arr.
   */
  multiply(marr) {
    const m0 = this.m[0] * marr[0] + this.m[2] * marr[1];
    const m1 = this.m[1] * marr[0] + this.m[3] * marr[1];
    const m2 = this.m[0] * marr[2] + this.m[2] * marr[3];
    const m3 = this.m[1] * marr[2] + this.m[3] * marr[3];
    const m4 = this.m[0] * marr[4] + this.m[2] * marr[5] + this.m[4] * 1;
    const m5 = this.m[1] * marr[4] + this.m[3] * marr[5] + this.m[5] * 1;
    this.m[0] = m0;
    this.m[1] = m1;
    this.m[2] = m2;
    this.m[3] = m3;
    this.m[4] = m4;
    this.m[5] = m5;
    return this;
  }







  /**
   * Post multiply
   */
  postMultiply(marr) {
    const m0 = marr[0] * this.m[0] + marr[2] * this.m[1];
    const m1 = marr[1] * this.m[0] + marr[3] * this.m[1];
    const m2 = marr[0] * this.m[2] + marr[2] * this.m[3];
    const m3 = marr[1] * this.m[2] + marr[3] * this.m[3];
    const m4 = marr[0] * this.m[4] + marr[2] * this.m[5] + marr[4] * 1;
    const m5 = marr[1] * this.m[4] + marr[3] * this.m[5] + marr[5] * 1;
    this.m[0] = m0;
    this.m[1] = m1;
    this.m[2] = m2;
    this.m[3] = m3;
    this.m[4] = m4;
    this.m[5] = m5;
    return this;
  }



  /**
   * Set max scale
   */
  setMaxScale(maxScale) {
    this.maxScale = maxScale;
    return this;
  }

  /**
   * Set min scale
   */
  setMinScale(minScale) {
    this.minScale = minScale;
    return this;
  }


  /**
   * Scale matrix
   */
  setScale(sx, sy) {
    this.m[0] = sx;
    this.m[3] = sy;
    this.resetInverse();
    return this;
  }

  /**
   * Set matrix translate
   */
  setTranslate(tx, ty) {
    this.m[4] = tx;
    this.m[5] = ty;
    this.resetInverse();
    return this;
  }


  /**
   * Get scaleX scaleY
   */
  getScale() {
    var self = this;
    return {
      x: self.m[0],
      y: self.m[3]
    };
  }

  get scaleX(): number { return this.m[0] }
  get scaleY(): number { return this.m[3] }

  /**
   * Get translateX translateY
   */
  getTranslate() {
    var self = this;
    return {
      x: self.m[4],
      y: self.m[5]
    }
  }

  /**
   * Post scale 
   */
  postScale(sx, sy) {
    this.postMultiply([sx, 0, 0, sy, 0, 0]);
    this.resetInverse();
    return this;
  }

  /**
   * Post scale by pivot(x,y)
   */
  postScaleBy(sx: number, sy: number, x: number, y: number) {
    this.postMultiply([1, 0, 0, 1, (-1) * x, (-1) * y]);
    this.postMultiply([sx, 0, 0, sy, 0, 0]);
    this.postMultiply([1, 0, 0, 1, x, y]);
    this.resetInverse();
    return this;
  }


  /**
   * Post matrix translate
   */
  postTranslate(tx, ty) {
    this.postMultiply([1, 0, 0, 1, tx, ty]);
    this.resetInverse();
    return this;
  }

  /**
   * Set inverse to null while matrix changed.
   */
  resetInverse() {
    this.inverse = null;
  }


  /**
   * Inverse Map points
   */
  invMapPoint(x, y): Point {
    if (!this.inverse)
      this.inverse = this.clone().invert();
    return this.inverse.mapPoint(x, y)
  }


  /**
   * Map rect by matrix.
   */
  mapRect(rect): Rect {
    var lt = this.mapPoint(rect.left, rect.top);
    var rb = this.mapPoint(rect.right, rect.bottom);
    return new Rect(lt.x, lt.y, rb.x, rb.y);
  }


  /**
   * Map points
   */
  mapPoint(x, y): Point {
    const mx = this.m[0] * x + this.m[2] * y + this.m[4] * 1;
    const my = this.m[1] * x + this.m[3] * y + this.m[5] * 1;
    return {
      x: mx,
      y: my,
    };
  }


  /**
   * Reset to initial.
   */
  reset() {
    this.m = this.initial.slice();
  }


  toArray(): Array<number> {
    return this.m.slice();
  }

  toString() {
    return 'matrix(' + this.toArray().join(',') + ')';
  }


  /**
   * Check if scale between [minScale, maxScale]
   */
  validateScale(sx, sy) {
    var cur = this.getScale();
    var minX = this.minScale / cur.x;
    var minY = this.minScale / cur.y;
    var maxX = this.maxScale / cur.x;
    var maxY = this.maxScale / cur.y;

    sx = Math.max(minX, sx);
    sx = Math.min(maxX, sx);
    sy = Math.max(minY, sy);
    sy = Math.min(maxY, sy);

    return {
      x: sx,
      y: sy
    }
  }

}