/*
 * Planck.js
 *
 * Copyright (c) Erin Catto, Ali Shakiba
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import { options } from "../util/options";
import { Vec2, Vec2Value } from "../common/Vec2";
import { BroadPhase } from "../collision/BroadPhase";
import { Solver, ContactImpulse, TimeStep } from "./Solver";
import { Body, BodyDef } from "./Body";
import { Joint } from "./Joint";
import { Contact } from "./Contact";
import { AABBValue, RayCastInput, RayCastOutput } from "../collision/AABB";
import { Fixture, FixtureProxy } from "./Fixture";
import { Manifold } from "../collision/Manifold";

/** @internal */ const _ASSERT = typeof ASSERT === "undefined" ? false : ASSERT;
/** @internal */ const _CONSTRUCTOR_FACTORY = typeof CONSTRUCTOR_FACTORY === "undefined" ? false : CONSTRUCTOR_FACTORY;

export interface WorldDef {
  /** [default: { x : 0, y : 0}] */
  gravity?: Vec2Value;

  /** [default: true] */
  allowSleep?: boolean;

  /** [default: true] */
  warmStarting?: boolean;

  /** [default: true] */
  continuousPhysics?: boolean;

  /** [default: false] */
  subStepping?: boolean;

  /** [default: true] */
  blockSolve?: boolean;

  /** @internal [8] For the velocity constraint solver. */
  velocityIterations?: number;

  /** @internal [3] For the position constraint solver. */
  positionIterations?: number;
}

/** @internal */ const DEFAULTS: WorldDef = {
  gravity: Vec2.zero(),
  allowSleep: true,
  warmStarting: true,
  continuousPhysics: true,
  subStepping: false,
  blockSolve: true,
  velocityIterations: 8,
  positionIterations: 3,
};

/**
 * Callback function for ray casts, see {@link World.rayCast}.
 *
 * Called for each fixture found in the query.
 * The returned value replaces the ray-cast input maxFraction.
 * You control how the ray cast proceeds by returning a numeric/float value.
 *
 * - `0` to terminate the ray cast
 * - `fraction` to clip the ray cast at current point
 * - `1` don't clip the ray and continue
 * - `-1` (or anything else) to continue
 *
 * @param fixture The fixture hit by the ray
 * @param point The point of initial intersection
 * @param normal The normal vector at the point of intersection
 * @param fraction The fraction along the ray at the point of intersection
 *
 * @returns A number to update the maxFraction
 */
export type WorldRayCastCallback = (fixture: Fixture, point: Vec2, normal: Vec2, fraction: number) => number;

/**
 * Called for each fixture found in the query AABB. It may return `false` to terminate the query.
 */
export type WorldAABBQueryCallback = (fixture: Fixture) => boolean;

declare module "./World" {
  /** @hidden @deprecated Use new keyword. */
  // @ts-expect-error
  function World(deg: WorldDef): World;
  /** @hidden @deprecated Use new keyword. */
  // @ts-expect-error
  function World(gravity: Vec2): World;
  /** @hidden @deprecated Use new keyword. */
  // @ts-expect-error
  function World(): World;
}

/**
 * The `World` class contains the bodies and joints. It manages all aspects
 * of the simulation and allows for asynchronous queries (like AABB queries
 * and ray-casts). Much of your interactions with Planck.js will be with a
 * World object.
 */
// @ts-expect-error
export class World {
  /** @internal */ m_solver: Solver;
  /** @internal */ m_broadPhase: BroadPhase;
  /** @internal */ m_contactList: Contact | null;
  /** @internal */ m_contactCount: number;
  /** @internal */ m_bodyList: Body | null;
  /** @internal */ m_bodyCount: number;
  /** @internal */ m_jointList: Joint | null;
  /** @internal */ m_jointCount: number;
  /** @internal */ m_stepComplete: boolean;
  /** @internal */ m_allowSleep: boolean;
  /** @internal */ m_gravity: Vec2;
  /** @internal */ m_clearForces: boolean;
  /** @internal */ m_newFixture: boolean;
  /** @internal */ m_locked: boolean;
  /** @internal */ m_warmStarting: boolean;
  /** @internal */ m_continuousPhysics: boolean;
  /** @internal */ m_subStepping: boolean;
  /** @internal */ m_blockSolve: boolean;
  /** @internal */ m_velocityIterations: number;
  /** @internal */ m_positionIterations: number;
  /** @internal */ m_t: number;

