--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| 	Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]

--[[--------------------------------------------------------------------------
	Improved Stacker Module
	
	Author:
		Mista-Tea ([IJWTB] Thomas)
	
	License:
		The MIT License (MIT)

		Copyright (c) 2014-2020 Mista-Tea

		Permission is hereby granted, free of charge, to any person obtaining a copy
		of this software and associated documentation files (the "Software"), to deal
		in the Software without restriction, including without limitation the rights
		to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
		copies of the Software, and to permit persons to whom the Software is
		furnished to do so, subject to the following conditions:

		The above copyright notice and this permission notice shall be included in all
		copies or substantial portions of the Software.

		THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
		IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
		FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
		AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
		LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
		OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
		SOFTWARE.
			
	Changelog:
----------------------------------------------------------------------------]]

local math = math
local hook = hook
local Angle = Angle
local Vector = Vector
local GetConVar = GetConVar
local duplicator = duplicator
local CreateConVar = CreateConVar

--[[--------------------------------------------------------------------------
-- 	Namespace Tables
--------------------------------------------------------------------------]]--

module( "improvedstacker", package.seeall )

--[[--------------------------------------------------------------------------
-- 	Localized Functions & Variables
--------------------------------------------------------------------------]]--

-- enums for determining stack relativity
MODE_WORLD = 1 -- stacking relative to the world
MODE_PROP  = 2 -- stacking relative to the prop

-- lookup table for validating relative values
Modes = {
	[MODE_WORLD] = true,
	[MODE_PROP]  = true,
}

-- enums for determining the direction to stack props
DIRECTION_FRONT = 1
DIRECTION_BACK  = 2
DIRECTION_RIGHT = 3
DIRECTION_LEFT  = 4
DIRECTION_UP    = 5
DIRECTION_DOWN  = 6

-- lookup table for validating direction values
Directions = {
	[DIRECTION_FRONT] = true,
	[DIRECTION_BACK]  = true,
	[DIRECTION_RIGHT] = true,
	[DIRECTION_LEFT]  = true,
	[DIRECTION_UP]    = true,
	[DIRECTION_DOWN]  = true,
}

-- constants used for when stacking relative to the World
ANGLE_ZERO   =  Angle( 0, 0, 0 )
VECTOR_FRONT  = ANGLE_ZERO:Forward()
VECTOR_RIGHT  = ANGLE_ZERO:Right()
VECTOR_UP     = ANGLE_ZERO:Up()
VECTOR_BACK   = -VECTOR_FRONT
VECTOR_LEFT   = -VECTOR_RIGHT
VECTOR_DOWN   = -VECTOR_UP

-- there has been a longstanding problem where stacked entities were an inch apart (figuratively), causing gaps everywhere.
-- as it turns out, fixing this issue is as easy as subtracting 0.5 from the forward component of the offset vector.
MAGIC_OFFSET = -0.5

--[[--------------------------------------------------------------------------
--	Namespace Functions
--------------------------------------------------------------------------]]--

