import {LinearSystem2} from './linearSystem'
import {GeomConstants} from './geomConstants'
import {compareNumbers} from '../../utils/compare'
import {Assert} from '../../utils/assert'
export enum TriangleOrientation {
  Clockwise,
  Counterclockwise,
  Collinear,
}

export function distPP(a: Point, b: Point) {
  return a.sub(b).length
}

export class Point {
  // c is projected to line through a, b
  public static ProjectionToLine(a: Point, b: Point, c: Point): Point {
    let d = b.sub(a)
    const dLen = d.length
    if (dLen < GeomConstants.distanceEpsilon) {
      return a
    }
    d = d.div(dLen)
    const pr = c.sub(a).dot(d)
    // projection
    const ret = a.add(d.mul(pr))
    Assert.assert(Math.abs(c.sub(ret).dot(d)) < GeomConstants.distanceEpsilon)
    return ret
  }
  static RayIntersectsRayInteriors(aOrig: Point, aDirection: Point, bOrig: Point, bDirection: Point): Point | undefined {
    const x = Point.lineLineIntersection(aOrig, aOrig.add(aDirection), bOrig, bOrig.add(bDirection))
    if (!x) return undefined

    if (
      x.sub(aOrig).dot(aDirection.div(aDirection.l1)) > GeomConstants.distanceEpsilon &&
      x.sub(bOrig).dot(bDirection.div(bDirection.l1)) > GeomConstants.distanceEpsilon
    )
      return x

    return undefined
  }
  static IntervalIntersectsRay(segStart: Point, segEnd: Point, rayOrigin: Point, rayDirection: Point): Point | undefined {
    const x = Point.lineLineIntersection(segStart, segEnd, rayOrigin, rayOrigin.add(rayDirection))
    if (!x) {
      return
    }

    const ds = segStart.sub(x)
    const de = x.sub(segEnd)
    if (ds.dot(de) <= 0) {
      return
    }

    if (x.sub(rayOrigin).dot(rayDirection) < 0) {
      return
    }

    if (ds.dot(ds) > GeomConstants.squareOfDistanceEpsilon && de.dot(de) >= GeomConstants.squareOfDistanceEpsilon) return x
  }
  public static PointToTheLeftOfLineOrOnLine(point: Point, linePoint0: Point, linePoint1: Point): boolean {
    return Point.signedDoubledTriangleArea(point, linePoint0, linePoint1) >= 0
  }

  // returns true if "point" lies to the left of the line linePoint0, linePoint1
  public static PointToTheLeftOfLine(point: Point, linePoint0: Point, linePoint1: Point): boolean {
    return Point.signedDoubledTriangleArea(point, linePoint0, linePoint1) > 0
  }
  static PointIsInsideCone(p: Point, apex: Point, leftSideConePoint: Point, rightSideConePoint: Point): boolean {
    return (
      Point.PointToTheRightOfLineOrOnLine(p, apex, leftSideConePoint) && Point.PointToTheLeftOfLineOrOnLine(p, apex, rightSideConePoint)
    )
  }

  public static PointToTheRightOfLineOrOnLine(point: Point, linePoint0: Point, linePoint1: Point): boolean {
    return Point.signedDoubledTriangleArea(linePoint0, linePoint1, point) <= 0
  }

  public static PointToTheRightOfLine(point: Point, linePoint0: Point, linePoint1: Point): boolean {
    return Point.signedDoubledTriangleArea(linePoint0, linePoint1, point) < 0
  }
  static closeIntersections(a: Point, b: Point): boolean {
    return Point.close(a, b, GeomConstants.intersectionEpsilon)
  }
  get l1() {
    return Math.abs(this.x_) + Math.abs(this.y_)
  }
  dot(a: Point): number {
    return this.x * a.x + this.y * a.y
  }
  private x_: number
  private y_: number

  get x() {
    return this.x_
  }
  get y() {
    return this.y_
  }

  compareTo(other: Point): number {
    const r = compareNumbers(this.x, other.x)
    if (r != 0) return r
    return compareNumbers(this.y, other.y)
  }

  toString() {
    return '(' + this.x + ',' + this.y + ')'
  }

  static close(a: Point, b: Point, tol: number): boolean {
    return a.sub(b).length <= tol
  }