  /** @internal */ m_step_callback: ((world: World) => unknown)[];

  // TODO
  /** @internal */ _listeners: {
    [key: string]: any[];
  };

  /**
   * @param def World definition or gravity vector.
   */
  constructor(def?: WorldDef | Vec2Value) {
    if (_CONSTRUCTOR_FACTORY && !(this instanceof World)) {
      return new World(def);
    }

    this.s_step = new TimeStep();

    if (!def) {
      def = {};
    } else if (Vec2.isValid(def)) {
      def = { gravity: def as Vec2 };
    }

    def = options(def, DEFAULTS) as WorldDef;

    this.m_solver = new Solver(this);

    this.m_broadPhase = new BroadPhase();

    this.m_contactList = null;
    this.m_contactCount = 0;

    this.m_bodyList = null;
    this.m_bodyCount = 0;

    this.m_jointList = null;
    this.m_jointCount = 0;

    this.m_stepComplete = true;

    this.m_allowSleep = def.allowSleep;
    this.m_gravity = Vec2.clone(def.gravity);

    this.m_clearForces = true;
    this.m_newFixture = false;
    this.m_locked = false;

    // These are for debugging the solver.
    this.m_warmStarting = def.warmStarting;
    this.m_continuousPhysics = def.continuousPhysics;
    this.m_subStepping = def.subStepping;

    this.m_blockSolve = def.blockSolve;
    this.m_velocityIterations = def.velocityIterations;
    this.m_positionIterations = def.positionIterations;

    this.m_t = 0;

    this.m_step_callback = [];
  }

  /** @hidden */
  _serialize(): object {
    const bodies = [];
    const joints = [];

    for (let b = this.getBodyList(); b; b = b.getNext()) {
      bodies.push(b);
    }

    for (let j = this.getJointList(); j; j = j.getNext()) {
      // @ts-ignore
      if (typeof j._serialize === "function") {
        joints.push(j);
      }
    }

    return {
      gravity: this.m_gravity,
      bodies,
      joints,
    };
  }

  /** @hidden */
  static _deserialize(data: any, context: any, restore: any): World {
    if (!data) {
      return new World();
    }

    const world = new World(data.gravity);

    if (data.bodies) {
      for (let i = data.bodies.length - 1; i >= 0; i -= 1) {
        world._addBody(restore(Body, data.bodies[i], world));
      }
    }

    if (data.joints) {
      for (let i = data.joints.length - 1; i >= 0; i--) {
        world.createJoint(restore(Joint, data.joints[i], world));
      }
    }

    return world;
  }

  /**
   * Get the world body list. With the returned body, use Body.getNext to get the
   * next body in the world list. A null body indicates the end of the list.
   *
   * @return the head of the world body list.
   */
  getBodyList(): Body | null {
    return this.m_bodyList;
  }

  /**
   * Get the world joint list. With the returned joint, use Joint.getNext to get
   * the next joint in the world list. A null joint indicates the end of the list.
   *
   * @return the head of the world joint list.
   */
  getJointList(): Joint | null {
    return this.m_jointList;
  }

  /**
   * Get the world contact list. With the returned contact, use Contact.getNext to
   * get the next contact in the world list. A null contact indicates the end of
   * the list.
   *
   * Warning: contacts are created and destroyed in the middle of a time step.
   * Use ContactListener to avoid missing contacts.
   *
   * @return the head of the world contact list.
   */
  getContactList(): Contact | null {
    return this.m_contactList;
  }

  getBodyCount(): number {
    return this.m_bodyCount;
  }

  getJointCount(): number {
    return this.m_jointCount;
  }

  /**
   * Get the number of contacts (each may have 0 or more contact points).
   */
  getContactCount(): number {
    return this.m_contactCount;
  }

  /**
   * Change the global gravity vector.
   */
  setGravity(gravity: Vec2Value): void {
    this.m_gravity.set(gravity);
  }

  /**
   * Get the global gravity vector.
   */
  getGravity(): Vec2 {
    return this.m_gravity;
  }

  /**
   * Is the world locked (in the middle of a time step).
   */
  isLocked(): boolean {
    return this.m_locked;
  }

  /**
   * Enable/disable sleep.
   */
  setAllowSleeping(flag: boolean): void {
    if (flag == this.m_allowSleep) {
      return;
    }

    this.m_allowSleep = flag;
    if (this.m_allowSleep == false) {
      for (let b = this.m_bodyList; b; b = b.m_next) {
        b.setAwake(true);
      }
    }
  }

