import { EventTarget } from '../utils/EventTarget'
import { GSSolver } from '../solver/GSSolver'
import { NaiveBroadphase } from '../collision/NaiveBroadphase'
import { Narrowphase } from '../world/Narrowphase'
import { Vec3 } from '../math/Vec3'
import { Material } from '../material/Material'
import { ContactMaterial } from '../material/ContactMaterial'
import { ArrayCollisionMatrix } from '../collision/ArrayCollisionMatrix'
import { OverlapKeeper } from '../collision/OverlapKeeper'
import { TupleDictionary } from '../utils/TupleDictionary'
import { RaycastResult } from '../collision/RaycastResult'
import { Ray } from '../collision/Ray'
import { AABB } from '../collision/AABB'
import { Body } from '../objects/Body'
import type { Broadphase } from '../collision/Broadphase'
import type { Solver } from '../solver/Solver'
import type { ContactEquation } from '../equations/ContactEquation'
import type { FrictionEquation } from '../equations/FrictionEquation'
import type { RayOptions, RaycastCallback } from '../collision/Ray'
import type { Constraint } from '../constraints/Constraint'
import type { Shape } from '../shapes/Shape'

export type WorldOptions = ConstructorParameters<typeof World>[0]

/**
 * The physics world
 */
export class World extends EventTarget {
  /**
   * Currently / last used timestep. Is set to -1 if not available. This value is updated before each internal step, which means that it is "fresh" inside event callbacks.
   */
  dt: number

  /**
   * Makes bodies go to sleep when they've been inactive.
   * @default false
   */
  allowSleep: boolean

  /**
   * All the current contacts (instances of ContactEquation) in the world.
   */
  contacts: ContactEquation[]

  frictionEquations: FrictionEquation[]

  /**
   * How often to normalize quaternions. Set to 0 for every step, 1 for every second etc.. A larger value increases performance. If bodies tend to explode, set to a smaller value (zero to be sure nothing can go wrong).
   * @default 0
   */
  quatNormalizeSkip: number

  /**
   * Set to true to use fast quaternion normalization. It is often enough accurate to use.
   * If bodies tend to explode, set to false.
   * @default false
   */
  quatNormalizeFast: boolean

  /**
   * The wall-clock time since simulation start.
   */
  time: number

  /**
   * Number of timesteps taken since start.
   */
  stepnumber: number

  /**
   * Default and last timestep sizes.
   */
  default_dt: number

  nextId: number

  /**
   * The gravity of the world.
   */
  gravity: Vec3

  /**
   * The broadphase algorithm to use.
   * @default NaiveBroadphase
   */
  broadphase: Broadphase

  /**
   * All bodies in this world
   */
  bodies: Body[]

  /**
   * True if any bodies are not sleeping, false if every body is sleeping.
   */
  hasActiveBodies: boolean

  /**
   * The solver algorithm to use.
   * @default GSSolver
   */
  solver: Solver
  constraints: Constraint[]
  narrowphase: Narrowphase

  /**
   * collisionMatrix
   */
  collisionMatrix: ArrayCollisionMatrix

  /**
   * CollisionMatrix from the previous step.
   */
  collisionMatrixPrevious: ArrayCollisionMatrix
  bodyOverlapKeeper: OverlapKeeper
  shapeOverlapKeeper: OverlapKeeper

  /**
   * All added materials.
   * @deprecated
   * @todo Remove
   */
  materials: Material[]
  /**
   * All added contactmaterials.
   */
  contactmaterials: ContactMaterial[]

  /**
   * Used to look up a ContactMaterial given two instances of Material.
   */
  contactMaterialTable: TupleDictionary
  /**
   * The default material of the bodies.
   */
  defaultMaterial: Material

  /**
   * This contact material is used if no suitable contactmaterial is found for a contact.
   */
  defaultContactMaterial: ContactMaterial
  doProfiling: boolean
  profile: {
    solve: number
    makeContactConstraints: number
    broadphase: number
    integrate: number
    narrowphase: number
  }