  static closeSquare(a: Point, b: Point, tol: number): boolean {
    const d = b.sub(a)
    return d.dot(d) <= tol
  }
  static closeDistEps(a: Point, b: Point): boolean {
    return a.sub(b).length <= GeomConstants.distanceEpsilon
  }

  normalize() {
    const l = this.length
    return new Point(this.x / l, this.y / l)
  }
  get length() {
    return Math.sqrt(this.x * this.x + this.y * this.y)
  }

  get lengthSquared() {
    return this.x * this.x + this.y * this.y
  }

  constructor(x: number, y: number) {
    // Assert.assert(!(isNaN(x) || isNaN(y)))
    this.x_ = x
    this.y_ = y
  }

  static middle(a: Point, b: Point) {
    return a.add(b).div(2)
  }

  scale(sx: number, sy: number): Point {
    return new Point(this.x * sx, this.y * sy)
  }
  add(a: Point) {
    return new Point(this.x + a.x, this.y + a.y)
  }
  sub(a: Point) {
    return new Point(this.x - a.x, this.y - a.y)
  }
  mul(c: number) {
    return new Point(this.x * c, this.y * c)
  }
  div(c: number) {
    return new Point(this.x / c, this.y / c)
  }
  equal(a: Point) {
    return a.x == this.x && a.y == this.y
  }
  neg() {
    return new Point(-this.x, -this.y)
  }

  static lineLineIntersection(a: Point, b: Point, c: Point, d: Point): Point | undefined {
    //look for the solution of the form a+u*(b-a)=c+v*(d-c)
    const ba = b.sub(a)
    const cd = c.sub(d)
    const ca = c.sub(a)
    const ret = LinearSystem2.solve(ba.x, cd.x, ca.x, ba.y, cd.y, ca.y)
    if (ret != undefined) {
      return a.add(ba.mul(ret.x))
    } else {
      return
    }
  }

  static segSegIntersection(a: Point, b: Point, c: Point, d: Point): Point | undefined {
    //look for the solution of the form a+u*(b-a)=c+v*(d-c)
    const ba = b.sub(a)
    const cd = c.sub(d)
    const ca = c.sub(a)
    const eps = GeomConstants.tolerance
    const ret = LinearSystem2.solve(ba.x, cd.x, ca.x, ba.y, cd.y, ca.y)
    if (ret != undefined && ret.x > -eps && ret.x < 1.0 + eps && ret.y > -eps && ret.y < 1.0 + eps) {
      return a.add(ba.mul(ret.x))
    } else {
      return
    }
  }

  static parallelWithinEpsilon(a: Point, b: Point, eps: number) {
    const alength = a.length
    const blength = b.length
    if (alength < eps || blength < eps) return true

    a = a.div(alength)
    b = b.div(blength)

    return Math.abs(-a.x * b.y + a.y * b.x) < eps
  }

  static crossProduct(point0: Point, point1: Point) {
    return point0.x * point1.y - point0.y * point1.x
  }
  static dot(a: Point, b: Point) {
    return a.x * b.x + a.y * b.y
  }
  static add(a: Point, b: Point) {
    return a.add(b)
  }

  rotate90Ccw() {
    return new Point(-this.y, this.x)
  }

  rotate90Cw() {
    return new Point(this.y, -this.x)
  }

  clone() {
    return new Point(this.x, this.y)
  }

  // returns this rotated by the angle counterclockwise; does not change "this" value
  rotate(angle: number): Point {
    const c = Math.cos(angle)
    const s = Math.sin(angle)
    return new Point(c * this.x - s * this.y, s * this.x + c * this.y)
  }

  static mkPoint(x: number, a: Point, y: number, b: Point) {
    return a.mul(x).add(b.mul(y))
  }

  static convSum(x: number, a: Point, b: Point) {
    return a.add(b.sub(a).mul(x))
  }

  static anglePCP(point1: Point, center: Point, point3: Point) {
    return Point.angle(point1.sub(center), point3.sub(center))
  }

