
// ===========================================================================
// Asshat-Gaming Roleplay
// https://github.com/VortrexFTW/gtac_asshat_rp
// Copyright (c) 2020 Asshat-Gaming (https://asshatgaming.com)
// ---------------------------------------------------------------------------
// FILE: mouse-camera.js
// DESC: Provides a freelook camera similar to SA for III and VC
// TYPE: Client (JavaScript)
// ===========================================================================

// CREDITS TO LUCASC190 FOR MAKING THE MOUSE CAMERA
// WALKING CODE ADDED BY VORTREX

function SetStandardControlsEnabled(bEnabled)
{
	if(typeof gta == "undefined") {
		return false;
	}

	if (game.standardControls === undefined)
	{
		logToConsole(LOG_WARN, "game.standardControls not implemented");
		return;
	}
	game.standardControls = bEnabled;
}

function GetCurrentPlayerIndex()
{
	return 0;
}

function GetPlayerPed(uiIndex)
{
	if (uiIndex >= 1)
		throw new Error("player index out of range");
	return localPlayer;
}

function GetPedVehicle(pPed)
{
	return pPed.vehicle;
}

let ENTITYTYPE_BUILDING = 1;
let ENTITYTYPE_VEHICLE = 2;
let ENTITYTYPE_PED = 3;
let ENTITYTYPE_OBJECT = 4;
let ENTITYTYPE_DUMMY = 5;

function GetEntityType(Entity)
{
	if (Entity.isType(ELEMENT_BUILDING))
		return ENTITYTYPE_BUILDING;
	if (Entity.isType(ELEMENT_VEHICLE))
		return ENTITYTYPE_VEHICLE;
	if (Entity.isType(ELEMENT_PED))
		return ENTITYTYPE_PED;
	if (Entity.isType(ELEMENT_OBJECT))
		return ENTITYTYPE_OBJECT;
	//if (Entity.isType(ELEMENT_DUMMY))
	//	return ENTITYTYPE_DUMMY;
	return undefined;
}

function GetPlaceableMatrix(pPlaceable)
{
	if (pPlaceable == GetCamera())
		return game.cameraMatrix;
	return pPlaceable.matrix;
}

function GetEntityModel(pEntity)
{
	return pEntity;
}

function GetModelBoundingSphere(usModel)
{
	return [usModel.boundingRadius, usModel.boundingCentre.x, usModel.boundingCentre.y, usModel.boundingCentre.z];
}

function GetMouseSpeed()
{
	if (gui.cursorEnabled)
		return [0,0];
	let MouseSpeed = game.getMouseSpeed();
	return [MouseSpeed.x,-MouseSpeed.y];
}

function GetMouseSensitivity()
{
	if (game.getMouseSensitivity === undefined)
	{
		//logToConsole(LOG_ERROR, "game.getMouseSensitivity not implemented");
		return [0.0025,0.003];
	}
	let MouseSensitivity = game.getMouseSensitivity();
	return [MouseSensitivity.x,MouseSensitivity.y];
}

let GetCamera;
{
	const Camera = Symbol();

	GetCamera = function()
	{
		return Camera;
	}
}

function AreEntityCollisionsEnabled(pEntity)
{
	return pEntity.collisionsEnabled;
}

function SetEntityCollisionsEnabled(pEntity, bCollisionsEnabled)
{
	pEntity.collisionsEnabled = bCollisionsEnabled;
}

function ProcessLineOfSight(vecStartX, vecStartY, vecStartZ, vecEndX, vecEndY, vecEndZ, bCheckBuildings, bCheckVehicles, bCheckPeds, bCheckObjects, bCheckDummies, bCheckSeeThroughStuff, bIgnoreSomeObjectsForCamera)
{
	if (game.processLineOfSight === undefined)
	{
		logToConsole(LOG_WARN, "game.processLineOfSight not implemented");
		return [null];
	}
	let Result = game.processLineOfSight([vecStartX, vecStartY, vecStartZ], [vecEndX, vecEndY, vecEndZ], bCheckBuildings, bCheckVehicles, bCheckPeds, bCheckObjects, bCheckDummies, bCheckSeeThroughStuff, bIgnoreSomeObjectsForCamera);
	if (Result == null)
		return [null];
	return [Result.position.x, Result.position.y ,Result.position.z, Result.normal.x, Result.normal.y ,Result.normal.z, Result.entity];
}

function SetPlaceableMatrix(pPlaceable, mat)
{
	if (pPlaceable == GetCamera())
	{
		game.setCameraMatrix(mat);
		return;
	}
	pPlaceable.matrix = mat;
}

