import * as THREE from 'three';
import TrackballControls from 'three-trackballcontrols';
import { TransformControls } from "three/examples/jsm/controls/TransformControls";
import { DragControls } from "three/examples/jsm/controls/DragControls";
import {initiateSceneController, initiateSceneNonController} from './AppletControls.js';


// const middlePointDistance = 3.1;
const middlePointDistance = 3.1;
const fovDistance = 10;

/**
 * Main script which calls other functions. Loads DOM Elements and adds listeners.
 * @param resize
 */
function runMainScript(resize) {

	var sceneCreated = false;

	var scenesCanvases = {};

	var disableOrbit = document.getElementById("disableOrbit");
	var setFOVSlider = document.getElementById("setFOVSlider");
	var setFOVNumber = document.getElementById("setFOVNumber");
	var showFOVLines = document.getElementById("fovLines");
	var showFOVPlates = document.getElementById("fovPlates");
	var cameraRotateSlider = document.getElementById("cameraRotateSlider");
	var cameraRotateNumber = document.getElementById("cameraRotateNumber");

	setFOVNumber.addEventListener('change', function() {
		const fovNumber = parseInt(setFOVNumber.value);
		if (['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'].indexOf(setFOVNumber.value) === -1) {
			alert('Zadaná FOV hodnota je nesprávná, zadejte celé číslo v rozsahu 1 až 10.');
			setFOVNumber.value = sceneObjectsM.fov;
			return false;
		}
		setFOV(sceneObjectsM, sceneObjectsP, fovNumber);
		setFOVSlider.value = fovNumber;
	});

	setFOVSlider.addEventListener('change', function() {
		const fovSlider = parseInt(setFOVSlider.value);
		setFOV(sceneObjectsM, sceneObjectsP, fovSlider);
		setFOVNumber.value = fovSlider;
	});

	cameraRotateNumber.addEventListener('change', function() {
		const angleNumber = parseInt(cameraRotateNumber.value);
		if (angleNumber < 0 || angleNumber > 360 || !Number.isInteger(parseFloat(cameraRotateSlider.value))) {
			alert('Zadaný úhel kamery je nesprávný, zadejte úhel ve stupních jako celí číslo mezi 0 a 360.');
			cameraRotateNumber.value = sceneObjectsM.cameraRotation;
			return false;
		}
		rotateCamera(angleNumber, sceneObjectsM.cameraRotation, sceneObjectsM, sceneObjectsP);
		sceneObjectsM.cameraRotation = angleNumber;
		cameraRotateSlider.value = angleNumber;
	});

	cameraRotateSlider.addEventListener('change', function() {
		const angleSlider = parseInt(cameraRotateSlider.value);
		rotateCamera(angleSlider, sceneObjectsM.cameraRotation, sceneObjectsM, sceneObjectsP);
		sceneObjectsM.cameraRotation = angleSlider;
		cameraRotateNumber.value = angleSlider;
	});


	scenesCanvases.CanvasM = document.getElementById("scene_m");
	scenesCanvases.CanvasP = document.getElementById("scene_p");

	const sceneObjectsM = initiateSceneController(scenesCanvases.CanvasM);
	const sceneObjectsP = initiateSceneNonController(scenesCanvases.CanvasP);

	const originPosition = new THREE.Vector3(0, 0, 0);
	const cameraPosition = new THREE.Vector3(20, 0, 0);
	const targetPosition = new THREE.Vector3(-10,0,0);
	const objectPosition = new THREE.Vector3(0,2,4);
	const middlePoint = getPointInBetweenByLen(cameraPosition, targetPosition, middlePointDistance); // 4/100 10/50 || 5/10 10/20 || 3.1/10

	sceneObjectsM.targetObject = addObject(0x156611, '2', sceneObjectsM.scene, true);
	sceneObjectsM.squareObject = addSquare(8, 'black', sceneObjectsM.scene);
	sceneObjectsM.object1 = addObject(0x277AAC, '1', sceneObjectsM.scene, true);
	sceneObjectsM.cameraObject = createCameraObjectModel(sceneObjectsM.scene);
	sceneObjectsM.fov = 1;
	sceneObjectsM.cameraRotation = 0;
	sceneObjectsM.showFOVLines = true;
	sceneObjectsM.showFOVPlates = true;

	setObjectPositions(sceneObjectsM, targetPosition, cameraPosition, objectPosition, middlePoint );
	setCameraLookAt(sceneObjectsM.targetObject.position, sceneObjectsM.cameraObject, sceneObjectsM.squareObject);
	updateMatrixValues(sceneObjectsM);

	sceneObjectsM.connection = addCameraTargetConnect(sceneObjectsM, targetPosition, cameraPosition, sceneObjectsM.scene)

	sceneObjectsM.renderer.domElement.ondragstart = function (event) { event.preventDefault(); return false; };

	sceneObjectsM.targetObject.matrixAutoUpdate = false;
	sceneObjectsM.cameraObject.matrixAutoUpdate = false;

	sceneObjectsM.dragControls = addDragControlsM(sceneObjectsM, sceneObjectsP);
	sceneObjectsM.dragControls.deactivate();
	sceneObjectsM.transformControls = addTransformControlsM(sceneObjectsM, sceneObjectsP);
	sceneObjectsM.transformControls.detach();

	sceneObjectsM.cameraRotation = 0

	sceneObjectsM.cameraDefaultMatrix = new THREE.Matrix4();
	sceneObjectsM.squareDefaultMatrix = new THREE.Matrix4();
	sceneObjectsM.cameraDefaultMatrix.copy(sceneObjectsM.cameraObject.matrix);
	sceneObjectsM.squareDefaultMatrix.copy(sceneObjectsM.squareObject.matrix);
	updateFOVVisual(sceneObjectsM);


	window.addEventListener('keydown', function (event) {
		switch (event.key) {
			case "q":
				disableOrbit.checked = !disableOrbit.checked;
				resolveControls(sceneObjectsM.dragControls, sceneObjectsM.transformControls, sceneObjectsM.object1, disableOrbit.checked);
				break
			case "t":
				sceneObjectsM.transformControls.setMode("translate")
				break
			case "r":
				sceneObjectsM.transformControls.setMode("rotate")
				break
			case "s":
				sceneObjectsM.transformControls.setMode("scale")
				break
		}
	})

	sceneObjectsP.targetObject = addObject(0x156611, '1', sceneObjectsP.scene, true);
	sceneObjectsP.scene.remove(sceneObjectsP.targetObject);
	sceneObjectsP.squareObject = addSquare(8, 'black', sceneObjectsP.scene);
	sceneObjectsP.cameraObject = addObject(0x156611, '2', sceneObjectsP.scene, true);
	sceneObjectsP.object1 = addObject(0x277AAC, '1', sceneObjectsP.scene, false);

	sceneObjectsP.camera.setFocalLength(fovDistance);
	setObjectPositions(sceneObjectsP, targetPosition, cameraPosition, objectPosition, middlePoint);
	setCameraLookAt(sceneObjectsP.targetObject.position, sceneObjectsP.cameraObject, sceneObjectsP.squareObject);
	setCameraView(sceneObjectsP.camera, cameraPosition, targetPosition );


	disableOrbit.addEventListener('change', function() {
		resolveControls(sceneObjectsM.dragControls, sceneObjectsM.transformControls, sceneObjectsM.object1, disableOrbit.checked);
	});

	showFOVLines.addEventListener('change', function() {
		if (showFOVLines.checked === true) {
			sceneObjectsM.showFOVLines = true;
			sceneObjectsM.FOVLines = addFOVLines(sceneObjectsM);
		} else {
			sceneObjectsM.showFOVLines = false;
			removeFOVLines(sceneObjectsM.FOVLines, sceneObjectsM.scene);
		}
	});

	showFOVPlates.addEventListener('change', function() {
		if (showFOVPlates.checked === true) {
			sceneObjectsM.showFOVPlates = true;
			sceneObjectsM.fovPlates = addFOVPlates(sceneObjectsM.cameraObject.position, sceneObjectsM.fovCorners, sceneObjectsM.scene);
		} else {
			sceneObjectsM.showFOVPlates = false;
			removeFOVSquares(sceneObjectsM.fovPlates, sceneObjectsM.scene);
		}
	});

}