  /**
   * Time accumulator for interpolation.
   * @see https://gafferongames.com/game-physics/fix-your-timestep/
   */
  accumulator: number

  subsystems: any[]

  /**
   * Dispatched after a body has been added to the world.
   */
  addBodyEvent: { type: 'addBody'; body: Body | null }

  /**
   * Dispatched after a body has been removed from the world.
   */
  removeBodyEvent: { type: 'removeBody'; body: Body | null }

  idToBodyMap: { [id: number]: Body }

  constructor(
    options: {
      /**
       * The gravity of the world.
       */
      gravity?: Vec3
      /**
       * Makes bodies go to sleep when they've been inactive.
       * @default false
       */
      allowSleep?: boolean
      /**
       * The broadphase algorithm to use.
       * @default NaiveBroadphase
       */
      broadphase?: Broadphase
      /**
       * The solver algorithm to use.
       * @default GSSolver
       */
      solver?: Solver
      /**
       * Set to true to use fast quaternion normalization. It is often enough accurate to use.
       * If bodies tend to explode, set to false.
       * @default false
       */
      quatNormalizeFast?: boolean
      /**
       * How often to normalize quaternions. Set to 0 for every step, 1 for every second etc.. A larger value increases performance. If bodies tend to explode, set to a smaller value (zero to be sure nothing can go wrong).
       * @default 0
       */
      quatNormalizeSkip?: number
    } = {}
  ) {
    super()

    this.dt = -1
    this.allowSleep = !!options.allowSleep
    this.contacts = []
    this.frictionEquations = []
    this.quatNormalizeSkip = options.quatNormalizeSkip !== undefined ? options.quatNormalizeSkip : 0
    this.quatNormalizeFast = options.quatNormalizeFast !== undefined ? options.quatNormalizeFast : false
    this.time = 0.0
    this.stepnumber = 0
    this.default_dt = 1 / 60
    this.nextId = 0
    this.gravity = new Vec3()

    if (options.gravity) {
      this.gravity.copy(options.gravity)
    }

    this.broadphase = options.broadphase !== undefined ? options.broadphase : new NaiveBroadphase()
    this.bodies = []
    this.hasActiveBodies = false
    this.solver = options.solver !== undefined ? options.solver : new GSSolver()
    this.constraints = []
    this.narrowphase = new Narrowphase(this)
    this.collisionMatrix = new ArrayCollisionMatrix()
    this.collisionMatrixPrevious = new ArrayCollisionMatrix()
    this.bodyOverlapKeeper = new OverlapKeeper()
    this.shapeOverlapKeeper = new OverlapKeeper()
    this.materials = []
    this.contactmaterials = []
    this.contactMaterialTable = new TupleDictionary()
    this.defaultMaterial = new Material('default')
    this.defaultContactMaterial = new ContactMaterial(this.defaultMaterial, this.defaultMaterial, {
      friction: 0.3,
      restitution: 0.0,
    })
    this.doProfiling = false
    this.profile = {
      solve: 0,
      makeContactConstraints: 0,
      broadphase: 0,
      integrate: 0,
      narrowphase: 0,
    }

    this.accumulator = 0
    this.subsystems = []
    this.addBodyEvent = { type: 'addBody', body: null }
    this.removeBodyEvent = { type: 'removeBody', body: null }
    this.idToBodyMap = {}
    this.broadphase.setWorld(this)
  }

  /**
   * Get the contact material between materials m1 and m2
   * @return The contact material if it was found.
   */
  getContactMaterial(m1: Material, m2: Material): ContactMaterial {
    return this.contactMaterialTable.get(m1.id, m2.id)
  }

  /**
   * Get number of objects in the world.
   * @deprecated
   */
  numObjects(): number {
    return this.bodies.length
  }

