import * as THREE from "three";
// @ts-ignore
import * as CANNON from "cannon";
import * as _ from "lodash";
import * as Utils from "../core/FunctionLibrary";
import { KeyBinding } from "../core/KeyBinding";
import { VectorSpringSimulator } from "../physics/spring_simulation/VectorSpringSimulator";
import { RelativeSpringSimulator } from "../physics/spring_simulation/RelativeSpringSimulator";
import { Idle } from "./character_states/Idle";
import { ICharacterAI } from "../interfaces/ICharacterAI";
import { World } from "../world/World";
import { ICharacterState } from "../interfaces/ICharacterState";
import { IWorldEntity } from "../interfaces/IWorldEntity";
import { Item } from "../items/Item";
import { CollisionGroups } from "../enums/CollisionGroups";
import { CapsuleCollider } from "../physics/colliders/CapsuleCollider";
import { GroundImpactData } from "./GroundImpactData";
import { ClosestObjectFinder } from "../core/ClosestObjectFinder";
import { Bullet } from "./Bullet";
import { EntityType } from "../enums/EntityType";
import { EventType } from "../enums/EventType";
import { ModelType } from "../enums/WorldType";

export class Character extends THREE.Object3D implements IWorldEntity {
  public isHit: boolean = false;
  public updateOrder: number = 1;
  public entityType: EntityType = EntityType.Character;

  public height: number = 0;
  public tiltContainer: THREE.Group;
  public modelContainer: THREE.Group;
  public materials: THREE.Material[] = [];
  public mixer: THREE.AnimationMixer;
  public animations: any[];

  // Movement
  public acceleration: THREE.Vector3 = new THREE.Vector3();
  public velocity: THREE.Vector3 = new THREE.Vector3();
  public arcadeVelocityInfluence: THREE.Vector3 = new THREE.Vector3();
  public velocityTarget: THREE.Vector3 = new THREE.Vector3();
  public arcadeVelocityIsAdditive: boolean = false;

  public defaultVelocitySimulatorDamping: number = 0.8;
  public defaultVelocitySimulatorMass: number = 50;
  public velocitySimulator: VectorSpringSimulator;
  public moveSpeed: number = 4;
  public angularVelocity: number = 0;
  public orientation: THREE.Vector3 = new THREE.Vector3(0, 0, 1);
  public orientationTarget: THREE.Vector3 = new THREE.Vector3(0, 0, 1);
  public defaultRotationSimulatorDamping: number = 0.5;
  public defaultRotationSimulatorMass: number = 10;
  public rotationSimulator: RelativeSpringSimulator;
  public viewVector: THREE.Vector3;
  public actions: { [action: string]: KeyBinding };
  public characterCapsule: CapsuleCollider;

  // Ray casting
  public rayResult: CANNON.RaycastResult = new CANNON.RaycastResult();
  public rayHasHit: boolean = false;
  public rayCastLength: number = 0.57;
  public raySafeOffset: number = 0.03;
  public wantsToJump: boolean = false;
  public initJumpSpeed: number = -1;
  public groundImpactData: GroundImpactData = new GroundImpactData();
  public world: World;
  public charState: ICharacterState;
  public behaviour: ICharacterAI;

  private physicsEnabled: boolean = true;

  public shootDirection: THREE.Vector3 = new THREE.Vector3();
  public shootVelo: number = 15;
  public raycaster = new THREE.Raycaster();
  public type = ModelType.PLAYER;
  private gltf: any = null;
  private bulletTimePassed: boolean = true;