/**
 * Function used to rotate camera object in first scene around optic axis. Also rotates object in second scene in
 * opposite direction.
 * @param angle
 * @param oldAngle
 * @param sceneObjectsM
 * @param sceneObjectsP
 */
function rotateCamera(angle, oldAngle, sceneObjectsM, sceneObjectsP) {


	const pointA = sceneObjectsM.cameraObject.position;
	const pointB = sceneObjectsM.targetObject.position;

	const finalAngle = angle * Math.PI / 180;
	const finalOldAngle = oldAngle * Math.PI / 180;

	const cameraAxis = getNormalizedVector(pointA, pointB);

	const identityMatrix = new THREE.Matrix4();

	sceneObjectsM.cameraObject.position.set( 0, 0, 0 );
	sceneObjectsM.cameraObject.rotation.set( 0, 0, 0 );
	sceneObjectsM.cameraObject.scale.set( 1, 1, 1 );
	sceneObjectsM.cameraObject.updateMatrix();
	sceneObjectsM.cameraObject.applyMatrix4(sceneObjectsM.cameraDefaultMatrix);
	sceneObjectsM.cameraObject.updateMatrix();
	sceneObjectsM.cameraObject.lookAt(pointB);
	sceneObjectsM.cameraObject.updateMatrix();
	sceneObjectsM.cameraObject.rotateOnWorldAxis(cameraAxis, finalAngle);
	sceneObjectsM.cameraObject.updateMatrix();

	sceneObjectsM.squareObject.position.set( 0, 0, 0 );
	sceneObjectsM.squareObject.rotation.set( 0, 0, 0 );
	sceneObjectsM.squareObject.scale.set( 1, 1, 1 );
	sceneObjectsM.squareObject.updateMatrix();
	sceneObjectsM.squareObject.applyMatrix4(sceneObjectsM.squareDefaultMatrix);
	sceneObjectsM.squareObject.updateMatrix();
	sceneObjectsM.squareObject.rotateOnWorldAxis(cameraAxis, finalAngle);
	sceneObjectsM.squareObject.updateMatrix();

	sceneObjectsP.object1.rotateOnWorldAxis(cameraAxis, finalOldAngle);
	sceneObjectsP.object1.updateMatrix();
	sceneObjectsP.object1.rotateOnWorldAxis(cameraAxis,-finalAngle);
	sceneObjectsP.object1.updateMatrix();

	updateFOVVisual(sceneObjectsM);

	updateMatrixValues(sceneObjectsM);


}