  getAllowSleeping(): boolean {
    return this.m_allowSleep;
  }

  /**
   * Enable/disable warm starting. For testing.
   */
  setWarmStarting(flag: boolean): void {
    this.m_warmStarting = flag;
  }

  getWarmStarting(): boolean {
    return this.m_warmStarting;
  }

  /**
   * Enable/disable continuous physics. For testing.
   */
  setContinuousPhysics(flag: boolean): void {
    this.m_continuousPhysics = flag;
  }

  getContinuousPhysics(): boolean {
    return this.m_continuousPhysics;
  }

  /**
   * Enable/disable single stepped continuous physics. For testing.
   */
  setSubStepping(flag: boolean): void {
    this.m_subStepping = flag;
  }

  getSubStepping(): boolean {
    return this.m_subStepping;
  }

  /**
   * Set flag to control automatic clearing of forces after each time step.
   */
  setAutoClearForces(flag: boolean): void {
    this.m_clearForces = flag;
  }

  /**
   * Get the flag that controls automatic clearing of forces after each time step.
   */
  getAutoClearForces(): boolean {
    return this.m_clearForces;
  }

  /**
   * Manually clear the force buffer on all bodies. By default, forces are cleared
   * automatically after each call to step. The default behavior is modified by
   * calling setAutoClearForces. The purpose of this function is to support
   * sub-stepping. Sub-stepping is often used to maintain a fixed sized time step
   * under a variable frame-rate. When you perform sub-stepping you will disable
   * auto clearing of forces and instead call clearForces after all sub-steps are
   * complete in one pass of your game loop.
   *
   * See {@link World.setAutoClearForces}
   */
  clearForces(): void {
    for (let body = this.m_bodyList; body; body = body.getNext()) {
      body.m_force.setZero();
      body.m_torque = 0.0;
    }
  }

  /**
   * Query the world for all fixtures that potentially overlap the provided AABB.
   *
   * @param aabb The query box.
   * @param callback Called for each fixture found in the query AABB. It may return `false` to terminate the query.
   */
  queryAABB(aabb: AABBValue, callback: WorldAABBQueryCallback): void {
    if (_ASSERT) console.assert(typeof callback === "function");
    const broadPhase = this.m_broadPhase;
    this.m_broadPhase.query(aabb, function (proxyId: number): boolean {
      // TODO GC
      const proxy = broadPhase.getUserData(proxyId);
      return callback(proxy.fixture);
    });
  }

  /**
   * Ray-cast the world for all fixtures in the path of the ray. Your callback
   * controls whether you get the closest point, any point, or n-points. The
   * ray-cast ignores shapes that contain the starting point.
   *
   * @param point1 The ray starting point
   * @param point2 The ray ending point
   * @param callback A function that is called for each fixture that is hit by the ray. You control how the ray cast proceeds by returning a numeric/float value.
   */
  rayCast(point1: Vec2Value, point2: Vec2Value, callback: WorldRayCastCallback): void {
    if (_ASSERT) console.assert(typeof callback === "function");
    const broadPhase = this.m_broadPhase;

    this.m_broadPhase.rayCast(
      {
        maxFraction: 1.0,
        p1: point1,
        p2: point2,
      },
      function (input: RayCastInput, proxyId: number): number {
        // TODO GC
        const proxy = broadPhase.getUserData(proxyId);
        const fixture = proxy.fixture;
        const index = proxy.childIndex;
        // @ts-ignore
        const output: RayCastOutput = {}; // TODO GC
        const hit = fixture.rayCast(output, input, index);
        if (hit) {
          const fraction = output.fraction;
          const point = Vec2.add(Vec2.mulNumVec2(1.0 - fraction, input.p1), Vec2.mulNumVec2(fraction, input.p2));
          return callback(fixture, point, output.normal, fraction);
        }
        return input.maxFraction;
      },
    );
  }

  /**
   * Get the number of broad-phase proxies.
   */
  getProxyCount(): number {
    return this.m_broadPhase.getProxyCount();
  }

  /**
   * Get the height of broad-phase dynamic tree.
   */
  getTreeHeight(): number {
    return this.m_broadPhase.getTreeHeight();
  }

  /**
   * Get the balance of broad-phase dynamic tree.
   */
  getTreeBalance(): number {
    return this.m_broadPhase.getTreeBalance();
  }

