import { Rect } from "./2d";
import { Animator, Interpolator } from "./Animator";
import { Gesture } from "./Gesture";
import { m4 } from "./m4";

export interface Disposable {
  dispose(): void;
}

export interface Layer {
  preDraw(): void;
  draw(): void;
  tap(cx: number, cy: number, sx: number, sy: number): void;
  scale(scale: number): void;
  dispose(): void;
}

export class LayerAB implements Layer {
  preDraw(): void {}
  draw(): void {}
  tap(cx: number, cy: number, sx: number, sy: number): void {}
  scale(scale: number): void {}
  dispose(): void {}
}

export class Padding {
  constructor(
    readonly left: number,
    readonly top: number,
    readonly right: number,
    readonly bottom: number,
  ) {}

  equals(other: Padding) {
    return (
      this.left == other.left &&
      this.top == other.top &&
      this.right == other.right &&
      this.bottom == other.bottom
    );
  }
}

export class Scene implements Disposable {
  layers: Array<Layer> = [];
  testLayers: Array<Layer> = [];

  animators: Array<Animator> = [];

  userMat: m4.Matrix4 = m4.identity();
  bestFitMat: m4.Matrix4 = m4.identity();
  projectionMat: m4.Matrix4 = m4.identity();
  resultMat: m4.Matrix4 = m4.identity();

  width: number = 0;
  height: number = 0;
  contentWidth: number = 0;
  contentHeight: number = 0;

  padding = new Padding(0, 0, 0, 0);

  pendingDraw = false;
  // clearColorValue: [number, number, number, number] = [1, 1, 1, 1]; // 默认白色
  clearColorValue: [number, number, number, number] = [0, 0, 0, 0]; // 默认透明

  /** 游戏完成后调用，将 canvas 底色改为透明，让 body 渐变色透出 */
  setClearTransparent() {
    this.clearColorValue = [0, 0, 0, 0];
    this.invalidate();
  }

  constructor(
    readonly gl: WebGL2RenderingContext,
    public readonly ratio: number,
    public animationFrameProvider: AnimationFrameProvider = window,
    public interact: boolean = true,
  ) {
    this.updateViewport();

    let self = this;

    if (window && interact) {
      new Gesture(gl.canvas as HTMLElement, {
        // drag: self.drag.bind(self),
        // zoom: self.scaleAt.bind(self),
        tap: self.tap.bind(self),
      });
    }

    m4.identity();
  }

  updateViewport() {
    console.log("viewport update.");
    const gl = this.gl;
    const canvas = gl.canvas as HTMLCanvasElement;
    //canvas.width = canvas.clientWidth * this.ratio
    //canvas.height = canvas.clientHeight * this.ratio
    this.invalidate();

    if (canvas.width != this.width || canvas.height != this.height) {
      this.width = canvas.width;
      this.height = canvas.height;
      m4.projection(this.width, this.height, this.projectionMat);
      this.updateBestFit();
      this.updateResultMat();
    }
  }

  private isBestMatSet: boolean = false;

  private updateBestFit() {
    if (this.contentWidth == 0 || this.width == 0) return;

    const box = new Rect(
      this.padding.left,
      this.padding.top,
      this.width - this.padding.right - this.padding.left,
      this.height - this.padding.top - this.padding.bottom,
    );
    const scale = Math.min(
      box.width / this.contentWidth,
      box.height / this.contentHeight,
    );
    const tx = box.center.x - (this.contentWidth * scale) / 2;
    const ty = box.center.y - (this.contentHeight * scale) / 2;
    m4.identity(this.bestFitMat);
    this.bestFitMat[0] = scale;
    this.bestFitMat[5] = scale;
    this.bestFitMat[12] = tx;
    this.bestFitMat[13] = ty;

    if (!this.isBestMatSet) {
      m4.copy(this.bestFitMat, this.userMat);
      this.invalidate();
      this.isBestMatSet = true;
    }
  }

  updateResultMat() {
    if (this.contentWidth == 0 || this.width == 0) return;

    const box = new Rect(
      this.padding.left,
      this.padding.top,
      this.width - this.padding.right - this.padding.left,
      this.height - this.padding.top - this.padding.bottom,
    );
    // const scale = Math.min(box.width / this.contentWidth, box.height / this.contentHeight)
    // const tx = box.center.x - this.contentWidth * scale / 2
    // const ty = box.center.y - this.contentHeight * scale / 2
    // m4.identity(this.resultMat)
    // this.resultMat[0] = scale
    // this.resultMat[5] = scale
    // this.resultMat[12] = tx
    // this.resultMat[13] = ty

    let rate = this.width > this.height ? 0.6 : 0.7;
    let scaleX = (this.width * rate) / this.contentWidth;
    let scaleY = (this.height * rate) / this.contentHeight;
    let scale = Math.min(scaleX, scaleY);
    const tx = box.center.x - (this.contentWidth * scale) / 2;
    const ty = box.center.y - (this.contentHeight * scale) / 2;
    m4.identity(this.resultMat);
    this.resultMat[0] = scale;
    this.resultMat[5] = scale;
    this.resultMat[12] = tx;
    this.resultMat[13] = ty;
  }

