
turrets = {
	list = {},
	byColshape = {},
	byTurretElement = {},
	turretParentElement = createElement("turretParentElement", "turretParentElement"),
	updateActions = function ()
		
		local timeNow = getTickCount()
		local turretList = turrets.list
		local anyTargets = false
		for i=1, #turretList do
			local turret = turretList[i]
			if not turret.broken and turret.enabled then
				local turretElement = turret.turretElement
				local dimension = getElementDimension(turretElement)
				
				local targetElement, targetData
				local targetList = turret.targetList
				
				local loops = 0 -- extra security

				--[[

					Sort targets by distance

				]]
				targetList:sort(function (a, b) 
					return getDistanceBetweenPoints3D(turret.x, turret.y, turret.z, getElementPosition(a.key)) < getDistanceBetweenPoints3D(turret.x, turret.y, turret.z, getElementPosition(b.key))
				end)
				
				--[[

					Validate targets

				]]
				repeat
					targetElement, targetData = targetList:getNext()
					if not targetElement then
						break
					end

					if not targetData.inlineOfSight then
						targetElement = nil
					end

					if targetElement and (getElementDimension(targetElement) ~= dimension or isPedDead(targetElement)) then
						targetList:removeData(targetElement)
						if isPlayerLoaded(player) then
							triggerClientEvent(player, "turretInlineOfSightChecking", player, turret.id, nil, false)
						end
						
						targetElement = nil
					end
					loops = loops + 1
				until targetElement or loops > 100


				if targetElement then
					--[[

						The turret has found it's target

					]]
					turret.hasTarget = true
					anyTargets = true
					local projectileTarget = getPedOccupiedVehicle(targetElement) or targetElement
					local targetPositionX, targetPositionY, targetPositionZ = getElementPosition(projectileTarget)
					
					
					stopObject ( turret.samsElement )

					local _, _, turretRotZ = getElementRotation(turret.samsElement)
					local targetRotZ = findRotation ( turret.x, turret.y , targetPositionX, targetPositionY )
					
					local rotationDistanceZ = (turretRotZ - targetRotZ + 360) % 360;
					local rotationDifference
					if (rotationDistanceZ > 180) then
						rotationDifference = 360 - rotationDistanceZ
					else
						rotationDifference = -rotationDistanceZ
					end

					--[[

						Make the turret face it's target

					]]
					if math.abs(rotationDifference) > 5 then
						local turnRotationTimer = math.abs(rotationDifference) * 10
						moveObject (turret.samsElement, turnRotationTimer, turret.x, turret.y, turret.z, 0, 0, rotationDifference)
					end

					--[[

						If the turret is facing it's target,
						and it is ready to fire
						>
						Fire the missile
						------------------->

					]]
					if math.abs(rotationDifference) < 45 and timeNow > turret.nextFireTime then
						turret.nextFireTime = timeNow + 3000
						local startX, startY, startZ = getPositionFromElementOffset(turret.samsElement, 0, 2, 3)
						
						local velocity = Vector3(getElementVelocity(projectileTarget))
						local predictedTargetPosition = Vector3(targetPositionX, targetPositionY, targetPositionZ) + (velocity * 30)

				
						local projectileData = createCustomProjectile (nil, "seeking", startX, startY, startZ, predictedTargetPosition.x, predictedTargetPosition.y, predictedTargetPosition.z, 150, nil, nil, projectileTarget, 0, turret.projectileModel, nil,
							turret.projectileExtraProperties
						)
						
						
					end
				else
					--[[

						If the turret did have a target in the past, but does not have one now.
						>
						Stop the turret

					]]
					if turret.hasTarget then
						turret.hasTarget =  nil
						stopObject ( turret.samsElement )
					end
					--[[

						Double check if the targetlist is empty.
						If empty.
						>
						Rotate back and disable.

					]]
					if not targetList:getNext() then
						turret:rotateBack()
						turret:setEnabled(false)
					end
				end
			end
		end
		if not anyTargets then
			local updateActionsTimer = turrets.updateActionsTimer
			if updateActionsTimer  then
				if isTimer(turrets.updateActionsTimer) then
					killTimer(turrets.updateActionsTimer)
				end
				turrets.updateActionsTimer = nil
			end
		end
	end,
	methods = {
		{
			key = "setEnabled",
			func = function (self, state)

				if state then
					if not turrets.updateActionsTimer then
						turrets.updateActionsTimer = setTimer(turrets.updateActions, 500, 0)
					end
				end
				--[[
					Enable and disable turrets
				]]
				self.enabled = state
			end
		},
		{
			key = "rotateBack",
			func = function (self)
				
				--[[

					Rotate the turret back 
				
				]]
				stopObject ( self.samsElement )
				local _, _, turretRotZ = getElementRotation(self.samsElement)
				local targetRotZ = self.rotation
				
				local rotationDistanceZ = (turretRotZ - targetRotZ + 360) % 360;
				local rotationDifference
				if (rotationDistanceZ > 180) then
					rotationDifference = 360 - rotationDistanceZ
				else
					rotationDifference = -rotationDistanceZ
				end
					
				
				local turnRotationTimer = math.abs(rotationDifference) * 10
				moveObject (self.samsElement, turnRotationTimer, self.x, self.y, self.z, 0, 0, rotationDifference)
			end
		}
	},
	
}