  /**
   * Store old collision state info
   */
  collisionMatrixTick(): void {
    const temp = this.collisionMatrixPrevious
    this.collisionMatrixPrevious = this.collisionMatrix
    this.collisionMatrix = temp
    this.collisionMatrix.reset()

    this.bodyOverlapKeeper.tick()
    this.shapeOverlapKeeper.tick()
  }

  /**
   * Add a constraint to the simulation.
   */
  addConstraint(c: Constraint): void {
    this.constraints.push(c)
  }

  /**
   * Removes a constraint
   */
  removeConstraint(c: Constraint): void {
    const idx = this.constraints.indexOf(c)
    if (idx !== -1) {
      this.constraints.splice(idx, 1)
    }
  }

  /**
   * Raycast test
   * @deprecated Use .raycastAll, .raycastClosest or .raycastAny instead.
   */
  rayTest(from: Vec3, to: Vec3, result: RaycastResult | RaycastCallback): void {
    if (result instanceof RaycastResult) {
      // Do raycastClosest
      this.raycastClosest(from, to, { skipBackfaces: true }, result)
    } else {
      // Do raycastAll
      this.raycastAll(from, to, { skipBackfaces: true }, result)
    }
  }

  /**
   * Ray cast against all bodies. The provided callback will be executed for each hit with a RaycastResult as single argument.
   * @return True if any body was hit.
   */
  raycastAll(from?: Vec3, to?: Vec3, options: RayOptions = {}, callback?: RaycastCallback): boolean {
    options.mode = Ray.ALL
    options.from = from
    options.to = to
    options.callback = callback
    return tmpRay.intersectWorld(this, options)
  }

  /**
   * Ray cast, and stop at the first result. Note that the order is random - but the method is fast.
   * @return True if any body was hit.
   */
  raycastAny(from?: Vec3, to?: Vec3, options: RayOptions = {}, result?: RaycastResult): boolean {
    options.mode = Ray.ANY
    options.from = from
    options.to = to
    options.result = result
    return tmpRay.intersectWorld(this, options)
  }

  /**
   * Ray cast, and return information of the closest hit.
   * @return True if any body was hit.
   */
  raycastClosest(from?: Vec3, to?: Vec3, options: RayOptions = {}, result?: RaycastResult): boolean {
    options.mode = Ray.CLOSEST
    options.from = from
    options.to = to
    options.result = result
    return tmpRay.intersectWorld(this, options)
  }

  /**
   * Add a rigid body to the simulation.
   * @todo If the simulation has not yet started, why recrete and copy arrays for each body? Accumulate in dynamic arrays in this case.
   * @todo Adding an array of bodies should be possible. This would save some loops too
   */
  addBody(body: Body): void {
    if (this.bodies.includes(body)) {
      return
    }
    body.index = this.bodies.length
    this.bodies.push(body)
    body.world = this
    body.initPosition.copy(body.position)
    body.initVelocity.copy(body.velocity)
    body.timeLastSleepy = this.time
    if (body instanceof Body) {
      body.initAngularVelocity.copy(body.angularVelocity)
      body.initQuaternion.copy(body.quaternion)
    }
    this.collisionMatrix.setNumObjects(this.bodies.length)
    this.addBodyEvent.body = body
    this.idToBodyMap[body.id] = body
    this.dispatchEvent(this.addBodyEvent)
  }

  /**
   * Remove a rigid body from the simulation.
   */
  removeBody(body: Body): void {
    body.world = null
    const n = this.bodies.length - 1
    const bodies = this.bodies
    const idx = bodies.indexOf(body)
    if (idx !== -1) {
      bodies.splice(idx, 1) // Todo: should use a garbage free method

      // Recompute index
      for (let i = 0; i !== bodies.length; i++) {
        bodies[i].index = i
      }

      this.collisionMatrix.setNumObjects(n)
      this.removeBodyEvent.body = body
      delete this.idToBodyMap[body.id]
      this.dispatchEvent(this.removeBodyEvent)
    }
  }