if ( SERVER ) then
	
	-- the tables below are used internally and should only generally be interfaced with
	-- via the functions declared afterward.
	-- basically treat them as private, since they are only public for auto-refresh compatibility
	
	-- holds the current stacked entity count for every player
	m_EntCount  = m_EntCount  or {}
	-- holds the last stacker usage for every player
	m_StackTime = m_StackTime or {}
	-- holds every stacker entity created
	m_Ents      = m_Ents      or {}
		
	--[[--------------------------------------------------------------------------
	-- 	GetEntCount( player, number )
	--]]--
	function GetEntCount( ply, default )
		return m_EntCount[ ply:SteamID() ] or default
	end
	--[[--------------------------------------------------------------------------
	-- 	SetEntCount( player, number )
	--]]--
	function SetEntCount( ply, num )
		m_EntCount[ ply:SteamID() ] = num
	end
	--[[--------------------------------------------------------------------------
	-- 	IncrementEntCount( player, number )
	--]]--
	function IncrementEntCount( ply, num )
		m_EntCount[ ply:SteamID() ] = GetEntCount( ply, 0 ) + (num or 1)
	end
	--[[--------------------------------------------------------------------------
	-- 	DecrementEntCount( player, number )
	--]]--
	function DecrementEntCount( ply, num )
		m_EntCount[ ply:SteamID() ] = ( m_EntCount[ ply:SteamID() ] and m_EntCount[ ply:SteamID() ] - (num or 1) ) or 0
	end
	
	--[[--------------------------------------------------------------------------
	-- 	SetLastStackTime( player, number )
	--]]--
	function SetLastStackTime( ply, num )
		m_StackTime[ ply:SteamID() ] = num
	end
	--[[--------------------------------------------------------------------------
	-- 	GetLastStackTime( player, number )
	--]]--
	function GetLastStackTime( ply, default )
		return m_StackTime[ ply:SteamID() ] or default
	end
	
	--[[--------------------------------------------------------------------------
	--	Initialize( string )
	--
	--	This should be called immediately after including this file so that the follow
	--	variables/functions can use the stacker tool's mode (i.e., the name of the file itself
	--	and what is subsequently used in all of the cvars).
	--]]--
	function Initialize( mode )
		mode = mode or "stacker_improved"
		
		--[[--------------------------------------------------------------------------
		--  Hook :: PlayerInitialSpawn
		
		--	Sets the newly connected player's total stacker ents to 0.
		--	See TOOL:IsExceedingMax() for more details
		--]]--
		hook.Add( "PlayerInitialSpawn", mode.."_set_ent_count", function( ply )
			m_EntCount[ ply:SteamID() ] = 0
		end )
		--[[--------------------------------------------------------------------------
		--  Hook :: PlayerDisconnected
		--
		--	Removes the player from the table when they disconnect (for sanitation).
		--]]--
		hook.Add( "PlayerDisconnected", mode.."_remove_ent_count", function( ply )
			m_EntCount[ ply:SteamID() ] = nil
		end )
		
		--[[--------------------------------------------------------------------------
		-- 	MarkEntity( player, entity, table )
		--
		--	Marks the entity as a stacker entity. This allows the entity to be 
		--	collision-checked in GM.ShouldCollide.
		--]]--
		function MarkEntity( ply, ent, data )
			m_Ents[ ent ] = true
			duplicator.StoreEntityModifier( ent, mode, { StackerEnt = true } )
			ent:SetCustomCollisionCheck( true )
			
			-- when the entity is removed, sanitize our internal m_Ents array
			ent:CallOnRemove( mode, function( ent )
				ClearEntity( ent )
			end )
		end
		--duplicator.RegisterEntityModifier( mode, MarkEntity )
		--[[--------------------------------------------------------------------------
		-- 	ClearEntity( entity )
		--
		--	Removes the entity from the internal m_Ents array for sanitation purposes.
		--	This is called when an entity is just about to be removed.
		--]]--
		function ClearEntity( ent )
			if ( m_Ents[ ent ] ) then m_Ents[ ent ] = nil end
		end
		
		--[[--------------------------------------------------------------------------
		-- 	CanUnfreeze( player, entity, physObject )
		--]]--
		function CanUnfreeze( ply, ent, phys )
			if ( m_Ents[ ent ] ) then print("nope") return false end
		end
		--hook.Add( "CanPlayerUnfreeze", mode, CanUnfreeze )
		--hook.Add( "PhysgunPickup",     mode, CanUnfreeze )
		--hook.Remove( "CanPlayerUnfreeze", mode )
		--hook.Remove( "PhysgunPickup",     mode )
		
		local cvarNoCollideAll
		local cvarNoCollide
		--[[--------------------------------------------------------------------------
		-- 	ShouldCollide( entity, entity )
		--]]--
		function ShouldCollide( a, b )
			if ( not cvarNoCollideAll ) then cvarNoCollideAll = GetConVar( mode.."_force_nocollide_all" ) end
			if ( not cvarNoCollide )    then cvarNoCollide    = GetConVar( mode.."_force_nocollide" )     end
			
			if ( cvarNoCollideAll:GetBool() ) then
				if ( m_Ents[ a ] ) then
					if not ( b:IsPlayer() or b:IsWorld() or b:IsNPC() or b:IsVehicle() ) then return false end
				elseif ( m_Ents[ b ] ) then
					if not ( a:IsPlayer() or a:IsWorld() or b:IsNPC() or b:IsVehicle() ) then return false end
				end
			elseif ( cvarNoCollide:GetBool() ) then
				if ( m_Ents[ a ] and m_Ents[ b ] ) then return false end
			end
		end
		--hook.Add( "ShouldCollide", mode, ShouldCollide )
		--hook.Remove( "ShouldCollide", mode )
	end
	