  /**
   * Get the quality metric of broad-phase dynamic tree. The smaller the better.
   * The minimum is 1.
   */
  getTreeQuality(): number {
    return this.m_broadPhase.getTreeQuality();
  }

  /**
   * Shift the world origin. Useful for large worlds. The body shift formula is:
   * position -= newOrigin
   *
   * @param newOrigin The new origin with respect to the old origin
   *
   * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step.
   */
  shiftOrigin(newOrigin: Vec2Value): void {
    if (_ASSERT) console.assert(this.isLocked() == false);
    if (this.isLocked()) {
      return;
    }

    for (let b = this.m_bodyList; b; b = b.m_next) {
      b.m_xf.p.sub(newOrigin);
      b.m_sweep.c0.sub(newOrigin);
      b.m_sweep.c.sub(newOrigin);
    }

    for (let j = this.m_jointList; j; j = j.m_next) {
      j.shiftOrigin(newOrigin);
    }

    this.m_broadPhase.shiftOrigin(newOrigin);
  }

  /** @internal Used for deserialize. */
  _addBody(body: Body): void {
    if (_ASSERT) console.assert(this.isLocked() === false);
    if (this.isLocked()) {
      return;
    }

    // Add to world doubly linked list.
    body.m_prev = null;
    body.m_next = this.m_bodyList;
    if (this.m_bodyList) {
      this.m_bodyList.m_prev = body;
    }
    this.m_bodyList = body;
    ++this.m_bodyCount;

    this.publish("add-body", body);
  }

  /**
   * Create a rigid body given a definition. No reference to the definition is
   * retained.
   *
   * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step.
   */
  createBody(def?: BodyDef): Body;
  createBody(position: Vec2Value, angle?: number): Body;
  // tslint:disable-next-line:typedef
  createBody(arg1?, arg2?) {
    if (_ASSERT) console.assert(this.isLocked() == false);
    if (this.isLocked()) {
      return null;
    }

    let def: BodyDef = {};
    if (!arg1) {
    } else if (Vec2.isValid(arg1)) {
      def = { position: arg1, angle: arg2 };
    } else if (typeof arg1 === "object") {
      def = arg1;
    }

    const body = new Body(this, def);
    this._addBody(body);
    return body;
  }

  createDynamicBody(def?: BodyDef): Body;
  createDynamicBody(position: Vec2Value, angle?: number): Body;
  // tslint:disable-next-line:typedef
  createDynamicBody(arg1?, arg2?) {
    let def: BodyDef = {};
    if (!arg1) {
    } else if (Vec2.isValid(arg1)) {
      def = { position: arg1, angle: arg2 };
    } else if (typeof arg1 === "object") {
      def = arg1;
    }
    def.type = "dynamic";
    return this.createBody(def);
  }

  createKinematicBody(def?: BodyDef): Body;
  createKinematicBody(position: Vec2Value, angle?: number): Body;
  // tslint:disable-next-line:typedef
  createKinematicBody(arg1?, arg2?) {
    let def: BodyDef = {};
    if (!arg1) {
    } else if (Vec2.isValid(arg1)) {
      def = { position: arg1, angle: arg2 };
    } else if (typeof arg1 === "object") {
      def = arg1;
    }
    def.type = "kinematic";
    return this.createBody(def);
  }

  /**
   * Destroy a body from the world.
   *
   * Warning: This automatically deletes all associated shapes and joints.
   *
   * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step.
   */
  destroyBody(b: Body): boolean {
    if (_ASSERT) console.assert(this.m_bodyCount > 0);
    if (_ASSERT) console.assert(this.isLocked() == false);
    if (this.isLocked()) {
      return;
    }

    if (b.m_destroyed) {
      return false;
    }

    // Delete the attached joints.
    let je = b.m_jointList;
    while (je) {
      const je0 = je;
      je = je.next;

      this.publish("remove-joint", je0.joint);
      this.destroyJoint(je0.joint);

      b.m_jointList = je;
    }
    b.m_jointList = null;

    // Delete the attached contacts.
    let ce = b.m_contactList;
    while (ce) {
      const ce0 = ce;
      ce = ce.next;

      this.destroyContact(ce0.contact);

      b.m_contactList = ce;
    }
    b.m_contactList = null;

    // Delete the attached fixtures. This destroys broad-phase proxies.
    let f = b.m_fixtureList;
    while (f) {
      const f0 = f;
      f = f.m_next;

      this.publish("remove-fixture", f0);
      f0.destroyProxies(this.m_broadPhase);

      b.m_fixtureList = f;
    }
    b.m_fixtureList = null;

    // Remove world body list.
    if (b.m_prev) {
      b.m_prev.m_next = b.m_next;
    }

    if (b.m_next) {
      b.m_next.m_prev = b.m_prev;
    }

    if (b == this.m_bodyList) {
      this.m_bodyList = b.m_next;
    }

    b.m_destroyed = true;

    --this.m_bodyCount;

    this.publish("remove-body", b);

    return true;
  }