/**
 * Function returns vetors of corners of square.
 * @param squareMatrix
 * @returns {[]}
 */
function getSquareCorners(squareMatrix) {

	const squareCorners = [];
	squareCorners.push(new THREE.Vector3(4,4,0));
	squareCorners.push(new THREE.Vector3(4,-4,0));
	squareCorners.push(new THREE.Vector3(-4,4,0));
	squareCorners.push(new THREE.Vector3(-4,-4,0));

	for (let i = 0; i < squareCorners.length; i++) {
		squareCorners[i].applyMatrix4(squareMatrix);
	}

	return squareCorners;

}

/**
 * This function returns array with 4 instances of Vector3 which represent final vectors of FOV Lines.
 * @param cameraPosition
 * @param targetPosition
 * @param squareCorners
 * @returns {[]}
 */
function getFOVCorners(cameraPosition, targetPosition, squareCorners) {

	const fovCorners = [];

	const cameraTargetDistance = cameraPosition.distanceTo(targetPosition);

	for (let i = 0; i < squareCorners.length; i++) {
		fovCorners.push(getPointInBetweenByLen(cameraPosition, squareCorners[i], cameraTargetDistance*10));
	}

	return fovCorners;

}

/**
 * Function used to resolve if controls should be activated or disactivated.
 * @param dragControls
 * @param transformControls
 * @param object1
 * @param checkValue
 */
function resolveControls(dragControls, transformControls, object1, checkValue){
	if (checkValue === true) {
		dragControls.activate();
		transformControls.attach(object1);
	} else {
		dragControls.deactivate();
		transformControls.detach();
	}
}


/**
 * Function sets FOV, moves camera object on optic axis.
 * @param sceneObjectsM
 * @param sceneObjectsP
 * @param fov
 */
function setFOV(sceneObjectsM, sceneObjectsP, fov) {

	// const targetPositionM = getCenterPoint(sceneObjectsM.targetObject);
	// const cameraPositionM = getCenterPoint(sceneObjectsM.cameraObject);
	const targetPositionM = sceneObjectsM.targetObject.position;
	const cameraPositionM = sceneObjectsM.cameraObject.position;

	if(fov === sceneObjectsM.fov) return;


	const newFOV = sceneObjectsM.fov - fov;
	const cameraNewPositionM = getPointInBetweenByLen(cameraPositionM, targetPositionM, middlePointDistance * newFOV);

	sceneObjectsM.cameraObject.position.copy(cameraNewPositionM);
	sceneObjectsM.cameraObject.updateMatrix();

	sceneObjectsM.connection.connectLine.geometry.setFromPoints([targetPositionM, cameraNewPositionM]);

	sceneObjectsP.cameraObject.position.copy(cameraNewPositionM);
	sceneObjectsP.cameraObject.updateMatrix();

	setCameraView(sceneObjectsP.camera, cameraNewPositionM, targetPositionM );

	const newFOVDistance = fovDistance * fov;

	sceneObjectsP.camera.setFocalLength(newFOVDistance);

	sceneObjectsM.fov = fov;
	updateFOVVisual(sceneObjectsM);

	updateMatrixValues(sceneObjectsM);

}

/**
 * Function creates TransformControls object and connects it to main scene.
 * @param sceneObjectsM
 * @param sceneObjectsP
 * @returns {TransformControls}
 */
function addTransformControlsM(sceneObjectsM, sceneObjectsP) {
	const transformControlsM = new TransformControls(sceneObjectsM.camera, sceneObjectsM.renderer.domElement);
	transformControlsM.attach(sceneObjectsM.object1);
	transformControlsM.setMode("translate")
	transformControlsM.setSize(0.5);
	transformControlsM.name = 'transformControls';
	sceneObjectsM.scene.add(transformControlsM);


	transformControlsM.addEventListener('dragging-changed', function (event) {
		if(event.value === true) {
			sceneObjectsM.object1.material.opacity = 1;
		}

		if(event.value === false) {
			sceneObjectsP.object1.copy(sceneObjectsM.object1);
			sceneObjectsM.object1.material.opacity = 1;
		}
		sceneObjectsM.controls.enabled = !event.value;
		sceneObjectsM.dragControls.enabled = !event.value;
	});

	return transformControlsM;
}

/**
 * Function creates DragControls object and connects it to main scene.
 * @param sceneObjectsM
 * @param sceneObjectsP
 * @returns {DragControls}
 */