elseif ( CLIENT ) then
	
	-- the table below is used internally and should only generally be interfaced with
	-- via the functions declared afterward.
	-- basically treat it as private, since it is only public for auto-refresh compatibility
	
	m_Ghosts    = m_Ghosts    or {}
	m_LookingAt = m_LookingAt or nil
	m_LookedAt  = m_LookedAt  or nil
	
	--[[--------------------------------------------------------------------------
	-- 	GetGhosts()
	--]]--
	function GetGhosts()
		return m_Ghosts
	end
	--[[--------------------------------------------------------------------------
	-- 	SetGhosts( table )
	--]]--
	function SetGhosts( tbl )
		m_Ghosts = tbl
	end
	
	--[[--------------------------------------------------------------------------
	-- 	GetLookingAt()
	--]]--
	function GetLookingAt()
		return m_LookingAt
	end
	--[[--------------------------------------------------------------------------
	-- 	SetLookingAt( entity )
	--]]--
	function SetLookingAt( ent )
		m_LookingAt = ent
	end
	
	--[[--------------------------------------------------------------------------
	-- 	GetLookedAt()
	--]]--
	function GetLookedAt()
		return m_LookedAt
	end
	--[[--------------------------------------------------------------------------
	-- 	SetLookedAt( entity )
	--]]--
	function SetLookedAt( ent )
		m_LookedAt = ent
	end
	
	--[[--------------------------------------------------------------------------
	-- 	ReleaseGhosts()
	--	
	--	Attempts to remove all ghosted props in the stack. 
	--	This occurs when the player stops looking at a prop with the stacker tool equipped.
	--]]--
	function ReleaseGhosts()
		if ( #m_Ghosts == 0 ) then return end
		
		for i = 1, #m_Ghosts do
			if ( not IsValid( m_Ghosts[ i ] ) ) then continue end
			SafeRemoveEntityDelayed( m_Ghosts[ i ], 0 )
			m_Ghosts[ i ] = nil
		end
	end
	
	--[[--------------------------------------------------------------------------
	--	Initialize( string )
	--
	--	This should be called immediately after including this file so that the follow
	--	variables/functions can use the stacker tool's mode (i.e., the name of the file itself
	--	and what is subsequently used in all of the cvars).
	--]]--
	function Initialize( mode )
		mode = mode or "stacker_improved"
		
		SETTINGS_DEFAULT = {
			[mode.."_set_max_per_player"]    = "-1",
			[mode.."_set_max_per_stack"]     = "15",
			[mode.."_set_delay"]             = "0.5",
			[mode.."_set_max_offsetx"]       = "200",
			[mode.."_set_max_offsety"]       = "200",
			[mode.."_set_max_offsetz"]       = "200",
			[mode.."_set_force_freeze"]      = "0",
			[mode.."_set_force_weld"]        = "0",
			[mode.."_set_force_nocollide"]   = "0",
			[mode.."_set_force_stayinworld"] = "1",
		}
		
		SETTINGS_SANDBOX = {
			[mode.."_set_max_per_player"]    = "-1",
			[mode.."_set_max_per_stack"]     = "30",
			[mode.."_set_delay"]             = "0.5",
			[mode.."_set_max_offsetx"]       = "1000",
			[mode.."_set_max_offsety"]       = "1000",
			[mode.."_set_max_offsetz"]       = "1000",
			[mode.."_set_force_freeze"]      = "0",
			[mode.."_set_force_weld"]        = "0",
			[mode.."_set_force_nocollide"]   = "0",
			[mode.."_set_force_stayinworld"] = "0",
		}
		
		SETTINGS_DARKRP = {
			[mode.."_set_max_per_player"]    = "50",
			[mode.."_set_max_per_stack"]     = "5",
			[mode.."_set_delay"]             = "1",
			[mode.."_set_max_offsetx"]       = "200",
			[mode.."_set_max_offsety"]       = "200",
			[mode.."_set_max_offsetz"]       = "200",
			[mode.."_set_force_freeze"]      = "1",
			[mode.."_set_force_weld"]        = "0",
			[mode.."_set_force_nocollide"]   = "1",
			[mode.."_set_force_stayinworld"] = "1",
		}
		
		SETTINGS_SINGLEPLAYER = {
			[mode.."_set_max_per_player"]    = "-1",
			[mode.."_set_max_per_stack"]     = "100",
			[mode.."_set_delay"]             = "0",
			[mode.."_set_max_offsetx"]       = "10000",
			[mode.."_set_max_offsety"]       = "10000",
			[mode.."_set_max_offsetz"]       = "10000",
			[mode.."_set_force_freeze"]      = "0",
			[mode.."_set_force_weld"]        = "0",
			[mode.."_set_force_nocollide"]   = "0",
			[mode.."_set_force_stayinworld"] = "0",
		}
	end
	
