FSpectate = {}

local stopSpectating, startFreeRoam
local isSpectating = false
local specEnt
local thirdperson = true
local isRoaming = false
local roamPos
local roamVelocity = Vector(0)
local thirdPersonDistance = 100

function FSpectate.getSpecEnt()
	if isSpectating and !isRoaming then
		return IsValid(specEnt) and specEnt or nil
	else
		return nil
	end
end

surface.CreateFont("UiBold", {
	size = 16,
	weight = 800,
	antialias = true,
	shadow = false,
	font = "Verdana"
})

local function getThirdPersonPos(ent)
	local aimvector = LocalPlayer():GetAimVector()
	local startPos = ent:IsPlayer() and ent:GetShootPos() or ent:LocalToWorld(ent:OBBCenter())
	local endpos = startPos - aimvector * thirdPersonDistance
	local tracer = {
		start = startPos,
		endpos = endpos,
		filter = specEnt
	}
	local trace = util.TraceLine(tracer)
	return trace.HitPos + trace.HitNormal * 10
end

local view = {}
local function getCalcView()
	if !isRoaming then
		if thirdperson then
			view.origin = getThirdPersonPos(specEnt)
			view.angles = LocalPlayer():EyeAngles()
		else
			view.origin = specEnt:IsPlayer() and specEnt:GetShootPos() or specEnt:LocalToWorld(specEnt:OBBCenter())
			view.angles = specEnt:IsPlayer() and specEnt:EyeAngles() or specEnt:GetAngles()
		end
		roamPos = view.origin
		view.drawviewer = false
		return view
	end
	view.origin = roamPos
	view.angles = LocalPlayer():EyeAngles()
	view.drawviewer = true
	return view
end

local function specCalcView(ply, origin, angles, fov)
	if !IsValid(specEnt) and !isRoaming then
		startFreeRoam()
		return
	end
	view = getCalcView()
	if IsValid(specEnt) then
		specEnt:SetNoDraw(!thirdperson)
	end
	return view
end

local function findNearestObject()
	local aimvec = LocalPlayer():GetAimVector()
	local fromPos = !isRoaming and IsValid(specEnt) and specEnt:EyePos() or roamPos
	local lookingAt = util.QuickTrace(fromPos, aimvec * 5000, LocalPlayer())
	local ent = lookingAt.Entity
	if IsValid(ent) then return ent end
	local foundPly, foundDot = nil, 0
	for _, ply in ipairs(player.GetAll()) do
		if !IsValid(ply) or ply == LocalPlayer() then continue end
		local pos = ply:GetShootPos()
		local dot = (pos - fromPos):GetNormalized():Dot(aimvec)
		if dot < 0.97 then continue end
		if dot < foundDot then continue end
		local trace = util.QuickTrace(fromPos, pos - fromPos, ply)
		if trace.Hit then continue end
		foundPly, foundDot = ply, dot
	end
	return foundPly
end

local function spectateLookingAt()
	local obj = findNearestObject()
	if !IsValid(obj) then return end
	isRoaming = false
	specEnt = obj
	net.Start("FSpectateTarget")
		net.WriteEntity(obj)
	net.SendToServer()
end

local keysList = {
	[IN_FORWARD] = "FORWARD",
	[IN_BACK] = "BACK",
	[IN_MOVELEFT] = "MOVELEFT",
	[IN_MOVERIGHT] = "MOVERIGHT",
	[IN_SPEED] = "SPEED",
	[IN_WALK] = "WALK",
	[IN_DUCK] = "DUCK"
}

local keysDown = {}
local function keyOverride(ply, key)
	if keysDown["USE"] == true then
		if keysDown[keysList[key]] then
			keysDown[keysList[key]] = false
		end
	end
end