  constructor(gltf: any, texture: string, type: ModelType, name: string) {
    super();
    this.type = type;
    this.gltf = gltf;
    this.readCharacterData(gltf, texture);
    this.setAnimations(gltf.animations);

    // The visuals group is centered for easy character tilting
    this.tiltContainer = new THREE.Group();
    this.add(this.tiltContainer);

    // Model container is used to reliably ground the character, as animation can alter the position of the model itself
    this.modelContainer = new THREE.Group();
    this.modelContainer.position.y = -0.57;
    this.tiltContainer.add(this.modelContainer);
    this.modelContainer.add(gltf.scene);

    this.mixer = new THREE.AnimationMixer(gltf.scene);

    this.velocitySimulator = new VectorSpringSimulator(
      60,
      this.defaultVelocitySimulatorMass,
      this.defaultVelocitySimulatorDamping
    );
    this.rotationSimulator = new RelativeSpringSimulator(
      60,
      this.defaultRotationSimulatorMass,
      this.defaultRotationSimulatorDamping
    );

    this.viewVector = new THREE.Vector3();

    // Actions
    this.actions = {
      pick: new KeyBinding("KeyE"),
      respawn: new KeyBinding("KeyR"),
      up: new KeyBinding("KeyW"),
      down: new KeyBinding("KeyS"),
      left: new KeyBinding("KeyA"),
      right: new KeyBinding("KeyD"),
      run: new KeyBinding("ShiftLeft"),
      jump: new KeyBinding("Space"),
      primary: new KeyBinding("Mouse0"),
      secondary: new KeyBinding("Mouse1"),
    };

    const radius = this.type === ModelType.ENEMY ? 0.35 : 0.2;
    // Physics
    // Player Capsule
    this.characterCapsule = new CapsuleCollider({
      mass: 1,
			position: new CANNON.Vec3(),
			height: 0.4,
			radius,
			segments: 3,
			friction: 0.0,
      name
    });
    // capsulePhysics.physical.collisionFilterMask = ~CollisionGroups.Trimesh;
    this.characterCapsule.body.shapes.forEach((shape) => {
      // tslint:disable-next-line: no-bitwise
      shape.collisionFilterMask = ~CollisionGroups.TrimeshColliders;
    });
    this.characterCapsule.body.allowSleep = false;
    (this.characterCapsule.body as any).userData = {
      type,
      name,
      isHit: false
    };

    // Move character to different collision group for raycasting
    this.characterCapsule.body.collisionFilterGroup = 2;

    // Disable character rotation
    this.characterCapsule.body.fixedRotation = true;
    this.characterCapsule.body.updateMassProperties();
 
    // Physics pre/post step callback bindings
    this.characterCapsule.body.preStep = (body: CANNON.Body) => {
      this.physicsPreStep(body, this);
    };
    this.characterCapsule.body.postStep = (body: CANNON.Body) => {
      this.physicsPostStep(body, this);
    };

    // States
    this.setState(new Idle(this));
  }

  public setAnimations(animations: []): void {
    this.animations = animations;
  }

  public setArcadeVelocityInfluence(
    x: number,
    y: number = x,
    z: number = x
  ): void {
    this.arcadeVelocityInfluence.set(x, y, z);
  }

  public setViewVector(vector: THREE.Vector3): void {
    this.viewVector.copy(vector).normalize();
  }

  /**
   * Set state to the player. Pass state class (function) name.
   * @param {function} State
   */
  public setState(state: ICharacterState): void {
    this.charState = state;
    this.charState.onInputChange();
  }

  public setPosition(x: number, y: number, z: number): void {
    if (this.physicsEnabled) {
      this.characterCapsule.body.previousPosition = new CANNON.Vec3(x, y, z);
      this.characterCapsule.body.position = new CANNON.Vec3(x, y, z);
      this.characterCapsule.body.interpolatedPosition = new CANNON.Vec3(
        x,
        y,
        z
      );
    } else {
      this.position.x = x;
      this.position.y = y;
      this.position.z = z;
    }
  }

  public resetVelocity(): void {
    this.velocity.x = 0;
    this.velocity.y = 0;
    this.velocity.z = 0;

    this.characterCapsule.body.velocity.x = 0;
    this.characterCapsule.body.velocity.y = 0;
    this.characterCapsule.body.velocity.z = 0;

    this.velocitySimulator.init();
  }

  public setArcadeVelocityTarget(
    velZ: number,
    velX: number = 0,
    velY: number = 0
  ): void {
    this.velocityTarget.z = velZ;
    this.velocityTarget.x = velX;
    this.velocityTarget.y = velY;
  }

  public setOrientation(
    vector: THREE.Vector3,
    instantly: boolean = false
  ): void {
    let lookVector = new THREE.Vector3().copy(vector).setY(0).normalize();
    this.orientationTarget.copy(lookVector);

    if (instantly) {
      this.orientation.copy(lookVector);
    }
  }