function addDragControlsM(sceneObjectsM, sceneObjectsP) {

	const dragControlsM = new DragControls([sceneObjectsM.targetObject, sceneObjectsM.cameraObject], sceneObjectsM.camera, sceneObjectsM.renderer.domElement);

	dragControlsM.addEventListener('dragstart', function (event) {
		event.object.material.opacity = 0.8;
	})

	dragControlsM.addEventListener('dragend', function (event) {
		event.object.material.opacity = 1
		updateObjectsPosition(sceneObjectsM, sceneObjectsP);
	})

	dragControlsM.addEventListener("hoveron", function () {
		sceneObjectsM.controls.enabled = false;
		sceneObjectsM.targetObject.matrixAutoUpdate = true;
		sceneObjectsM.cameraObject.matrixAutoUpdate = true;

	});
	dragControlsM.addEventListener("hoveroff", function () {
		sceneObjectsM.controls.enabled = true;
		sceneObjectsM.targetObject.matrixAutoUpdate = false;
		sceneObjectsM.cameraObject.matrixAutoUpdate = false;
	});

	return dragControlsM;

}

/**
 * Function used to update FOV lines and plates after moving camera object.
 * @param sceneObjectsM
 */
function updateFOVVisual (sceneObjectsM) {

	if(sceneObjectsM.showFOVLines === true) {
		if (sceneObjectsM.FOVLines) removeFOVLines(sceneObjectsM.FOVLines, sceneObjectsM.scene);
		sceneObjectsM.FOVLines = addFOVLines(sceneObjectsM);
	}

	if(sceneObjectsM.showFOVPlates === true) {
		if (sceneObjectsM.fovPlates) removeFOVSquares(sceneObjectsM.fovPlates, sceneObjectsM.scene);
		sceneObjectsM.fovPlates = addFOVPlates(sceneObjectsM.cameraObject.position, sceneObjectsM.fovCorners, sceneObjectsM.scene);
	}
}

/**
 * Function creates FOV Lines and adds them to the main scene.
 * @param sceneObjectsM
 * @returns {Line<BufferGeometry, MeshLambertMaterial>[]}
 */
function addFOVLines (sceneObjectsM) {

	sceneObjectsM.squareCorners = getSquareCorners(sceneObjectsM.squareObject.matrix);
	sceneObjectsM.fovCorners = getFOVCorners(sceneObjectsM.cameraObject.position, sceneObjectsM.targetObject.position, sceneObjectsM.squareCorners);

	// const materialLine = new THREE.LineBasicMaterial( { color: 0x000000 } );
	const materialLine = new THREE.MeshLambertMaterial({color: 'black', transparent: true, opacity: 0.2, flatShading: false});
	materialLine.depthTest = false;

	const cameraCenter = sceneObjectsM.cameraObject.position;

	const points1 = [cameraCenter];
	const points2 = [cameraCenter];
	const points3 = [cameraCenter];
	const points4 = [cameraCenter];

	points1.push(sceneObjectsM.fovCorners[0]);
	points2.push(sceneObjectsM.fovCorners[1]);
	points3.push(sceneObjectsM.fovCorners[2]);
	points4.push(sceneObjectsM.fovCorners[3]);

	const geometryLine1 = new THREE.BufferGeometry().setFromPoints( points1 );
	const geometryLine2 = new THREE.BufferGeometry().setFromPoints( points2 );
	const geometryLine3 = new THREE.BufferGeometry().setFromPoints( points3 );
	const geometryLine4 = new THREE.BufferGeometry().setFromPoints( points4 );

	const line1 = new THREE.Line( geometryLine1, materialLine );
	const line2 = new THREE.Line( geometryLine2, materialLine );
	const line3 = new THREE.Line( geometryLine3, materialLine );
	const line4 = new THREE.Line( geometryLine4, materialLine );

	line1.name = 'line1';
	line2.name = 'line2';
	line3.name = 'line3';
	line4.name = 'line4';

	sceneObjectsM.scene.add(line1);
	sceneObjectsM.scene.add(line2);
	sceneObjectsM.scene.add(line3);
	sceneObjectsM.scene.add(line4);

	return[line1, line2, line3, line4];

}

/**
 * Function creates FOV paltes and adds them to the scene.
 * @param cameraPosition
 * @param fovCorners
 * @param scene
 * @returns {Mesh<BufferGeometry, MeshLambertMaterial>[]}
 */
