import * as THREE from "three";
// @ts-ignore
import * as CANNON from "cannon";
import * as _ from "lodash";
import * as Utils from "../core/FunctionLibrary";
import { VectorSpringSimulator } from "../physics/spring_simulation/VectorSpringSimulator";
import { RelativeSpringSimulator } from "../physics/spring_simulation/RelativeSpringSimulator";
import { World } from "../world/World";
import { IWorldEntity } from "../interfaces/IWorldEntity";
import { SphereCollider } from "../physics/colliders/SphereCollider";
import { EntityType } from "../enums/EntityType";
import { CollisionGroups } from "../enums/CollisionGroups";
import { GroundImpactData } from "./GroundImpactData";
import { Death } from './character_states/Death'
import { EventType } from "../enums/EventType";
import { ModelType } from "../enums/WorldType";

export class Bullet extends THREE.Object3D implements IWorldEntity {
  public updateOrder: number = 1;
  public entityType: EntityType = EntityType.Bullet;

  public groundImpactData: GroundImpactData = new GroundImpactData();

  // Movement
  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 bulletBox: SphereCollider;

  // Ray casting
  public rayResult: CANNON.RaycastResult = new CANNON.RaycastResult();
  public rayHasHit: boolean = false;
  public rayCastLength: number = 0;
  public raySafeOffset: number = 0;
  public raycastBox: THREE.Mesh;
  public world: World;

  private physicsEnabled: boolean = true;
  public prevStep: number = 0;
  public enemyNameFound = null;

  constructor() {
    super();
    const ballRadius = 0.15;
    const ballSegments = 8;
    const ballMaterial = new THREE.MeshLambertMaterial({ color: 0xdddddd });
    const ballGeometry = new THREE.SphereGeometry(ballRadius, ballSegments, ballSegments);

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

    this.viewVector = new THREE.Vector3();

    // Physics
    // Player Capsule
    this.bulletBox = new SphereCollider({
      mass: 1,
      position: new CANNON.Vec3(),
      radius: ballRadius,
      friction: 0,
    });
    // capsulePhysics.physical.collisionFilterMask = ~CollisionGroups.Trimesh;
    this.bulletBox.body.shapes.forEach((shape) => {
      // tslint:disable-next-line: no-bitwise
      shape.collisionFilterMask = ~CollisionGroups.TrimeshColliders;
    });
    this.bulletBox.body.allowSleep = false;

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

    // Disable character rotation
    this.bulletBox.body.fixedRotation = true;
    this.bulletBox.body.updateMassProperties();
    this.raycastBox = new THREE.Mesh(ballGeometry, ballMaterial);
    this.raycastBox.visible = true;
    // Physics pre/post step callback bindings
    this.bulletBox.body.preStep = (body: CANNON.Body) => {
      this.physicsPreStep(body, this);
    };
    this.bulletBox.body.postStep = (body: CANNON.Body) => {
      this.physicsPostStep(body, this);
    };
  }

  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();
  }

  public setPosition(x: number, y: number, z: number): void {
    if (this.physicsEnabled) {
      this.bulletBox.body.previousPosition = new CANNON.Vec3(x, y, z);
      this.bulletBox.body.position = new CANNON.Vec3(x, y, z);
      this.bulletBox.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.bulletBox.body.velocity.x = 0;
    this.bulletBox.body.velocity.y = 0;
    this.bulletBox.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 setPhysicsEnabled(value: boolean): void {
    this.physicsEnabled = value;
    
    if (value === true) {
      this.world.physicsWorld.addBody(this.bulletBox.body);
    } else {
      this.world.physicsWorld.remove(this.bulletBox.body);
    }
  }

  public update(timeStep: number): void {
    if (this.prevStep !== undefined) {
      this.prevStep += timeStep;
      if (Math.floor(this.prevStep) > 5) {
        this.world.remove(this);
      }
    }
  }

  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 physicsPreStep(body: CANNON.Body, character: Bullet): void {
    this.feetRaycast();
    if (character.rayHasHit) {
      if (character.raycastBox.visible) {
        character.raycastBox.position.x = character.rayResult.hitPointWorld.x;
        character.raycastBox.position.y = character.rayResult.hitPointWorld.y;
        character.raycastBox.position.z = character.rayResult.hitPointWorld.z;
      }
    } else {
      if (character.raycastBox.visible) {
        character.raycastBox.position.set(
          body.position.x,
          body.position.y - character.rayCastLength - character.raySafeOffset,
          body.position.z
        );
      }
    }
  }

  public feetRaycast(): void {
    // Player ray casting
    // Create ray
    let body = this.bulletBox.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: Bullet): 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;
    }
  }

  public collideListener(colider: any) {
    const that = this;
    if (colider.body?.userData?.type === ModelType.ENEMY) {
      if (!that.enemyNameFound) {
        that.enemyNameFound = colider.body?.userData?.name;
      }
      setTimeout(function() {
        if (that.world && that.world.characters) {
          const character = that.world.characterNames[that.enemyNameFound];
          if (that.world && character && !character.isHit) {
            character.isHit = true;
            // TODO: set somehow to true when hit
            //(character.characterCapsule.body as any)?.userData?.isHit = true;
            that.bulletBox.body.removeEventListener("collide", that.collideListener);
            character.setState(new Death(character));
            that.world.triggerEvent({
              type: EventType.ENEMY,
              targets: [`${character.characterCapsule.body.id}`]
            });
            that.world.remove(that);
          }
        }
      }, 0);
    }
  }

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

      // Register character
      world.bullets.push(this);

      // Register physics
      world.physicsWorld.addBody(this.bulletBox.body);
      
      // listen for collisions
      this.bulletBox.body.addEventListener("collide", this.collideListener.bind(this));

      // Add to graphicsWorld
      world.graphicsWorld.add(this);
      world.graphicsWorld.add(this.raycastBox);
    }
  }

  public removeFromWorld(world: World): void {
    if (!_.includes(world.bullets, this)) {
      console.warn("Removing Bullet from a world in which it isn't present.");
    } else {
      this.world = undefined;

      // Remove from items
      _.pull(world.bullets, this);

      // Remove physics
      world.physicsWorld.remove(this.bulletBox.body);

      // Remove visuals
      world.graphicsWorld.remove(this);
      world.graphicsWorld.remove(this.raycastBox);

      this.raycastBox.geometry.dispose();
      if (!Array.isArray(this.raycastBox.material)) {
        this.raycastBox.material.dispose();
      }

      this.raycastBox = undefined;
      this.velocitySimulator = undefined;
      this.rotationSimulator = undefined;
      this.viewVector = undefined;
      this.bulletBox = undefined;

      this.updateOrder = undefined;
      this.entityType = 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.groundImpactData = undefined;
      this.physicsEnabled = undefined;
      this.type = undefined;
      this.enemyNameFound = undefined;
      this.prevStep = undefined;
    }
  }
}