  getBodyById(id: number): Body {
    return this.idToBodyMap[id]
  }

  /**
   * @todo Make a faster map
   */
  getShapeById(id: number): Shape | null {
    const bodies = this.bodies
    for (let i = 0; i < bodies.length; i++) {
      const shapes = bodies[i].shapes
      for (let j = 0; j < shapes.length; j++) {
        const shape = shapes[j]
        if (shape.id === id) {
          return shape
        }
      }
    }

    return null
  }

  /**
   * Adds a material to the World.
   * @deprecated
   * @todo Remove
   */
  addMaterial(m: Material): void {
    this.materials.push(m)
  }

  /**
   * Adds a contact material to the World
   */
  addContactMaterial(cmat: ContactMaterial): void {
    // Add contact material
    this.contactmaterials.push(cmat)

    // Add current contact material to the material table
    this.contactMaterialTable.set(cmat.materials[0].id, cmat.materials[1].id, cmat)
  }

  /**
   * Step the physics world forward in time.
   *
   * There are two modes. The simple mode is fixed timestepping without interpolation. In this case you only use the first argument. The second case uses interpolation. In that you also provide the time since the function was last used, as well as the maximum fixed timesteps to take.
   *
   * @param dt The fixed time step size to use.
   * @param timeSinceLastCalled The time elapsed since the function was last called.
   * @param maxSubSteps Maximum number of fixed steps to take per function call.
   * @see https://web.archive.org/web/20180426154531/http://bulletphysics.org/mediawiki-1.5.8/index.php/Stepping_The_World#What_do_the_parameters_to_btDynamicsWorld::stepSimulation_mean.3F
   * @example
   *     // fixed timestepping without interpolation
   *     world.step(1 / 60)
   */
  step(dt: number, timeSinceLastCalled?: number, maxSubSteps = 10): void {
    if (timeSinceLastCalled === undefined) {
      // Fixed, simple stepping

      this.internalStep(dt)

      // Increment time
      this.time += dt
    } else {
      this.accumulator += timeSinceLastCalled

      const t0 = performance.now()
      let substeps = 0
      while (this.accumulator >= dt && substeps < maxSubSteps) {
        // Do fixed steps to catch up
        this.internalStep(dt)
        this.accumulator -= dt
        substeps++
        if (performance.now() - t0 > dt * 1000) {
          // The framerate is not interactive anymore.
          // We are below the target framerate.
          // Better bail out.
          break
        }
      }

      // Remove the excess accumulator, since we may not
      // have had enough substeps available to catch up
      this.accumulator = this.accumulator % dt

      const t = this.accumulator / dt
      for (let j = 0; j !== this.bodies.length; j++) {
        const b = this.bodies[j]
        b.previousPosition.lerp(b.position, t, b.interpolatedPosition)
        b.previousQuaternion.slerp(b.quaternion, t, b.interpolatedQuaternion)
        b.previousQuaternion.normalize()
      }
      this.time += timeSinceLastCalled
    }
  }

