import { Broadphase } from '../collision/Broadphase'
import { Vec3 } from '../math/Vec3'
import { Shape } from '../shapes/Shape'
import type { Body } from '../objects/Body'
import type { Sphere } from '../shapes/Sphere'
import type { Plane } from '../shapes/Plane'
import type { World } from '../world/World'

/**
 * Axis aligned uniform grid broadphase.
 * @todo Needs support for more than just planes and spheres.
 */
export class GridBroadphase extends Broadphase {
  /**
   * Number of boxes along x
   */
  nx: number

  /**
   * Number of boxes along y
   */
  ny: number

  /**
   * Number of boxes along z
   */
  nz: number

  /**
   * aabbMin
   */
  aabbMin: Vec3

  /**
   * aabbMax
   */
  aabbMax: Vec3

  /**
   * bins
   */
  bins: Body[][]

  /**
   * binLengths
   */
  binLengths: number[]

  /**
   * @param nx Number of boxes along x.
   * @param ny Number of boxes along y.
   * @param nz Number of boxes along z.
   */
  constructor(aabbMin = new Vec3(100, 100, 100), aabbMax = new Vec3(-100, -100, -100), nx = 10, ny = 10, nz = 10) {
    super()

    this.nx = nx
    this.ny = ny
    this.nz = nz
    this.aabbMin = aabbMin
    this.aabbMax = aabbMax
    const nbins = this.nx * this.ny * this.nz
    if (nbins <= 0) {
      throw "GridBroadphase: Each dimension's n must be >0"
    }
    this.bins = []
    this.binLengths = [] // Rather than continually resizing arrays (thrashing the memory), just record length and allow them to grow
    this.bins.length = nbins
    this.binLengths.length = nbins
    for (let i = 0; i < nbins; i++) {
      this.bins[i] = []
      this.binLengths[i] = 0
    }
  }