end

--
-- The functions below are used both serverside and clientside for properly orienting
-- and spacing props in a stack
--

-- Lookup table that holds functions related to determining the direction of a stack
DirectionFunctions = {
	[MODE_WORLD] = {
		[DIRECTION_FRONT] = function() return VECTOR_FRONT end,
		[DIRECTION_BACK]  = function() return VECTOR_BACK  end,
		[DIRECTION_RIGHT] = function() return VECTOR_RIGHT end,
		[DIRECTION_LEFT]  = function() return VECTOR_LEFT  end,
		[DIRECTION_UP]    = function() return VECTOR_UP    end,
		[DIRECTION_DOWN]  = function() return VECTOR_DOWN  end,
	},
	
	[MODE_PROP]  = {
		[DIRECTION_FRONT] = function( angle ) return  angle:Forward() end,
		[DIRECTION_BACK]  = function( angle ) return -angle:Forward() end,
		[DIRECTION_RIGHT] = function( angle ) return  angle:Right()   end,
		[DIRECTION_LEFT]  = function( angle ) return -angle:Right()   end,
		[DIRECTION_UP]    = function( angle ) return  angle:Up()      end,
		[DIRECTION_DOWN]  = function( angle ) return -angle:Up()      end,
	}
}

-- Lookup table that holds functions related to determining the distance to offset each prop in a stack
-- before applying the client's actual x/y/z offset values
DistanceFunctions = {
	[DIRECTION_FRONT] = function( min, max ) return math.abs(max.x - min.x) end,
	[DIRECTION_BACK]  = function( min, max ) return math.abs(max.x - min.x) end,
	[DIRECTION_RIGHT] = function( min, max ) return math.abs(max.y - min.y) end,
	[DIRECTION_LEFT]  = function( min, max ) return math.abs(max.y - min.y) end,
	[DIRECTION_UP]    = function( min, max ) return math.abs(max.z - min.z) end,
	[DIRECTION_DOWN]  = function( min, max ) return math.abs(max.z - min.z) end,
}

-- Lookup table that holds functions related to determining the distance to offset each prop in a stack
-- based on the client's x/y/z offset values
OffsetFunctions = {
	[DIRECTION_FRONT] = function( angle, offset ) return ( angle:Forward() * offset.x) + ( angle:Up()      * offset.z) + ( angle:Right()   * offset.y) end,
	[DIRECTION_BACK]  = function( angle, offset ) return (-angle:Forward() * offset.x) + ( angle:Up()      * offset.z) + (-angle:Right()   * offset.y) end,
	[DIRECTION_RIGHT] = function( angle, offset ) return ( angle:Right()   * offset.x) + ( angle:Up()      * offset.z) + (-angle:Forward() * offset.y) end,
	[DIRECTION_LEFT]  = function( angle, offset ) return (-angle:Right()   * offset.x) + ( angle:Up()      * offset.z) + ( angle:Forward() * offset.y) end,
	[DIRECTION_UP]    = function( angle, offset ) return ( angle:Up()      * offset.x) + (-angle:Forward() * offset.z) + ( angle:Right()   * offset.y) end,
	[DIRECTION_DOWN]  = function( angle, offset ) return (-angle:Up()      * offset.x) + ( angle:Forward() * offset.z) + ( angle:Right()   * offset.y) end,
}