  /**
   * Create a joint to constrain bodies together. No reference to the definition
   * is retained. This may cause the connected bodies to cease colliding.
   *
   * Note: creating a joint doesn't wake the bodies.
   *
   * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step.
   */
  createJoint<T extends Joint>(joint: T): T | null {
    if (_ASSERT) console.assert(!!joint.m_bodyA);
    if (_ASSERT) console.assert(!!joint.m_bodyB);
    if (_ASSERT) console.assert(this.isLocked() == false);
    if (this.isLocked()) {
      return null;
    }

    // Connect to the world list.
    joint.m_prev = null;
    joint.m_next = this.m_jointList;
    if (this.m_jointList) {
      this.m_jointList.m_prev = joint;
    }
    this.m_jointList = joint;
    ++this.m_jointCount;

    // Connect to the bodies' doubly linked lists.
    joint.m_edgeA.joint = joint;
    joint.m_edgeA.other = joint.m_bodyB;
    joint.m_edgeA.prev = null;
    joint.m_edgeA.next = joint.m_bodyA.m_jointList;
    if (joint.m_bodyA.m_jointList) joint.m_bodyA.m_jointList.prev = joint.m_edgeA;
    joint.m_bodyA.m_jointList = joint.m_edgeA;

    joint.m_edgeB.joint = joint;
    joint.m_edgeB.other = joint.m_bodyA;
    joint.m_edgeB.prev = null;
    joint.m_edgeB.next = joint.m_bodyB.m_jointList;
    if (joint.m_bodyB.m_jointList) joint.m_bodyB.m_jointList.prev = joint.m_edgeB;
    joint.m_bodyB.m_jointList = joint.m_edgeB;

    // If the joint prevents collisions, then flag any contacts for filtering.
    if (joint.m_collideConnected == false) {
      for (let edge = joint.m_bodyB.getContactList(); edge; edge = edge.next) {
        if (edge.other == joint.m_bodyA) {
          // Flag the contact for filtering at the next time step (where either
          // body is awake).
          edge.contact.flagForFiltering();
        }
      }
    }

    this.publish("add-joint", joint);
    return joint;
  }

  /**
   * Destroy a joint.
   *
   * Warning: This may cause the connected bodies to begin colliding.
   *
   * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step.
   */
  destroyJoint(joint: Joint): void {
    if (_ASSERT) console.assert(this.isLocked() == false);
    if (this.isLocked()) {
      return;
    }

    // Remove from the doubly linked list.
    if (joint.m_prev) {
      joint.m_prev.m_next = joint.m_next;
    }

    if (joint.m_next) {
      joint.m_next.m_prev = joint.m_prev;
    }

    if (joint == this.m_jointList) {
      this.m_jointList = joint.m_next;
    }

    // Disconnect from bodies.
    const bodyA = joint.m_bodyA;
    const bodyB = joint.m_bodyB;

    // Wake up connected bodies.
    bodyA.setAwake(true);
    bodyB.setAwake(true);

    // Remove from body 1.
    if (joint.m_edgeA.prev) {
      joint.m_edgeA.prev.next = joint.m_edgeA.next;
    }

    if (joint.m_edgeA.next) {
      joint.m_edgeA.next.prev = joint.m_edgeA.prev;
    }

    if (joint.m_edgeA == bodyA.m_jointList) {
      bodyA.m_jointList = joint.m_edgeA.next;
    }

    joint.m_edgeA.prev = null;
    joint.m_edgeA.next = null;

    // Remove from body 2
    if (joint.m_edgeB.prev) {
      joint.m_edgeB.prev.next = joint.m_edgeB.next;
    }

    if (joint.m_edgeB.next) {
      joint.m_edgeB.next.prev = joint.m_edgeB.prev;
    }

    if (joint.m_edgeB == bodyB.m_jointList) {
      bodyB.m_jointList = joint.m_edgeB.next;
    }

    joint.m_edgeB.prev = null;
    joint.m_edgeB.next = null;

    if (_ASSERT) console.assert(this.m_jointCount > 0);
    --this.m_jointCount;

    // If the joint prevents collisions, then flag any contacts for filtering.
    if (joint.m_collideConnected == false) {
      let edge = bodyB.getContactList();
      while (edge) {
        if (edge.other == bodyA) {
          // Flag the contact for filtering at the next time step (where either
          // body is awake).
          edge.contact.flagForFiltering();
        }

        edge = edge.next;
      }
    }

    this.publish("remove-joint", joint);
  }