  /**
   * Get all the collision pairs in the physics world
   */
  collisionPairs(world: World, pairs1: Body[], pairs2: Body[]): void {
    const N = world.numObjects()
    const bodies = world.bodies
    const max = this.aabbMax
    const min = this.aabbMin
    const nx = this.nx
    const ny = this.ny
    const nz = this.nz

    const xstep = ny * nz
    const ystep = nz
    const zstep = 1

    const xmax = max.x
    const ymax = max.y
    const zmax = max.z
    const xmin = min.x
    const ymin = min.y
    const zmin = min.z
    const xmult = nx / (xmax - xmin)
    const ymult = ny / (ymax - ymin)
    const zmult = nz / (zmax - zmin)
    const binsizeX = (xmax - xmin) / nx
    const binsizeY = (ymax - ymin) / ny
    const binsizeZ = (zmax - zmin) / nz

    const binRadius = Math.sqrt(binsizeX * binsizeX + binsizeY * binsizeY + binsizeZ * binsizeZ) * 0.5

    const types = Shape.types
    const SPHERE = types.SPHERE
    const PLANE = types.PLANE
    const BOX = types.BOX
    const COMPOUND = types.COMPOUND
    const CONVEXPOLYHEDRON = types.CONVEXPOLYHEDRON
    const bins = this.bins
    const binLengths = this.binLengths
    const Nbins = this.bins.length

    // Reset bins
    for (let i = 0; i !== Nbins; i++) {
      binLengths[i] = 0
    }

    const ceil = Math.ceil

    function addBoxToBins(x0: number, y0: number, z0: number, x1: number, y1: number, z1: number, bi: Body): void {
      let xoff0 = ((x0 - xmin) * xmult) | 0
      let yoff0 = ((y0 - ymin) * ymult) | 0
      let zoff0 = ((z0 - zmin) * zmult) | 0
      let xoff1 = ceil((x1 - xmin) * xmult)
      let yoff1 = ceil((y1 - ymin) * ymult)
      let zoff1 = ceil((z1 - zmin) * zmult)

      if (xoff0 < 0) {
        xoff0 = 0
      } else if (xoff0 >= nx) {
        xoff0 = nx - 1
      }
      if (yoff0 < 0) {
        yoff0 = 0
      } else if (yoff0 >= ny) {
        yoff0 = ny - 1
      }
      if (zoff0 < 0) {
        zoff0 = 0
      } else if (zoff0 >= nz) {
        zoff0 = nz - 1
      }
      if (xoff1 < 0) {
        xoff1 = 0
      } else if (xoff1 >= nx) {
        xoff1 = nx - 1
      }
      if (yoff1 < 0) {
        yoff1 = 0
      } else if (yoff1 >= ny) {
        yoff1 = ny - 1
      }
      if (zoff1 < 0) {
        zoff1 = 0
      } else if (zoff1 >= nz) {
        zoff1 = nz - 1
      }

      xoff0 *= xstep
      yoff0 *= ystep
      zoff0 *= zstep
      xoff1 *= xstep
      yoff1 *= ystep
      zoff1 *= zstep

      for (let xoff = xoff0; xoff <= xoff1; xoff += xstep) {
        for (let yoff = yoff0; yoff <= yoff1; yoff += ystep) {
          for (let zoff = zoff0; zoff <= zoff1; zoff += zstep) {
            const idx = xoff + yoff + zoff
            bins[idx][binLengths[idx]++] = bi
          }
        }
      }
    }

    // Put all bodies into the bins
    for (let i = 0; i !== N; i++) {
      const bi = bodies[i]
      const si = bi.shapes[0]

      switch (si.type) {
        case SPHERE: {
          const shape = si as Sphere
          // Put in bin
          // check if overlap with other bins
          const x = bi.position.x
          const y = bi.position.y
          const z = bi.position.z
          const r = shape.radius

          addBoxToBins(x - r, y - r, z - r, x + r, y + r, z + r, bi)
          break
        }
        case PLANE: {
          const shape = si as Plane

          if (shape.worldNormalNeedsUpdate) {
            shape.computeWorldNormal(bi.quaternion)
          }
          const planeNormal = shape.worldNormal

          //Relative position from origin of plane object to the first bin
          //Incremented as we iterate through the bins
          const xreset = xmin + binsizeX * 0.5 - bi.position.x

          const yreset = ymin + binsizeY * 0.5 - bi.position.y
          const zreset = zmin + binsizeZ * 0.5 - bi.position.z

          const d = GridBroadphase_collisionPairs_d
          d.set(xreset, yreset, zreset)

          for (let xi = 0, xoff = 0; xi !== nx; xi++, xoff += xstep, d.y = yreset, d.x += binsizeX) {
            for (let yi = 0, yoff = 0; yi !== ny; yi++, yoff += ystep, d.z = zreset, d.y += binsizeY) {
              for (let zi = 0, zoff = 0; zi !== nz; zi++, zoff += zstep, d.z += binsizeZ) {
                if (d.dot(planeNormal) < binRadius) {
                  const idx = xoff + yoff + zoff
                  bins[idx][binLengths[idx]++] = bi
                }
              }
            }
          }
          break
        }
        default: {
          if (bi.aabbNeedsUpdate) {
            bi.updateAABB()
          }

          addBoxToBins(
            bi.aabb.lowerBound.x,
            bi.aabb.lowerBound.y,
            bi.aabb.lowerBound.z,
            bi.aabb.upperBound.x,
            bi.aabb.upperBound.y,
            bi.aabb.upperBound.z,
            bi
          )
          break
        }
      }
    }

    // Check each bin
    for (let i = 0; i !== Nbins; i++) {
      const binLength = binLengths[i]
      //Skip bins with no potential collisions
      if (binLength > 1) {
        const bin = bins[i]

        // Do N^2 broadphase inside
        for (let xi = 0; xi !== binLength; xi++) {
          const bi = bin[xi]
          for (let yi = 0; yi !== xi; yi++) {
            const bj = bin[yi]
            if (this.needBroadphaseCollision(bi, bj)) {
              this.intersectionTest(bi, bj, pairs1, pairs2)
            }
          }
        }
      }
    }

    //	for (let zi = 0, zoff=0; zi < nz; zi++, zoff+= zstep) {
    //		console.log("layer "+zi);
    //		for (let yi = 0, yoff=0; yi < ny; yi++, yoff += ystep) {
    //			const row = '';
    //			for (let xi = 0, xoff=0; xi < nx; xi++, xoff += xstep) {
    //				const idx = xoff + yoff + zoff;
    //				row += ' ' + binLengths[idx];
    //			}
    //			console.log(row);
    //		}
    //	}

    this.makePairsUnique(pairs1, pairs2)
  }
}

const GridBroadphase_collisionPairs_d = new Vec3()
const GridBroadphase_collisionPairs_binPos = new Vec3()
