import {mat3} from 'gl-matrix';

import clamp from 'sentry/utils/number/clamp';

// Computes the transformation matrix that is used to render scaled
// elements to the DOM and draw the view.
export class TraceView {
  // Represents the space of the entire trace, for example
  // a trace starting at 0 and ending at 1000 would have a space of [0, 1000]
  to_origin: number = 0;
  trace_space: DOMView = DOMView.Empty();
  // The view defines what the user is currently looking at, it is a subset
  // of the trace space. For example, if the user is currently looking at the
  // trace from 500 to 1000, the view would be represented by [x, width] = [500, 500]
  trace_view: DOMView = DOMView.Empty();
  // Represents the pixel space of the entire trace - this is the container
  // that we render to. For example, if the container is 1000px wide, the
  // pixel space would be [0, 1000]
  trace_physical_space: DOMView = DOMView.Empty();
  // the encapsulating container that the entire view is rendered to
  trace_container_physical_space: DOMView = DOMView.Empty();
  public readonly MAX_ZOOM_PRECISION_MS = 1;

  setTracePhysicalSpace(
    container_space: [x: number, y: number, width: number, height: number],
    space: [x: number, y: number, width: number, height: number]
  ) {
    this.trace_container_physical_space = new DOMView(
      0,
      0,
      container_space[2],
      container_space[3]
    );
    this.trace_physical_space = new DOMView(0, 0, space[2], space[3]);
  }

  setTraceSpace(space: [x: number, y: number, width: number, height: number]) {
    this.to_origin = space[0];
    this.trace_space = new DOMView(0, 0, space[2], space[3]);
    this.trace_view = new DOMView(0, 0, space[2], space[3]);
  }

  setTraceView(view: {width?: number; x?: number}) {
    // In cases where a trace might have a single error, there is no concept of a timeline
    if (this.trace_view.width === 0) {
      return;
    }

    const x = view.x ?? this.trace_view.x;
    const width = view.width ?? this.trace_view.width;

    this.trace_view.x = clamp(
      x,
      0,
      Math.max(this.trace_space.width - width, this.MAX_ZOOM_PRECISION_MS)
    );
    this.trace_view.width = clamp(
      width,
      this.MAX_ZOOM_PRECISION_MS,
      this.trace_space.width - this.trace_view.x
    );
  }

  getConfigSpaceCursor(cursor: {x: number; y: number}): [number, number] {
    const left_percentage = cursor.x / this.trace_physical_space.width;
    const left_view = left_percentage * this.trace_view.width;

    return [this.trace_view.x + left_view, 0];
  }
}

/**
 * Helper class that handles computing transformations between different views to and from DOM space
 */
class DOMView {
  public x: number;
  public y: number;
  public width: number;
  public height: number;

  constructor(x: number, y: number, width: number, height: number) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
  }

  static From(view: DOMView): DOMView {
    return new DOMView(view.x, view.y, view.width, view.height);
  }

  static Empty(): DOMView {
    return new DOMView(0, 0, 1000, 1);
  }

  serialize() {
    return [this.x, this.y, this.width, this.height];
  }

  between(to: DOMView): mat3 {
    return mat3.fromValues(
      to.width / this.width,
      0,
      0,
      to.height / this.height,
      0,
      0,
      to.x - this.x * (to.width / this.width),
      to.y - this.y * (to.height / this.height),
      1
    );
  }

  transform(mat: mat3): [number, number, number, number] {
    const x = this.x * mat[0] + this.y * mat[3] + mat[6];
    const y = this.x * mat[1] + this.y * mat[4] + mat[7];
    const width = this.width * mat[0] + this.height * mat[3];
    const height = this.width * mat[1] + this.height * mat[4];
    return [x, y, width, height];
  }

  get center() {
    return this.x + this.width / 2;
  }

  get left() {
    return this.x;
  }
  get right() {
    return this.x + this.width;
  }
  get top() {
    return this.y;
  }
  get bottom() {
    return this.y + this.height;
  }
}
