import { AABB } from '../collision/AABB'
import { Vec3 } from '../math/Vec3'
import type { Transform } from '../math/Transform'
import type { Ray } from '../collision/Ray'

/**
 * OctreeNode
 */
class OctreeNode {
  /** The root node */
  root: OctreeNode | null
  /** Boundary of this node */
  aabb: AABB
  /** Contained data at the current node level */
  data: number[]
  /** Children to this node */
  children: OctreeNode[]

  constructor(
    options: {
      /** The root node */
      root?: Octree | null
      /** Boundary of this node */
      aabb?: AABB
    } = {}
  ) {
    this.root = options.root || null
    this.aabb = options.aabb ? options.aabb.clone() : new AABB()
    this.data = []
    this.children = []
  }

  /**
   * reset
   */
  reset(): void {
    this.children.length = this.data.length = 0
  }

  /**
   * Insert data into this node
   * @return True if successful, otherwise false
   */
  insert(aabb: AABB, elementData: number, level = 0): boolean {
    const nodeData = this.data

    // Ignore objects that do not belong in this node
    if (!this.aabb.contains(aabb)) {
      return false // object cannot be added
    }

    const children = this.children
    const maxDepth = (this as any).maxDepth || (this.root! as any).maxDepth

    if (level < maxDepth) {
      // Subdivide if there are no children yet
      let subdivided = false
      if (!children.length) {
        this.subdivide()
        subdivided = true
      }

      // add to whichever node will accept it
      for (let i = 0; i !== 8; i++) {
        if (children[i].insert(aabb, elementData, level + 1)) {
          return true
        }
      }

      if (subdivided) {
        // No children accepted! Might as well just remove em since they contain none
        children.length = 0
      }
    }

    // Too deep, or children didnt want it. add it in current node
    nodeData.push(elementData)

    return true
  }

  /**
   * Create 8 equally sized children nodes and put them in the `children` array.
   */
  subdivide(): void {
    const aabb = this.aabb
    const l = aabb.lowerBound
    const u = aabb.upperBound

    const children = this.children

    children.push(
      new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(0, 0, 0) }) }),
      new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(1, 0, 0) }) }),
      new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(1, 1, 0) }) }),
      new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(1, 1, 1) }) }),
      new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(0, 1, 1) }) }),
      new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(0, 0, 1) }) }),
      new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(1, 0, 1) }) }),
      new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(0, 1, 0) }) })
    )

    u.vsub(l, halfDiagonal)
    halfDiagonal.scale(0.5, halfDiagonal)

    const root = this.root || this

    for (let i = 0; i !== 8; i++) {
      const child = children[i]

      // Set current node as root
      child.root = root

      // Compute bounds
      const lowerBound = child.aabb.lowerBound
      lowerBound.x *= halfDiagonal.x
      lowerBound.y *= halfDiagonal.y
      lowerBound.z *= halfDiagonal.z

      lowerBound.vadd(l, lowerBound)

      // Upper bound is always lower bound + halfDiagonal
      lowerBound.vadd(halfDiagonal, child.aabb.upperBound)
    }
  }

  /**
   * Get all data, potentially within an AABB
   * @return The "result" object
   */
  aabbQuery(aabb: AABB, result: number[]): number[] {
    const nodeData = this.data

    // abort if the range does not intersect this node
    // if (!this.aabb.overlaps(aabb)){
    //     return result;
    // }

    // Add objects at this level
    // Array.prototype.push.apply(result, nodeData);

    // Add child data
    // @todo unwrap recursion into a queue / loop, that's faster in JS
    const children = this.children

    // for (let i = 0, N = this.children.length; i !== N; i++) {
    //     children[i].aabbQuery(aabb, result);
    // }

    const queue = [this]
    while (queue.length) {
      const node = queue.pop() as OctreeNode
      if (node.aabb.overlaps(aabb)) {
        Array.prototype.push.apply(result, node.data)
      }
      Array.prototype.push.apply(queue, node.children)
    }

    return result
  }

  /**
   * Get all data, potentially intersected by a ray.
   * @return The "result" object
   */
  rayQuery(ray: Ray, treeTransform: Transform, result: number[]): number[] {
    // Use aabb query for now.
    /** @todo implement real ray query which needs less lookups */
    ray.getAABB(tmpAABB)
    tmpAABB.toLocalFrame(treeTransform, tmpAABB)
    this.aabbQuery(tmpAABB, result)

    return result
  }

  /**
   * removeEmptyNodes
   */
  removeEmptyNodes(): void {
    for (let i = this.children.length - 1; i >= 0; i--) {
      this.children[i].removeEmptyNodes()
      if (!this.children[i].children.length && !this.children[i].data.length) {
        this.children.splice(i, 1)
      }
    }
  }
}

/**
 * Octree
 */
export class Octree extends OctreeNode {
  /**
   * Maximum subdivision depth
   * @default 8
   */
  maxDepth: number

  /**
   * @param aabb The total AABB of the tree
   */
  constructor(
    aabb?: AABB,
    options: {
      /**
       * Maximum subdivision depth
       * @default 8
       */
      maxDepth?: number
    } = {}
  ) {
    super({ root: null, aabb })

    this.maxDepth = typeof options.maxDepth !== 'undefined' ? options.maxDepth : 8
  }
}

const halfDiagonal = new Vec3()

const tmpAABB = new AABB()