  /** @internal */
  s_step: TimeStep; // reuse

  /**
   * Take a time step. This performs collision detection, integration, and
   * constraint solution.
   *
   * Broad-phase, narrow-phase, solve and solve time of impacts.
   *
   * @param timeStep Time step, this should not vary.
   */
  step(timeStep: number, velocityIterations?: number, positionIterations?: number): void {
    this.publish("pre-step", timeStep);

    if ((velocityIterations | 0) !== velocityIterations) {
      // TODO: remove this in future
      velocityIterations = 0;
    }

    velocityIterations = velocityIterations || this.m_velocityIterations;
    positionIterations = positionIterations || this.m_positionIterations;

    // If new fixtures were added, we need to find the new contacts.
    if (this.m_newFixture) {
      this.findNewContacts();
      this.m_newFixture = false;
    }

    this.m_locked = true;

    this.s_step.reset(timeStep);
    this.s_step.velocityIterations = velocityIterations;
    this.s_step.positionIterations = positionIterations;
    this.s_step.warmStarting = this.m_warmStarting;
    this.s_step.blockSolve = this.m_blockSolve;

    // Update contacts. This is where some contacts are destroyed.
    this.updateContacts();

    // Integrate velocities, solve velocity constraints, and integrate positions.
    if (this.m_stepComplete && timeStep > 0.0) {
      this.m_solver.solveWorld(this.s_step);

      // Synchronize fixtures, check for out of range bodies.
      for (let b = this.m_bodyList; b; b = b.getNext()) {
        // If a body was not in an island then it did not move.
        if (b.m_islandFlag == false) {
          continue;
        }

        if (b.isStatic()) {
          continue;
        }

        // Update fixtures (for broad-phase).
        b.synchronizeFixtures();
      }
      // Look for new contacts.
      this.findNewContacts();
    }

    // Handle TOI events.
    if (this.m_continuousPhysics && timeStep > 0.0) {
      this.m_solver.solveWorldTOI(this.s_step);
    }

    if (this.m_clearForces) {
      this.clearForces();
    }

    this.m_locked = false;

    let callback: (world: World) => unknown;
    while ((callback = this.m_step_callback.shift())) {
      callback(this);
    }

    this.publish("post-step", timeStep);
  }

  /**
   * Queue a function to be called after ongoing simulation step. If no simulation is in progress call it immediately.
   */
  queueUpdate(callback: (world: World) => unknown): void {
    if (!this.isLocked()) {
      callback(this);
    } else {
      this.m_step_callback.push(callback);
    }
  }

  /**
   * @internal
   * Call this method to find new contacts.
   */
  findNewContacts(): void {
    this.m_broadPhase.updatePairs((proxyA: FixtureProxy, proxyB: FixtureProxy) => this.createContact(proxyA, proxyB));
  }

  /**
   * @internal
   * Callback for broad-phase.
   */
  createContact(proxyA: FixtureProxy, proxyB: FixtureProxy): void {
    const fixtureA = proxyA.fixture;
    const fixtureB = proxyB.fixture;

    const indexA = proxyA.childIndex;
    const indexB = proxyB.childIndex;

    const bodyA = fixtureA.getBody();
    const bodyB = fixtureB.getBody();

    // Are the fixtures on the same body?
    if (bodyA == bodyB) {
      return;
    }

    // TODO_ERIN use a hash table to remove a potential bottleneck when both
    // bodies have a lot of contacts.
    // Does a contact already exist?
    let edge = bodyB.getContactList(); // ContactEdge
    while (edge) {
      if (edge.other == bodyA) {
        const fA = edge.contact.getFixtureA();
        const fB = edge.contact.getFixtureB();
        const iA = edge.contact.getChildIndexA();
        const iB = edge.contact.getChildIndexB();

        if (fA == fixtureA && fB == fixtureB && iA == indexA && iB == indexB) {
          // A contact already exists.
          return;
        }

        if (fA == fixtureB && fB == fixtureA && iA == indexB && iB == indexA) {
          // A contact already exists.
          return;
        }
      }

      edge = edge.next;
    }

    if (bodyB.shouldCollide(bodyA) == false) {
      return;
    }
    if (fixtureB.shouldCollide(fixtureA) == false) {
      return;
    }

    // Call the factory.
    const contact = Contact.create(fixtureA, indexA, fixtureB, indexB);
    if (contact == null) {
      return;
    }

    // Insert into the world.
    contact.m_prev = null;
    if (this.m_contactList != null) {
      contact.m_next = this.m_contactList;
      this.m_contactList.m_prev = contact;
    }
    this.m_contactList = contact;

    ++this.m_contactCount;
  }

