import { Vector2 } from "./vector2"
import { Graphics } from "pixi.js";
import { epsGreaterThan, epsLessThan } from "../epsilon_math";

export class Line {
  private _x1: number;
  private _x2: number;
  private _y1: number;
  private _y2: number;

  public get x1(): number { return this._x1; }
  public get x2(): number { return this._x2; }
  public get y1(): number { return this._y1; }
  public get y2(): number { return this._y2; }

  public get start(): Vector2 { return new Vector2({ x: this.x1, y: this.y1 }); }
  public get end()  : Vector2 { return new Vector2({ x: this.x2, y: this.y2 }); }

  public get angleInDegrees(): number {
    const cx = this._x1;
    const cy = this._y1;

    const ex = this._x2;
    const ey = this._y2;

    const dy = ey - cy;
    const dx = ex - cx;

    let theta = Math.atan2(dy, dx);

    theta *= 180 / Math.PI;

    if (theta < 0) {
      theta = 360 + theta;
    }

    return theta;
  }

  public serialized = "";

  constructor(props: { x1: number, x2: number, y1: number, y2: number } |
                     { start: Vector2, end: Vector2 }) {
    let x1, x2, y1, y2;

    if ('x1' in props) {
      x1 = props.x1;
      x2 = props.x2;
      y1 = props.y1;
      y2 = props.y2;
    } else {
      x1 = props.start.x;
      x2 = props.end.x;
      y1 = props.start.y;
      y2 = props.end.y;
    }

    this._x1 = x1;
    this._y1 = y1;
    this._x2 = x2;
    this._y2 = y2;

    this.serialized = `${ this.x1 }|${ this.x2 }|${ this.y1 }|${ this.y2 }`;
  }

  public get length(): number {
    return Math.sqrt(
      (this.x2 - this.x1) * (this.x2 - this.x1) +
      (this.y2 - this.y1) * (this.y2 - this.y1)
    );
  }

  public get isDegenerate(): boolean {
    return this.length === 0;
  }

  public rotateAbout(origin: Vector2, angle: number): Line {
    const start = this.start;
    const end = this.end;

    return new Line({
      start: start.rotate(origin, angle),
      end: end.rotate(origin, angle),
    });
  }

  public scaleAbout(about: Vector2, amount: Vector2): Line {
    return new Line({
      start: this.start.scale(about, amount),
      end: this.end.scale(about, amount),
    });
  }

  sharesAVertexWith(other: Line): Vector2 | null {
    if (this.start.equals(other.start)) { return this.start; }
    if (this.start.equals(other.end))   { return this.start; }

    if (this.end.equals(other.start)) { return this.end; }
    if (this.end.equals(other.end))   { return this.end; }

    return null;
  }

  static DeserializeLine(s: string): Line {
    const [ x1, x2, y1, y2 ] = s.split("|").map(x => Number(x));

    return new Line({ x1, x2, y1, y2 });
  }

  isXAligned(): boolean {
    return this.x1 === this.x2;
  }

  isYAligned(): boolean {
    return this.y1 === this.y2;
  }

  // Must be horizontally/vertically oriented lines
  // Does not consider intersection, only overlap
  getOverlap(other: Line): Line | undefined {
    const orientedByX = (
      this.x1 === this.x2 &&
      this.x1 === other.x1 &&
      this.x1 === other.x2
    );

    const orientedByY = (
      this.y1 === this.y2 &&
      this.y1 === other.y1 &&
      this.y1 === other.y2
    );

    if (!orientedByX && !orientedByY) { return undefined; }

    const summedLength  = this.length + other.length;
    const overallLength = new Line({
      x1: Math.min(this.x1, other.x1),
      y1: Math.min(this.y1, other.y1),
      x2: Math.max(this.x2, other.x2),
      y2: Math.max(this.y2, other.y2),
    }).length;

    if (overallLength >= summedLength) {
      // These lines do not overlap.

      return undefined;
    }

    if (orientedByX) {
      return new Line({
        x1: this.x1,
        x2: this.x2,
        y1: Math.max(this.y1, other.y1),
        y2: Math.min(this.y2, other.y2),
      });
    } else /* if (orientedByY) */ {
      return new Line({
        y1: this.y1,
        y2: this.y2,
        x1: Math.max(this.x1, other.x1),
        x2: Math.min(this.x2, other.x2),
      });
    }
  }

  // A----B----C----D
  // AD - BC returns AB and CD.
  getNonOverlappingSections(other: Line): Line[] | undefined {
    const orientedByX = (
      this.x1 === this.x2 &&
      this.x1 === other.x1 &&
      this.x1 === other.x2
    );

    const orientedByY = (
      this.y1 === this.y2 &&
      this.y1 === other.y1 &&
      this.y1 === other.y2
    );

    if (!orientedByX && !orientedByY) { return undefined; }

    const summedLength  = new Line(this).length + new Line(other).length;
    const overallLength = new Line({
      x1: Math.min(this.x1, other.x1),
      y1: Math.min(this.y1, other.y1),
      x2: Math.max(this.x1, other.x1),
      y2: Math.max(this.y1, other.y1),
    }).length;

    if (overallLength >= summedLength) {
      // These lines do not overlap.

      return undefined;
    }

    if (orientedByX) {
      return [
        new Line({ x1: this.x1, x2: this.x2, y1: Math.min(this.y1, other.y1), y2: Math.max(this.y1, other.y1), }),
        new Line({ x1: this.x1, x2: this.x2, y1: Math.min(this.y2, other.y2), y2: Math.max(this.y2, other.y2), }),
      ].filter(l => !l.isDegenerate);
    } else /* if (orientedByY) */ {
      return [
        new Line({ y1: this.y1, y2: this.y2, x1: Math.min(this.x1, other.x1), x2: Math.max(this.x1, other.x1), }),
        new Line({ y1: this.y1, y2: this.y2, x1: Math.min(this.x2, other.x2), x2: Math.max(this.x2, other.x2), }),
      ].filter(l => !l.isDegenerate);
    }
  }