  internalStep(dt: number): void {
    this.dt = dt

    const world = this
    const that = this
    const contacts = this.contacts
    const p1 = World_step_p1
    const p2 = World_step_p2
    const N = this.numObjects()
    const bodies = this.bodies
    const solver = this.solver
    const gravity = this.gravity
    const doProfiling = this.doProfiling
    const profile = this.profile
    const DYNAMIC = Body.DYNAMIC
    let profilingStart = -Infinity
    const constraints = this.constraints
    const frictionEquationPool = World_step_frictionEquationPool
    const gnorm = gravity.length()
    const gx = gravity.x
    const gy = gravity.y
    const gz = gravity.z
    let i = 0

    if (doProfiling) {
      profilingStart = performance.now()
    }

    // Add gravity to all objects
    for (i = 0; i !== N; i++) {
      const bi = bodies[i]
      if (bi.type === DYNAMIC) {
        // Only for dynamic bodies
        const f = bi.force

        const m = bi.mass
        f.x += m * gx
        f.y += m * gy
        f.z += m * gz
      }
    }

    // Update subsystems
    for (let i = 0, Nsubsystems = this.subsystems.length; i !== Nsubsystems; i++) {
      this.subsystems[i].update()
    }

    // Collision detection
    if (doProfiling) {
      profilingStart = performance.now()
    }
    p1.length = 0 // Clean up pair arrays from last step
    p2.length = 0
    this.broadphase.collisionPairs(this, p1, p2)
    if (doProfiling) {
      profile.broadphase = performance.now() - profilingStart
    }

    // Remove constrained pairs with collideConnected == false
    let Nconstraints = constraints.length
    for (i = 0; i !== Nconstraints; i++) {
      const c = constraints[i]
      if (!c.collideConnected) {
        for (let j = p1.length - 1; j >= 0; j -= 1) {
          if ((c.bodyA === p1[j] && c.bodyB === p2[j]) || (c.bodyB === p1[j] && c.bodyA === p2[j])) {
            p1.splice(j, 1)
            p2.splice(j, 1)
          }
        }
      }
    }

    this.collisionMatrixTick()

    // Generate contacts
    if (doProfiling) {
      profilingStart = performance.now()
    }
    const oldcontacts = World_step_oldContacts
    const NoldContacts = contacts.length

    for (i = 0; i !== NoldContacts; i++) {
      oldcontacts.push(contacts[i])
    }
    contacts.length = 0

    // Transfer FrictionEquation from current list to the pool for reuse
    const NoldFrictionEquations = this.frictionEquations.length
    for (i = 0; i !== NoldFrictionEquations; i++) {
      frictionEquationPool.push(this.frictionEquations[i])
    }
    this.frictionEquations.length = 0

    this.narrowphase.getContacts(
      p1,
      p2,
      this,
      contacts,
      oldcontacts, // To be reused
      this.frictionEquations,
      frictionEquationPool
    )

    if (doProfiling) {
      profile.narrowphase = performance.now() - profilingStart
    }

    // Loop over all collisions
    if (doProfiling) {
      profilingStart = performance.now()
    }

    // Add all friction eqs
    for (i = 0; i < this.frictionEquations.length; i++) {
      solver.addEquation(this.frictionEquations[i])
    }

    const ncontacts = contacts.length
    for (let k = 0; k !== ncontacts; k++) {
      // Current contact
      const c = contacts[k]

      // Get current collision indeces
      const bi = c.bi

      const bj = c.bj
      const si = c.si
      const sj = c.sj

      // Get collision properties
      let cm
      if (bi.material && bj.material) {
        cm = this.getContactMaterial(bi.material, bj.material) || this.defaultContactMaterial
      } else {
        cm = this.defaultContactMaterial
      }

      // c.enabled = bi.collisionResponse && bj.collisionResponse && si.collisionResponse && sj.collisionResponse;

      let mu = cm.friction
      // c.restitution = cm.restitution;

      // If friction or restitution were specified in the material, use them
      if (bi.material && bj.material) {
        if (bi.material.friction >= 0 && bj.material.friction >= 0) {
          mu = bi.material.friction * bj.material.friction
        }

        if (bi.material.restitution >= 0 && bj.material.restitution >= 0) {
          c.restitution = bi.material.restitution * bj.material.restitution
        }
      }

      // c.setSpookParams(
      //           cm.contactEquationStiffness,
      //           cm.contactEquationRelaxation,
      //           dt
      //       );

      solver.addEquation(c)

      // // Add friction constraint equation
      // if(mu > 0){

      // 	// Create 2 tangent equations
      // 	const mug = mu * gnorm;
      // 	const reducedMass = (bi.invMass + bj.invMass);
      // 	if(reducedMass > 0){
      // 		reducedMass = 1/reducedMass;
      // 	}
      // 	const pool = frictionEquationPool;
      // 	const c1 = pool.length ? pool.pop() : new FrictionEquation(bi,bj,mug*reducedMass);
      // 	const c2 = pool.length ? pool.pop() : new FrictionEquation(bi,bj,mug*reducedMass);
      // 	this.frictionEquations.push(c1, c2);

      // 	c1.bi = c2.bi = bi;
      // 	c1.bj = c2.bj = bj;
      // 	c1.minForce = c2.minForce = -mug*reducedMass;
      // 	c1.maxForce = c2.maxForce = mug*reducedMass;

      // 	// Copy over the relative vectors
      // 	c1.ri.copy(c.ri);
      // 	c1.rj.copy(c.rj);
      // 	c2.ri.copy(c.ri);
      // 	c2.rj.copy(c.rj);

      // 	// Construct tangents
      // 	c.ni.tangents(c1.t, c2.t);

      //           // Set spook params
      //           c1.setSpookParams(cm.frictionEquationStiffness, cm.frictionEquationRelaxation, dt);
      //           c2.setSpookParams(cm.frictionEquationStiffness, cm.frictionEquationRelaxation, dt);

      //           c1.enabled = c2.enabled = c.enabled;

      // 	// Add equations to solver
      // 	solver.addEquation(c1);
      // 	solver.addEquation(c2);
      // }

      if (
        bi.allowSleep &&
        bi.type === Body.DYNAMIC &&
        bi.sleepState === Body.SLEEPING &&
        bj.sleepState === Body.AWAKE &&
        bj.type !== Body.STATIC
      ) {
        const speedSquaredB = bj.velocity.lengthSquared() + bj.angularVelocity.lengthSquared()
        const speedLimitSquaredB = bj.sleepSpeedLimit ** 2
        if (speedSquaredB >= speedLimitSquaredB * 2) {
          bi.wakeUpAfterNarrowphase = true
        }
      }

      if (
        bj.allowSleep &&
        bj.type === Body.DYNAMIC &&
        bj.sleepState === Body.SLEEPING &&
        bi.sleepState === Body.AWAKE &&
        bi.type !== Body.STATIC
      ) {
        const speedSquaredA = bi.velocity.lengthSquared() + bi.angularVelocity.lengthSquared()
        const speedLimitSquaredA = bi.sleepSpeedLimit ** 2
        if (speedSquaredA >= speedLimitSquaredA * 2) {
          bj.wakeUpAfterNarrowphase = true
        }
      }

      // Now we know that i and j are in contact. Set collision matrix state
      this.collisionMatrix.set(bi, bj, true)

      if (!this.collisionMatrixPrevious.get(bi, bj)) {
        // First contact!
        // We reuse the collideEvent object, otherwise we will end up creating new objects for each new contact, even if there's no event listener attached.
        World_step_collideEvent.body = bj
        World_step_collideEvent.contact = c
        bi.dispatchEvent(World_step_collideEvent)

        World_step_collideEvent.body = bi
        bj.dispatchEvent(World_step_collideEvent)
      }

      this.bodyOverlapKeeper.set(bi.id, bj.id)
      this.shapeOverlapKeeper.set(si.id, sj.id)
    }

    this.emitContactEvents()

    if (doProfiling) {
      profile.makeContactConstraints = performance.now() - profilingStart
      profilingStart = performance.now()
    }

    // Wake up bodies
    for (i = 0; i !== N; i++) {
      const bi = bodies[i]
      if (bi.wakeUpAfterNarrowphase) {
        bi.wakeUp()
        bi.wakeUpAfterNarrowphase = false
      }
    }

    // Add user-added constraints
    Nconstraints = constraints.length
    for (i = 0; i !== Nconstraints; i++) {
      const c = constraints[i]
      c.update()
      for (let j = 0, Neq = c.equations.length; j !== Neq; j++) {
        const eq = c.equations[j]
        solver.addEquation(eq)
      }
    }

    // Solve the constrained system
    solver.solve(dt, this)

    if (doProfiling) {
      profile.solve = performance.now() - profilingStart
    }

    // Remove all contacts from solver
    solver.removeAllEquations()

    // Apply damping, see http://code.google.com/p/bullet/issues/detail?id=74 for details
    const pow = Math.pow
    for (i = 0; i !== N; i++) {
      const bi = bodies[i]
      if (bi.type & DYNAMIC) {
        // Only for dynamic bodies
        const ld = pow(1.0 - bi.linearDamping, dt)
        const v = bi.velocity
        v.scale(ld, v)
        const av = bi.angularVelocity
        if (av) {
          const ad = pow(1.0 - bi.angularDamping, dt)
          av.scale(ad, av)
        }
      }
    }

    this.dispatchEvent(World_step_preStepEvent)

    // Invoke pre-step callbacks
    for (i = 0; i !== N; i++) {
      const bi = bodies[i]
      if (bi.preStep) {
        bi.preStep.call(bi)
      }
    }

    // Leap frog
    // vnew = v + h*f/m
    // xnew = x + h*vnew
    if (doProfiling) {
      profilingStart = performance.now()
    }
    const stepnumber = this.stepnumber
    const quatNormalize = stepnumber % (this.quatNormalizeSkip + 1) === 0
    const quatNormalizeFast = this.quatNormalizeFast

    for (i = 0; i !== N; i++) {
      bodies[i].integrate(dt, quatNormalize, quatNormalizeFast)
    }
    this.clearForces()

    this.broadphase.dirty = true

    if (doProfiling) {
      profile.integrate = performance.now() - profilingStart
    }

    // Update step number
    this.stepnumber += 1

    this.dispatchEvent(World_step_postStepEvent)

    // Invoke post-step callbacks
    for (i = 0; i !== N; i++) {
      const bi = bodies[i]
      const postStep = bi.postStep
      if (postStep) {
        postStep.call(bi)
      }
    }

    // Sleeping update
    let hasActiveBodies = true
    if (this.allowSleep) {
      hasActiveBodies = false
      for (i = 0; i !== N; i++) {
        const bi = bodies[i]
        bi.sleepTick(this.time)

        if (bi.sleepState !== Body.SLEEPING) {
          hasActiveBodies = true
        }
      }
    }
    this.hasActiveBodies = hasActiveBodies
  }