  public resetOrientation(): void {
    const forward = Utils.getForward(this);
    this.setOrientation(forward, true);
  }

  public setBehaviour(behaviour: ICharacterAI): void {
    behaviour.character = this;
    this.behaviour = behaviour;
  }

  public setPhysicsEnabled(value: boolean): void {
    this.physicsEnabled = value;

    if (value === true) {
      this.world.physicsWorld.addBody(this.characterCapsule.body);
    } else {
      this.world.physicsWorld.remove(this.characterCapsule.body);
    }
  }

  public readCharacterData(gltf: any, textureUrl: string): void {
    const image = new Image();
    image.crossOrigin = "Anonymous";
    const texture = new THREE.Texture(image);
    image.onload = () => {
      texture.needsUpdate = true;
    };
    image.src = textureUrl;
    texture.flipY = false;
    const material = new THREE.MeshPhongMaterial({
      shininess: 0,
      map: texture,
    });
    //material.map.encoding = THREE.sRGBEncoding;
    material.skinning = true;
    material.map.flipY = false;
    gltf.scene.traverse((child) => {
      if (child.isMesh) {
        //Utils.setupMeshProperties(child);
        if (child.material.isGLTFSpecularGlossinessMaterial) {
          child.onBeforeRender = function () {};
        }
        child.material = material;
        child.castShadow = true;
        child.receiveShadow = true;
        this.materials.push(material);
      }
    });
  }

  public handleKeyboardEvent(
    event: KeyboardEvent,
    code: string,
    pressed: boolean
  ): void {
    // Free camera
    if (code === "KeyC" && pressed === true && event.shiftKey === true) {
      this.resetControls();
      this.world.cameraOperator.characterCaller = this;
      this.world.inputManager.setInputReceiver(this.world.cameraOperator);
    } else if (code === "KeyR" && pressed === true) {
      this.world.outOfBoundsRespawn(this.characterCapsule.body);
    } else {
      for (const action in this.actions) {
        if (this.actions.hasOwnProperty(action)) {
          const binding = this.actions[action];

          if (_.includes(binding.eventCodes, code)) {
            this.triggerAction(action, pressed);
          }
        }
      }
    }
  }

  public handleMouseButton(
    event: MouseEvent,
    code: string,
    pressed: boolean
  ): void {
    if (code === "mouse0" && pressed === true && this.bulletTimePassed) {
      // Shoot balls
      const bullet = new Bullet();
      this.world.add(bullet);
      let x = this.position.x;
      let y = this.position.y;
      let z = this.position.z;
      const cameraDirection = this.getCameraRelativeVector();
      this.shootDirection.copy(cameraDirection);
      this.setOrientation(cameraDirection, true);
      bullet.bulletBox.body.velocity.set(
        this.shootDirection.x * this.shootVelo,
        this.shootDirection.y * this.shootVelo,
        this.shootDirection.z * this.shootVelo
      );

      // Move the ball outside the player sphere
      x +=
        this.shootDirection.x *
        (this.characterCapsule.options.radius +
          bullet.bulletBox.options.radius);
      y += this.shootDirection.y;
      z +=
        this.shootDirection.z *
        (this.characterCapsule.options.radius +
          bullet.bulletBox.options.radius);
      bullet.bulletBox.body.position.set(x, y, z);
      const that = this;
      this.bulletTimePassed = false;
      const timeout = setTimeout(() => {
        that.bulletTimePassed = true;
        clearTimeout(timeout);
      }, 1000);
    } else {
      for (const action in this.actions) {
        if (this.actions.hasOwnProperty(action)) {
          const binding = this.actions[action];

          if (_.includes(binding.eventCodes, code)) {
            this.triggerAction(action, pressed);
          }
        }
      }
    }
  }

  public handleMouseMove(
    event: MouseEvent,
    deltaX: number,
    deltaY: number
  ): void {
    this.world.cameraOperator.move(deltaX, deltaY);
  }

  public handleMouseWheel(event: WheelEvent, value: number): void {
    //this.world.scrollTheTimeScale(value);
  }