const UpdateCamera = game.updateCamera;

let GetTickCount;
{
	let FrameCount = 0;

	setInterval(() =>
	{
		++FrameCount;
	}, 0);

	let GTAFrameCount = 0;

	addEventHandler("OnProcess", (event, deltaTime) =>
	{
		++GTAFrameCount;
	});

	GetTickCount = function(bGTA, bFrames)
	{
		if (bFrames)
			return bGTA ? GTAFrameCount : FrameCount;
		else
			return bGTA ? game.tickCount : sdl.ticks;
	}
}

function easingSinusoidalInOut(t,b,c,d)//TODO: Move this to MathUtil.js
{
	return -c/2 * (Math.cos((Math.PI)*t/d) - 1) + b;
}

//TODO: extract

function applyMultiplierTimeStep(m,t)//TODO: Move this to MathUtil.js
{
	return Math.max(Math.min(1.0-(1.0-m)*(t),1),0);
}

//TODO: getOffset
//TODO: round
//TODO: getNumberBetween
//TODO: split
//TODO: isWhiteSpaceCharacter
//TODO: isControlCharacter
//TODO: alert
//TODO: confirm

const identityMatrix = new Matrix4x4();
if (identityMatrix.setIdentity === undefined)
{
	identityMatrix.m11 = 1;
	identityMatrix.m12 = 0;
	identityMatrix.m13 = 0;
	identityMatrix.m14 = 0;
	identityMatrix.m21 = 0;
	identityMatrix.m22 = 1;
	identityMatrix.m23 = 0;
	identityMatrix.m24 = 0;
	identityMatrix.m31 = 0;
	identityMatrix.m32 = 0;
	identityMatrix.m33 = 1;
	identityMatrix.m34 = 0;
	identityMatrix.m41 = 0;
	identityMatrix.m42 = 0;
	identityMatrix.m43 = 0;
	identityMatrix.m44 = 1;
}

const cameraIdentityMatrix = new Matrix4x4();
cameraIdentityMatrix.m11 = -1;
cameraIdentityMatrix.m12 = 0;
cameraIdentityMatrix.m13 = 0;
cameraIdentityMatrix.m14 = 0;
cameraIdentityMatrix.m21 = 0;
cameraIdentityMatrix.m22 = 1;
cameraIdentityMatrix.m23 = 0;
cameraIdentityMatrix.m24 = 0;
cameraIdentityMatrix.m31 = 0;
cameraIdentityMatrix.m32 = 0;
cameraIdentityMatrix.m33 = 1;
cameraIdentityMatrix.m34 = 0;
cameraIdentityMatrix.m41 = 0;
cameraIdentityMatrix.m42 = 0;
cameraIdentityMatrix.m43 = 0;
cameraIdentityMatrix.m44 = 1;

function createMultipliedMatrix()
{
	let matrix = new Matrix4x4();
	matrix.setMultiply.apply(matrix, arguments);
	return matrix;
}

function createXRotationMatrix(x)
{
	let matrix = new Matrix4x4();
	matrix.setRotateX(x);
	return matrix;
}

function createYRotationMatrix(x)
{
	let matrix = new Matrix4x4();
	matrix.setRotateY(x);
	return matrix;
}

function createZRotationMatrix(z)
{
	let matrix = new Matrix4x4();
	matrix.setRotateZ(z);
	return matrix;
}

function createTranslationMatrix(x,y,z)
{
	let matrix = new Matrix4x4();
	matrix.setTranslate([x,y,z]);
	return matrix;
}

//TODO: createScaleMatrix

function getDotProduct(x,y,z,x2,y2,z2)
{
	return x*x2 + y*y2 + z*z2;
}

function getCrossProduct(x,y,z,x2,y2,z2)
{
	return [y*z2-z*y2, z*x2-x*z2, x*y2-y*x2];
}

function getLength(x,y,z)
{
	return Math.sqrt(getDotProduct(x,y,z,x,y,z));
}

function normalise(x,y,z)
{
	let length = getLength(x,y,z);
	if (length == 0)
		throw new Error("an attempt was made to normalise a three dimensional vector with a length of zero");
	return [x/length, y/length, z/length];
}

