import {mat3, vec2} from 'gl-matrix';

import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
import {
  computeClampedConfigView,
  Rect,
  transformMatrixBetweenRect,
} from 'sentry/utils/profiling/gl/utils';

export class CanvasView<T extends {configSpace: Rect}> {
  configView: Rect = Rect.Empty();
  configSpace: Readonly<Rect> = Rect.Empty();
  configSpaceTransform: mat3 = mat3.create();

  inverted: boolean;
  minWidth: number;
  depthOffset: number;
  barHeight: number;
  model: T;

  constructor({
    canvas,
    options,
    model,
  }: {
    canvas: FlamegraphCanvas;
    model: T;
    options: {
      barHeight: number;
      configSpaceTransform?: Rect;
      depthOffset?: number;
      inverted?: boolean;
      minWidth?: number;
    };
  }) {
    this.inverted = !!options.inverted;
    this.minWidth = options.minWidth ?? 0;
    this.model = model;
    this.depthOffset = options.depthOffset ?? 0;
    this.barHeight = options.barHeight ? options.barHeight * window.devicePixelRatio : 0;

    // This is a transformation matrix that is applied to the configView, it allows us to
    // transform an entire view and render it without having to recompute the models.
    // This is useful for example when we want to offset a profile by some duration.
    this.configSpaceTransform = options.configSpaceTransform
      ? mat3.fromValues(
          options.configSpaceTransform.width || 1,
          0,
          0,
          0,
          options.configSpaceTransform.height || 1,
          0,
          options.configSpaceTransform.x || 0,
          options.configSpaceTransform.y || 0,
          1
        )
      : mat3.create();

    this.initConfigSpace(canvas);
  }

  private _initConfigSpace(canvas: FlamegraphCanvas): void {
    this.configSpace = new Rect(
      0,
      0,
      this.model.configSpace.width,
      Math.max(
        this.model.configSpace.height + this.depthOffset,
        canvas.physicalSpace.height / this.barHeight
      )
    );
  }

  private _initConfigView(canvas: FlamegraphCanvas, space: Rect): void {
    this.configView = Rect.From(space).withHeight(
      canvas.physicalSpace.height / this.barHeight
    );
  }

  initConfigSpace(canvas: FlamegraphCanvas): void {
    this._initConfigSpace(canvas);
    this._initConfigView(canvas, this.configSpace);
  }

  resizeConfigSpace(canvas: FlamegraphCanvas): void {
    this._initConfigSpace(canvas);
    this._initConfigView(canvas, this.configView);
  }

  resetConfigView(canvas: FlamegraphCanvas): void {
    this._initConfigView(canvas, this.configSpace);
  }

  setConfigView(configView: Rect) {
    this.configView = computeClampedConfigView(configView, {
      width: {
        min: this.minWidth,
        max: this.configSpace.width,
      },
      height: {
        min: 0,
        max: this.configSpace.height,
      },
    });
  }

  transformConfigView(transformation: mat3) {
    this.setConfigView(this.configView.transformRect(transformation));
  }

  toConfigSpace(space: Rect): mat3 {
    const toConfigSpace = transformMatrixBetweenRect(space, this.configSpace);

    if (this.inverted) {
      mat3.multiply(toConfigSpace, this.configSpace.invertYTransform(), toConfigSpace);
    }

    return toConfigSpace;
  }

  toConfigView(space: Rect): mat3 {
    const toConfigView = transformMatrixBetweenRect(space, this.configView);

    if (this.inverted) {
      mat3.multiply(toConfigView, this.configView.invertYTransform(), toConfigView);
    }

    return toConfigView;
  }

  fromConfigSpace(space: Rect): mat3 {
    const fromConfigSpace = transformMatrixBetweenRect(this.configSpace, space);

    if (this.inverted) {
      mat3.multiply(fromConfigSpace, space.invertYTransform(), fromConfigSpace);
    }

    return fromConfigSpace;
  }

  fromConfigView(space: Rect): mat3 {
    const fromConfigView = transformMatrixBetweenRect(this.configView, space);

    if (this.inverted) {
      mat3.multiply(fromConfigView, space.invertYTransform(), fromConfigView);
    }

    return fromConfigView;
  }

  fromTransformedConfigView(space: Rect): mat3 {
    const fromConfigView = mat3.multiply(
      mat3.create(),
      transformMatrixBetweenRect(this.configView, space),
      this.configSpaceTransform
    );

    if (this.inverted) {
      mat3.multiply(fromConfigView, space.invertYTransform(), fromConfigView);
    }

    return fromConfigView;
  }

  fromTransformedConfigSpace(space: Rect): mat3 {
    const fromConfigView = mat3.multiply(
      mat3.create(),
      transformMatrixBetweenRect(this.configSpace, space),
      this.configSpaceTransform
    );

    if (this.inverted) {
      mat3.multiply(fromConfigView, space.invertYTransform(), fromConfigView);
    }

    return fromConfigView;
  }

  getConfigSpaceCursor(logicalSpaceCursor: vec2, canvas: FlamegraphCanvas): vec2 {
    const physicalSpaceCursor = vec2.transformMat3(
      vec2.create(),
      logicalSpaceCursor,
      canvas.logicalToPhysicalSpace
    );

    return vec2.transformMat3(
      vec2.create(),
      physicalSpaceCursor,
      this.toConfigSpace(canvas.physicalSpace)
    );
  }

  getTransformedConfigSpaceCursor(
    logicalSpaceCursor: vec2,
    canvas: FlamegraphCanvas
  ): vec2 {
    const physicalSpaceCursor = vec2.transformMat3(
      vec2.create(),
      logicalSpaceCursor,
      canvas.logicalToPhysicalSpace
    );

    const finalMatrix = mat3.multiply(
      mat3.create(),
      mat3.invert(mat3.create(), this.configSpaceTransform),
      this.toConfigSpace(canvas.physicalSpace)
    );

    const configViewCursor = vec2.transformMat3(
      vec2.create(),
      physicalSpaceCursor,
      finalMatrix
    );

    return configViewCursor;
  }

  getConfigViewCursor(logicalSpaceCursor: vec2, canvas: FlamegraphCanvas): vec2 {
    const physicalSpaceCursor = vec2.transformMat3(
      vec2.create(),
      logicalSpaceCursor,
      canvas.logicalToPhysicalSpace
    );

    return vec2.transformMat3(
      vec2.create(),
      physicalSpaceCursor,
      this.toConfigView(canvas.physicalSpace)
    );
  }

  getTransformedConfigViewCursor(
    logicalSpaceCursor: vec2,
    canvas: FlamegraphCanvas
  ): vec2 {
    const physicalSpaceCursor = vec2.transformMat3(
      vec2.create(),
      logicalSpaceCursor,
      canvas.logicalToPhysicalSpace
    );

    const finalMatrix = mat3.multiply(
      mat3.create(),
      mat3.invert(mat3.create(), this.configSpaceTransform),
      this.toConfigView(canvas.physicalSpace)
    );

    const configViewCursor = vec2.transformMat3(
      vec2.create(),
      physicalSpaceCursor,
      finalMatrix
    );

    return configViewCursor;
  }

  /**
   * Applies the inverse of the config space transform to the given config space rect
   * @returns Rect
   */
  toOriginConfigView(space: Rect): Rect {
    return space.transformRect(mat3.invert(mat3.create(), this.configSpaceTransform));
  }
}