  public triggerAction(actionName: string, value: boolean): void {
    // Get action and set it's parameters
    let action = this.actions[actionName];
    if (action && action.isPressed !== value) {
      // Set value
      action.isPressed = value;

      // Reset the 'just' attributes
      action.justPressed = false;
      action.justReleased = false;

      // Set the 'just' attributes
      if (value) action.justPressed = true;
      else action.justReleased = true;

      // Tell player to handle states according to new input
      this.charState.onInputChange();

      // Reset the 'just' attributes
      action.justPressed = false;
      action.justReleased = false;
    }
  }

  public takeControl(): void {
    if (this.world !== undefined) {
      this.world.inputManager.setInputReceiver(this);
    } else {
      console.warn(
        "Attempting to take control of a character that doesn't belong to a world."
      );
    }
  }

  public resetControls(): void {
    for (const action in this.actions) {
      if (this.actions.hasOwnProperty(action)) {
        this.triggerAction(action, false);
      }
    }
  }

  public update(timeStep: number): void {
      if (this.behaviour) this.behaviour?.update(timeStep);
      if(this.charState) this.charState?.update(timeStep);
      if (this.physicsEnabled) this.springMovement(timeStep);
      if (this.physicsEnabled) this.springRotation(timeStep);
      if (this.physicsEnabled) this.rotateModel();
      if (this.mixer !== undefined) this.mixer.update(timeStep);

      // Sync physics/graphics
      if (this.physicsEnabled) {
        this.position.set(
          this.characterCapsule.body.interpolatedPosition.x,
          this.characterCapsule.body.interpolatedPosition.y,
          this.characterCapsule.body.interpolatedPosition.z
        );
      } else if (this.characterCapsule) {
        let newPos = new THREE.Vector3();
        this.getWorldPosition(newPos);

        this.characterCapsule.body.position.copy(Utils.cannonVector(newPos));
        this.characterCapsule.body.interpolatedPosition.copy(
          Utils.cannonVector(newPos)
        );
      }

      this.updateMatrixWorld();
  }

  public inputReceiverInit(): void {
    this.world.cameraOperator.setRadius(1.6, true);
    this.world.cameraOperator.followMode = false;
    // this.world.dirLight.target = this;
  }

  public inputReceiverUpdate(timeStep: number): void {
    // Look in camera's direction
    this.viewVector = new THREE.Vector3().subVectors(
      this.position,
      this.world.camera.position
    );
    this.getWorldPosition(this.world.cameraOperator.target);
  }

  public setAnimation(clipName: string, fadeIn: number): number {
    if (this.mixer !== undefined) {
      // gltf
      let clip = THREE.AnimationClip.findByName(this.animations, clipName);
      let action = this.mixer.clipAction(clip);
      if (action === null) {
        //console.error(`Animation ${clipName} not found!`);
        return 0;
      }
      action.setEffectiveWeight(1);
      action.enabled = true;

      this.mixer.stopAllAction();
      action.fadeIn(fadeIn);
      action.play();

      return action.getClip().duration;
    }
  }

  public springMovement(timeStep: number): void {
    // Simulator
    this.velocitySimulator.target.copy(this.velocityTarget);
    this.velocitySimulator.simulate(timeStep);

    // Update values
    this.velocity.copy(this.velocitySimulator.position);
    this.acceleration.copy(this.velocitySimulator.velocity);
  }

  public springRotation(timeStep: number): void {
    // Spring rotation
    // Figure out angle between current and target orientation
    let angle = Utils.getSignedAngleBetweenVectors(
      this.orientation,
      this.orientationTarget
    );

    // Simulator
    this.rotationSimulator.target = angle;
    this.rotationSimulator.simulate(timeStep);
    let rot = this.rotationSimulator.position;

    // Updating values
    this.orientation.applyAxisAngle(new THREE.Vector3(0, 1, 0), rot);
    this.angularVelocity = this.rotationSimulator.velocity;
  }

  public getLocalMovementDirection(): THREE.Vector3 {
    const positiveX = this.actions.right.isPressed ? -1 : 0;
    const negativeX = this.actions.left.isPressed ? 1 : 0;
    const positiveZ = this.actions.up.isPressed ? 1 : 0;
    const negativeZ = this.actions.down.isPressed ? -1 : 0;

    return new THREE.Vector3(
      positiveX + negativeX,
      0,
      positiveZ + negativeZ
    ).normalize();
  }