function addFOVPlates(cameraPosition, fovCorners, scene) {

	const meshMaterial = new THREE.MeshLambertMaterial({color: 'white', transparent: true, opacity: 0.05, flatShading: false, side: THREE.DoubleSide});
	meshMaterial.depthTest = false;
	// const cameraCenter = sceneObjectsM.cameraObject.position;

	const points1 = [cameraPosition, fovCorners[0], fovCorners[1]];
	const points2 = [cameraPosition, fovCorners[0], fovCorners[2]];
	const points3 = [cameraPosition, fovCorners[2], fovCorners[3]];
	const points4 = [cameraPosition, fovCorners[3], fovCorners[1]];

	var geometryMesh1 = new THREE.BufferGeometry().setFromPoints(points1);
	var geometryMesh2 = new THREE.BufferGeometry().setFromPoints(points2);
	var geometryMesh3 = new THREE.BufferGeometry().setFromPoints(points3);
	var geometryMesh4 = new THREE.BufferGeometry().setFromPoints(points4);

	var mesh1 = new THREE.Mesh( geometryMesh1, meshMaterial );
	var mesh2 = new THREE.Mesh( geometryMesh2, meshMaterial );
	var mesh3 = new THREE.Mesh( geometryMesh3, meshMaterial );
	var mesh4 = new THREE.Mesh( geometryMesh4, meshMaterial );

	mesh1.name = 'mesh1';
	mesh2.name = 'mesh2';
	mesh3.name = 'mesh3';
	mesh4.name = 'mesh4';

	scene.add(mesh1);
	scene.add(mesh2);
	scene.add(mesh3);
	scene.add(mesh4);

	return [mesh1, mesh2, mesh3, mesh4];

}

/**
 * Function used to remove FOV Lines from scene.
 * @param fovLines
 * @param scene
 */
function removeFOVLines(fovLines, scene) {

	// for (let i = 0; i < sceneObjectsM.FOVLines.length; i++) {
	// 	sceneObjectsM.scene.remove(sceneObjectsM.FOVLines[i]);
	// }
	for (let i = 0; i < fovLines.length; i++) {
		scene.remove(fovLines[i]);
	}

}

/**
 * Function is used to remove FOV plates from scene.
 * @param fovPlates
 * @param scene
 */
function removeFOVSquares(fovPlates, scene) {

	// for (let i = 0; i < sceneObjectsM.fovPlates.length; i++) {
	// 	sceneObjectsM.scene.remove(sceneObjectsM.fovPlates[i]);
	// }
	for (let i = 0; i < fovPlates.length; i++) {
		scene.remove(fovPlates[i]);
	}

}

/**
 * Function is used to update position of objects, when any change is applied to the scene.
 * @param sceneObjectsM
 * @param sceneObjectsP
 */
function updateObjectsPosition( sceneObjectsM, sceneObjectsP ) {


	// const targetPositionM = getCenterPoint(sceneObjectsM.targetObject);
	const targetPositionM = sceneObjectsM.targetObject.position;
	// sceneObjectsM.connection.targetPoint.position.copy(targetPositionM);
	addPoint(targetPositionM, 4, 'black', sceneObjectsM.scene);
	// const cameraPositionM = getCenterPoint(sceneObjectsM.cameraObject);
	const cameraPositionM = sceneObjectsM.cameraObject.position;
	// sceneObjectsM.connection.cameraPoint.position.copy(cameraPositionM);

	const object1PositionM = getCenterPoint(sceneObjectsM.object1);

	sceneObjectsM.connection.connectLine.geometry.setFromPoints([targetPositionM, cameraPositionM]);

	// const cameraNewPositionM = getPointInBetweenByLen(cameraPositionM, targetPositionM, middlePointDistance * sceneObjectsM.fov);

	sceneObjectsM.cameraObject.lookAt(targetPositionM);

	const squarePosition = getPointInBetweenByLen(cameraPositionM, targetPositionM, middlePointDistance * sceneObjectsM.fov);
	sceneObjectsM.squareObject.position.copy(squarePosition);
	sceneObjectsM.squareObject.updateMatrix();
	sceneObjectsM.squareObject.lookAt(targetPositionM);
	// sceneObjectsM.squareObject.lookAt(new THREE.Vector3(0,0,0));
	sceneObjectsM.squareObject.updateMatrix();

	updateFOVVisual(sceneObjectsM);

	sceneObjectsP.squareObject.position.copy(squarePosition);
	sceneObjectsP.squareObject.updateMatrix();
	sceneObjectsP.squareObject.lookAt(targetPositionM);
	sceneObjectsP.squareObject.updateMatrix();

	sceneObjectsP.targetObject.position.copy(targetPositionM);

	sceneObjectsP.cameraObject.position.copy(cameraPositionM);

	setCameraView(sceneObjectsP.camera, cameraPositionM, targetPositionM );

	updateMatrixValues(sceneObjectsM);

	sceneObjectsM.cameraDefaultMatrix.copy(sceneObjectsM.cameraObject.matrix);
	sceneObjectsM.squareDefaultMatrix.copy(sceneObjectsM.squareObject.matrix);

	// sceneObjectsM.cameraObject.lookAt(targetPositionM);
	rotateCamera(sceneObjectsM.cameraRotation, 0, sceneObjectsM, sceneObjectsP);

}

/**
 * Function updates values of matrices in front end.
 * @param sceneObjects
 */