  clone(): Line {
    return new Line({ x1: this.x1, x2: this.x2, y1: this.y1, y2: this.y2 });
  }

  translate(p: Vector2): Line {
    return new Line({
      x1: this.x1 + p.x,
      x2: this.x2 + p.x,

      y1: this.y1 + p.y,
      y2: this.y2 + p.y,
    });
  }

  transform(trans: Vector2, scale: number): Line {
    return new Line({
      start: this.start.transform(trans, scale),
      end: this.end.transform(trans, scale),
    });
  }

  toJSON(): any {
    return {
      x1     : this.x1,
      x2     : this.x2,
      y1     : this.y1,
      y2     : this.y2,
      reviver: "Line",
    };
  }

  toString(): string {
    return `Line: [(${ this.x1 },${ this.y1 }) -> (${ this.x2 },${ this.y2 })]`;
  }

  equals(other: Line | null) {
    if (other === null) { return false; }

    return (
      this.x1 === other.x1 &&
      this.x2 === other.x2 &&
      this.y1 === other.y1 &&
      this.y2 === other.y2
    ) || (
      this.x1 === other.x2 &&
      this.x2 === other.x1 &&
      this.y1 === other.y2 &&
      this.y2 === other.y1
    );
  }

  withNewEnd(newEnd: Vector2): Line {
    return new Line({
      x1: this.x1,
      y1: this.y1,
      x2: newEnd.x,
      y2: newEnd.y,
    });
  }

  withNewStart(newStart: Vector2): Line {
    return new Line({
      x1: newStart.x,
      y1: newStart.y,
      x2: this.x2,
      y2: this.y2,
    });
  }

  static Deserialize(obj: any): Line {
    if (
      !obj.hasOwnProperty("x1") ||
      !obj.hasOwnProperty("y1") ||
      !obj.hasOwnProperty("x2") ||
      !obj.hasOwnProperty("y2")) {

      console.error("Failed deserializing Rect");
    }

    return new Line({
      x1: obj.x1,
      y1: obj.y1,
      x2: obj.x2,
      y2: obj.y2,
    });
  }

  static Serialize(obj: Line): string {
    return JSON.stringify({
      x1: obj.x1,
      y1: obj.y1,
      x2: obj.x2,
      y2: obj.y2,
    });
  }

  drawOnto(graphics: Graphics, color = 0xff0000) {
    graphics.lineStyle(3, color, 1);

    graphics.moveTo(this.x1, this.y1);
    graphics.lineTo(this.x2, this.y2);
  }

  /** 
   * Returns the point where these two lines, if extended arbitrarily, would
   * intersect.
   */
  lineIntersection(other: Line): Vector2 {
    const p1 = this.start;
    const p2 = this.end;
    const p3 = other.start;
    const p4 = other.end;

    const s = (
      (p4.x - p3.x) * 
      (p1.y - p3.y) - 
      (p4.y - p3.y) * 
      (p1.x - p3.x)) / (
      (p4.y - p3.y) * 
      (p2.x - p1.x) - 
      (p4.x - p3.x) * 
      (p2.y - p1.y)
    );

    const x = p1.x + s * (p2.x - p1.x);
    const y = p1.y + s * (p2.y - p1.y);

    return new Vector2({ x, y });
  }

  /**
   * Returns the point where these two segments exist, if there is one.
   */
  segmentIntersection(other: Line): Vector2 | null {
    const lineIntersection = this.lineIntersection(other);

    const x = lineIntersection.x;
    const y = lineIntersection.y;

    if (
      (
        // within us

        epsGreaterThan(x, Math.min(this.x1, this.x2)) &&
        epsLessThan   (x, Math.max(this.x1, this.x2)) &&
        epsGreaterThan(y, Math.min(this.y1, this.y2)) &&
        epsLessThan   (y, Math.max(this.y1, this.y2))
      ) && (
        // within other

        epsGreaterThan(x, Math.min(other.x1, other.x2)) &&
        epsLessThan   (x, Math.max(other.x1, other.x2)) &&
        epsGreaterThan(y, Math.min(other.y1, other.y2)) &&
        epsLessThan   (y, Math.max(other.y1, other.y2))
      )
    ) {
      return lineIntersection;
    }

    return null;
  }

  normalize(): Line {
    const mag = Math.sqrt(
      (this.x1 - this.x2) ** 2 +
      (this.y1 - this.y2) ** 2
    );

    return new Line({
      start: this.start,
      end: new Vector2({
        x: this.start.x + (this.end.x - this.start.x) / mag,
        y: this.start.x + (this.end.y - this.start.y) / mag,
      })
    })
  }

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

  add(x: Vector2): Line {
    return new Line({ 
      start: this.start.add(x),
      end: this.end.add(x),
    })
  }
}