  public getCameraRelativeMovementVector(): THREE.Vector3 {
    const localDirection = this.getLocalMovementDirection();
    const flatViewVector = new THREE.Vector3(
      this.viewVector.x,
      0,
      this.viewVector.z
    ).normalize();

    return Utils.appplyVectorMatrixXZ(flatViewVector, localDirection);
  }

  public getCameraRelativeVector(): THREE.Vector3 {
    const localDirection = new THREE.Vector3(0, 0, 1).normalize();
    const flatViewVector = new THREE.Vector3(
      this.viewVector.x,
      0,
      this.viewVector.z
    ).normalize();

    return Utils.appplyVectorMatrixXZ(flatViewVector, localDirection);
  }

  public setCameraRelativeOrientationTarget(): void {
    let moveVector = this.getCameraRelativeMovementVector();

    if (moveVector.x === 0 && moveVector.y === 0 && moveVector.z === 0) {
      this.setOrientation(this.orientation);
    } else {
      this.setOrientation(moveVector);
    }
  }

  public rotateModel(): void {
    this.lookAt(
      this.position.x + this.orientation.x,
      this.position.y + this.orientation.y,
      this.position.z + this.orientation.z
    );
    this.tiltContainer.rotation.z =
      -this.angularVelocity * 2.3 * this.velocity.length();
    this.tiltContainer.position.setY(
      Math.cos(Math.abs(this.angularVelocity * 2.3 * this.velocity.length())) /
        2 -
        0.5
    );
  }

  public jump(initJumpSpeed: number = -1): void {
    this.wantsToJump = true;
    this.initJumpSpeed = initJumpSpeed;
  }

  public findItemToPick(): void {
    let itemFinder = new ClosestObjectFinder<Item>(this.position, 1);
    if (this.world) {
      this.world.items.forEach((item) => {
        itemFinder.consider(item, item.position);
      });

      if (itemFinder.closestObject !== undefined) {
        let item = itemFinder.closestObject;
        if (!item.isPicked) {
          item.isPicked = true;
          item.removeFromWorld(this.world);
          this.world.triggerEvent({
            type: EventType.ITEM,
            targets: [],
          });
        }
      }
    }
  }

  public physicsPreStep(body: CANNON.Body, character: Character): void {
    character.feetRaycast();
  }

  public feetRaycast(): void {
    // Player ray casting
    // Create ray
    let body = this.characterCapsule.body;
    const start = new CANNON.Vec3(
      body.position.x,
      body.position.y,
      body.position.z
    );
    const end = new CANNON.Vec3(
      body.position.x,
      body.position.y - this.rayCastLength - this.raySafeOffset,
      body.position.z
    );
    // Raycast options
    const rayCastOptions = {
      collisionFilterMask: CollisionGroups.Default,
      skipBackfaces: true /* ignore back faces */,
    };
    // Cast the ray
    this.rayHasHit = this.world.physicsWorld.raycastClosest(
      start,
      end,
      rayCastOptions,
      this.rayResult
    );
  }