  emitContactEvents(): void {
    const hasBeginContact = this.hasAnyEventListener('beginContact')
    const hasEndContact = this.hasAnyEventListener('endContact')

    if (hasBeginContact || hasEndContact) {
      this.bodyOverlapKeeper.getDiff(additions, removals)
    }

    if (hasBeginContact) {
      for (let i = 0, l = additions.length; i < l; i += 2) {
        beginContactEvent.bodyA = this.getBodyById(additions[i])
        beginContactEvent.bodyB = this.getBodyById(additions[i + 1])
        this.dispatchEvent(beginContactEvent)
      }
      beginContactEvent.bodyA = beginContactEvent.bodyB = null
    }

    if (hasEndContact) {
      for (let i = 0, l = removals.length; i < l; i += 2) {
        endContactEvent.bodyA = this.getBodyById(removals[i])
        endContactEvent.bodyB = this.getBodyById(removals[i + 1])
        this.dispatchEvent(endContactEvent)
      }
      endContactEvent.bodyA = endContactEvent.bodyB = null
    }

    additions.length = removals.length = 0

    const hasBeginShapeContact = this.hasAnyEventListener('beginShapeContact')
    const hasEndShapeContact = this.hasAnyEventListener('endShapeContact')

    if (hasBeginShapeContact || hasEndShapeContact) {
      this.shapeOverlapKeeper.getDiff(additions, removals)
    }

    if (hasBeginShapeContact) {
      for (let i = 0, l = additions.length; i < l; i += 2) {
        const shapeA = this.getShapeById(additions[i])
        const shapeB = this.getShapeById(additions[i + 1])
        beginShapeContactEvent.shapeA = shapeA
        beginShapeContactEvent.shapeB = shapeB
        if (shapeA) beginShapeContactEvent.bodyA = shapeA.body
        if (shapeB) beginShapeContactEvent.bodyB = shapeB.body
        this.dispatchEvent(beginShapeContactEvent)
      }
      beginShapeContactEvent.bodyA = beginShapeContactEvent.bodyB = beginShapeContactEvent.shapeA = beginShapeContactEvent.shapeB = null
    }

    if (hasEndShapeContact) {
      for (let i = 0, l = removals.length; i < l; i += 2) {
        const shapeA = this.getShapeById(removals[i])
        const shapeB = this.getShapeById(removals[i + 1])
        endShapeContactEvent.shapeA = shapeA
        endShapeContactEvent.shapeB = shapeB
        if (shapeA) endShapeContactEvent.bodyA = shapeA.body
        if (shapeB) endShapeContactEvent.bodyB = shapeB.body
        this.dispatchEvent(endShapeContactEvent)
      }
      endShapeContactEvent.bodyA = endShapeContactEvent.bodyB = endShapeContactEvent.shapeA = endShapeContactEvent.shapeB = null
    }
  }

