--[[
#include "umf.lua"
#include "tool_skins.lua"
#include "version.lua"
]] --
local config = OptionsKeys {
	legacy = { "boolean", false },
	rotspeed = { "number", 1 },
	bottom = { "boolean", true },
	key = {
		invert = { "boolean", false },
		rotate = { "string", "interact" },
		throw = { "string", "r" },
		unfreeze = { "string", "r" },
		rotlock = { "string", "mmb" },
	},
	experimental = { constrain = { "boolean", true }, rotlock = { "boolean", false }, toolgroup = { "integer", 6 } },
	skin = { "string", DEFAULT_SKIN },
}

local state = util.structured_table( "game.tool.physgun", {
	bottom = "boolean",
	grabbed = "body",
	grabbing = "boolean",
	relative = "vector",
	dist = "number",
	action = "string",
	rotating = "boolean",
	effect = { freeze_time = "number", freeze_body = "body", unfreeze_time = "number", unfreeze_body = "body" },
} )
state.bottom = config.bottom

local DEBUG = false

if DEBUG then
	PRINTTOSCREEN = true
end

local TOOL = {}

if config.skin == "" then
	config.skin = DEFAULT_SKIN
end
TOOL.noregister = ActivateSkin( "physgun", config.skin, config.experimental.toolgroup or 6 )

TOOL.ammo = 10000

-- TODO: make invert code readable somehow
local _lmb, _rmb = "usetool", "grab"
if config.key.invert then
	_lmb, _rmb = "grab", "usetool"
end

function TOOL:Initialize()
	self.mode = false
	self.scrolllock = 0

	self.world = {}
	if GetWorldBody then
		self.world[GetWorldBody()] = true
	end
	local allBodies = FindBodies( nil, true )
	local n, max_size = 0, 0
	for i, b in ipairs( allBodies ) do
		if not IsBodyDynamic( b ) then
			local min, max = GetBodyBounds( b )
			local size = VecLength( VecSub( max, min ) )
			if size > max_size and size < 1000000000 then
				n = i
				max_size = size
			end
		end
	end
	self.world[allBodies[n] or -1] = true
end

---@return Body?
---@return Vector?
---@return number?
function TOOL:GetTarget()
	QueryRejectBody(GetToolBody())
	---@diagnostic disable-next-line: deprecated
	local ray = PLAYER:GetCamera():Raycast( 750, -1 )
	---@cast ray { hit: boolean, dist: number, normal: Vector, shape: Shape, hitpos: Vector }
	if not IsValid( ray.shape ) then
		return
	end
	local body = ray.shape:GetBody()

	if GetEntityParent and GetEntityType(GetEntityParent(GetEntityHandle(body))) == "animator" then
		-- reject unmoveable animators
		if not ray.shape:HasTag("proxy") then
			return
		end
	end

	if self.world[GetEntityHandle( body )] or ray.shape:HasTag( "ground" ) or body:HasTag( "ground" ) then
		return
	end

	return body, ray.hitpos, ray.dist
end

function TOOL:AttemptGrab()
	local body, hitpos, dist = self:GetTarget()
	if not body then
		return
	end
	---@cast hitpos Vector
	---@cast dist number

	self.grabbed = body
	self.grabbed:SetDynamic( true )
	self.grabbed:SetVelocity( VEC_ZERO )
	local grtr = self.grabbed:GetTransform()
	self.relative = grtr:ToLocal( hitpos )
	self.rotation = PLAYER:GetTransform():ToLocal( self.grabbed:GetTransform() )
	self.clamped_rotation = nil
	state.dist = dist
	state.grabbed = self.grabbed
	state.relative = self.relative
	state.action = "pickup"
	return self.grabbed
end

function TOOL:Primary()
	if not self:AttemptGrab() then
		self.grabbing = true
		state.grabbing = true
	end
end

function TOOL:Secondary()
	if self.grabbed then
		state.effect.freeze_time = GetTime()
		state.effect.freeze_body = self.grabbed
		ReleasePlayerGrab()
		self.grabbed:SetVelocity( VEC_ZERO )
		self.grabbed:SetAngularVelocity( VEC_ZERO )
		self.grabbed:SetDynamic( false )
		self.grabbed = nil
		state.grabbed = nil
		state.action = "freeze"
		self.scrolllock = GetTime()
		SetBool( "game.player.disableinput", false )
	else
		self.mode = not self.mode
	end
end

function TOOL:PrimaryReleased()
	if self.grabbed then
		state.action = "drop"
	end
	self.grabbed = nil
	self.grabbing = false
	self.startrot = nil
	state.grabbed = nil
	state.grabbing = false
	SetBool( "game.player.disableinput", false )
end

function TOOL:MouseWheel( ds )
	if self.grabbed then
		state.dist = math.max( state.dist + ds, 2 )
		self.scrolllock = GetTime()
	end
end

function TOOL:ShouldLockMouseWheel()
	if self.grabbed or self.scrolllock > GetTime() - 0.5 then
		return true
	end
end

function TOOL:DrawBeam( source, target, object, r, g, b )
	if target == object then
		return DrawLine( source, target, r, g, b )
	end
	local prev = source
	for i = 1, 15 do
		local t = i / 16
		local t2 = t ^ 2
		local newpoint = source:Lerp( target, t2 ):Lerp( source:Lerp( object, t2 ), t2 )
		DrawLine( prev, newpoint, r, g, b )
		prev = newpoint
	end
	DrawLine( prev, object, r, g, b )