function createLookAtLHMatrix(eyeX, eyeY, eyeZ, atX, atY, atZ, upX,upY,upZ)
{
	let matrix = new Matrix4x4();
	let [lookX, lookY, lookZ] = normalise(atX-eyeX,atY-eyeY,atZ-eyeZ);
	let [rightX, rightY, rightZ] = normalise.apply(null,getCrossProduct(upX,upY,upZ,lookX, lookY, lookZ));
	[upX,upY,upZ] = getCrossProduct(lookX, lookY, lookZ,rightX, rightY, rightZ);
	matrix.m11 = rightX;
	matrix.m12 = rightY;
	matrix.m13 = rightZ;
	matrix.m14 = 0;

	matrix.m21 = lookX;
	matrix.m22 = lookY;
	matrix.m23 = lookZ;
	matrix.m24 = 0;

	matrix.m31 = upX;
	matrix.m32 = upY;
	matrix.m33 = upZ;
	matrix.m34 = 0;

	matrix.m41 = eyeX;
	matrix.m42 = eyeY;
	matrix.m43 = eyeZ;
	matrix.m44 = 1;

	matrix.m41 = eyeX;
	matrix.m42 = eyeY;
	matrix.m43 = eyeZ;
	matrix.m44 = 1;
	return matrix;
}

function getDifferenceBetweenAngles(current,target)
{
	let f = (((target-current)+Math.PI)/(Math.PI*2));
	return ((f-Math.floor(f))*(Math.PI*2))-Math.PI;
}

let easeCamera = false;
let easeStartTicks;
let easeDuration;
let easeStartPosX, easeStartPosY, easeStartPosZ;
let easeStartLookX, easeStartLookY, easeStartLookZ;
let easeStartUpX, easeStartUpY, easeStartUpZ;

function getCameraPositionInfo(matrix)
{
	return [matrix.m41, matrix.m42, matrix.m43, matrix.m21, matrix.m22, matrix.m23, matrix.m31, matrix.m32, matrix.m33];
}

function startCameraEase()
{
	easeCamera = true;
	easeStartTicks = GetTickCount(true,false);
	easeDuration = 1000;
	let matrix = GetPlaceableMatrix(GetCamera());
	[easeStartPosX, easeStartPosY, easeStartPosZ, easeStartLookX, easeStartLookY, easeStartLookZ, easeStartUpX, easeStartUpY, easeStartUpZ] = getCameraPositionInfo(matrix);
}

function applyCameraEase(matrix)
{
	if (!easeCamera)
		return matrix;
	let ease = (GetTickCount(true,false)-easeStartTicks)/easeDuration;
	if (ease < 1)
	{
		ease = easingSinusoidalInOut(ease,0,1,1);
		let [newPosX, newPosY, newPosZ, newLookX, newLookY, newLookZ, newUpX, newUpY, newUpZ] = getCameraPositionInfo(matrix);
		let easePosX = easeStartPosX+(newPosX-easeStartPosX)*ease;
		let easePosY = easeStartPosY+(newPosY-easeStartPosY)*ease;
		let easePosZ = easeStartPosZ+(newPosZ-easeStartPosZ)*ease;
		let easeLookX = easeStartLookX+(newLookX-easeStartLookX)*ease;
		let easeLookY = easeStartLookY+(newLookY-easeStartLookY)*ease;
		let easeLookZ = easeStartLookZ+(newLookZ-easeStartLookZ)*ease;
		let easeUpX = easeStartUpX+(newUpX-easeStartUpX)*ease;
		let easeUpY = easeStartUpY+(newUpY-easeStartUpY)*ease;
		let easeUpZ = easeStartUpZ+(newUpZ-easeStartUpZ)*ease;
		return createLookAtLHMatrix(easePosX,easePosY,easePosZ,easePosX+easeLookX,easePosY+easeLookY,easePosZ+easeLookZ,easeUpX,easeUpY,easeUpZ);
	}
	return matrix;
}

function isCameraEasing()
{
	return easeCamera && GetTickCount(true,false) < (easeStartTicks+easeDuration);
}

let oldCameraTarget = null;
let OldPosition = null;//2019 Lucas was here!
let cameraRotZ;
let cameraRotY;

function getCameraTarget()
{
	let playerPed = GetPlayerPed(GetCurrentPlayerIndex());
	let vehicle = GetPedVehicle(playerPed);
	if (vehicle != null)
		return vehicle;
	if (playerPed != null)
	{
		//if (playerPed.health <= 1)//Breaks because of fade//2019 Lucas was here!
		//	return null;
		return playerPed;
	}
	return null;
}

function isRelativeToTarget(target)
{
	if (GetEntityType(target) == ENTITYTYPE_PED)
		return false;
	return false
}

function isClipped(target)
{
	if (GetEntityType(target) == ENTITYTYPE_PED)
		return true;
	return true;
}

