import * as RE from 'rogue-engine';
import * as CANNON from 'cannon-es';
import * as THREE from 'three';
import * as RogueCannon from '../Lib/RogueCannon';
import CannonBody from './CannonBody.re';

export default class CannonConfig extends RE.Component {
  private _defaultFriction = 0.01;
  private _defaultRestitution = 0;

  @RE.props.num() maxSubSteps: number = 1;

  @RE.props.num()
  get defaultFriction() {
    return this._defaultFriction;
  }

  set defaultFriction(value: number) {
    this._defaultFriction = value;
    RogueCannon.getWorld().defaultContactMaterial.friction = value;
  }

  @RE.props.num() 
  get defaultRestitution() {
    return this._defaultRestitution;
  }

  set defaultRestitution(value: number) {
    this._defaultRestitution = value;
    RogueCannon.getWorld().defaultContactMaterial.restitution = value;
  }

  @RE.props.vector3() gravity: THREE.Vector3 = new THREE.Vector3(0, -9.82, 0);

  private contacts: CANNON.ContactEquation[] = [];
  // At the index of the body.id we hold the CannonBody with which body is colliding.
  // private activeCollisions: (CANNON.ContactEquation[])[] = []; 
  private activeCollisions: {[id: string]: boolean} = {};

  start() {
    this.contacts = [];
    this.activeCollisions = {};

    CANNON.Body.idCounter = 0;
    CANNON.ContactEquation.idCounter = 0;
    CANNON.Shape.idCounter = 0;
    CANNON.Equation.idCounter = 0;

    const world = new CANNON.World()
    world.nextId = 0;

    RogueCannon.setWorld(world);
    RogueCannon.getWorld().gravity.set(this.gravity.x, this.gravity.y, this.gravity.z);
    RogueCannon.getWorld().broadphase = new CANNON.NaiveBroadphase();
    RogueCannon.getWorld().defaultContactMaterial.friction = this.defaultFriction;
    RogueCannon.getWorld().defaultContactMaterial.restitution = this.defaultRestitution;
  }

  beforeUpdate() {
    RogueCannon.getWorld().step(RE.Runtime.deltaTime, RE.Runtime.deltaTime, this.maxSubSteps || 1);
  }

  afterUpdate() {
    this.checkCollisions();
  }

  checkCollisions() {
    const contacts = RogueCannon.getWorld().contacts;

    const newContacts: CANNON.ContactEquation[] = [];

    contacts.forEach(contact => {
      newContacts[contact.id] = contact;

      const key1 = contact.bi.id + "_" + contact.bj.id;
      const key2 = contact.bj.id + "_" + contact.bi.id;

      const key1Collision = this.activeCollisions[key1];
      const key2Collision = this.activeCollisions[key2];

      if (!this.contacts[contact.id] && !key1Collision && !key2Collision) {
        this.activeCollisions[key1] = true;
        this.activeCollisions[key2] = true;
        this.sendNewCollisionEvents(contact);
      }
    });

    this.contacts.forEach(contact => {

      const key1 = contact.bi.id + "_" + contact.bj.id;
      const key2 = contact.bj.id + "_" + contact.bi.id;

      const foundActiveCollision = this.findActiveCollision(newContacts, contact);

      const newContact = newContacts[contact.id];

      if ((newContact && newContact.enabled) || foundActiveCollision) {
        this.sendOnCollisionStayEvents(contact);
      }

      if ((!newContact || (newContact && !newContact.enabled)) && !foundActiveCollision) {
        this.activeCollisions[key1] = false;
        this.activeCollisions[key2] = false;
        this.sendOnCollisionExitEvents(contact);
      }
    });

    this.contacts = newContacts;
  }

  private findActiveCollision(contactList: CANNON.ContactEquation[], sample: CANNON.ContactEquation) {
    return contactList.find(contact => {
      if (!contact || !contact.enabled) return false;
      if (contact.bi === sample.bi && contact.bj === sample.bj) return true;
      if (contact.bi === sample.bj && contact.bj === sample.bi) return true;

      return false;
    });
  }

  private sendOnCollisionStayEvents(contact: CANNON.ContactEquation) {
    const bodyA = CannonBody.findByBody(contact.bi);
    const bodyB = CannonBody.findByBody(contact.bj);

    if (!bodyB || !bodyA) return;

    bodyA.onCollisionStayListeners.forEach(cb => {
      cb({other: bodyB, contact});
    });

    bodyB.onCollisionStayListeners.forEach(cb => {
      cb({other: bodyA, contact});
    });
  }

  private sendOnCollisionExitEvents(contact: CANNON.ContactEquation) {
    const bodyA = CannonBody.findByBody(contact.bi);
    const bodyB = CannonBody.findByBody(contact.bj);

    if (!bodyB || !bodyA) return;

    bodyA.onCollisionExitListeners.forEach(cb => {
      cb({other: bodyB, contact});
    });

    bodyB.onCollisionExitListeners.forEach(cb => {
      cb({other: bodyA, contact});
    });
  }

  private sendNewCollisionEvents(contact: CANNON.ContactEquation) {
    const bodyA = CannonBody.findByBody(contact.bi);
    const bodyB = CannonBody.findByBody(contact.bj);

    if (!bodyB || !bodyA) return;

    bodyA.onCollisionEnterListeners.forEach(cb => {
      cb({other: bodyB, contact});
    });

    bodyB.onCollisionEnterListeners.forEach(cb => {
      cb({other: bodyA, contact});
    });
  }
}

RE.registerComponent( CannonConfig );