function updateMatrixValues(sceneObjects) {

	const cameraPerspMatrix = getCameraPerspectiveMatrix(sceneObjects);

	// const cameraMatrix = sceneObjects.cameraObject.matrix.clone(); // TODO probably delete

	setMatrix(cameraPerspMatrix, 0);

	const projectionPerspMatrix = getPerspectiveMatrix(sceneObjects);

	setMatrix(projectionPerspMatrix, 1);

	projectionPerspMatrix.transpose();
	cameraPerspMatrix.transpose();
	const finalMatrix = projectionPerspMatrix.multiply(cameraPerspMatrix);

	setMatrix(finalMatrix, 2);

}

/**
 *
 * @param sceneObjectsM
 * @returns {Matrix4}
 */
function getCameraPerspectiveMatrix(sceneObjectsM) {

	const squarePosition3 = sceneObjectsM.squareObject.position;
	const squarePosition = new THREE.Vector4(squarePosition3.getComponent(0),squarePosition3.getComponent(1),squarePosition3.getComponent(2), 1);
	const cameraPosition3 = sceneObjectsM.cameraObject.position;
	const cameraPosition = new THREE.Vector4(cameraPosition3.getComponent(0),cameraPosition3.getComponent(1),cameraPosition3.getComponent(2), 1);
	const targetPosition3 = sceneObjectsM.targetObject.position;
	const targetPosition = new THREE.Vector4(targetPosition3.getComponent(0),targetPosition3.getComponent(1),targetPosition3.getComponent(2), 1);
	var midTopPoint = new THREE.Vector4(0,4,0, 1);
	var midRightPoint = new THREE.Vector4(4,0,0, 1);

	const squareMatrix = sceneObjectsM.squareObject.matrix;

	midTopPoint.applyMatrix4(squareMatrix);
	midRightPoint.applyMatrix4(squareMatrix);

	var l = getNormalizedVector(squarePosition, targetPosition);

	var h = getNormalizedVector(squarePosition, midTopPoint);

	var p = getNormalizedVector(squarePosition, midRightPoint);

	const lE = multiplyVectors(l.toArray(), cameraPosition.toArray());
	const hE = multiplyVectors(h.toArray(), cameraPosition.toArray());
	const pE = multiplyVectors(p.toArray(), cameraPosition.toArray());

	const cameraPerspectiveMatrix = new THREE.Matrix4();

	cameraPerspectiveMatrix.set( p.getComponent(0), p.getComponent(1), p.getComponent(2), -pE,
		h.getComponent(0), h.getComponent(1), h.getComponent(2), -hE,
		-l.getComponent(0), -l.getComponent(1), -l.getComponent(2), lE,
		0, 0, 0, 1 );

	const lApplied = l.applyMatrix4(cameraPerspectiveMatrix);
	const hApplied = h.applyMatrix4(cameraPerspectiveMatrix);
	const pApplied = p.applyMatrix4(cameraPerspectiveMatrix);

	return cameraPerspectiveMatrix;

}

/**
 * Function can be used to round vector values.
 * @param oldVector
 * @returns {Vector4}
 */
function getRoundedVector(oldVector) {

	console.log('getRoundedVector');

	var x = oldVector.getComponent(0);
	var y = oldVector.getComponent(1);
	var z = oldVector.getComponent(2);
	var w = oldVector.getComponent(3);


	var xs = x.toString();
	var ys = y.toString();
	var zs = z.toString();
	var ws = w.toString();

	console.log(xs);

	if (xs.length > 16) xs = xs.slice(0, 15);
	if (ys.length > 16) ys = ys.slice(0, 15);
	if (zs.length > 16) zs = zs.slice(0, 15);
	if (ws.length > 16) ws = ws.slice(0, 15);

	const xf = parseFloat(xs);
	const yf = parseFloat(ys);
	const zf = parseFloat(zs);
	const wf = parseFloat(ws);

	return new Vector4(xf, yf, zf, wf)
}


/**
 * Function calculates values of perspective projection matrix and returns it in Matrix4 object.
 * @param sceneObjectsM
 * @returns {Matrix4}
 */
function getPerspectiveMatrix(sceneObjectsM) {

	const perspectiveMatrix = new THREE.Matrix4();

	const squareMatrix = sceneObjectsM.squareObject.position;

	// const z = squareMatrix.getComponent(2);
	const z = 3.1 * sceneObjectsM.fov;

	var zf = 1/z;

	perspectiveMatrix.set(
		1, 0, 0, 0,
		0, 1, 0, 0,
		0, 0,1, 0,
		0, 0, zf, 0
	);

	return perspectiveMatrix;

}

/**
 * Pure function used to normalize vector.
 * @param pointA
 * @param pointB
 * @returns {*}
 */
function getNormalizedVector(pointA, pointB) {
	var dir = pointB.clone().sub(pointA);
	dir.normalize();
	return dir;
}

/**
 * Function will set values from Matrix4 object to front end matrix.
 * @param matrix4
 * @param matrixId
 */
function setMatrix(matrix4, matrixId ) {

	const nameSuffix = '-' + matrixId;

	matrix4.transpose();

	for (let i = 0; i < 16; i++) {
		document.getElementById(i + nameSuffix).value = matrix4.elements[i];
	}

}