//2019 Lucas was here!
function ShouldReturnToRestRotation(Target)
{
	if (GetEntityType(Target) == ENTITYTYPE_PED)
		return false;
	return true;
}

function getCameraRestRotation(target)
{
	let targetMatrix = GetPlaceableMatrix(target);
	let rotZ;
	if (isRelativeToTarget(target))
		rotZ = 0;
	else
		rotZ = -Math.atan2(targetMatrix.m21,targetMatrix.m22);
	let rotY = -0.2;
	return [rotZ, rotY];
}

function resetCameraRotation()
{
	[cameraRotZ, cameraRotY] = getCameraRestRotation(getCameraTarget());
}

//2019 Lucas was here!
let DeltaTime = 0;
addEventHandler("OnProcess", (event, deltaTime) =>
{
	DeltaTime = deltaTime;
	if(!localPlayer) {
		return false;
	}
});

let IdleTime = 0;//2019 Lucas was here!

function processReturnToRestRotation()
{
	//resetCameraRotation();//2019 Lucas was here!

	//2019 Lucas was here!
	let Target = getCameraTarget();
	if (!ShouldReturnToRestRotation(Target))
		return;
	IdleTime += DeltaTime;
	if (IdleTime > 1.5)
	{
		let Velocity = Target.velocity;
		let Matrix = Target.matrix;
		let Speed = getDotProduct(Velocity.x,Velocity.y,Velocity.z,Matrix.getElement(1*4+0),Matrix.getElement(1*4+1),Matrix.getElement(1*4+2));
		let AbsSpeed = Math.abs(Speed);
		let Multiplier = Math.min(AbsSpeed/0.75, 1);
		if (Multiplier != 0)
		{
			let [TargetCameraRotZ2, TargetCameraRotY2] = getCameraRestRotation(Target);
			if (Speed < 0)
				TargetCameraRotZ2 += Math.PI;
			let TimeStep = game.timeStep/50*60;
			cameraRotZ += getDifferenceBetweenAngles(cameraRotZ,TargetCameraRotZ2)*applyMultiplierTimeStep(1/20,TimeStep)*Multiplier;
			cameraRotY += getDifferenceBetweenAngles(cameraRotY,TargetCameraRotY2)*applyMultiplierTimeStep(1/20,TimeStep)*Multiplier;
		}
	}
}

function cancelReturnToRestRotation()
{
	IdleTime = 0;//2019 Lucas was here!
}

let distance;
let zIncrease;

function getCameraOffsetInfo(target)
{
	if (GetEntityType(target) == ENTITYTYPE_PED)
	{
		let distance = 4;
		let zIncrease = 0.8;
		let offsetX = 0;
		let offsetY = 0;
		let offsetZ = 0;
		return [distance, zIncrease, offsetX, offsetY, offsetZ];
	}
	let model = GetEntityModel(target);
	let [radius] = GetModelBoundingSphere(model);
	let minDistance;
	let maxDistance;
	let minZIncrease;
	let maxZIncrease;
	let minRadius;
	let maxRadius;
	let offsetX;
	let offsetY;
	let offsetZ;
	if (radius <= 3.0535011291504)
	{
		minDistance = 4;
		maxDistance = 8;
		minZIncrease = 0.5;
		maxZIncrease = 1;
		minRadius = 2;
		maxRadius = 3.0535011291504;
	}
	else
	{
		minDistance = 8;
		maxDistance = 16;
		minZIncrease = 1;
		maxZIncrease = 2;
		minRadius = 3.05350112915042;
		maxRadius = 6.3955960273743;
	}
	offsetX = 0;
	offsetY = 0;
	offsetZ = 0;
	distance = minDistance+(radius-minRadius)/(maxRadius-minRadius)*(maxDistance-minDistance);
	zIncrease = minZIncrease+(radius-minRadius)/(maxRadius-minRadius)*(maxZIncrease-minZIncrease);
	return [distance, zIncrease, offsetX, offsetY, offsetZ];
}