  public physicsPostStep(body: CANNON.Body, character: Character): void {
    // Get velocities
    let simulatedVelocity = new THREE.Vector3(
      body.velocity.x,
      body.velocity.y,
      body.velocity.z
    );

    // Take local velocity
    let arcadeVelocity = new THREE.Vector3()
      .copy(character.velocity)
      .multiplyScalar(character.moveSpeed);
    // Turn local into global
    arcadeVelocity = Utils.appplyVectorMatrixXZ(
      character.orientation,
      arcadeVelocity
    );

    let newVelocity = new THREE.Vector3();

    // Additive velocity mode
    if (character.arcadeVelocityIsAdditive) {
      newVelocity.copy(simulatedVelocity);

      let globalVelocityTarget = Utils.appplyVectorMatrixXZ(
        character.orientation,
        character.velocityTarget
      );
      let add = new THREE.Vector3()
        .copy(arcadeVelocity)
        .multiply(character.arcadeVelocityInfluence);

      if (
        Math.abs(simulatedVelocity.x) <
          Math.abs(globalVelocityTarget.x * character.moveSpeed) ||
        Utils.haveDifferentSigns(simulatedVelocity.x, arcadeVelocity.x)
      ) {
        newVelocity.x += add.x;
      }
      if (
        Math.abs(simulatedVelocity.y) <
          Math.abs(globalVelocityTarget.y * character.moveSpeed) ||
        Utils.haveDifferentSigns(simulatedVelocity.y, arcadeVelocity.y)
      ) {
        newVelocity.y += add.y;
      }
      if (
        Math.abs(simulatedVelocity.z) <
          Math.abs(globalVelocityTarget.z * character.moveSpeed) ||
        Utils.haveDifferentSigns(simulatedVelocity.z, arcadeVelocity.z)
      ) {
        newVelocity.z += add.z;
      }
    } else {
      newVelocity = new THREE.Vector3(
        THREE.MathUtils.lerp(
          simulatedVelocity.x,
          arcadeVelocity.x,
          character.arcadeVelocityInfluence.x
        ),
        THREE.MathUtils.lerp(
          simulatedVelocity.y,
          arcadeVelocity.y,
          character.arcadeVelocityInfluence.y
        ),
        THREE.MathUtils.lerp(
          simulatedVelocity.z,
          arcadeVelocity.z,
          character.arcadeVelocityInfluence.z
        )
      );
    }

    // If we're hitting the ground, stick to ground
    if (character.rayHasHit) {
      // Flatten velocity
      newVelocity.y = 0;

      // Move on top of moving objects
      if (character.rayResult.body.mass > 0) {
        let pointVelocity = new CANNON.Vec3();
        character.rayResult.body.getVelocityAtWorldPoint(
          character.rayResult.hitPointWorld,
          pointVelocity
        );
        newVelocity.add(Utils.threeVector(pointVelocity));
      }

      // Measure the normal vector offset from direct "up" vector
      // and transform it into a matrix
      let up = new THREE.Vector3(0, 1, 0);
      let normal = new THREE.Vector3(
        character.rayResult.hitNormalWorld.x,
        character.rayResult.hitNormalWorld.y,
        character.rayResult.hitNormalWorld.z
      );
      let q = new THREE.Quaternion().setFromUnitVectors(up, normal);
      let m = new THREE.Matrix4().makeRotationFromQuaternion(q);

      // Rotate the velocity vector
      newVelocity.applyMatrix4(m);

      // Compensate for gravity
      // newVelocity.y -= body.world.physicsWorld.gravity.y / body.character.world.physicsFrameRate;

      // Apply velocity
      body.velocity.x = newVelocity.x;
      body.velocity.y = newVelocity.y;
      body.velocity.z = newVelocity.z;
      // Ground character
      body.position.y =
        character.rayResult.hitPointWorld.y +
        character.rayCastLength +
        newVelocity.y / character.world.physicsFrameRate;
    } else {
      // If we're in air
      body.velocity.x = newVelocity.x;
      body.velocity.y = newVelocity.y;
      body.velocity.z = newVelocity.z;

      // Save last in-air information
      character.groundImpactData.velocity.x = body.velocity.x;
      character.groundImpactData.velocity.y = body.velocity.y;
      character.groundImpactData.velocity.z = body.velocity.z;
    }

    // Jumping
    if (character.wantsToJump) {
      // If initJumpSpeed is set
      if (character.initJumpSpeed > -1) {
        // Flatten velocity
        body.velocity.y = 0;
        let speed = Math.max(
          character.velocitySimulator.position.length() * 4,
          character.initJumpSpeed
        );
        body.velocity = Utils.cannonVector(
          character.orientation.clone().multiplyScalar(speed)
        );
      } else {
        // Moving objects compensation
        let add = new CANNON.Vec3();
        character.rayResult.body.getVelocityAtWorldPoint(
          character.rayResult.hitPointWorld,
          add
        );
        body.velocity.vsub(add, body.velocity);
      }

      // Add positive vertical velocity
      body.velocity.y += 4;
      // Move above ground by 2x safe offset value
      body.position.y += character.raySafeOffset * 2;
      // Reset flag
      character.wantsToJump = false;
    }
  }