  /**
   * @internal
   * Removes old non-overlapping contacts, applies filters and updates contacts.
   */
  updateContacts(): void {
    // Update awake contacts.
    let c: Contact;
    let next_c = this.m_contactList;
    while ((c = next_c)) {
      next_c = c.getNext();
      const fixtureA = c.getFixtureA();
      const fixtureB = c.getFixtureB();
      const indexA = c.getChildIndexA();
      const indexB = c.getChildIndexB();
      const bodyA = fixtureA.getBody();
      const bodyB = fixtureB.getBody();

      // Is this contact flagged for filtering?
      if (c.m_filterFlag) {
        if (bodyB.shouldCollide(bodyA) == false) {
          this.destroyContact(c);
          continue;
        }

        if (fixtureB.shouldCollide(fixtureA) == false) {
          this.destroyContact(c);
          continue;
        }

        // Clear the filtering flag.
        c.m_filterFlag = false;
      }

      const activeA = bodyA.isAwake() && !bodyA.isStatic();
      const activeB = bodyB.isAwake() && !bodyB.isStatic();

      // At least one body must be awake and it must be dynamic or kinematic.
      if (activeA == false && activeB == false) {
        continue;
      }

      const proxyIdA = fixtureA.m_proxies[indexA].proxyId;
      const proxyIdB = fixtureB.m_proxies[indexB].proxyId;
      const overlap = this.m_broadPhase.testOverlap(proxyIdA, proxyIdB);

      // Here we destroy contacts that cease to overlap in the broad-phase.
      if (overlap == false) {
        this.destroyContact(c);
        continue;
      }

      // The contact persists.
      c.update(this);
    }
  }

  /** @internal */
  destroyContact(contact: Contact): void {
    // Remove from the world.
    if (contact.m_prev) {
      contact.m_prev.m_next = contact.m_next;
    }
    if (contact.m_next) {
      contact.m_next.m_prev = contact.m_prev;
    }
    if (contact == this.m_contactList) {
      this.m_contactList = contact.m_next;
    }

    Contact.destroy(contact, this);

    --this.m_contactCount;
  }

