clientSide = triggerServerEvent and true or false
projectiles = {}
unitSteps = 0.5
projectileUpdateState = false

--[[ 

	Optimize constants access speed for this script

]]
local projectiles = projectiles
local clientSide = clientSide
local unitSteps = unitSteps

local function updateProjectiles ()
	
	local timeNow = getTickCount()
	local computeProjectile = computeProjectile
	for projectileIndex=#projectiles, 1, -1  do
		local projectileData = projectiles[projectileIndex]
		if clientSide then
			if not projectileData.disabled then
				computeProjectile(projectileData)
			elseif not isElement(projectileData.projectileElement) then -- < ... wait for the server to destroy the element ... >
				removeProjectileData (projectileData)
			end
		else
			computeProjectile(projectileData)
		end
	end
	
	if not clientSide then
		-- Prepare the data packets for each player
		local syncerUpdateCollection = {}
		
		for updateIndex=1, #projectileUpdateBuffer do
			local update = projectileUpdateBuffer[updateIndex].update
			local syncers = projectileUpdateBuffer[updateIndex].syncers
			for syncerIndex=1, #syncers do
				local syncer = syncers[syncerIndex]
				local updates = syncerUpdateCollection[syncer]
				if not updates then 
					updates = {}
					syncerUpdateCollection[syncer] = updates
				end
				updates[#updates + 1] = update
			end
		end
		
		for player, updates in pairs(syncerUpdateCollection) do
			triggerClientEvent(player, "onClientUpdateProjectile", resourceRoot, updates )
		end
		
		projectileUpdateBuffer = {}
	end
	
	if projectileUpdateState and #projectiles == 0 then
		projectileUpdateState = false
		if clientSide then
			removeEventHandler("onClientRender", root, updateProjectiles)
		else
			if isTimer(projectileUpdateTimer) then
				killTimer(projectileUpdateTimer)
			end
			projectileUpdateTimer = nil
		end
	end
end

do
	local findRotation, findPitch, createMatrix, getPositionFromMatrixOffset, mathFloor, mathMin, mathPi
	= findRotation, findPitch, createMatrix, getPositionFromMatrixOffset, math.floor, math.min, math.pi
	function computeProjectile (projectileData, computeOnly)

		local timeNow = getTickCount()
		
		local destroyProjectile = false
		
		local projectileElement = projectileData.projectileElement
		if clientSide and not isElement(projectileElement) then -- and timeNow >= projectileData.endTime or
			return removeProjectileData (projectileData)
		end
		
		local startTime = projectileData.startTime
		local endTime = projectileData.endTime
		local rotationSpeed = projectileData.rotationSpeed
		local unitStepIndex = projectileData.unitStepIndex
		
		local speed = projectileData.speed / 1000 -- speed / 1000 = travelDistance per milisecond
		
		local travelDistance = (timeNow - startTime) * speed
		
		local previousTravelDistance = projectileData.previousTravelDistance
		projectileData.previousTravelDistance = travelDistance
		
		local previousStep = mathFloor(previousTravelDistance / unitSteps)
		local startStep = previousStep
		local targetPosition = projectileData.targetPosition
		

		local loops = 0
		repeat
			
			if loops == 3000 then
				break
			end
			loops = loops + 1
			
			local currentStep = mathFloor(travelDistance / unitSteps)
			

			
			local projectilePosition = projectileData.position
			local projectileRotation = projectileData.rotation
		
			local rotX = projectileRotation.x
			local rotZ = projectileRotation.z
			local rotY = projectileRotation.y
			
			local previousStepPosition = projectileData.previousStepPosition
		
		
			--[[

			]]
			if previousStep ~= currentStep then -- and projectileData.theType == "seeking"
				local targetRotZ = (findRotation(previousStepPosition.x, previousStepPosition.y, targetPosition.x, targetPosition.y) + 180) --% 360
				if targetRotZ < 0 then
					targetRotZ = 360 - targetRotZ
				end
				
				local pitch = findPitch(previousStepPosition.x, previousStepPosition.y, previousStepPosition.z, targetPosition.x, targetPosition.y, targetPosition.z)
				local targetRotX = (pitch*(180/mathPi) + 180) % 360
			
			
				local rotationDistanceX = (rotX - targetRotX + 360) % 360;
				
				if (rotationDistanceX > 180) then
					rotationDistanceX = 360 - rotationDistanceX
					rotX = rotX + mathMin(rotationDistanceX, rotationSpeed)
				else

					rotX = rotX + -mathMin(rotationDistanceX, rotationSpeed)
				end
				
				if rotX < 0 then
					rotX = 360 + rotX
				end
				
				
				local rotationDistanceZ = (rotZ - targetRotZ + 360) % 360;
				if (rotationDistanceZ > 180) then
					rotationDistanceZ = 360 - rotationDistanceZ
					rotZ = rotZ + mathMin(rotationDistanceZ, rotationSpeed)
				else
					rotZ = rotZ + -mathMin(rotationDistanceZ, rotationSpeed)
				end
				
				if rotZ < 0 then
					rotZ = 360 + rotZ
				end
				
				
				projectileRotation.x = rotX
				projectileRotation.z = rotZ
				
			end
			
			
			--[[ 
				compute the next previousTravelDistance (since there could be multiple steps until the current travelDistance)
			]]
			
			
			local matrix = createMatrix(previousStepPosition.x, previousStepPosition.y, previousStepPosition.z, rotX, rotY, rotZ)
			
			local newVectorOffset
			
			
			if previousStep ~= currentStep then
				newVectorOffset = (projectileData.direction) * unitSteps

				projectileData.previousStepPosition = Vector3(getPositionFromMatrixOffset(createMatrix(previousStepPosition.x, previousStepPosition.y, previousStepPosition.z, rotX, rotY, rotZ), newVectorOffset.x, newVectorOffset.y, newVectorOffset.z))
				
				previousTravelDistance = (previousStep + 1) * unitSteps
				previousStep = (previousTravelDistance / unitSteps)
				
				if debugMode and projectileTestLine then
					projectileTestLine[#projectileTestLine + 1] = {projectileData.previousStepPosition.x, projectileData.previousStepPosition.y, projectileData.previousStepPosition.z, previousStepPosition.x, previousStepPosition.y, previousStepPosition.z, tocolor(255,255,255, 200)}
					if #projectileTestLine > 1000 then
						table.remove(projectileTestLine, 1)
					end
				end
			else
				newVectorOffset = (projectileData.direction) * (travelDistance % unitSteps)
			end
			
			
			local newX, newY, newZ = getPositionFromMatrixOffset(matrix, newVectorOffset.x, newVectorOffset.y, newVectorOffset.z)
			

			if not computeOnly then
				local creator = projectileData.creator
				if clientSide and getDistanceBetweenPoints3D(projectileData.position.x, projectileData.position.y, projectileData.position.z, getElementPosition(localPlayer)) < 250 then
					--[[
						and (not creator or creator == localPlayer) 
					]]

					local ignoreList = projectileData.ignoreList
					local hit, hitX, hitY, hitZ, hitElement = processLineOfSight (
						projectileData.position.x, projectileData.position.y, projectileData.position.z, 
						newX, newY, newZ, 
						true, -- checkBuildings
						true,  -- checkVehicles
						true, -- checkPlayers
						true, -- checkObjects
						false, -- checkDummies
						false, -- seeThroughStuff
						false, -- ignoreSomeObjectsForCamera
						false -- shootThroughStuff
					)

					if hit and not (ignoreList and ignoreList[hitElement]) then
						destroyProjectile = true
						if hitElement then
							projectileData.hitElement = hitElement
						end
						break
					end
				end
				
				
				if not clientSide and projectileData.closeImpact and getDistanceBetweenPoints3D(newX, newY, newZ, targetPosition.x, targetPosition.y, targetPosition.z) < projectileData.closeImpact then
					destroyProjectile = true
					break
				end
			end
			projectilePosition.x, projectilePosition.y, projectilePosition.z = newX, newY, newZ
			
			
		until previousStep >= currentStep

		if not computeOnly then
			if destroyProjectile then
				local hitElement = projectileData.hitElement
				projectileData.hitElement = nil
				detonateProjectile (projectileData, nil, nil, nil, hitElement)
			elseif clientSide then
				if projectileData.visualElement and isElement(projectileData.visualElement) then
					local visualElementOrientation = projectileData.visualElementOrientation
					if visualElementOrientation then
						setElementRotation(projectileData.visualElement, projectileData.rotation.x + visualElementOrientation.x, visualElementOrientation.y, projectileData.rotation.z + visualElementOrientation.z, "ZXY")
					else
						setElementRotation(projectileData.visualElement, projectileData.rotation.x, 0, projectileData.rotation.z, "ZXY")
					end
					setElementPosition(projectileData.visualElement, projectileData.position.x, projectileData.position.y, projectileData.position.z)
				end
				
				if debugMode then
					-- Show predicted target positions
					dxDrawLine3D(projectileData.previousStepPosition.x, projectileData.previousStepPosition.y, projectileData.previousStepPosition.z, projectileData.position.x, projectileData.position.y, projectileData.position.z, tocolor(255,0,0), 10)
				
					dxDrawLine3D(projectileData.previousStepPosition.x, projectileData.previousStepPosition.y, projectileData.previousStepPosition.z, targetPosition.x, targetPosition.y, targetPosition.z, tocolor(0,255,0))
				end
				
				local smokeElement = projectileData.smokeElement
				if not isElement(smokeElement) then
					smokeElement = createEffect("heli_dust", projectileData.position.x, projectileData.position.y, projectileData.position.z, 0,0,0, 300, true)
					projectileData.smokeElement = smokeElement
					--setEffectSpeed(smokeElement, 0.2)
					setElementParent(smokeElement, projectileElement)
				else
					setElementPosition(smokeElement, projectileData.position.x, projectileData.position.y, projectileData.position.z)
				end
				
				
			elseif projectileData.theType == "seeking"  and timeNow > projectileData.nextTargetCheck then
				projectileData.nextTargetCheck = timeNow + 1500
				local targetElement = projectileData.targetElement
				if isElement(targetElement) then
					local currentTargetPosition = Vector3(getElementPosition(targetElement))
					
					if (currentTargetPosition - targetPosition):getLength() > 1 then
						
						
						local currentLifeSpan = projectileData.lifeSpan - (timeNow - startTime)
						if currentLifeSpan > 0 then
						
							local velocity = Vector3(getElementVelocity(targetElement))
							local predictedTargetPosition = currentTargetPosition + (velocity * projectileData.positionPredictMultiplier)
							
							updateProjectile (projectileData, projectileElement, projectileData.position, currentLifeSpan, predictedTargetPosition, projectileData.rotation)
							
						end
					end
				end
			end
		end
	end
end


function updateProjectile (projectileData, projectileElement, position, lifeSpan, targetPosition, rotation)

	local timeNow = getTickCount()
	projectileData.startTime = timeNow
	
	projectileData.lifeSpan = lifeSpan
	projectileData.endTime = timeNow + lifeSpan
	
	projectileData.previousTravelDistance = 0
	
	--[[
		extra sync
		rotation
	]]
	if clientSide then
		projectileData.position = tableToVector(position)
		projectileData.targetPosition = tableToVector(targetPosition)
		projectileData.disabled = nil
		projectileData.rotation = tableToVector(rotation)
	else
		projectileData.position = position
		projectileData.targetPosition = targetPosition
		projectileData.previousStepPosition = Vector3(position.x, position.y, position.z)
		
		validateProjectileSyncers (projectileData)
		
		local updateVersion = projectileData.updateVersion + 1
		projectileData.updateVersion = updateVersion
		
		projectileUpdateBuffer[#projectileUpdateBuffer + 1] = {
			syncers = projectileData.syncers,
			update = {projectileElement, updateVersion, vectorToTable(position), lifeSpan, vectorToTable(targetPosition), vectorToTable(rotation)}
		}
	end
	
end



function detonateProjectile (projectileData, x, y, z, hitElement)
	local projectilePosition = projectileData.position

	if clientSide then
		if not projectileData.disabled then
			if isElement( projectileData.projectileElement) then
				triggerServerEvent("onServerCustomProjectileDestroy", projectileData.projectileElement, projectilePosition.x, projectilePosition.y, projectilePosition.z, projectileData.updateVersion, hitElement)
			end
			projectileData.disabled = true
		end
	else
		if not x then
			computeProjectile (projectileData, true)
			local projectilePosition = projectileData.position
			x, y, z = projectilePosition.x, projectilePosition.y, projectilePosition.z
		end
		
		
		destroyElement(projectileData.projectileElement)

		local impactDamage = projectileData.impactDamage
		if impactDamage then

			if hitElement and isElement(hitElement) then
				
				local elementType = getElementType(hitElement) 
				if elementType == "player" or elementType == "ped" then
					local newHealth = getElementHealth(hitElement) - impactDamage
					if newHealth > 0 then
						setElementHealth(hitElement, newHealth)
					else
						killPed(hitElement, projectileData.creator, 19)
					end
				elseif elementType == "vehicle" then
					setElementHealth(hitElement, math.max(getElementHealth(hitElement) - impactDamage, 0))
				end
			end
		end

		if projectileData.explosionType then
			createExplosion(x, y, z, projectileData.explosionType, projectileData.creator)
		end

		if projectileData.timer then
			if isTimer(projectileData.timer) then
				killTimer(projectileData.timer)
			end
			projectileData.timer = nil
		end
	
		projectileData.visualElement = nil
		removeProjectileData (projectileData)
	end
end

function removeProjectileData (projectileData)
	for i=1, #projectiles do
		if projectiles[i] == projectileData then
			table.remove(projectiles, i)
			return true
		end
	end
	return false
end



function initializeCustomProjectile (...) -- projectileData, 
	if clientSide then
		projectileData = arg[1]
	else
		
		local creator, theType, startX, startY, startZ, targetX, targetY, targetZ, rotX, rotY, rotZ, targetElement, dimension, projectileModel, ignoreElementsList, extraProperties = arg[1], arg[2], arg[3], arg[4], arg[5], arg[6], arg[7], arg[8], arg[9], arg[10], arg[11], arg[12], arg[13], arg[14], arg[15], arg[16]

		if not extraProperties then
			extraProperties = {}
		end

		local projectileElement = createElement("customProjectile")
		setElementPosition(projectileElement, startX, startY, startZ)
		setElementParent(projectileElement, projectileParentElement)
			
		
		
		local visualElement = createObject(projectileModel, 10000, 10000, 10000, 0, 0, 0, false)
		setElementFrozen(visualElement, true)
		setElementCollisionsEnabled(visualElement, false)
		setElementParent(visualElement, projectileElement)
		
		local customBlip = extraProperties.customBlip
		local blip
		if customBlip then
			blip = createBlipAttachedTo ( visualElement, unpack(customBlip) )
		else
			if targetElement then
				blip = createBlipAttachedTo ( visualElement, 0, 1, 255, 0, 0, 255, 100, 300 )
			end
		end
		if blip then
			setElementParent(blip, visualElement)
		end

		local projectilePosition = Vector3(startX, startY, startZ)
		
		local targetPosition = Vector3(targetX, targetY, targetZ)
		
		projectileData = {
			theType = theType,
			projectileElement = projectileElement,
			position = vectorToTable(projectilePosition),
			rotation = {
					x = rotX,
					y = rotY,
					z = rotZ
			},
			targetElement = targetElement,
			direction = vectorToTable(Vector3(0, 1, 0)),
			lifeSpan = extraProperties.lifeSpan or 6000,
			targetPosition = vectorToTable(targetPosition),
			speed = extraProperties.speed or 90,
			rotationSpeed = unitSteps * 3,
			visualElement = visualElement,
			
			--[[
			-- 345 (missile default) -- works		
			-- 362 (minigun)
			-- 1636 (rc bomb) -- works
			-- 3786 (missile big) -- bugged
			-- 3790 (missle small) -- bugged
			]]
		}

		if ignoreElementsList then
			local ignoreList = {}
			projectileData.ignoreList = ignoreList
			for ignoreElementIndex=1, #ignoreElementsList do
				ignoreList[ignoreElementsList[ignoreElementIndex]] = true
			end
		end

		--[[
			Depending on the projectile model, you might need to rotate the element a bit
		]]
		if extraProperties.visualElementOrientation and type(extraProperties.visualElementOrientation) == "table" then
			local visualElementOrientation = extraProperties.visualElementOrientation
			visualElementOrientation.x = tonumber(visualElementOrientation.x) or 0
			visualElementOrientation.y = tonumber(visualElementOrientation.y) or 0
			visualElementOrientation.z = tonumber(visualElementOrientation.z) or 0
			projectileData.visualElementOrientation = visualElementOrientation 
		end



		

		
		projectileData.previousStepPosition = vectorToTable(Vector3(projectileData.position.x, projectileData.position.y, projectileData.position.z))
		
		if not rotX then
			local pitch = findPitch(projectileData.previousStepPosition.x, projectileData.previousStepPosition.y, projectileData.previousStepPosition.z, projectileData.targetPosition.x, projectileData.targetPosition.y, projectileData.targetPosition.z)
			projectileData.rotation.x = (pitch*(180/math.pi) + 180) % 360
		end

		if not rotY then
			projectileData.rotation.y = 0
		end

		if not rotZ then
			projectileData.rotation.z =  (findRotation(projectileData.previousStepPosition.x, projectileData.previousStepPosition.y, projectileData.targetPosition.x, projectileData.targetPosition.y) + 180)
		end

		local syncers = getLoadedPlayers()
		
		filterElementsBasedOnDistanceFromPoint(syncers, projectileData.position.x, projectileData.position.y, projectileData.position.z, 1000)
		
		if not dimension then
			dimension = 0
		end
		
		filterElementsBasedOnDimension(syncers, dimension)
		
		
		if #syncers > 0 then
			triggerClientEvent(syncers, "onClientCreateCustomProjectile",  projectileElement, projectileData)
		end
		
		-- serverside only START
		projectileData.timer = setTimer(destroyProjectileByTimer, projectileData.lifeSpan, 1, projectileElement)
		projectileData.nextTargetCheck = 0
		projectileData.syncers = syncers
		projectileData.dimension = dimension
		projectileData.explosionType = 2
		projectileData.closeImpact = 1
		projectileData.impactDamage = tonumber(extraProperties.impactDamage) or nil
		projectileData.positionPredictMultiplier = tonumber(extraProperties.positionPredictMultiplier) or 60
		


		if extraProperties.explosionType ~= nil then
			--[[
				The value false, can be used to disable explosions
			]] 
			local explosionType = extraProperties.explosionType 
			if explosionType == false then
				projectileData.explosionType = explosionType
			else
				projectileData.explosionType = tonumber(explosionType) or 2
			end
		end

		if extraProperties.closeImpact ~= nil then
			local closeImpact = extraProperties.closeImpact 
			if closeImpact == false then
				projectileData.closeImpact = closeImpact
			else
				projectileData.closeImpact = tonumber(closeImpact) or 1
			end
		end


		
		-- serverside only END
	end
	
	-- set the start/end time for clientside and serverside
	local timeNow = getTickCount()
	projectileData.startTime = timeNow
	projectileData.endTime = timeNow + projectileData.lifeSpan
	projectileData.updateVersion = 0
	
	projectileData.previousTravelDistance = 0
	
	-- since we can't send over vectors...
	projectileData.position = tableToVector(projectileData.position)
	projectileData.direction = tableToVector(projectileData.direction)
	projectileData.previousStepPosition = tableToVector(projectileData.previousStepPosition)
	projectileData.targetPosition = tableToVector(projectileData.targetPosition)
	
	projectiles[#projectiles + 1] = projectileData
	
	if clientSide then
		local launchAudio = playSound3D("audio/launch.wav", projectileData.position, false)
		if launchAudio then
			setSoundMinDistance (launchAudio, 50 )
			setSoundMaxDistance (launchAudio, 600 )
			setElementParent(launchAudio, projectileData.projectileElement)
		end
		local missileAudio = playSound3D("audio/missile.wav", projectileData.position, true)
		if missileAudio then
			setSoundMinDistance (missileAudio, 20 )
			setSoundMaxDistance (missileAudio, 100 )
			setSoundVolume(missileAudio, 3)
			setElementParent(missileAudio, projectileData.visualElement) -- attach to visual element to make use of the propagation of setElementPosition.
		end
	end
	
	if not projectileUpdateState then
		projectileUpdateState = true
		if clientSide then
			addEventHandler("onClientRender", root, updateProjectiles)
		else
			projectileUpdateTimer = setTimer(updateProjectiles, 100, 0)
		end
		
	end
	
	return projectileData
end



function findProjectileData (element)
	for i=1, #projectiles do
		local projectileData = projectiles[i]
		if projectileData.projectileElement == element then
			return projectileData
		end
	end
	return false
end