RotationFunctions = {
	[DIRECTION_FRONT] = function( angle ) return  angle:Right(),   angle:Up(),       angle:Forward() end,
	[DIRECTION_BACK]  = function( angle ) return -angle:Right(),   angle:Up(),      -angle:Forward() end,
	[DIRECTION_RIGHT] = function( angle ) return -angle:Forward(), angle:Up(),       angle:Right()   end,
	[DIRECTION_LEFT]  = function( angle ) return  angle:Forward(), angle:Up(),      -angle:Right()   end,
	[DIRECTION_UP]    = function( angle ) return -angle:Right(),   angle:Forward(),  angle:Up()      end,
	[DIRECTION_DOWN]  = function( angle ) return  angle:Right(),   angle:Forward(), -angle:Up()      end,
}

--[[--------------------------------------------------------------------------
-- 	GetDirection( number, number, angle )
--
--	Calculates the direction to point the entity to by depending on whether the stack is
--	created relative to the world or the original prop, and the direction to stack in.
--]]--
function GetDirection( stackMode, stackDir, angle )
	return DirectionFunctions[ stackMode ][ stackDir ]( angle )
end

--[[--------------------------------------------------------------------------
-- 	GetDistance( number, number, entity )
--
--	Calculates the space occupied by the entity depending on the stack direction.
--	This represents the number of units to offset the stack entities so they appear
--	directly in front of the previous entity (depending on direction).
--]]--
function GetDistance( stackMode, stackDir, ent )
	if ( stackMode == MODE_WORLD ) then
		return DistanceFunctions[ stackDir ]( ent:WorldSpaceAABB() )
	elseif ( stackMode == MODE_PROP ) then
		return DistanceFunctions[ stackDir ]( ent:OBBMins(), ent:OBBMaxs() )
	end
end

--[[--------------------------------------------------------------------------
-- 	GetOffset( number, number, angle, vector )
--
--	Calculates a direction vector used for offsetting a stacked entity based on the facing angle of the previous entity.
--	This function uses a lookup table for added optimization as opposed to an if-else block.
--]]--
function GetOffset( stackMode, stackDir, angle, offset )
	-- if stacking relative to the world, apply the magic offset fix to the correct direction
	if ( stackMode == MODE_WORLD ) then
		local direction = DirectionFunctions[ stackMode ][ stackDir ]()
			  direction = direction * MAGIC_OFFSET
		return offset + direction
	-- if stacking relative to the prop, apply the magic offset only to the forward (x) component of the vector
	elseif ( stackMode == MODE_PROP ) then
		local trueOffset = Vector()
		      trueOffset:Set( offset )
			  trueOffset.x = trueOffset.x + MAGIC_OFFSET
		return OffsetFunctions[ stackDir ]( angle, trueOffset )
	end
end

--[[--------------------------------------------------------------------------
-- 	RotateAngle( angle, angle )
--
--	Rotates the first angle by the second angle. This ensures proper rotation
--	along all three axes and prevents various problems related to simply adding
--	two angles together. The first angle is modified directly by refence, so this does not
--	return anything.
--]]--
function RotateAngle( stackMode, stackDir, angle, rotation )
	local axisPitch, axisYaw, axisRoll = RotationFunctions[ stackDir ]( angle )

	angle:RotateAroundAxis( axisPitch,  rotation.p )
	angle:RotateAroundAxis( axisYaw,   -rotation.y )
	angle:RotateAroundAxis( axisRoll,   rotation.r )
end