  /**
   * Called when two fixtures begin to touch.
   *
   * Implement contact callbacks to get contact information. You can use these
   * results for things like sounds and game logic. You can also get contact
   * results by traversing the contact lists after the time step. However, you
   * might miss some contacts because continuous physics leads to sub-stepping.
   * Additionally you may receive multiple callbacks for the same contact in a
   * single time step. You should strive to make your callbacks efficient because
   * there may be many callbacks per time step.
   *
   * Warning: You cannot create/destroy world entities inside these callbacks.
   */
  on(name: "begin-contact", listener: (contact: Contact) => void): World;
  /**
   * Called when two fixtures cease to touch.
   *
   * Implement contact callbacks to get contact information. You can use these
   * results for things like sounds and game logic. You can also get contact
   * results by traversing the contact lists after the time step. However, you
   * might miss some contacts because continuous physics leads to sub-stepping.
   * Additionally you may receive multiple callbacks for the same contact in a
   * single time step. You should strive to make your callbacks efficient because
   * there may be many callbacks per time step.
   *
   * Warning: You cannot create/destroy world entities inside these callbacks.
   */
  on(name: "end-contact", listener: (contact: Contact) => void): World;
  /**
   * This is called after a contact is updated. This allows you to inspect a
   * contact before it goes to the solver. If you are careful, you can modify the
   * contact manifold (e.g. disable contact). A copy of the old manifold is
   * provided so that you can detect changes. Note: this is called only for awake
   * bodies. Note: this is called even when the number of contact points is zero.
   * Note: this is not called for sensors. Note: if you set the number of contact
   * points to zero, you will not get an end-contact callback. However, you may get
   * a begin-contact callback the next step.
   *
   * Warning: You cannot create/destroy world entities inside these callbacks.
   */
  on(name: "pre-solve", listener: (contact: Contact, oldManifold: Manifold) => void): World;
  /**
   * This lets you inspect a contact after the solver is finished. This is useful
   * for inspecting impulses. Note: the contact manifold does not include time of
   * impact impulses, which can be arbitrarily large if the sub-step is small.
   * Hence the impulse is provided explicitly in a separate data structure. Note:
   * this is only called for contacts that are touching, solid, and awake.
   *
   * Warning: You cannot create/destroy world entities inside these callbacks.
   */
  on(name: "post-solve", listener: (contact: Contact, impulse: ContactImpulse) => void): World;
  /** Listener is called when a body is removed. */
  on(name: "remove-body", listener: (body: Body) => void): World;
  /** Listener is called when a joint is removed implicitly or explicitly. */
  on(name: "remove-joint", listener: (joint: Joint) => void): World;
  /** Listener is called when a fixture is removed implicitly or explicitly. */
  on(name: "remove-fixture", listener: (fixture: Fixture) => void): World;
  /** Listener is called when a body is added. */
  on(name: "add-body", listener: (body: Body) => void): World;
  /** Listener is called when a joint is added. */
  on(name: "add-joint", listener: (joint: Joint) => void): World;
  /** Listener is called when a fixture is added. */
  on(name: "add-fixture", listener: (fixture: Fixture) => void): World;
  /**
   * Register an event listener.
   */
  // tslint:disable-next-line:typedef
  on(name, listener) {
    if (typeof name !== "string" || typeof listener !== "function") {
      return this;
    }
    if (!this._listeners) {
      this._listeners = {};
    }
    if (!this._listeners[name]) {
      this._listeners[name] = [];
    }
    this._listeners[name].push(listener);
    return this;
  }

  off(name: "begin-contact", listener: (contact: Contact) => void): World;
  off(name: "end-contact", listener: (contact: Contact) => void): World;
  off(name: "pre-solve", listener: (contact: Contact, oldManifold: Manifold) => void): World;
  off(name: "post-solve", listener: (contact: Contact, impulse: ContactImpulse) => void): World;
  off(name: "remove-body", listener: (body: Body) => void): World;
  off(name: "remove-joint", listener: (joint: Joint) => void): World;
  off(name: "remove-fixture", listener: (fixture: Fixture) => void): World;
  off(name: "add-body", listener: (body: Body) => void): World;
  off(name: "add-joint", listener: (joint: Joint) => void): World;
  off(name: "add-fixture", listener: (fixture: Fixture) => void): World;
  /**
   * Remove an event listener.
   */
  // tslint:disable-next-line:typedef
  off(name, listener) {
    if (typeof name !== "string" || typeof listener !== "function") {
      return this;
    }
    const listeners = this._listeners && this._listeners[name];
    if (!listeners || !listeners.length) {
      return this;
    }
    const index = listeners.indexOf(listener);
    if (index >= 0) {
      listeners.splice(index, 1);
    }
    return this;
  }

  publish(name: string, arg1?: any, arg2?: any, arg3?: any): number {
    const listeners = this._listeners && this._listeners[name];
    if (!listeners || !listeners.length) {
      return 0;
    }
    for (let l = 0; l < listeners.length; l++) {
      listeners[l].call(this, arg1, arg2, arg3);
    }
    return listeners.length;
  }

  /** @internal */
  beginContact(contact: Contact): void {
    this.publish("begin-contact", contact);
  }

  /** @internal */
  endContact(contact: Contact): void {
    this.publish("end-contact", contact);
  }

  /** @internal */
  preSolve(contact: Contact, oldManifold: Manifold): void {
    this.publish("pre-solve", contact, oldManifold);
  }

  /** @internal */
  postSolve(contact: Contact, impulse: ContactImpulse): void {
    this.publish("post-solve", contact, impulse);
  }

  /**
   * Joints and fixtures are destroyed when their associated body is destroyed.
   * Register a destruction listener so that you may nullify references to these
   * joints and shapes.
   *
   * `function(object)` is called when any joint or fixture is about to
   * be destroyed due to the destruction of one of its attached or parent bodies.
   */

  /**
   * Register a contact filter to provide specific control over collision.
   * Otherwise the default filter is used (defaultFilter). The listener is owned
   * by you and must remain in scope.
   *
   * Moved to Fixture.
   */
}