/**
 * Function adds optic axis to the scene.
 * @param sceneObjects
 * @param targetPosition
 * @param cameraPosition
 * @param scene
 * @returns {{connectLine: Line<BufferGeometry, LineBasicMaterial>}}
 */
function addCameraTargetConnect(sceneObjects, targetPosition, cameraPosition ,scene) {

	const linePoints = [targetPosition, cameraPosition];

	var connectLine = createLine(0x000000, linePoints);
	scene.add(connectLine);

	// return {connectLine, cameraPoint, targetPoint};
	return {connectLine};

}

/**
 * Function will create and add square object to scene.
 * @param size
 * @param color
 * @param scene
 * @returns {LineSegments<EdgesGeometry, LineBasicMaterial>}
 */
function addSquare (size, color, scene) {

	const geometry = new THREE.BoxGeometry( size, size, 0 );
	const edges = new THREE.EdgesGeometry( geometry );
	const square = new THREE.LineSegments( edges, new THREE.LineBasicMaterial( { color: color, depthTest : true } ) );

	// square.material.depthTest = true;

	scene.add(square);

	square.matrixAutoUpdate = false;

	return square;
}

/**
 * Sets camera in scene to look at camera target.
 * @param target
 * @param camera
 * @param square
 */
function setCameraLookAt(target, camera, square) {

	camera.lookAt(target);
	camera.updateMatrix();

	if (square) {
		// console.log('square lookAt set');
		square.lookAt(target);
		square.updateMatrix();
	}

}

/**
 *
 * @param objects
 * @param targetPosition
 * @param cameraPosition
 * @param objectPosition
 * @param squarePosition
 */
function setObjectPositions(objects, targetPosition, cameraPosition, objectPosition, squarePosition) {

	objects.targetObject.position.copy(targetPosition);
	objects.targetObject.updateMatrix();
	objects.cameraObject.position.copy(cameraPosition);

	objects.object1.position.copy(objectPosition);
	objects.object1.updateMatrix();
	if( squarePosition ) {
		objects.squareObject.position.copy(squarePosition);
		objects.squareObject.updateMatrix();
	}

}

/**
 * Function set view of camera.
 * @param camera
 * @param position
 * @param target
 */
function setCameraView(camera, position, target) {

	const positionX = position.getComponent(0);
	const positionY = position.getComponent(1);
	const positionZ = position.getComponent(2);

	camera.position.set(positionX,positionY,positionZ);
	camera.lookAt(target);

}

/**
 * Function creates object and adds it to the scene.
 * @param color
 * @param selectID
 * @param scene
 * @param depthValue
 * @returns {Mesh<BoxGeometry | SphereGeometry | EdgesGeometry, MeshPhongMaterial>}
 */
function addObject(color, selectID, scene, depthValue) {
	// 0x7B0303 // 	0x156611 0x277AAC

	// var material = new THREE.MeshLambertMaterial({color: color, transparent: true, opacity: 0.8, flatShading: false});
	var material = new THREE.MeshPhongMaterial({color: color, transparent: true, opacity: 1, flatShading: false});
	// var material = new THREE.MeshLambertMaterial({color: color, transparent: false, opacity: 0.8, flatShading: true});
	material.depthTest = depthValue;

	var geometry;

	switch (selectID) {
		case '1':
			geometry = new THREE.BoxGeometry(2, 2, 2);
			material = new THREE.MeshPhongMaterial({color: color, transparent: true, opacity: 1, flatShading: false, vertexColors : true});
			for ( let i = 0; i < geometry.faces.length; i ++ ) {

				if(i >= 0 && i <= 3) {
					geometry.faces[ i ].color.setHex( 0xcc0000 ); // 0xcc0000
				}
				if(i >= 4 && i <= 7) {
					geometry.faces[ i ].color.setHex( 0x009900 ); // 0x009900
				}
				if(i >= 8 && i <=11) {
					geometry.faces[ i ].color.setHex( 0x0066cc); // 0x0066cc
				}

			}
			break;
		case '2':
			geometry = new THREE.SphereGeometry(0.5, 16, 16);
			break;
		case '3':
			geometry = new THREE.BoxGeometry(1, 1, 1);
			break;
		case '4':
			const geometryBox = new THREE.BoxGeometry( 10, 10, 0 );
			geometry = new THREE.EdgesGeometry( geometryBox );
			break;
	}

	const object = new THREE.Mesh(geometry, material);

	object.geometry.computeBoundingBox();
	object.geometry.computeBoundingSphere();

	scene.add(object);

	// object.matrixAutoUpdate = false;

	return object;

}

/**
 * Function used to creat object of line.
 * @param color
 * @param points
 * @returns {Line<BufferGeometry, LineBasicMaterial>}
 */
function createLine(color, points) {

	const material = new THREE.LineBasicMaterial( { color: color } );

	const geometry = new THREE.BufferGeometry().setFromPoints( points );

	const line = new THREE.Line( geometry, material );

	return line;
}


/**
 * Initial function used to create projection plate.
 * @param color
 * @param position
 * @returns {Line<BufferGeometry, LineBasicMaterial>}
 */
