// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// This library provides interfaces and classes for handling 2D geometry
// operations.

/**
 * Interface representing a point in 2D space.
 */
export interface Point2D {
  readonly x: number;
  readonly y: number;
}

/**
 * Class representing a 2D vector with methods for vector operations.
 *
 * Note: This class is immutable in TypeScript (not enforced at runtime). Any
 * method that modifies the vector returns a new instance, leaving the original
 * unchanged.
 */
export class Vector2D implements Point2D {
  readonly x: number;
  readonly y: number;

  constructor({x, y}: Point2D) {
    this.x = x;
    this.y = y;
  }

  /**
   * Adds the given point to this vector and returns a new vector.
   *
   * @param point - The point to add.
   * @returns A new Vector2D instance representing the result.
   */
  add(point: Point2D): Vector2D {
    return new Vector2D({x: this.x + point.x, y: this.y + point.y});
  }

  /**
   * Subtracts the given point from this vector and returns a new vector.
   *
   * @param point - The point to subtract.
   * @returns A new Vector2D instance representing the result.
   */
  sub(point: Point2D): Vector2D {
    return new Vector2D({x: this.x - point.x, y: this.y - point.y});
  }

  /**
   * Scales this vector by the given scalar and returns a new vector.
   *
   * @param scalar - The scalar value to multiply the vector by.
   * @returns A new Vector2D instance representing the scaled vector.
   */
  scale(scalar: number): Vector2D {
    return new Vector2D({x: this.x * scalar, y: this.y * scalar});
  }

  /**
   * Computes the Manhattan distance, which is the sum of the absolute values of
   * the x and y components of the vector. This represents the distance
   * travelled along axes at right angles (grid-based distance).
   */
  get manhattanDistance(): number {
    return Math.abs(this.x) + Math.abs(this.y);
  }

  /**
   * Computes the Euclidean magnitude (or length) of the vector. This is the
   * straight-line distance from the origin (0, 0) to the point (x, y) in 2D
   * space.
   */
  get magnitude(): number {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }
}

/**
 * Interface representing the vertical bounds of an object (top and bottom).
 */
export interface VerticalBounds {
  readonly top: number;
  readonly bottom: number;
}

/**
 * Interface representing the horizontal bounds of an object (left and right).
 */
export interface HorizontalBounds {
  readonly left: number;
  readonly right: number;
}

/**
 * Interface combining vertical and horizontal bounds to describe a 2D bounding
 * box.
 */
export interface Bounds2D extends VerticalBounds, HorizontalBounds {}

/**
 * Interface representing the size of a 2D object.
 */
export interface Size2D {
  readonly width: number;
  readonly height: number;
}

/**
 * Immutable class representing a 2D rectangle with a 2D position and size which
 * has functions to mutate and test the rect and can be polymorphically used as
 * any of the following:
 * - Bounds2D
 * - Size2D
 * - Point2D
 */
export class Rect2D implements Bounds2D, Size2D, Point2D {
  readonly left: number;
  readonly top: number;
  readonly right: number;
  readonly bottom: number;
  readonly x: number; // Always equal to left
  readonly y: number; // Always equal to top
  readonly width: number; // Always equal to (right - left)
  readonly height: number; // Always equal to (bottom - top)

  /**
   * Creates a new rect from two points, automatically ordering them to avoid
   * negative rect dimensions.
   *
   * E.g. Rect2D.fromPoints({x: 10, y: 20}, {x: 20, y: 25})
   *
   * @returns A new Rect2D object.
   */
  static fromPoints(a: Point2D, b: Point2D) {
    return new Rect2D({
      top: Math.min(a.y, b.y),
      left: Math.min(a.x, b.x),
      right: Math.max(a.x, b.x),
      bottom: Math.max(a.y, b.y),
    });
  }

  /**
   * Creates a new rect given a point and size.
   *
   * E.g. Rect2D.fromPointAndSize({x: 10, y: 20, width: 100, height: 80})
   *
   * @param pointAndSize - The combined point and size.
   * @returns A new Rect2D object.
   */
  static fromPointAndSize(pointAndSize: Point2D & Size2D) {
    const {x, y, width, height} = pointAndSize;
    return new Rect2D({
      top: y,
      left: x,
      right: x + width,
      bottom: y + height,
    });
  }

  constructor({left, top, right, bottom}: Bounds2D) {
    this.left = this.x = left;
    this.top = this.y = top;
    this.right = right;
    this.bottom = bottom;
    this.width = right - left;
    this.height = bottom - top;
  }

  /**
   * Returns a new rectangle representing the intersection with another
   * rectangle.
   *
   * @param bounds - The bounds of the other rectangle to intersect with.
   * @returns A new Rect2D instance representing the intersected rectangle.
   */
  intersect(bounds: Bounds2D): Rect2D {
    return new Rect2D({
      top: Math.max(this.top, bounds.top),
      left: Math.max(this.left, bounds.left),
      bottom: Math.min(this.bottom, bounds.bottom),
      right: Math.min(this.right, bounds.right),
    });
  }