  public collideListener(colider: any) {
    //console.log('Character colider: ', colider.body.characterType);
    const that = this;
    //console.log('Character colider 2: ', colider.body.characterType);
    if (colider.body?.userData?.type === ModelType.ENEMY) {
      // TODO: should work once I put isHit into userData
      const enemyIsHit = colider.body?.userData?.isHit;
      //console.log('Character Enemy found')
      setTimeout(function() {
        if (that && that.world && that.characterCapsule && !that.isHit && !enemyIsHit) {
          that.isHit = true;
          that.characterCapsule.body.removeEventListener("collide", that.collideListener);
          // Player dies
          that.world.triggerEvent({
            type: EventType.PLAYER,
            targets: [],
          });
          that.world.remove(that);
        }
      }, 0);
    }
  }

  public addToWorld(world: World): void {
    if (_.includes(world.characters, this)) {
      console.warn("Adding character to a world in which it already exists.");
    } else {
      // Set world
      this.world = world;
      // Register character
      world.characters.push(this);

      // Register for bullet collision
      world.characterNames[(this.characterCapsule.body as any).userData.name] = this;
      // Register physics
      world.physicsWorld.addBody(this.characterCapsule.body);
      // Add to graphicsWorld
      world.graphicsWorld.add(this);
      if (this.type === ModelType.PLAYER) {
        this.characterCapsule.body.addEventListener("collide", this.collideListener.bind(this));
      }
      // Shadow cascades
      this.materials.forEach((mat) => {
        world.sky.csm.setupMaterial(mat);
      });
    }
  }

  public removeFromWorld(world: World): void {
    // TODO: dealocate memory properly https://stackoverflow.com/questions/20997669/memory-leak-in-three-js
  
    if (!_.includes(world.characters, this)) {
      console.warn(
        "Removing character from a world in which it isn't present."
      );
    } else {
      if (world.inputManager.inputReceiver === this) {
        world.inputManager.inputReceiver = undefined;
      }

      this.world = undefined;
      // Remove from characters
      _.pull(world.characters, this);
      // Remove physics
      world.physicsWorld.remove(this.characterCapsule.body);
      // Remove visuals
      world.graphicsWorld.remove(this);

      this.modelContainer.remove(this.gltf.scene);
      this.tiltContainer.remove(this.modelContainer);
      this.remove(this.tiltContainer);
      this.materials.forEach(material => material.dispose());
      if (this.behaviour) {
        this.behaviour.character = undefined;
        this.behaviour = undefined;
      }

      this.materials = undefined;
      this.modelContainer = undefined;
      this.tiltContainer = undefined;
      this.mixer = undefined;
      this.velocitySimulator = undefined;
      this.rotationSimulator = undefined;
      this.viewVector = undefined;
      this.characterCapsule = undefined;
      this.actions = undefined;

      this.isHit = undefined;
      this.updateOrder = undefined;
      this.entityType = undefined;
      this.height = undefined;
      this.animations = undefined;

      this.acceleration = undefined;
      this.velocity = undefined;
      this.arcadeVelocityInfluence = undefined;
      this.velocityTarget = undefined;
      this.arcadeVelocityIsAdditive = undefined;

      this.defaultVelocitySimulatorDamping = undefined;
      this.defaultVelocitySimulatorMass = undefined;
      this.moveSpeed = undefined;
      this.angularVelocity = undefined;
      this.orientation = undefined;
      this.orientationTarget = undefined;
      this.defaultRotationSimulatorDamping = undefined;
      this.defaultRotationSimulatorMass = undefined;

      this.rayResult = undefined;
      this.rayHasHit = undefined;
      this.rayCastLength = undefined;
      this.raySafeOffset = undefined;
      this.wantsToJump = undefined;
      this.initJumpSpeed = undefined;
      this.groundImpactData = undefined;
      this.charState = undefined;
      this.behaviour = undefined;

      this.physicsEnabled = undefined;

      this.shootDirection = undefined;
      this.shootVelo = undefined;
      this.raycaster = undefined;
      this.type = undefined;
    }
  }
}