function createProjectionPlane(color, position) {

const planeSize = 5;

	const points = [];

	const cameraX = position.getComponent(0);
	const cameraY = position.getComponent(1);
	const cameraZ = position.getComponent(2);

	points.push(new THREE.Vector3(cameraX + planeSize, cameraY + planeSize, cameraZ + planeSize));
	// points.push(new THREE.Vector3(cameraX + planeSize, cameraY - planeSize, cameraZ));
	// points.push(new THREE.Vector3(cameraX - planeSize, cameraY + planeSize, cameraZ));
	points.push(new THREE.Vector3(cameraX - planeSize, cameraY - planeSize, cameraZ - planeSize));


	const material = new THREE.LineBasicMaterial( { color: color } );

	const geometry = new THREE.BufferGeometry().setFromPoints( points );

	const plane = new THREE.Line( geometry, material );

	return plane;
}

/**
 * Function returns vector on line defined by length.
 * @param pointA
 * @param pointB
 * @param length
 * @returns {*}
 */
function getPointInBetweenByLen(pointA, pointB, length) {

	var dir = pointB.clone().sub(pointA).normalize().multiplyScalar(length);
	return pointA.clone().add(dir);

}

/**
 * Function adds point to the scene.
 * @param vector
 * @param size
 * @param color
 * @param scene
 * @param name
 * @returns {Points<Geometry, PointsMaterial>}
 */
function addPoint(vector ,size, color, scene, name) {
	var dotGeometry = new THREE.Geometry();
	var dotMaterial = new THREE.PointsMaterial({size: size, sizeAttenuation: false, color: color});
	dotGeometry.vertices.push(vector);
	var dot = new THREE.Points(dotGeometry, dotMaterial)
	if (name) dot.name = name;
	scene.add(dot);

	return dot;
}

/**
 * Function returns center point of mesh.
 * @param mesh
 * @returns {Vector3}
 */
function getCenterPoint(mesh) {
	var geometry = mesh.geometry;
	geometry.computeBoundingBox();
	var center = new THREE.Vector3();
	geometry.boundingBox.getCenter( center );
	mesh.localToWorld( center );
	return center;
}

/**
 * Function used for scalar multiplying vectors which are passed as array.
 * @param v1
 * @param v2
 * @returns {number}
 */
function multiplyVectors(v1, v2) {

	var result = 0;

	for (let i = 0; i < v1.length; i++) {
		result += v1[i] * v2[i];
	}

	return result;

}

/**
 * This function
 * @param sceneObjectsM
 * @returns {Mesh<SphereGeometry, MeshLambertMaterial>}
 */

function createCameraObjectModel (scene) {

	var material = new THREE.MeshLambertMaterial({color: 0x7B0303, transparent: true, opacity: 0.9, flatShading: false});
	material.depthTest = true;

	const geometryCamera = new THREE.SphereGeometry(0.1, 16, 16);
	const geometryBox = new THREE.BoxGeometry(1, 1, 2);
	const geometrySphere1 = new THREE.SphereGeometry(0.4, 16, 16);
	const geometrySphere2 = new THREE.SphereGeometry(0.35, 16, 16);
	const geometryCone = new THREE.ConeGeometry( 1.2, 1, 4 );


	const cameraModel = new THREE.Mesh(geometryCamera, material);
	const objectBox = new THREE.Mesh(geometryBox, material);
	const objectSphere1 = new THREE.Mesh(geometrySphere1, material);
	const objectSphere2 = new THREE.Mesh(geometrySphere2, material);
	const objectCone = new THREE.Mesh(geometryCone, material);

	objectBox.matrixAutoUpdate = false;

	objectBox.position.copy(new THREE.Vector3(0, 0, -1));
	objectBox.updateMatrix();

	objectSphere1.position.copy(new THREE.Vector3(0, 0.9, -0.7));
	objectSphere1.updateMatrix();

	objectSphere2.position.copy(new THREE.Vector3(0, 0.85, -1.42));
	objectSphere2.updateMatrix();

	objectCone.position.copy(new THREE.Vector3(0, 0, 0));
	objectCone.updateMatrix();
	objectCone.rotation.x = -Math.PI / 2;
	objectCone.updateMatrix();
	objectCone.rotation.y = Math.PI / 4;
	// objectCone.rotation.set(new THREE.Vector3( 0, 0, Math.PI / 2));
	objectCone.updateMatrix();
	objectBox.geometry.computeBoundingBox();
	objectBox.geometry.computeBoundingSphere();

	scene.add(cameraModel);
	// sceneObjectsM.scene.add(objectSphere);

	cameraModel.geometry.merge(objectBox.geometry, objectBox.matrix);
	cameraModel.geometry.merge(objectSphere1.geometry, objectSphere1.matrix);
	cameraModel.geometry.merge(objectSphere2.geometry, objectSphere2.matrix);
	cameraModel.geometry.merge(objectCone.geometry, objectCone.matrix);

	cameraModel.position.copy(new THREE.Vector3(0, 0, 0));
	cameraModel.updateMatrix();

	return cameraModel;

}

export default runMainScript;