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 { CollisionGroups } from '../enums/CollisionGroups';
import { CapsuleCollider } from '../physics/colliders/CapsuleCollider';
import { EntityType } from '../enums/EntityType';

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

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

	// 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 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 itemBox: CapsuleCollider;
	
	public world: World;
	public isPicked: boolean = false;

	private physicsEnabled: boolean = true;
	private gltf: any = null;
	

	constructor(gltf: any, texture: string)
	{
		super();

		this.gltf = gltf;
		this.readItemData(gltf, texture);

		// 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.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.itemBox = new CapsuleCollider({
			mass: 1,
			position: new CANNON.Vec3(),
			height: 0.5,
			radius: 0.25,
			segments: 8,
			friction: 1
		});
		// capsulePhysics.physical.collisionFilterMask = ~CollisionGroups.Trimesh;
		this.itemBox.body.shapes.forEach((shape) => {
			// tslint:disable-next-line: no-bitwise
			shape.collisionFilterMask = ~CollisionGroups.TrimeshColliders;
		});
		this.itemBox.body.allowSleep = true;

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

		// Disable character rotation
		this.itemBox.body.fixedRotation = true;
		this.itemBox.body.updateMassProperties();

		// Physics pre/post step callback bindings
		this.itemBox.body.preStep = (body: CANNON.Body) => { this.physicsPreStep(body, this); };
		this.itemBox.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.itemBox.body.previousPosition = new CANNON.Vec3(x, y, z);
			this.itemBox.body.position = new CANNON.Vec3(x, y, z);
			this.itemBox.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.itemBox.body.velocity.x = 0;
		this.itemBox.body.velocity.y = 0;
		this.itemBox.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.itemBox.body);
		}
		else
		{
			this.world.physicsWorld.remove(this.itemBox.body);
		}
	}

	public readItemData(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,
			color: 0xff0000
		});
		material.skinning = 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;
				const scale = 0.006;
        child.scale.set(scale, scale, scale);
				this.materials.push(material);
			}
		});
	}


	public update(timeStep: number): void
	{
		this.rotation.y += 0.01;
		// Sync physics/graphics
		if (this.physicsEnabled)
		{
			this.position.set(
				this.itemBox.body.interpolatedPosition.x,
				this.itemBox.body.interpolatedPosition.y,
				this.itemBox.body.interpolatedPosition.z
			);
		}
		else if (this.itemBox) {
			let newPos = new THREE.Vector3();
			this.getWorldPosition(newPos);

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

		this.updateMatrixWorld();
	}

	public physicsPreStep(body: CANNON.Body, character: Item): void
	{

	}

	public physicsPostStep(body: CANNON.Body, character: Item): 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),
			);
		}
	}

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

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

			// Register physics
			world.physicsWorld.addBody(this.itemBox.body);

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

			// Shadow cascades
			this.materials.forEach((mat) =>
			{
				world.sky.csm.setupMaterial(mat);
			});
		}
	}

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

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

			// Remove physics
			world.physicsWorld.remove(this.itemBox.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());
			this.itemBox = undefined;

      this.materials = undefined;
      this.modelContainer = undefined;
      this.tiltContainer = undefined;
      this.velocitySimulator = undefined;
      this.rotationSimulator = undefined;
      this.viewVector = undefined;
      this.updateOrder = undefined;
      this.entityType = undefined;
      this.height = undefined;

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

      this.defaultVelocitySimulatorDamping = undefined;
      this.defaultVelocitySimulatorMass = undefined;
      this.moveSpeed = undefined;
      this.orientation = undefined;
      this.orientationTarget = undefined;
      this.defaultRotationSimulatorDamping = undefined;
      this.defaultRotationSimulatorMass = undefined;
      this.physicsEnabled = undefined;
      this.type = undefined;
			this.isPicked = undefined;
		}
	}
}