local function specBinds(ply, bind, pressed)
	local pressing = input.IsKeyDown(KEY_E)
	local key = input.LookupBinding(bind)
	if !pressing and bind == "+jump" then
		stopSpectating()
		return true
	elseif !pressing and bind == "+reload" and pressed then
		local pos = getCalcView().origin - Vector(0, 0, 64)
		RunConsoleCommand("FTPToPos", string.format("%d, %d, %d", pos.x, pos.y, pos.z), string.format("%d, %d, %d", roamVelocity.x, roamVelocity.y, roamVelocity.z))
		stopSpectating()
		return true
	elseif !pressing and bind == "+attack" and pressed then
		if !isRoaming then
			startFreeRoam()
		else
			spectateLookingAt()
		end
		return true
	elseif !pressing and bind == "+attack2" and pressed then
		if isRoaming then
			roamPos = roamPos + LocalPlayer():GetAimVector() * 500
			return true
		end
		thirdperson = !thirdperson
		return true
	elseif !pressing and !isRoaming and thirdperson and (key == "MWHEELDOWN" or key == "MWHEELUP") then
		thirdPersonDistance = thirdPersonDistance + 10 * (key == "MWHEELDOWN" and 1 or -1)
		return true
	else
		local keybind = string.lower(string.match(bind, "+([a-z A-Z 0-9]+)") or "")
		if !keybind or keybind == "" or keybind == "showscores" or string.find(bind, "messagemode") then return end
		keysDown[keybind:upper()] = pressed
		if !pressing and keybind != "use" then
			return true
		end
	end
end

local LineMat = Material("cable/new_cable_lit")
local linesToDraw = {}
local function lookingLines()
	if !linesToDraw[0] then return end
	render.SetMaterial(LineMat)
	cam.Start3D(view.origin, view.angles)
		for i = 0, #linesToDraw, 3 do
			render.DrawBeam(linesToDraw[i], linesToDraw[i + 1], 4, 0.01, 10, linesToDraw[i + 2])
		end
	cam.End3D()
end

local function gunpos(ply)
	local wep = ply:GetActiveWeapon()
	if !wep:IsValid() then return ply:EyePos() end
	local att = wep:GetAttachment(1)
	if !att then return ply:EyePos() end
	return att.Pos
end

local function specThink()
	local ply = LocalPlayer()
	local pls = player.GetAll()
	local lastPly = 0
	local skip = 0
	for i = 0, #pls - 1 do
		local p = pls[i + 1]
		if !IsValid(p) then continue end
		if !isRoaming and p == specEnt and !thirdperson then skip = skip + 3 continue end
		local tr = p:GetEyeTrace()
		local sp = gunpos(p)
		local pos = i * 3 - skip
		linesToDraw[pos] = tr.HitPos
		linesToDraw[pos + 1] = sp
		linesToDraw[pos + 2] = team.GetColor(p:Team())
		lastPly = i
	end
	for i = #linesToDraw, lastPly * 3 + 3, -1 do linesToDraw[i] = nil end
	if !isRoaming or input.IsKeyDown(KEY_E) then return end
	local roamSpeed = 1000
	local aimVec = ply:GetAimVector()
	local direction
	local frametime = RealFrameTime()
	if keysDown["FORWARD"] then
		direction = aimVec
	elseif keysDown["BACK"] then
		direction = -aimVec
	end
	if keysDown["MOVELEFT"] then
		local right = aimVec:Angle():Right()
		direction = direction and (direction - right):GetNormalized() or -right
	elseif keysDown["MOVERIGHT"] then
		local right = aimVec:Angle():Right()
		direction = direction and (direction + right):GetNormalized() or right
	end
	if keysDown["SPEED"] then
		roamSpeed = 2500
	elseif keysDown["WALK"] or keysDown["DUCK"] then
		roamSpeed = 300
	end
	roamVelocity = (direction or Vector(0, 0, 0)) * roamSpeed
	roamPos = roamPos + roamVelocity * frametime
end

