import { Vec3 } from '../math/Vec3'
import type { Ray } from '../collision/Ray'
import type { Transform } from '../math/Transform'
import type { Quaternion } from '../math/Quaternion'

/**
 * Axis aligned bounding box class.
 */
export class AABB {
  /**
   * The lower bound of the bounding box
   */
  lowerBound: Vec3
  /**
   * The upper bound of the bounding box
   */
  upperBound: Vec3

  constructor(
    options: {
      /**
       * The lower bound of the bounding box
       */
      upperBound?: Vec3
      /**
       * The upper bound of the bounding box
       */
      lowerBound?: Vec3
    } = {}
  ) {
    this.lowerBound = new Vec3()
    this.upperBound = new Vec3()

    if (options.lowerBound) {
      this.lowerBound.copy(options.lowerBound)
    }

    if (options.upperBound) {
      this.upperBound.copy(options.upperBound)
    }
  }

  /**
   * Set the AABB bounds from a set of points.
   * @param points An array of Vec3's.
   * @return The self object
   */
  setFromPoints(points: Vec3[], position?: Vec3, quaternion?: Quaternion, skinSize?: number): AABB {
    const l = this.lowerBound
    const u = this.upperBound
    const q = quaternion

    // Set to the first point
    l.copy(points[0])
    if (q) {
      q.vmult(l, l)
    }
    u.copy(l)

    for (let i = 1; i < points.length; i++) {
      let p = points[i]

      if (q) {
        q.vmult(p, tmp)
        p = tmp
      }

      if (p.x > u.x) {
        u.x = p.x
      }
      if (p.x < l.x) {
        l.x = p.x
      }
      if (p.y > u.y) {
        u.y = p.y
      }
      if (p.y < l.y) {
        l.y = p.y
      }
      if (p.z > u.z) {
        u.z = p.z
      }
      if (p.z < l.z) {
        l.z = p.z
      }
    }

    // Add offset
    if (position) {
      position.vadd(l, l)
      position.vadd(u, u)
    }

    if (skinSize) {
      l.x -= skinSize
      l.y -= skinSize
      l.z -= skinSize
      u.x += skinSize
      u.y += skinSize
      u.z += skinSize
    }

    return this
  }

  /**
   * Copy bounds from an AABB to this AABB
   * @param aabb Source to copy from
   * @return The this object, for chainability
   */
  copy(aabb: AABB): AABB {
    this.lowerBound.copy(aabb.lowerBound)
    this.upperBound.copy(aabb.upperBound)
    return this
  }

  /**
   * Clone an AABB
   */
  clone(): AABB {
    return new AABB().copy(this)
  }

  /**
   * Extend this AABB so that it covers the given AABB too.
   */
  extend(aabb: AABB): void {
    this.lowerBound.x = Math.min(this.lowerBound.x, aabb.lowerBound.x)
    this.upperBound.x = Math.max(this.upperBound.x, aabb.upperBound.x)
    this.lowerBound.y = Math.min(this.lowerBound.y, aabb.lowerBound.y)
    this.upperBound.y = Math.max(this.upperBound.y, aabb.upperBound.y)
    this.lowerBound.z = Math.min(this.lowerBound.z, aabb.lowerBound.z)
    this.upperBound.z = Math.max(this.upperBound.z, aabb.upperBound.z)
  }

  /**
   * Returns true if the given AABB overlaps this AABB.
   */
  overlaps(aabb: AABB): boolean {
    const l1 = this.lowerBound
    const u1 = this.upperBound
    const l2 = aabb.lowerBound
    const u2 = aabb.upperBound

    //      l2        u2
    //      |---------|
    // |--------|
    // l1       u1

    const overlapsX = (l2.x <= u1.x && u1.x <= u2.x) || (l1.x <= u2.x && u2.x <= u1.x)
    const overlapsY = (l2.y <= u1.y && u1.y <= u2.y) || (l1.y <= u2.y && u2.y <= u1.y)
    const overlapsZ = (l2.z <= u1.z && u1.z <= u2.z) || (l1.z <= u2.z && u2.z <= u1.z)

    return overlapsX && overlapsY && overlapsZ
  }