  /**
   * Sets all body forces in the world to zero.
   */
  clearForces(): void {
    const bodies = this.bodies
    const N = bodies.length
    for (let i = 0; i !== N; i++) {
      const b = bodies[i]
      const force = b.force
      const tau = b.torque

      b.force.set(0, 0, 0)
      b.torque.set(0, 0, 0)
    }
  }
}

// Temp stuff
const tmpAABB1 = new AABB()
const tmpArray1 = []
const tmpRay = new Ray()

// performance.now() fallback on Date.now()
const performance = (globalThis.performance || {}) as Performance

if (!performance.now) {
  let nowOffset = Date.now()
  if (performance.timing && performance.timing.navigationStart) {
    nowOffset = performance.timing.navigationStart
  }
  performance.now = () => Date.now() - nowOffset
}

const step_tmp1 = new Vec3()

// Dispatched after the world has stepped forward in time.
// Reusable event objects to save memory.
const World_step_postStepEvent = { type: 'postStep' }

// Dispatched before the world steps forward in time.
const World_step_preStepEvent = { type: 'preStep' }

const World_step_collideEvent: {
  type: typeof Body.COLLIDE_EVENT_NAME
  body: Body | null
  contact: ContactEquation | null
} = { type: Body.COLLIDE_EVENT_NAME, body: null, contact: null }

// Pools for unused objects
const World_step_oldContacts: ContactEquation[] = []
const World_step_frictionEquationPool: FrictionEquation[] = []

// Reusable arrays for collision pairs
const World_step_p1: Body[] = []
const World_step_p2: Body[] = []

// Stuff for emitContactEvents
const additions: number[] = []
const removals: number[] = []
type ContactEvent = {
  type: string
  bodyA: Body | null
  bodyB: Body | null
}
const beginContactEvent: ContactEvent = {
  type: 'beginContact',
  bodyA: null,
  bodyB: null,
}
const endContactEvent: ContactEvent = {
  type: 'endContact',
  bodyA: null,
  bodyB: null,
}
type ShapeContactEvent = {
  type: string
  bodyA: Body | null
  bodyB: Body | null
  shapeA: Shape | null
  shapeB: Shape | null
}
const beginShapeContactEvent: ShapeContactEvent = {
  type: 'beginShapeContact',
  bodyA: null,
  bodyB: null,
  shapeA: null,
  shapeB: null,
}
const endShapeContactEvent: ShapeContactEvent = {
  type: 'endShapeContact',
  bodyA: null,
  bodyB: null,
  shapeA: null,
  shapeB: null,
}