  /**
   * Expands the rectangle by the given amount on all sides and returns a new
   * rectangle.
   *
   * @param amount - The amount to expand the rectangle by. This can be a number
   * which is applied evenly to each side, or it can be a Size2D object which
   * applies a different expansion amount in the x and y dimensions.
   * @returns A new Rect2D instance representing the expanded rectangle.
   */
  expand(amount: number | Size2D): Rect2D {
    if (typeof amount === 'number') {
      return new Rect2D({
        top: this.top - amount,
        left: this.left - amount,
        bottom: this.bottom + amount,
        right: this.right + amount,
      });
    } else {
      const {width, height} = amount;
      return new Rect2D({
        top: this.top - height,
        left: this.left - width,
        bottom: this.bottom + height,
        right: this.right + width,
      });
    }
  }

  /**
   * Reframes the rectangle by shifting its origin by the given point.
   *
   * @param point - The point by which to shift the origin.
   * @returns A new Rect2D instance representing the reframed rectangle.
   */
  reframe(point: Point2D): Rect2D {
    return new Rect2D({
      left: this.left - point.x,
      right: this.right - point.x,
      top: this.top - point.y,
      bottom: this.bottom - point.y,
    });
  }

  /**
   * Checks if this rectangle fully contains another set of bounds.
   *
   * @param bounds - The bounds to check containment for.
   * @returns True if this rectangle contains the given bounds, false otherwise.
   */
  contains(bounds: Bounds2D): boolean {
    return !(
      bounds.top < this.top ||
      bounds.bottom > this.bottom ||
      bounds.left < this.left ||
      bounds.right > this.right
    );
  }

  /**
   * Checks if this rectangle contains a point in 2D space.
   *
   * @param point - The point to check.
   * @returns True if this rectangle contains the given point, false otherwise.
   */
  containsPoint(point: Point2D): boolean {
    return (
      point.y >= this.top &&
      point.y < this.bottom &&
      point.x >= this.left &&
      point.x < this.right
    );
  }

  /**
   * Checks if this rectangle overlaps another set of bounds.
   *
   * @param bounds - The bounds to check overlap for.
   * @returns rue if this rectangle overlaps the given bounds, false otherwise.
   */
  overlaps(bounds: Bounds2D): boolean {
    return (
      this.left < bounds.right &&
      this.right > bounds.left &&
      this.top < bounds.bottom &&
      this.bottom > bounds.top
    );
  }

  /**
   * Translates the rectangle by the given point and returns a new rectangle.
   *
   * @param point - The point by which to translate the rectangle.
   * @returns A new Rect2D instance representing the translated rectangle.
   */
  translate(point: Point2D): Rect2D {
    return new Rect2D({
      top: this.top + point.y,
      left: this.left + point.x,
      bottom: this.bottom + point.y,
      right: this.right + point.x,
    });
  }

  equals(bounds: Bounds2D): boolean {
    return (
      bounds.top === this.top &&
      bounds.left === this.left &&
      bounds.right === this.right &&
      bounds.bottom === this.bottom
    );
  }
}

// 2D transformation (offset + scale).
// - Offsets add: newOffset = currentOffset + transform.offset
// - Scales multiply: newScale = currentScale * transform.scale
// For time-to-pixel conversion, use scaleX as pixels-per-time-unit.
// A 1D scale+translate transform: value' = value * scale + offset
export interface Transform1D {
  readonly scale: number;
  readonly offset: number;
}

export namespace Transform1D {
  export const Identity: Transform1D = {
    scale: 1,
    offset: 0,
  };
}

export interface Transform2D {
  readonly offsetX: number; // Pixel offset in X
  readonly offsetY: number; // Pixel offset in Y
  readonly scaleX: number; // Scale factor for X (use as pxPerTime for time conversion)
  readonly scaleY: number; // Scale factor for Y
}

export namespace Transform2D {
  export const Identity: Transform2D = {
    offsetX: 0,
    offsetY: 0,
    scaleX: 1,
    scaleY: 1,
  };

  export function compose(
    a: Transform2D,
    b: Partial<Transform2D>,
  ): Transform2D {
    const {offsetX = 0, offsetY = 0, scaleX = 1, scaleY = 1} = b;
    return {
      offsetX: a.offsetX + offsetX * a.scaleX,
      offsetY: a.offsetY + offsetY * a.scaleY,
      scaleX: a.scaleX * scaleX,
      scaleY: a.scaleY * scaleY,
    };
  }

  // Column-major mat3 for use with WebGL uniformMatrix3fv.
  // | scaleX  0       0 |
  // | 0       scaleY  0 |
  // | offsetX offsetY 1 |
  export function toMat3(t: Transform2D): Float32Array {
    // prettier-ignore
    return new Float32Array([
      t.scaleX, 0, 0,
      0, t.scaleY, 0,
      t.offsetX, t.offsetY, 1,
    ]);
  }
}