function update()
{
	let target = getCameraTarget();
	if (target != null)
	{
		if (oldCameraTarget != target)
		{
			//if (oldCameraTarget != null)//2019 Lucas was here!
			let Position = target.position;
			if (OldPosition == null || getLength(Position.x-OldPosition.x,Position.y-OldPosition.y,Position.z-OldPosition.z) < 10)
				startCameraEase()
			resetCameraRotation()
		}
		let [mouseSpeedX, mouseSpeedY] = GetMouseSpeed();
		let [mouseSensitivityX, mouseSensitivityY] = GetMouseSensitivity();
		mouseSpeedX = mouseSpeedX*mouseSensitivityX*2;
		mouseSpeedY = mouseSpeedY*mouseSensitivityY*2;
		if (mouseSpeedX == 0 && mouseSpeedY == 0)
		{
			processReturnToRestRotation();
		}
		else
		{
			cameraRotZ = cameraRotZ-mouseSpeedX;
			cameraRotY = cameraRotY-mouseSpeedY;
			cancelReturnToRestRotation();
		}
		cameraRotY = Math.max(cameraRotY,-Math.PI/2+0.01);
		if (GetEntityType(target) != ENTITYTYPE_PED)
			cameraRotY = Math.min(cameraRotY,Math.PI/8.5);//2019 Lucas was here!
		else
			cameraRotY = Math.min(cameraRotY,Math.PI/4);
		let camera = GetCamera();
		let targetMatrix = GetPlaceableMatrix(target);
		let [distance, zIncrease, offsetX, offsetY, offsetZ] = getCameraOffsetInfo(target);
		let offsetTranslationMatrix = createTranslationMatrix(offsetX, offsetY, offsetZ);
		targetMatrix = createMultipliedMatrix(offsetTranslationMatrix,targetMatrix);
		let targetPosX, targetPosY, targetPosZ;
		if (isRelativeToTarget(target))
			[targetPosX, targetPosY, targetPosZ] = [0,0,0];
		else
			[targetPosX, targetPosY, targetPosZ] = [targetMatrix.m41,targetMatrix.m42,targetMatrix.m43];
		let distanceTranslationMatrix = createTranslationMatrix(0,-distance,0);
		targetPosZ = targetPosZ+zIncrease;
		let targetTranslationMatrix = createTranslationMatrix(targetPosX, targetPosY, targetPosZ);
		let offsetRotationX = createXRotationMatrix(cameraRotY);
		let offsetRotationZ = createZRotationMatrix(cameraRotZ);
		let cameraMatrix = createMultipliedMatrix(cameraIdentityMatrix,distanceTranslationMatrix,offsetRotationX,offsetRotationZ,targetTranslationMatrix);
		if (isRelativeToTarget(target))
		{
			cameraMatrix = createMultipliedMatrix(cameraMatrix,targetMatrix);
			targetTranslationMatrix = createMultipliedMatrix(targetTranslationMatrix,targetMatrix);
		}
		if (isClipped(target))
		{
			let startX = targetTranslationMatrix.m41;
			let startY = targetTranslationMatrix.m42;
			let startZ = targetTranslationMatrix.m43;
			let endX = cameraMatrix.m41;
			let endY = cameraMatrix.m42;
			let endZ = cameraMatrix.m43;
			let checkBuildings = true;
			let checkVehicles = true;
			let checkPeds = true;
			let checkObjects = true;
			let checkDummies = false;
			let checkSeeThroughStuff = false;
			let ignoreSomeObjectsForCamera = true;
			let collisionsEnabled = AreEntityCollisionsEnabled(target);
			if (collisionsEnabled)
				SetEntityCollisionsEnabled(target,false);
			let [positionX,positionY,positionZ,normalX,normalY,normalZ,targetEntity] = ProcessLineOfSight(startX,startY,startZ,endX,endY,endZ,checkBuildings,checkVehicles,checkPeds,checkObjects,checkDummies,checkSeeThroughStuff,ignoreSomeObjectsForCamera);
			if (collisionsEnabled)
				SetEntityCollisionsEnabled(target,true);
			if (positionX != null)
			{
				//2019 Lucas was here!
				let Distance = 0.3;
				positionX += normalX*Distance;
				positionY += normalY*Distance;
				positionZ += normalZ*Distance;

				cameraMatrix.m41 = positionX;
				cameraMatrix.m42 = positionY;
				cameraMatrix.m43 = positionZ;
			}
		}
		if (isCameraEasing())
			cameraMatrix = applyCameraEase(cameraMatrix);
		SetPlaceableMatrix(camera,cameraMatrix);
		UpdateCamera(camera);
	}
	oldCameraTarget = target;
	OldPosition = (target != null) ? target.position : null;//2019 Lucas was here!
	return target != null;
}

addEventHandler("OnCameraProcess", (event) =>
{
	if(mouseCameraEnabled) {
		update();
		event.preventDefault();
	}
});

function toggleMouseCamera() {
	mouseCameraEnabled = !mouseCameraEnabled;
}