  // Mostly for debugging
  volume(): number {
    const l = this.lowerBound
    const u = this.upperBound
    return (u.x - l.x) * (u.y - l.y) * (u.z - l.z)
  }

  /**
   * Returns true if the given AABB is fully contained in this AABB.
   */
  contains(aabb: AABB): boolean {
    const l1 = this.lowerBound
    const u1 = this.upperBound
    const l2 = aabb.lowerBound
    const u2 = aabb.upperBound

    //      l2        u2
    //      |---------|
    // |---------------|
    // l1              u1

    return l1.x <= l2.x && u1.x >= u2.x && l1.y <= l2.y && u1.y >= u2.y && l1.z <= l2.z && u1.z >= u2.z
  }

  getCorners(a: Vec3, b: Vec3, c: Vec3, d: Vec3, e: Vec3, f: Vec3, g: Vec3, h: Vec3): void {
    const l = this.lowerBound
    const u = this.upperBound

    a.copy(l)
    b.set(u.x, l.y, l.z)
    c.set(u.x, u.y, l.z)
    d.set(l.x, u.y, u.z)
    e.set(u.x, l.y, u.z)
    f.set(l.x, u.y, l.z)
    g.set(l.x, l.y, u.z)
    h.copy(u)
  }

  /**
   * Get the representation of an AABB in another frame.
   * @return The "target" AABB object.
   */
  toLocalFrame(frame: Transform, target: AABB): AABB {
    const corners = transformIntoFrame_corners
    const a = corners[0]
    const b = corners[1]
    const c = corners[2]
    const d = corners[3]
    const e = corners[4]
    const f = corners[5]
    const g = corners[6]
    const h = corners[7]

    // Get corners in current frame
    this.getCorners(a, b, c, d, e, f, g, h)

    // Transform them to new local frame
    for (let i = 0; i !== 8; i++) {
      const corner = corners[i]
      frame.pointToLocal(corner, corner)
    }

    return target.setFromPoints(corners)
  }

  /**
   * Get the representation of an AABB in the global frame.
   * @return The "target" AABB object.
   */
  toWorldFrame(frame: Transform, target: AABB): AABB {
    const corners = transformIntoFrame_corners
    const a = corners[0]
    const b = corners[1]
    const c = corners[2]
    const d = corners[3]
    const e = corners[4]
    const f = corners[5]
    const g = corners[6]
    const h = corners[7]

    // Get corners in current frame
    this.getCorners(a, b, c, d, e, f, g, h)

    // Transform them to new local frame
    for (let i = 0; i !== 8; i++) {
      const corner = corners[i]
      frame.pointToWorld(corner, corner)
    }

    return target.setFromPoints(corners)
  }

  /**
   * Check if the AABB is hit by a ray.
   */
  overlapsRay(ray: Ray): boolean {
    const { direction, from } = ray
    // const t = 0

    // ray.direction is unit direction vector of ray
    const dirFracX = 1 / direction.x
    const dirFracY = 1 / direction.y
    const dirFracZ = 1 / direction.z

    // this.lowerBound is the corner of AABB with minimal coordinates - left bottom, rt is maximal corner
    const t1 = (this.lowerBound.x - from.x) * dirFracX
    const t2 = (this.upperBound.x - from.x) * dirFracX
    const t3 = (this.lowerBound.y - from.y) * dirFracY
    const t4 = (this.upperBound.y - from.y) * dirFracY
    const t5 = (this.lowerBound.z - from.z) * dirFracZ
    const t6 = (this.upperBound.z - from.z) * dirFracZ

    // const tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)));
    // const tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)));
    const tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6))
    const tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6))

    // if tmax < 0, ray (line) is intersecting AABB, but whole AABB is behing us
    if (tmax < 0) {
      //t = tmax;
      return false
    }

    // if tmin > tmax, ray doesn't intersect AABB
    if (tmin > tmax) {
      //t = tmax;
      return false
    }

    return true
  }
}

const tmp = new Vec3()

const transformIntoFrame_corners = [
  new Vec3(),
  new Vec3(),
  new Vec3(),
  new Vec3(),
  new Vec3(),
  new Vec3(),
  new Vec3(),
  new Vec3(),
]
