-- Useful constants.
local screenW = 320
local screenH = 200
local halfScreenW = screenW / 2
local halfScreenH = screenH / 2
local invAspectRatio = FixedDiv(screenH, screenW)
local oneMeter = 48 -- Sonic's height in SRB2 = Sonic's canon height = 1 meter.
local minX = 16
local maxX = screenW - minX
local minY = 4
local maxY = screenH - 16
local splitscreenFovXHack = FixedDiv(1333, 1000)
local splitscreenFovYHack = FixedDiv(850, 1000)
local debug = false

-- CVars.
local showNameTagsCvar = CV_RegisterVar({
	name = "cot_name_tags",
	defaultvalue = "On",
	possiblevalue = CV_OnOff
})

-- Rotates a point (X and Y coordinates) horizontally (around Z axis).
local function rotateHorizontally(x, y, angle)
    local cosAngle = cos(angle)
    local sinAngle = sin(angle)
    return FixedMul(x, cosAngle) - FixedMul(y, sinAngle), FixedMul(x, sinAngle) + FixedMul(y, cosAngle)
end

-- Rotates a point (X and Z coordinates) vertically (around Y axis).
local function rotateVertically(x, z, angle)
    local cosAngle = cos(angle)
    local sinAngle = sin(angle)
    return FixedMul(x, cosAngle) - FixedMul(z, sinAngle), FixedMul(x, sinAngle) + FixedMul(z, cosAngle)
end

-- Crops a line segment such that it fits inside a box.
local function cropLineSegment(ox, oy, dx, dy, tlx, tly, brx, bry, forceToEdge)
	-- Initial checks.
	if dx - ox == 0 and dy - oy == 0 then return dx, dy end
	
	-- Translate so that ox,oy is at the origin.
	ox = ox * FRACUNIT
	oy = oy * FRACUNIT
	dx = dx * FRACUNIT - ox
	tlx = tlx * FRACUNIT - ox
	brx = brx * FRACUNIT - ox
	dy = dy * FRACUNIT - oy
	tly = tly * FRACUNIT - oy
	bry = bry * FRACUNIT - oy
	
	-- Get the scaling factors.
	local scaleX = INT32_MAX
	local scaleY = INT32_MAX
	if dx < 0
		scaleX = FixedDiv(tlx, dx)
	elseif dx > 0
		scaleX = FixedDiv(brx, dx)
	end
	if dy < 0
		scaleY = FixedDiv(tly, dy)
	elseif dy > 0
		scaleY = FixedDiv(bry, dy)
	end
	
	-- Scale the entire line segment by the smallest factor. And only if we're shrinking or forcing.
	local scale
	if scaleX < scaleY then scale = scaleX else scale = scaleY end
	
	if forceToEdge or scale < 1 * FRACUNIT
		dx = FixedMul(dx, scale)
		dy = FixedMul(dy, scale)
	end
	
	-- Translate back and finish.
	dx = FixedInt(dx + ox)
	dy = FixedInt(dy + oy)
	return dx, dy
end

