import { EPSILON } from "../epsilon_math";
import { Util } from "../misc";

export interface IVector2 {
  x: number;
  y: number;
}

export class Vector2 {
  private _x: number;
  private _y: number;

  public get x(): number { return this._x; }
  public get y(): number { return this._y; }

  constructor();
  constructor(x: number, y: number);
  constructor(props: { x: number, y: number });
  constructor(propsOrX: { x: number, y: number } | number = { x: 0, y: 0 }, y?: number) {
    if (typeof propsOrX === "number") {
      this._x = propsOrX;
      this._y = y!;
    } else {
      this._x = propsOrX.x;
      this._y = propsOrX.y;
    }
  }

  public get half(): Vector2 {
    return new Vector2({ x: this.x / 2, y: this.y / 2 });
  }

  public static Zero: Vector2 = new Vector2(0, 0);
  public static One: Vector2 = new Vector2(1, 1);

  static IsVector2(x: any): x is Vector2 {
    return x instanceof Vector2;
  }

  static Random(highX: number, highY: number, lowX = 0, lowY = 0) {
    return new Vector2({
      x: Util.RandRange(lowX, highX),
      y: Util.RandRange(lowY, highY),
    });
  }

  hash(): string {
    return this.toString();
  }

  toString(): string {
    return `[${this.x}, ${this.y}]`;
  }

  invert(): Vector2 {
    return new Vector2({
      x: -this.x,
      y: -this.y,
    });
  }

  round(): Vector2 {
    return new Vector2({
      x: Math.round(this.x),
      y: Math.round(this.y),
    });
  }

  floor(): Vector2 {
    return new Vector2({
      x: Math.floor(this.x),
      y: Math.floor(this.y),
    });
  }

  taxicabDistance(p: Vector2): number {
    return Math.abs(p.x - this.x) + Math.abs(p.y - this.y);
  }

  diagonalDistance(p: IVector2): number {
    return Math.max(Math.abs(p.x - this.x), Math.abs(p.y - this.y));
  }

  distance(p: IVector2): number {
    let dx = Math.abs(p.x - this.x);
    let dy = Math.abs(p.y - this.y);

    return Math.sqrt(dx * dx + dy * dy);
  }

  translate(p: { x: number, y: number }): Vector2 {
    return new Vector2({
      x: this.x + p.x,
      y: this.y + p.y,
    });
  }

  subtract(p: { x: number, y: number }): Vector2 {
    return new Vector2({
      x: this.x - p.x,
      y: this.y - p.y,
    });
  }

  add(p: { x: number, y: number }): Vector2 {
    return new Vector2({
      x: this.x + p.x,
      y: this.y + p.y,
    });
  }

  addX(x: number): Vector2 {
    return new Vector2({
      x: this.x + x,
      y: this.y,
    });
  }

  addY(y: number): Vector2 {
    return new Vector2({
      x: this.x,
      y: this.y + y,
    });
  }

  subtractX(x: number): Vector2 {
    return new Vector2({
      x: this.x - x,
      y: this.y,
    });
  }

  subtractY(y: number): Vector2 {
    return new Vector2({
      x: this.x,
      y: this.y - y,
    });
  }

  clampY(low: number, high: number): Vector2 {
    let newY = this.y;

    if (newY < low) { newY = low; }
    if (newY > high) { newY = high; }

    return new Vector2({
      x: this.x,
      y: newY,
    });
  }

  scale(about: { x: number; y: number }, amount: { x: number; y: number }): Vector2 {
    return new Vector2({
      x: (this.x - about.x) * amount.x + about.x,
      y: (this.y - about.y) * amount.y + about.y,
    });
  }

  rotate(origin: Vector2, angle: number): Vector2 {
    angle = angle / (180 / Math.PI);

    return new Vector2({
      x: Math.cos(angle) * (this.x - origin.x) - Math.sin(angle) * (this.y - origin.y) + origin.x,
      y: Math.sin(angle) * (this.x - origin.x) + Math.cos(angle) * (this.y - origin.y) + origin.y,
    });
  }

  equals(other: Vector2 | undefined): boolean {
    if (other === undefined) {
      return false;
    }

    return (
      Math.abs(this.x - other.x) < EPSILON &&
      Math.abs(this.y - other.y) < EPSILON
    );
  }

  multiply(other: Vector2 | number): Vector2 {
    if (typeof other === "number") {
      return new Vector2({
        x: this.x * other,
        y: this.y * other,
      });
    } else {
      return new Vector2({
        x: this.x * other.x,
        y: this.y * other.y,
      });
    }
  }

  divide(other: Vector2 | number): Vector2 {
    if (typeof other === "number") {
      return new Vector2({
        x: this.x / other,
        y: this.y / other,
      });
    } else {
      return new Vector2({
        x: this.x / other.x,
        y: this.y / other.y,
      });
    }
  }

  toJSON(): any {
    return {
      __type: "Vector2",
      x: this.x,
      y: this.y,
    }
  }

  transform(trans: Vector2, scale: number): Vector2 {
    return new Vector2({
      x: Math.floor((this.x - trans.x) * scale),
      y: Math.floor((this.y - trans.y) * scale),
    });
  }

  normalize(): Vector2 {
    if (this.x === 0 && this.y === 0) {
      return this;
    }

    const length = Math.sqrt(this.x * this.x + this.y * this.y);

    return new Vector2({
      x: this.x / length,
      y: this.y / length
    });
  }

  withX(newX: number): Vector2 {
    return new Vector2({
      x: newX,
      y: this.y,
    });
  }

  withY(newY: number): Vector2 {
    return new Vector2({
      x: this.x,
      y: newY,
    });
  }

  invertX(): Vector2 {
    return new Vector2({
      x: -this.x,
      y: this.y,
    });
  }

  lerp(other: Vector2, t: number): Vector2 {
    if (t > 1 || t < 0) { console.error("Lerp t must be between 0 and 1."); }
    if (t === 0) return this;
    if (t === 1) return other;

    return this.scale({ x: 0, y: 0 }, { x: 1 - t, y: 1 - t }).add(other.scale({ x: 0, y: 0 }, { x: t, y: t }))
  }

  lerp2D(other: Vector2, tx: number, ty: number): Vector2 {
    if (tx > 1 || tx < 0 || ty > 1 || ty < 0) { console.error("Lerp t must be between 0 and 1."); }
    return this.scale({ x: 0, y: 0 }, { x: 1 - tx, y: 1 - ty }).add(other.scale({ x: 0, y: 0 }, { x: tx, y: ty }))
  }

  coserp(other: Vector2, t: number): Vector2 {
    t = 0.5 * (1 + Math.cos(2 * t * Math.PI));

    return this.lerp(other, t);
  }

  static Deserialize(obj: any): Vector2 {
    if (!obj.hasOwnProperty("x") || !obj.hasOwnProperty("y")) {
      console.error("Failed deserializing point");
    }

    return new Vector2({
      x: obj.x,
      y: obj.y,
    });
  }

  static Serialize(obj: Vector2): string {
    return JSON.stringify({ x: obj.x, y: obj.y });
  }
}