  // The angle you need to turn "side0" counterclockwise to make it collinear with "side1"
  static angle(side0: Point, side1: Point): number {
    const ax = side0.x
    const ay = side0.y
    const bx = side1.x
    const by = side1.y

    const cross = ax * by - ay * bx
    const dot = ax * bx + ay * by

    if (Math.abs(dot) < GeomConstants.tolerance) {
      if (Math.abs(cross) < GeomConstants.tolerance) return 0

      if (cross < -GeomConstants.tolerance) return (3 * Math.PI) / 2
      return Math.PI / 2
    }

    if (Math.abs(cross) < GeomConstants.tolerance) {
      if (dot < -GeomConstants.tolerance) return Math.PI
      return 0.0
    }

    const atan2 = Math.atan2(cross, dot)
    if (cross >= -GeomConstants.tolerance) return atan2
    return Math.PI * 2.0 + atan2
  }

  static signedDoubledTriangleArea(a: Point, b: Point, c: Point): number {
    return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)
  }

  static getTriangleOrientation(cornerA: Point, cornerB: Point, cornerC: Point) {
    const area = Point.signedDoubledTriangleArea(cornerA, cornerB, cornerC)
    if (area > GeomConstants.distanceEpsilon) return TriangleOrientation.Counterclockwise
    if (area < -GeomConstants.distanceEpsilon) return TriangleOrientation.Clockwise
    return TriangleOrientation.Collinear
  }

  static getTriangleOrientationWithIntersectionEpsilon(cornerA: Point, cornerB: Point, cornerC: Point) {
    const area = Point.signedDoubledTriangleArea(cornerA, cornerB, cornerC)
    if (area > GeomConstants.intersectionEpsilon) return TriangleOrientation.Counterclockwise
    if (area < -GeomConstants.intersectionEpsilon) return TriangleOrientation.Clockwise
    return TriangleOrientation.Collinear
  }
  static ClosestPointAtLineSegment(point: Point, segmentStart: Point, segmentEnd: Point): Point {
    const bc = segmentEnd.sub(segmentStart)
    const ba = point.sub(segmentStart)
    const c1 = bc.dot(ba)
    const c2 = bc.dot(bc)
    if (c1 <= 0.0 + GeomConstants.tolerance) return segmentStart

    if (c2 <= c1 + GeomConstants.tolerance) return segmentEnd

    return segmentStart.add(bc.mul(c1 / c2))
  }

  static pointToTheLeftOfLineOrOnLine(point: Point, linePoint0: Point, linePoint1: Point): boolean {
    return Point.signedDoubledTriangleArea(point, linePoint0, linePoint1) >= 0
  }

  //  returns true if "point" lies to the left of the line linePoint0, linePoint1
  public static pointToTheLeftOfLine(point: Point, linePoint0: Point, linePoint1: Point): boolean {
    return Point.signedDoubledTriangleArea(point, linePoint0, linePoint1) > 0
  }

  // returns true if "point" lies to the right of the line linePoint0, linePoint1
  static pointToTheRightOfLineOrOnLine(point: Point, linePoint0: Point, linePoint1: Point): boolean {
    return Point.signedDoubledTriangleArea(linePoint0, linePoint1, point) <= 0
  }

  static pointToTheRightOfLine(point: Point, linePoint0: Point, linePoint1: Point): boolean {
    return Point.signedDoubledTriangleArea(linePoint0, linePoint1, point) < 0
  }

  static canProject(point: Point, segmentStart: Point, segmentEnd: Point): boolean {
    const bc = segmentEnd.sub(segmentStart)

    const ba = point.sub(segmentStart)

    if (ba.dot(bc) < 0)
      // point belongs to the halfplane before the segment
      return false

    const ca = point.sub(segmentEnd)
    if (ca.dot(bc) > 0)
      //point belongs to the halfplane after the segment
      return false

    return true
  }
  static distToLineSegment(a: Point, b: Point, c: Point): {par: number; dist: number} {
    const bc = c.sub(b)
    const ba = a.sub(b)
    let c1: number, c2: number
    if ((c1 = bc.dot(ba)) <= GeomConstants.tolerance) {
      return {par: 0, dist: ba.length}
    }
    if ((c2 = bc.dot(bc)) <= c1 + GeomConstants.tolerance) {
      return {par: 1, dist: a.sub(c).length}
    }
    const p = c1 / c2
    return {par: p, dist: b.add(bc.mul(p)).length}
  }
}