  setContentPadding(padding: Padding) {
    if (!this.padding.equals(padding)) {
      this.padding = padding;
      this.updateBestFit();
      this.updateResultMat();
    }
  }

  setContentSize(width: number, height: number) {
    if (width != this.contentWidth || height != this.contentWidth) {
      this.contentWidth = width;
      this.contentHeight = height;
      this.updateBestFit();
      this.updateResultMat();
    }
  }

  addLayer(layer: Layer) {
    this.layers.push(layer);
    this.invalidate();
  }

  invalidate() {
    if (this.pendingDraw) return;
    this.pendingDraw = true;

    //requestAnimationFrame(() => {
    this.animationFrameProvider.requestAnimationFrame(() => {
      this.draw();
    });
  }

  draw() {
    this.animators.forEach((a) => a.update());

    this.layers.forEach((l) => l.preDraw());

    const gl = this.gl;

    gl.viewport(0, 0, this.width, this.height);
    gl.clearColor(...this.clearColorValue);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    gl.enable(gl.BLEND);
    gl.disable(gl.DEPTH_TEST);

    this.layers.forEach((l) => l.draw());
    this.pendingDraw = false;

    this.animators = this.animators.filter((a) => !a.removable());

    if (this.animators.length > 0) {
      this.invalidate();
    }
  }

  get drawMatrix(): m4.Matrix4 {
    return m4.multiply(this.projectionMat, this.userMat);
  }

  drag(dx: number, dy: number) {
    let t = m4.translation(dx * this.ratio, dy * this.ratio, 0);
    this.userMat = m4.multiply(t, this.userMat);
    this.invalidate();
  }

  scaleAt(scale: number, focusX: number, focusY: number) {
    focusX *= this.ratio;
    focusY *= this.ratio;
    let t = m4.scaleAt(scale, scale, focusX, focusY);
    this.userMat = m4.multiply(t, this.userMat);
    this.layers.forEach((l) => l.scale(this.userMat[0]));
    this.invalidate();
  }

  tap(x: number, y: number) {
    let sx = x * this.ratio;
    let sy = y * this.ratio;
    let [cx, cy] = m4.transformPoint(
      m4.inverse(this.userMat),
      new Float32Array([sx, sy, 0]),
    );
    this.layers.forEach((l) => l.tap(cx, cy, sx, sy));
  }

  addAnimator(animator: Animator) {
    this.animators.push(animator);
    this.invalidate();
  }

  addTestLayer(layer: Layer) {
    this.testLayers.push(layer);
    if (this.testLayers.length <= 1) this.layers.push(layer);
    this.invalidate();
  }

  toggleTestLayer() {
    if (this.testLayers.length == 0) return;

    let testLayerIndex = this.testLayers.findIndex((l) => {
      return this.layers.indexOf(l) >= 0;
    });

    let next = (testLayerIndex + 1) % this.testLayers.length;
    let nextLayer = this.testLayers[next];

    if (testLayerIndex >= 0) {
      this.layers = this.layers.filter(
        (l) => l != this.testLayers[testLayerIndex],
      );
    }

    console.log(`toggleTestLayer, layer=${nextLayer}`);

    this.layers.push(nextLayer);
    this.invalidate();
  }

  setScale(scale: number) {
    m4.scaling(scale, scale, 1, this.userMat);
    this.invalidate();
  }

  updateUserMat(mat: m4.Matrix4) {
    m4.copy(mat, this.userMat);
    this.invalidate();
  }

  matrixAnimationTo(
    endMat: m4.Matrix4,
    duration: number,
    interpolator?: Interpolator,
    onEnd?: Function,
  ) {
    const startMat = m4.copy(this.userMat);
    const mat = new Float32Array(16);
    const animator = new Animator(
      duration,
      () => {
        m4.lerp(startMat, endMat, mat, animator.value());
        this.updateUserMat(mat);
      },
      () => {
        onEnd?.();
      },
      interpolator,
    );
    this.addAnimator(animator);
  }

  resetToBestFit() {
    this.matrixAnimationTo(this.bestFitMat, 600);
  }

  dispose() {
    while (this.layers.length > 0) {
      let layer = this.layers.pop();
      layer?.dispose();
    }
  }
}