-- Maps some world coordinates (XYZ) into screen coordinates (XY) based on the camera's properties.
local function mapToScreen(px, py, pz, camX, camY, camZ, camAngle, camAiming)
    local tanHalfFov = tan(FixedAngle(CV_FindVar("fov").value / 2))
	
    -- Translate world coordinates by the camera position.
    local deltaX = px - camX
    local deltaY = py - camY
    local deltaZ = pz - camZ

    -- Rotate the translated coordinates by the camera's angle (yaw).
    local deltaX, deltaY = rotateHorizontally(deltaX, deltaY, -camAngle)
    -- Rotate the rotated coordinates by the camera's pitch.
    local deltaX, deltaZ = rotateVertically(deltaX, deltaZ, -camAiming)
	
	local relativeX = deltaY
	local relativeY = deltaZ
	local relativeZ = deltaX
	-- At this point, relativeX is the left-right distance to camera center, relativeY is the up-down distance to camera center, relativeZ is the front-back distance to camera center.
	if FixedInt(relativeZ) == 0 then relativeZ = FRACUNIT end -- Helps against near-zero weirdnesses and divisions-by-zero.
	if debug then print("Rel: " + FixedInt(relativeX) + " | " + FixedInt(relativeY) + " | " + FixedInt(relativeZ)) end
	
    -- Perspective divide (projecting 3D point to 2D screen space).
	local xFovFactor = FixedMul(abs(relativeZ), tanHalfFov)
	if FixedInt(xFovFactor) == 0 then xFovFactor = FRACUNIT end -- Helps against near-zero weirdnesses and divisions-by-zero.
	local yFovFactor = FixedMul(xFovFactor, invAspectRatio) -- This is needed for some reason.
	if splitscreen then xFovFactor = FixedMul(xFovFactor, splitscreenFovXHack) end -- This is needed for some reason.
	if splitscreen then yFovFactor = FixedMul(yFovFactor, splitscreenFovYHack) end -- This is needed for some reason.
	if debug then print("xFovFactor: " + FixedInt(xFovFactor)) end
	
    local screenX = -FixedDiv(relativeX, xFovFactor)
    local screenY = -FixedDiv(relativeY, yFovFactor)
	
    -- Convert to screen coordinates.
    screenX = FixedMul(screenX, halfScreenW) + halfScreenW
    screenY = FixedMul(screenY, halfScreenH) + halfScreenH
	if debug then print("Screen: " + screenX + " | " + screenY) end

    return screenX, screenY, relativeZ, yFovFactor
end

-- Draw the HUD elements for the other players's name tags.
hud.add(
	function(v, stplyr, cam)
		if not showNameTagsCvar.value return end
		if not stplyr.mo return end
		
		for player in players.iterate
			-- Initial checks.
			if not player.mo continue end
			if player == stplyr then continue end
			
			-- Blink if the player is dead.
			if player.playerstate == PST_DEAD
				if player.mo.fuse % 4 < 2 then continue end
			end
			
			-- Get the screen coordinates for this player.
			local camX, camY, camZ, camAngle, camAiming
			if cam.chase
				camX = cam.x
				camY = cam.y
				camZ = cam.z
				camAngle = cam.angle
				camAiming = cam.aiming
			else
				camX = stplyr.mo.x
				camY = stplyr.mo.y
				camZ = stplyr.mo.z + stplyr.viewheight
				camAngle = stplyr.mo.angle
				camAiming = stplyr.aiming
			end
			local screenX, screenY, relativeZ, yFovFactor = mapToScreen(player.mo.x, player.mo.y, player.mo.z, camX, camY, camZ, camAngle, camAiming)
			
			-- Adjust for being upside-down.
			if stplyr.mo.eflags & MFE_VERTICALFLIP > 0
				local mustFlip = false
				if not cam.chase then mustFlip = true end
				if stplyr.pflags & PF_FLIPCAM > 0 and cam.chase then mustFlip = true end
				if mustFlip then screenY = screenH - screenY end
			end
			
			-- Adjust for when they're off-camera.
			local verticalOffset = 0
			if screenX < minX
				screenX = minX
			elseif screenX > maxX
				screenX = maxX
			end
			if screenY < minY
				screenY = minY
			elseif screenY > maxY or relativeZ < 0
				screenY = maxY
			else
				verticalOffset = FixedDiv(8, yFovFactor)
			end
			local verticalOffset = 0
			--screenX, screenY = cropLineSegment(halfScreenW, halfScreenH, screenX, screenY, minX, minY, maxX, maxY, relativeZ < 0) -- Alternative approach, with its set of problems.
			if debug then print("Cropped: " + screenX + " | " + screenY) end
			
			-- Adjust for split-screen.
			local gscale = FRACUNIT
			if splitscreen
				screenY = $ / 2
				if stplyr == players[1]
					screenY = $ + 100
				end
			end
			
			-- Draw the player's name.
			v.drawString(screenX, screenY + verticalOffset, player.name, V_20TRANS, "small-center")
			
			-- Draw the player's distance.
			local dist = P_AproxDistance(P_AproxDistance(stplyr.mo.x - player.mo.x, stplyr.mo.y - player.mo.y), stplyr.mo.z - player.mo.z)
			dist = FixedInt(dist / oneMeter)
			if dist > 10
				v.drawString(screenX, screenY + verticalOffset + 4, dist + "m", V_30TRANS | V_ALLOWLOWERCASE, "small-center")
			end
		end
	end
)