local turretColshapeParent = createElement("turretColshapeParent")


turrets.create = function (self, theType, x, y, z, properties)
	local turret
	if theType == "anti-air" then
		turretElement = createElement("turret")
		z = z - 0.15

		setElementPosition(turretElement, x, y, z)
		setElementParent(turretElement, self.turretParentElement)

		--[[

			Base element

		]]
		
		local baseElement = createObject(3277, x, y, z - 0.05)
		setElementParent(baseElement, turretElement)
		
		-- set lowLOD
		local baseElementLowLOD = createObject(3277, x, y, z - 0.2, 0,0,0, true)
		setLowLODElement ( baseElement, baseElementLowLOD ) -- swap the models based on streamed in/out
		setElementParent(baseElementLowLOD, baseElement)
		setElementCollisionsEnabled(baseElementLowLOD, false) -- just in case
		
		setElementDoubleSided (baseElement, true )
		setElementDoubleSided (baseElementLowLOD, true )
		

		--[[

			Sams element

		]]

		local samsElement = createObject(3267, x, y, z)
		setElementParent(samsElement, turretElement)
		setElementData(samsElement, "sams", true)
		setElementFrozen(samsElement, true)

		-- set lowLOD
		local samsLowLOD = createObject(3267, x, y, z, 0,0,0, true)
		setLowLODElement ( samsElement, samsLowLOD ) -- swap the models based on streamed in/out
		setElementParent(samsLowLOD, turretElement)
		
		attachElements(samsLowLOD, samsElement) -- the non lowLOD element does rotate

		setElementCollisionsEnabled(samsLowLOD, false) -- just in case
		

		--[[

			Detection area

		]]
		
		local colshape = createColTube ( x, y, z + 2, 400, 300)
		setElementParent(colshape, turretColshapeParent)
		
		--[[

			Blips

		]]

		local blipIconDefault = 23
		local blipIconBroken = 27
		
		local blip = createBlip (x, y, z, blipIconDefault, 1, 255, 255, 255, 255, 0, 500)
		setElementParent(blip, turretElement)
		

		--[[

			The container that keeps everything together

		]]

		turret = {
			id = tostring(turretElement) .. getTickCount(),
			x = x,
			y = y,
			z = z,
			rotation = 0,
			health = 1000,
			maxHealth = 1000,
			nextFireTime = 0,
			enabled = false,
			samsElement = samsElement,
			turretElement = turretElement,
			blip = blip,
			colshape = colshape,
			targetList = dataManagerConstructor:new(),
			broken = false,
			blipIcons = {
				default = blipIconDefault,
				broken = blipIconBroken
			},
			projectileModel = 345,
			projectileExtraProperties = {
				explosionType = 7, -- 7 = big heli explosion
				speed = 90, -- fast missile
				lifeSpan = 15000
			}
		}
		
		--[[
			Apply/Modify properties from other scripts
		]]
		if properties then
			for propertyIndex=1, #properties do
				local property = properties[propertyIndex]
				if property.key then
					turret[property.key] = property.value 
				end
			end
		end

		setElementRotation(samsElement, 0,0, (tonumber(turret.rotation) or 0) % 360 )
	end
	if turret then
	
		local turretList = turrets.list
		turretList[#turretList + 1] = turret

		--[[
			Make referce points for data can be accessed through multiple ways...
		]]
		turrets.byColshape[turret.colshape] = turret
		turrets.byTurretElement[turret.turretElement] = turret
		
		local methods = self.methods
		-- The following can also be done with just a meta table, but that would make the methods slower. Making an instance is just something you do once.
		for i=1, #methods do
			local method = methods[i]
			turret[method.key] = method.func
		end
		
		setElementCallPropagationEnabled ( turret.turretElement, false)
		
		setElementData(turret.turretElement, "theType", theType)
		
		setElementCallPropagationEnabled ( turret.turretElement, true)
		
		return turret
	end
	return false
end

--[[

	Destroy a turret

]]

turrets.destroy = function  (self, turret)
	local turretList = self.list
	for turretIndex=1, #turretList do
		if turretList[turretIndex] == turret then
			table.remove(turrets, turretIndex)
			break
		end
	end
	local colshape = turret.colshape
	if isElement(colshape) then
		destroyElement(colshape)
		self.byColshape[colshape] = nil
		self.byTurretElement[self.turretElement] = nil
	end
end

turrets.destroyBy_ID = function  (self, id)
	local turretList = self.list

	for turretIndex=1, #turretList do
		if turretList[turretIndex].id == id then

			local turret = turretList[turretIndex]
			
			destroyElement(turret.turretElement)

			table.remove(turretList, turretIndex)

			local colshape = turret.colshape
			if isElement(colshape) then
				destroyElement(colshape)
				self.byColshape[colshape] = nil
				self.byTurretElement[turret.turretElement] = nil
			end
			return true
		end
	end
	return false
end





function isPlayerTurretOwner (player, owners)
	if owners then
		local account = getPlayerAccount(player)
		if not isGuestAccount(account) then
			local accountName = getAccountName(account)
			if accountName and owners[accountName] then
				return true
			end
		end
	end
	return false
end


--[[

	Turret target detection

]]

local aircraftVehicleTypes = {
	["Plane"] = true,
	["Helicopter"] = true
}

addEventHandler("onColShapeHit", turretColshapeParent, 
function (player, matchingDimension) 
	if matchingDimension and getElementType(player) == "player" and not isPedDead(player) then
		local vehicle = getPedOccupiedVehicle(player)
		if vehicle and not isVehicleOnGround ( vehicle ) and aircraftVehicleTypes[getVehicleType(vehicle)] or doesPedHaveJetPack(player) then
			local turret = turrets.byColshape[source]
			if turret and not turret.broken then
				if not isPlayerTurretOwner(player, turret.owners) then
					checkPlayerTurretTarget (turret, player)
					turret:setEnabled(true)
				end
			end
		end
	end
end)

function checkPlayerTurretTarget (turret, player)
	local targetData = turret.targetList:getData(player)
	if not targetData then
		targetData = {
			inlineOfSight = false
		}
		
		if isPlayerLoaded(player) then
			triggerClientEvent(player, "turretInlineOfSightChecking", player, turret.id, turret.turretElement, true)
		else
			targetData.hasNotBeenInitialize = true
		end
		turret.targetList:setData(player, targetData)
		
	else
		--[[
			For the slow loaders
		]] 
		if targetData.hasNotBeenInitialize and isPlayerLoaded(player) then
			triggerClientEvent(player, "turretInlineOfSightChecking", player, turret.id, turret.turretElement, true)
			targetData.hasNotBeenInitialize = nil
		end
	end
end


function setPlayerInTurretInlineOfSight (hasChangedStatus)
	local player = client

	local turretList = turrets.list

	for hasChangedStatusIndex = 1, #hasChangedStatus do
		local statusChange = hasChangedStatus[hasChangedStatusIndex]
		
		local turret_ID = statusChange.turret_ID
		local status = statusChange.status

		local turret
		for turretIndex=1, #turretList do
			if turretList[turretIndex].id == turret_ID then
				turret = turretList[turretIndex]
			end
		end
		if turret then
			local targetData = turret.targetList:getData(player)
			if targetData then
				targetData.inlineOfSight = status
			end
		end
	end
end

addEvent("onPlayerInTurretInlineOfSight", true)
addEventHandler("onPlayerInTurretInlineOfSight", resourceRoot, setPlayerInTurretInlineOfSight, false)


addEventHandler("onColShapeLeave", turretColshapeParent, 
function (player, matchingDimension) 
	if matchingDimension and getElementType(player) == "player" then
		local turret = turrets.byColshape[source]
		if turret and not turret.broken then
			
			
			if turret.targetList:getData(player) then
				if isPlayerLoaded(player) then
					triggerClientEvent(player, "turretInlineOfSightChecking", player, turret.id, nil, false)
				end
				turret.targetList:removeData(player)
				
				if not turret.targetList:getNext() then
					turret:rotateBack()
					turret:setEnabled(false)
				end
			end
		end
	end
end)


local function checkAllTurrets ()
	--[[
		This loop is required to fix issues that are related to:
		- When a player is inside of a colshape, but does not have met the requirements yet to trigger the turret.
		> When the requirements are met and the player is inside of the colshape, this loop will make sure the detection still works.
	]]
	
	local turretList = turrets.list
	for turretIndex=1, #turretList do
		local turret = turretList[turretIndex]
		if not turret.broken then
			local turretDimension = getElementDimension(turret.turretElement)
			local colshape = turret.colshape
			local players = getElementsWithinColShape ( colshape, "player" )
			
			local targetList = turret.targetList
			
			for playerIndex=1, #players do
				local player = players[playerIndex]
				if getElementDimension(player) == turretDimension and not isPedDead(player) then
					local vehicle = getPedOccupiedVehicle(player)
					if vehicle and not isVehicleOnGround ( vehicle ) and aircraftVehicleTypes[getVehicleType(vehicle)] or doesPedHaveJetPack(player) then
						if not isPlayerTurretOwner(player, turret.owners) then
							checkPlayerTurretTarget (turret, player)
							turret:setEnabled(true)
						elseif turret.targetList:getData(player) then
							turret.targetList:removeData(player)
							if isPlayerLoaded(player) then
								triggerClientEvent(player, "turretInlineOfSightChecking", player, turret.id, nil, false)
							end
						end
					elseif turret.targetList:getData(player) then
						turret.targetList:removeData(player)
						if isPlayerLoaded(player) then
							triggerClientEvent(player, "turretInlineOfSightChecking", player, turret.id, nil, false)
						end
					end
				elseif turret.targetList:getData(player) then
					turret.targetList:removeData(player)
					if isPlayerLoaded(player) then
						triggerClientEvent(player, "turretInlineOfSightChecking", player, turret.id, nil, false)
					end
				end
			end
			if not turret.targetList:getNext() then
				turret:rotateBack()
				turret:setEnabled(false)
			end
		end
	end
end
setTimer(checkAllTurrets, 5000, 0)



function removePlayerFromAllTurretTargetLists (player) 
	local turretList = turrets.list
	
	local isCheckingInlineOfSight = false

	for i=1, #turretList do
		local turret = turretList[i]
		if turret.targetList:getData(player) then
			turret.targetList:removeData(player)
			isCheckingInlineOfSight = true
		end
	end
	if eventName == "onPlayerWasted" and isCheckingInlineOfSight and isPlayerLoaded(player) then
		triggerClientEvent(player, "turretInlineOfSightChecking", player)
	end
end

addEventHandler("onPlayerQuit", root, 
function () 
	removePlayerFromAllTurretTargetLists(source)
end)

addEventHandler("onPlayerWasted", root, 
function () 
	removePlayerFromAllTurretTargetLists(source)
end)


--[[

	All serverside code related to destroying and restoring a turret

]]

local function restoreTurret (turretElement)
	if isElement(turretElement) then
		local turret = turrets.byTurretElement[turretElement]
		if turret then
			turret.health = turret.maxHealth
			turret.broken = false
			turret.restoreTimer = false
			
			local samsElement = turret.samsElement
			if isElement(samsElement) then
				setElementCollisionsEnabled(samsElement, true)
				setElementAlpha(samsElement, 255)
				local lowLOD = getAttachedElements ( samsElement )[1]
				if lowLOD then
					setElementAlpha(lowLOD, 255)
				end
			end
			
			local blip = turret.blip
			if isElement(blip) then
				setBlipIcon ( blip, turret.blipIcons.default )
			end
			
			local colshape = turret.colshape
			if isElement(colshape) then -- check for target players
				local players = getElementsWithinColShape ( colshape, "player")
				if #players > 0 then
					local turretDimension = getElementDimension(turretElement)
					local targetList = turret.targetList
					for i=1, #players do
						local player = players[i]
						if getElementDimension(player) == turretDimension and not isPedDead(player) then
							if not isPlayerTurretOwner(player, turret.owners) then
								checkPlayerTurretTarget (turret, player)
								turret:setEnabled(true)
							end
						end
					end
				end
			end
		end
	end
end


addEvent("onTurretDamage", true)
addEventHandler("onTurretDamage", turrets.turretParentElement, 
function (loss, attacker) 

	if isElement(source) and attacker == client then	
		local turret = turrets.byTurretElement[source]
		if turret then
			
			local health = turret.health
			local newHealth = health - loss
			if newHealth < 0 then
				newHealth = 0
			end

			turret.health = newHealth
			if newHealth == 0 and not turret.broken then
				turret:setEnabled(false)
				local samsElement = turret.samsElement
				if isElement(samsElement) then
					setElementCollisionsEnabled(samsElement, false)
					setElementAlpha(samsElement, 0)
					local lowLOD = getAttachedElements ( samsElement )[1]
					if lowLOD then
						setElementAlpha(lowLOD, 0)
					end
				end

				local blip = turret.blip
				if isElement(blip) then
					setBlipIcon ( blip, turret.blipIcons.broken )
				end

				turret.broken = true
				turret.targetList:clear()
				createExplosion( turret.x, turret.y, turret.z, 0, client)
				turret.restoreTimer = setTimer(restoreTurret, 120000, 1, source)
			end
		end
	end
end)