end

function TOOL:Tick()
	state.action = nil
	if GetBool( "game.player.canusetool" ) then
		if InputPressed( _lmb ) then
			self:Primary()
		end
		if InputPressed( _rmb ) then
			self:Secondary()
		end
		if InputReleased( _lmb ) then
			self:PrimaryReleased()
		end
		if config.experimental.rotlock and InputPressed( config.key.rotlock ) then
			self.disable_rot = not self.disable_rot
			if self.grabbed then
				self.startrot = nil
				self.rotation = PLAYER:GetTransform():ToLocal( self.grabbed:GetTransform() )
			end
		end
	end
	local camtr = PLAYER:GetCamera()

	if not self.grabbed or not self.grabbed:IsValid() then
		state.rotating = false
		if InputPressed( config.key.unfreeze ) then
			local body = self:GetTarget()
			if body then
				if not body:IsDynamic() then
					state.effect.unfreeze_time = GetTime()
					state.effect.unfreeze_body = body
					state.action = "unfreeze"
				end
				body:SetDynamic( true )
				body:SetVelocity( Vector( 0, 0, 0 ) )
			end
		end

		if self.grabbing then
			if self:AttemptGrab() then
				self.grabbing = false
				state.grabbing = false
				return
			end
			if not InputDown( _lmb ) then
				self:PrimaryReleased()
			end
		end
		return
	end

	if InputPressed( config.key.throw ) then
		self.grabbed:SetVelocity( camtr.rot:Forward() * -100 )
		self.grabbed = nil
		state.grabbed = nil
		state.action = "throw"
		return
	end
	SetBool("game.disableinteract", true)

	if InputDown( config.key.rotate ) and not self.disable_rot then
		state.rotating = true
		if not self.startrot then
			self.startrot = self.rotation.rot
			self.mousex, self.mousey = 0, 0
			self.playerlock = GetPlayerTransform( true )
		end
		if config.legacy then
			UiMakeInteractive()
			local dx, dy = UiGetMousePos()
			local w, h = UiWidth(), UiHeight()
			dx, dy = dx - w / 2, dy - h / 2
			self.rotation.rot = QuatEuler( (dy - self.mousey) / h * 360, (dx - self.mousex) / w * 360, 0 ) * self.rotation.rot
			self.mousex, self.mousey = dx, dy
		else
			if GetWorldBody then
				SetBool( "game.player.disableinput", true )
			else
				SetPlayerTransform( self.playerlock, true )
			end
			local s = 500 * config.rotspeed / 6
			self.rotation.rot = QuatEuler( s * InputValue( "cameray" ), s * InputValue( "camerax" ), 0 ) * self.rotation.rot
		end

		if InputDown( "shift" ) and self.startrot then
			local nrot = PLAYER:GetTransform():ToGlobal( self.rotation )
			local lp, ly, lr = GetQuatEuler( nrot.rot )
			lp = math.floor( lp / 45 + .5 ) * 45
			ly = math.floor( ly / 45 + .5 ) * 45
			lr = math.floor( lr / 45 + .5 ) * 45
			self.clamped_rotation = MakeQuaternion( QuatEuler( lp, ly, lr ) )
		else
			self.clamped_rotation = nil
		end

		if InputPressed( _rmb ) then
			self:Secondary()
			return
		end
	else
		SetBool( "game.player.disableinput", false )
		state.rotating = false
		self.startrot = nil
	end

	if not config.experimental.constrain then
		local bodytr = self.grabbed:GetTransform()
		local onbody = bodytr:ToGlobal( self.relative )
		local aimpos = camtr:ToGlobal( Vector( 0, 0, -state.dist ) )

		self.grabbed:SetVelocity( (aimpos - onbody) * 15 )

		if not self.disable_rot then
			local rotation = self.clamped_rotation or PLAYER:GetTransform():ToGlobal( self.rotation ).rot

			local p, y, r = (bodytr.rot * rotation:Conjugate()):ToEuler()
			local diff = -Vector( p, y, r )

			self.grabbed:SetAngularVelocity( diff / 5 )
		end
	end

	if not InputDown( _lmb ) or GetPlayerVehicle() ~= 0 then
		self:PrimaryReleased()
	end
end

function TOOL:Update( dt )
	if config.experimental.constrain then
		if not self.grabbed or not self.grabbed:IsValid() then
			return
		end

		local bodytr = self.grabbed:GetTransform()
		local onbody = bodytr:ToGlobal( self.relative )
		local aimpos = PLAYER:GetCamera():ToGlobal( Vector( 0, 0, -state.dist ) )

		ConstrainPosition( self.grabbed.handle, 0, onbody, aimpos )

		if not self.disable_rot then
			local rotation = self.clamped_rotation or PLAYER:GetTransform():ToGlobal( self.rotation ).rot
			ConstrainOrientation( self.grabbed.handle, 0, bodytr.rot, rotation )
		end
	end
end

RegisterToolUMF( "physgun", TOOL, config.bottom )
function firsttick()
	if not GetBool( "game.tool.physgun.enabled" ) then
		TOOL.noregister = false
		RegisterToolUMF( "physgun", TOOL, config.bottom )
	end
end