local uiForeground, uiBackground = Color(240, 240, 255, 255), Color(20, 20, 20, 120)
local red = Color(255, 0, 0, 255)
local function drawHelp()
	local scrHalfH = math.floor(ScrH() / 2)
	draw.WordBox(2, 10, scrHalfH, "Linksklick: Spieler (de-)selektieren", "UiBold", uiBackground, uiForeground)
	draw.WordBox(2, 10, scrHalfH + 20, isRoaming and "Rechtsklick: Schnell vorwärts bewegen" or "Rechtsklick: Third Person umschalten", "UiBold", uiBackground, uiForeground)
	draw.WordBox(2, 10, scrHalfH + 40, "Springen: Aufhören, zu zuschauen", "UiBold", uiBackground, uiForeground)
	draw.WordBox(2, 10, scrHalfH + 60, "Nachladen: Aufhören und teleportieren", "UiBold", uiBackground, uiForeground)
	draw.WordBox(2, 10, scrHalfH + 80, "E: Erlaubt das Ausführen anderer Tastenbefehle", "UiBold", uiBackground, uiForeground)
	local target = findNearestObject()
	local pls = player.GetAll()
	for i = 1, #pls do
		local ply = pls[i]
		if !IsValid(ply) then continue end
		if !isRoaming and ply == specEnt then continue end
		local pos = ply:GetShootPos():ToScreen()
		if !pos.visible then continue end
		local x, y = pos.x, pos.y
		draw.RoundedBox(2, x, y - 6, 12, 12, team.GetColor(ply:Team()))
		draw.WordBox(2, x, y - 66, ply:Name(), "UiBold", uiBackground, uiForeground)
		draw.WordBox(2, x, y - 46, "Leben: "..ply:Health(), "UiBold", uiBackground, uiForeground)
		draw.WordBox(2, x, y - 26, ply:GetUserGroup(), "UiBold", uiBackground, uiForeground)
	end
	if !isRoaming then return end
	if !IsValid(target) then return end
	local center = target:LocalToWorld(target:OBBCenter())
	local eyeAng = EyeAngles()
	local leftUp = target:IsPlayer() and eyeAng:Right() * -16 + eyeAng:Up() * 32 or eyeAng:Right() * -16 + eyeAng:Up() * 16
	local topLeft = (center + leftUp):ToScreen()
	local bottomRight = (center - leftUp):ToScreen()
	surface.SetDrawColor(red)
	surface.DrawOutlinedRect(topLeft.x, topLeft.y, math.abs(topLeft.x - bottomRight.x), math.abs(topLeft.y - bottomRight.y), 1)
	draw.WordBox(2, bottomRight.x, bottomRight.y + 12, "Linksklick zum Beobachten!", "UiBold", uiBackground, uiForeground)
end

startFreeRoam = function()
	roamPos = isSpectating and roamPos or LocalPlayer():GetShootPos()
	if IsValid(specEnt) then
		if specEnt:IsPlayer() then
			roamPos = thirdperson and getThirdPersonPos(specEnt) or specEnt:GetShootPos()
	end
		specEnt:SetNoDraw(false)
	end
	specEnt = nil
	isRoaming = true
	keysDown = {}
end

local function startSpectate(um)
	isRoaming = net.ReadBool()
	specEnt = net.ReadEntity()
	specEnt = IsValid(specEnt) and specEnt or nil
	if isRoaming then
		startFreeRoam()
	end
	isSpectating = true
	keysDown = {}
	hook.Add("CalcView", "FSpectate", specCalcView)
	hook.Add("PlayerBindPress", "FSpectate", specBinds)
	hook.Add("ShouldDrawLocalPlayer", "FSpectate", function() return isRoaming or thirdperson end)
	hook.Add("KeyRelease", "FSpectate", keyOverride)
	hook.Add("Think", "FSpectate", specThink)
	hook.Add("HUDPaint", "FSpectate", drawHelp)
	hook.Add("RenderScreenspaceEffects", "FSpectate", lookingLines)
	timer.Create("FSpectatePosUpdate", 0.5, 0, function()
		if !isRoaming then return end
		RunConsoleCommand("_FSpectatePosUpdate", roamPos.x, roamPos.y, roamPos.z)
	end)
end
net.Receive("FSpectate", startSpectate)

stopSpectating = function()
	hook.Remove("CalcView", "FSpectate")
	hook.Remove("PlayerBindPress", "FSpectate")
	hook.Remove("ShouldDrawLocalPlayer", "FSpectate")
	hook.Remove("KeyRelease", "FSpectate")
	hook.Remove("Think", "FSpectate")
	hook.Remove("HUDPaint", "FSpectate")
	hook.Remove("RenderScreenspaceEffects", "FSpectate")
	timer.Remove("FSpectatePosUpdate")
	if IsValid(specEnt) then
		specEnt:SetNoDraw(false)
	end
	RunConsoleCommand("FSpectate_StopSpectating")
	isSpectating = false
end