-- Global to all starfalls
local checkluatype = SF.CheckLuaType
local registerprivilege = SF.Permissions.registerPrivilege
local Ent_IsValid = FindMetaTable("Entity").IsValid
local Phys_IsValid = FindMetaTable("PhysObj").IsValid

-- Register privileges
registerprivilege("prop.create", "Create prop", "Allows the user to create props")
registerprivilege("prop.createRagdoll", "Create a ragdoll", "Allows the user to create ragdolls")
registerprivilege("prop.createCustom", "Create custom prop", "Allows the user to create custom props")


local entList = SF.EntManager("props", "props", -1, "The number of props allowed to spawn via Starfall")
local plyPropBurst = SF.BurstObject("props", "props", 4, 4, "Rate props can be spawned per second.", "Number of props that can be spawned in a short time.")


--- Library for creating and manipulating physics-less models AKA "Props".
-- @name prop
-- @class library
-- @libtbl props_library
SF.RegisterLibrary("prop")


return function(instance)
local checkpermission = instance.player ~= SF.Superuser and SF.Permissions.check or function() end


local props_library = instance.Libraries.prop
local owrap, ounwrap = instance.WrapObject, instance.UnwrapObject
local ent_meta, ewrap, eunwrap = instance.Types.Entity, instance.Types.Entity.Wrap, instance.Types.Entity.Unwrap
local ang_meta, awrap, aunwrap = instance.Types.Angle, instance.Types.Angle.Wrap, instance.Types.Angle.Unwrap
local vec_meta, vwrap, vunwrap = instance.Types.Vector, instance.Types.Vector.Wrap, instance.Types.Vector.Unwrap

local propConfig = {clean = true, undo = false, propList = entList}
instance.data.props = propConfig

local vunwrap1
local aunwrap1
instance:AddHook("initialize", function()
	vunwrap1 = vec_meta.QuickUnwrap1
	aunwrap1 = ang_meta.QuickUnwrap1
end)

instance:AddHook("deinitialize", function()
	entList:deinitialize(instance, propConfig.clean)
end)

--- Creates a prop
-- @server
-- @param Vector pos Initial entity position
-- @param Angle ang Initial entity angles
-- @param string model Model path
-- @param boolean? frozen True to spawn the entity in a frozen state. Default = False
-- @return Entity The prop object
function props_library.create(pos, ang, model, frozen)

	checkpermission(instance, nil, "prop.create")
	checkluatype(model, TYPE_STRING)
	if frozen~=nil then checkluatype(frozen, TYPE_BOOL) else frozen = false end

	pos = SF.clampPos(vunwrap1(pos))
	ang = aunwrap1(ang)

	local ply = instance.player
	model = SF.CheckModel(model, ply, true)

	plyPropBurst:use(ply, 1)
	entList:checkuse(ply, 1)
	if ply ~= SF.Superuser and gamemode.Call("PlayerSpawnProp", ply, model)==false then SF.Throw("Another hook prevented the prop from spawning", 2) end

	local propent
	local ok, err = instance:runExternal(function()
		propent = ents.Create("prop_physics")
		propent:SetPos(pos)
		propent:SetAngles(ang)
		propent:SetModel(model)
		propent:Spawn()

		if not propent:GetModel() then error("Invalid model") end

		for I = 0, propent:GetPhysicsObjectCount() - 1 do
			local obj = propent:GetPhysicsObjectNum(I)
			if Phys_IsValid(obj) then
				obj:EnableMotion(not frozen)
			end
		end
		FixInvalidPhysicsObject(propent)

		if ply ~= SF.Superuser then
			gamemode.Call("PlayerSpawnedProp", ply, model, propent)

			if propConfig.undo then
				undo.Create("Prop")
					undo.SetPlayer(ply)
					undo.AddEntity(propent)
				undo.Finish("Prop (" .. tostring(model) .. ")")
			end
			ply:AddCleanup("props", propent)
		end
	end)
	if not ok then
		if Ent_IsValid(propent) then propent:Remove() end
		SF.Throw("Failed to create entity (" .. tostring(err) .. ")", 2)
	end
	entList:register(instance, propent)
	instance:checkCpu()

	return ewrap(propent)
end

--- Creates a ragdoll
-- @server
-- @param string model Model path
-- @param boolean? frozen True to spawn the entity in a frozen state. Default = False
-- @return Entity The ragdoll entity
function props_library.createRagdoll(model, frozen)
	checkpermission(instance, nil, "prop.createRagdoll")
	checkluatype(model, TYPE_STRING)
	if frozen~=nil then checkluatype(frozen, TYPE_BOOL) else frozen = false end

	local ply = instance.player
	model = SF.CheckRagdoll(model, ply)
	if not model then SF.Throw("Invalid model", 2) end

	plyPropBurst:use(ply, 1)
	entList:checkuse(ply, 1)
	if ply ~= SF.Superuser and gamemode.Call("PlayerSpawnRagdoll", ply, model)==false then SF.Throw("Another hook prevented the ragdoll from spawning", 2) end

	local ragdoll
	local ok, err = instance:runExternal(function()
		ragdoll = ents.Create("prop_ragdoll")
		ragdoll:SetModel(model)
		ragdoll:Spawn()

		if not ragdoll:GetModel() then error("Invalid model") end

		if frozen then
			for I = 0, ragdoll:GetPhysicsObjectCount() - 1 do
				local obj = ragdoll:GetPhysicsObjectNum(I)
				if Phys_IsValid(obj) then
					obj:EnableMotion(false)
				end
			end
		end

		if ply ~= SF.Superuser then
			gamemode.Call("PlayerSpawnedRagdoll", ply, model, ragdoll)

			if propConfig.undo then
				undo.Create("Ragdoll")
					undo.SetPlayer(ply)
					undo.AddEntity(ragdoll)
				undo.Finish("Ragdoll (" .. tostring(model) .. ")")
			end
			ply:AddCleanup("ragdolls", ragdoll)
		end
	end)
	if not ok then
		if Ent_IsValid(ragdoll) then ragdoll:Remove() end
		SF.Throw("Failed to create entity (" .. tostring(err) .. ")", 2)
	end
	entList:register(instance, ragdoll)
	instance:checkCpu()

	return ewrap(ragdoll)
end

--- Creates a custom prop.
-- @server
-- @param Vector pos The position to spawn the prop
-- @param Angle ang The angles to spawn the prop
-- @param table meshConvexes The table of tables of vertices that make up the physics mesh {{v1,v2,...},{v1,v2,...},...}
-- @param boolean? frozen True to spawn the entity in a frozen state. Default = False
-- @return Entity The prop object
function props_library.createCustom(pos, ang, meshConvexes, frozen)
	pos = SF.clampPos(vunwrap1(pos))
	ang = aunwrap1(ang)
	checkluatype(meshConvexes, TYPE_TABLE)
	if frozen~=nil then checkluatype(frozen, TYPE_BOOL) else frozen = false end

	checkpermission(instance, nil, "prop.createCustom")

	local ply = instance.player

	plyPropBurst:use(ply, 1)
	entList:checkuse(ply, 1)
	if instance.player ~= SF.Superuser and gamemode.Call("PlayerSpawnProp", ply, "starfall_prop")==false then SF.Throw("Another hook prevented the prop from spawning", 2) end

	local meshConvexesUnwrapped = {}
	for k, v in ipairs(meshConvexes) do
		local t = {}
		for o, p in ipairs(v) do
			t[o] = vunwrap(p)
		end
		meshConvexesUnwrapped[k] = t
	end

	local propent
	local ok, err = instance:runExternal(function()
		propent = SF.createCustomProp(instance.player, pos, ang, meshConvexesUnwrapped)

		local physobj = propent:GetPhysicsObject()
		if Phys_IsValid(physobj) then
			physobj:EnableMotion(not frozen)
			physobj:Wake()
		end

		if ply ~= SF.Superuser then
			gamemode.Call("PlayerSpawnedProp", ply, "starfall_prop", propent)

			if propConfig.undo then
				undo.Create("Prop")
					undo.SetPlayer(ply)
					undo.AddEntity(propent)
				undo.Finish("Starfall Prop")
			end
			ply:AddCleanup("props", propent)
		end
	end)
	if not ok then
		if Ent_IsValid(propent) then propent:Remove() end
		SF.Throw("Failed to create entity (" .. tostring(err) .. ")", 2)
	end

	entList:register(instance, propent)
	instance:checkCpu()

	return ewrap(propent)
end

local allowed_components = {
	["starfall_screen"] = true,
	["starfall_hud"] = true,
}
--- Creates starfall component
-- Allowed components:
-- starfall_hud
-- starfall_screen
-- @param Vector pos Position of created component
-- @param Angle ang Angle of created component
-- @param string class Class of created component
-- @param string model Model of created component
-- @param boolean? frozen True to spawn the entity in a frozen state. Default = False
-- @server
-- @return Entity Component entity
function props_library.createComponent(pos, ang, class, model, frozen)
	checkpermission(instance,  nil, "prop.create")
	checkluatype(class, TYPE_STRING)
	checkluatype(model, TYPE_STRING)
	if frozen~=nil then checkluatype(frozen, TYPE_BOOL) else frozen = false end

	if not allowed_components[class] then return SF.Throw("Invalid class!", 1) end

	local pos = SF.clampPos(vunwrap1(pos))
	local ang = aunwrap1(ang)

	local ply = instance.player
	model = SF.CheckModel(model, ply, true)

	if ply ~= SF.Superuser then
		if not ply:CheckLimit("starfall_components") then SF.Throw("Limit of components reached!", 2) end
		plyPropBurst:use(ply, 1)
		entList:checkuse(ply, 1)
		if gamemode.Call("PlayerSpawnSENT", ply, class)==false then SF.Throw("Another hook prevented the component from spawning", 2) end
	end

	local comp
	local ok, err = instance:runExternal(function()
		comp = ents.Create(class)
		comp:SetPos(pos)
		comp:SetAngles(ang)
		comp:SetModel(model)
		comp:Spawn()

		local mdl = comp:GetModel()
		if not mdl or mdl == "models/error.mdl" then error("Invalid model!") end

		for I = 0,  comp:GetPhysicsObjectCount() - 1 do
			local obj = comp:GetPhysicsObjectNum(I)
			if Phys_IsValid(obj) then
				obj:EnableMotion(not frozen)
			end
		end

		if ply ~= SF.Superuser then
			if propConfig.undo then
				undo.Create(class)
					undo.SetPlayer(ply)
					undo.AddEntity(comp)
				undo.Finish("Prop (" .. tostring(model) .. ")")
			end

			ply:AddCount("starfall_components", comp)
			ply:AddCleanup("starfall_components", comp)
		end
	end)
	if not ok then
		if Ent_IsValid(comp) then comp:Remove() end
		SF.Throw("Failed to create entity (" .. tostring(err) .. ")", 2)
	end
	entList:register(instance, comp)
	instance:checkCpu()

	return ewrap(comp)
end

--- Get a list of all spawnable sents.
-- @param boolean? categorized True to get an categorized list
-- @return table The table
function props_library.getSpawnableSents(categorized)
	local tbl = {}

	local add
	if categorized then
		add = function(list_name)
			tbl[list_name] = {}
			for class, _ in pairs(list.GetForEdit(list_name)) do
				table.insert(tbl[list_name], class)
			end
		end
	else
		add = function(list_name)
			for class, _ in pairs(list.GetForEdit(list_name)) do
				table.insert(tbl, class)
			end
		end
	end

	add("Weapon")
	add("SpawnableEntities")
	add("NPC")
	add("Vehicles")
	add("starfall_creatable_sent")

	return tbl
end

--- Creates a seat.
-- @param Vector pos Position of created seat
-- @param Angle ang Angle of created seat
-- @param string model Model of created seat
-- @param boolean? frozen True to spawn the entity in a frozen state. Default = False
-- @server
-- @return Entity The seat object
function props_library.createSeat(pos, ang, model, frozen)
	checkpermission(instance, nil, "prop.create")
	checkluatype(model, TYPE_STRING)
	if frozen~=nil then checkluatype(frozen, TYPE_BOOL) else frozen = false end

	local pos = SF.clampPos(vunwrap1(pos))
	local ang = aunwrap1(ang)

	local ply = instance.player
	model = SF.CheckModel(model, ply, true)

	plyPropBurst:use(ply, 1)
	entList:checkuse(ply, 1)

	local class = "prop_vehicle_prisoner_pod"

	local prop

	local ok, err = instance:runExternal(function()
		prop = ents.Create(class)
		prop:SetModel(model)

		prop:SetPos(pos)
		prop:SetAngles(ang)
		prop:Spawn()
		prop:SetKeyValue( "limitview", 0 )
		prop:Activate()

		local phys = prop:GetPhysicsObject()
		if Phys_IsValid(phys) then
			phys:EnableMotion(not frozen)
		end

		if ply ~= SF.Superuser then
			prop:SetCreator( ply )

			if propConfig.undo then
				undo.Create("SF")
					undo.SetPlayer(ply)
					undo.AddEntity(prop)
				undo.Finish("SF (" .. class .. ")")
			end

			ply:AddCleanup("props", prop)
			gamemode.Call("PlayerSpawnedVehicle", ply, prop)
		end
	end)
	if not ok then
		if Ent_IsValid(entity) then entity:Remove() end
		SF.Throw("Failed to create entity (" .. tostring(err) .. ")", 2)
	end
	entList:register(instance, prop)
	instance:checkCpu()

	return owrap(prop)
end

--- Creates a sent.
-- @param Vector pos Position of created sent
-- @param Angle ang Angle of created sent
-- @param string class Class of created sent
-- @param boolean? frozen True to spawn the entity in a frozen state. Default = False
-- @param table? data Optional table, additional entity data to be supplied to certain SENTs. See prop.SENT_Data_Structures table in Docs for list of SENTs
-- @server
-- @return Entity The sent object
function props_library.createSent(pos, ang, class, frozen, data)
	checkpermission(instance,  nil, "prop.create")

	checkluatype(class, TYPE_STRING)
	if frozen~=nil then checkluatype(frozen, TYPE_BOOL) else frozen = false end

	pos = SF.clampPos(vunwrap1(pos))
	ang = aunwrap1(ang)

	local ply = instance.player
	plyPropBurst:use(ply, 1)
	entList:checkuse(ply, 1)

	local swep = list.GetForEdit("Weapon")[class]
	local sent = list.GetForEdit("SpawnableEntities")[class]
	local npc = list.GetForEdit("NPC")[class]
	local vehicle = list.GetForEdit("Vehicles")[class]
	local sent2 = list.GetForEdit("starfall_creatable_sent")[class]

	local entity, entOk, entErr, hookcall

	if swep then
		if ply ~= SF.Superuser then
			if ((not swep.Spawnable and not ply:IsAdmin()) or
					(swep.AdminOnly and not ply:IsAdmin())) then SF.Throw("This swep is admin only!", 2) end
			if gamemode.Call("PlayerSpawnSWEP", ply, class, swep) == false then SF.Throw("Another hook prevented the swep from spawning", 2) end
		end

		entOk, entErr = instance:runExternal(function()
			entity = ents.Create(swep.ClassName)
			entity:SetPos(pos)
			entity:SetAngles(ang)
			entity:Spawn()
			entity:Activate()
		end)

		hookcall = "PlayerSpawnedSWEP"
	elseif sent then
		if ply ~= SF.Superuser then
			if sent.AdminOnly and not ply:IsAdmin() then SF.Throw("This sent is admin only!", 2) end
			if gamemode.Call("PlayerSpawnSENT", ply, class) == false then SF.Throw("Another hook prevented the sent from spawning", 2) end
		end

		entOk, entErr = instance:runExternal(function()
			local sent = scripted_ents.GetStored( class )
			if sent and sent.t.SpawnFunction then
				entity = sent.t.SpawnFunction( sent.t, ply, SF.dumbTrace(NULL, pos), class )
			else
				entity = ents.Create( class )
				if Ent_IsValid(entity) then
					entity:SetPos(pos)
					entity:SetAngles(ang)
					entity:Spawn()
					entity:Activate()
				end
			end
		end)

		hookcall = "PlayerSpawnedSENT"
	elseif npc then
		if ply ~= SF.Superuser then
			if npc.AdminOnly and not ply:IsAdmin() then SF.Throw("This npc is admin only!", 2) end
			if gamemode.Call("PlayerSpawnNPC", ply, class, "") == false then SF.Throw("Another hook prevented the npc from spawning", 2) end
		end

		
		entOk, entErr = instance:runExternal(function()
			entity = ents.Create(npc.Class)

			if (npc.Model) then
				entity:SetModel(npc.Model)
			end
			if (npc.Material) then
				entity:SetMaterial(npc.Material)
			end
			local SpawnFlags = bit.bor(SF_NPC_FADE_CORPSE, SF_NPC_ALWAYSTHINK)
			if (npc.SpawnFlags) then SpawnFlags = bit.bor(SpawnFlags, npc.SpawnFlags) end
			if (npc.TotalSpawnFlags) then SpawnFlags = npc.TotalSpawnFlags end
			entity:SetKeyValue("spawnflags", SpawnFlags)
			entity.SpawnFlags = SpawnFlags
			if (npc.KeyValues) then
				for k, v in pairs(npc.KeyValues) do
					entity:SetKeyValue(k, v)
				end
			end
			if (npc.Skin) then
				entity:SetSkin(npc.Skin)
			end
			entity:SetPos(pos)
			entity:SetAngles(ang)
			entity:Spawn()
			entity:Activate()
		end)

		hookcall = "PlayerSpawnedNPC"
	elseif vehicle then
		if ply ~= SF.Superuser and gamemode.Call("PlayerSpawnVehicle", ply, vehicle.Model, vehicle.Class, vehicle) == false then SF.Throw("Another hook prevented the vehicle from spawning", 2) end

		entOk, entErr = instance:runExternal(function()
			entity = ents.Create(vehicle.Class)
			entity:SetModel(vehicle.Model)

			if (vehicle.Model == "models/buggy.mdl") then
				entity:SetKeyValue("vehiclescript", "scripts/vehicles/jeep_test.txt")
			end
			if (vehicle.Model == "models/vehicle.mdl") then
				entity:SetKeyValue("vehiclescript", "scripts/vehicles/jalopy.txt")
			end
			if (vehicle.KeyValues) then
				for k, v in pairs(vehicle.KeyValues) do

					local kLower = string.lower(k)

					if (kLower == "vehiclescript" or
						kLower == "limitview"     or
						kLower == "vehiclelocked" or
						kLower == "cargovisible"  or
						kLower == "enablegun")
					then
						entity:SetKeyValue(k, v)
					end

				end
			end

			if (vehicle.Members) then
				table.Merge(entity, vehicle.Members)
				duplicator.StoreEntityModifier(entity, "VehicleMemDupe", vehicle.Members)
			end

			if (entity.SetVehicleClass) then entity:SetVehicleClass(class) end
			entity.VehicleName = class
			entity.VehicleTable = vehicle

			entity.ClassOverride = vehicle.Class
			entity:SetPos(pos)
			entity:SetAngles(ang)
			entity:Spawn()
			entity:Activate()
		end)

		hookcall = "PlayerSpawnedVehicle"
	elseif sent2 then
		local classTbl = scripted_ents.GetStored(class)
		if not classTbl then SF.Throw("This entity doesn't exist!", 2) end
		if ply ~= SF.Superuser then
			if classTbl.t.AdminOnly and not ply:IsAdmin() then SF.Throw("This sent is admin only!", 2) end
			if gamemode.Call("PlayerSpawnSENT", ply, class) == false then SF.Throw("Another hook prevented the sent from spawning", 2) end
		end

		local enttbl = {}
		local sentparams = sent2[1]
		if data ~= nil then checkluatype(data, TYPE_TABLE) else data = {} end
		if data.Model and isstring(data.Model) then
			data.Model = SF.CheckModel(data.Model, ply, true)
		end

		for k, v in pairs(data) do
			if not sentparams[k] then SF.Throw("Invalid parameter in data: " .. tostring(k), 2) end
		end

		-- Apply data
		for param, org in pairs(sentparams) do
			local value = data[param]

			if value~=nil then
				value = ounwrap(value) or value

				if org[1]==TYPE_COLOR then
					if not IsColor(value) then SF.ThrowTypeError("Color", SF.GetType(value), 2, "Parameter: " .. param) end
				elseif org[1]==TYPE_NUMBER then
					SF.CheckValidNumber(value, nil, "Parameter: " .. param)
				else
					checkluatype(value, org[1], nil, "Parameter: " .. param)
				end
				enttbl[param] = value

			elseif org[2]~=nil then
				enttbl[param] = org[2]
			else
				SF.Throw("Missing data parameter: " .. param, 2)
			end
		end

		-- Supply additional data
		enttbl.Data = enttbl
		enttbl.Name = ""
		enttbl.Class = class
		enttbl.Pos = pos
		enttbl.Angle = ang

		entOk, entErr = instance:runExternal(function()
			if sent2._preFactory then
				sent2._preFactory(ply, enttbl)
			end

			entity = duplicator.CreateEntityFromTable(ply ~= SF.Superuser and ply or game.GetWorld(), enttbl)
			if not isentity(entity) then
				entity = nil
				error("Factory func returned invalid value", 2)
			end

			if sent2._postFactory then
				sent2._postFactory(ply, entity, enttbl)
			end

			if entity.PreEntityCopy then
				entity:PreEntityCopy() -- To build dupe modifiers
			end
			if entity.PostEntityCopy then
				entity:PostEntityCopy()
			end
			if entity.PostEntityPaste then
				entity:PostEntityPaste(ply, entity, {[entity:EntIndex()] = entity})
			end
		end)
	end
	if not entOk then
		if Ent_IsValid(entity) then entity:Remove() end
		SF.Throw("Failed to create entity (" .. tostring(entErr) .. ")", 2)
	end

	if Ent_IsValid(entity) then
		entList:register(instance, entity)

		if CPPI then entity:CPPISetOwner(ply == SF.Superuser and NULL or ply) end

		local phys = entity:GetPhysicsObject()
		if Phys_IsValid(phys) then
			phys:EnableMotion(not frozen)
		end

		if ply ~= SF.Superuser then
			entity:SetCreator( ply )

			if propConfig.undo then
				undo.Create("SF")
					undo.SetPlayer(ply)
					undo.AddEntity(entity)
				undo.Finish("SF (" .. class .. ")")
			end

			ply:AddCleanup("props", entity)
			if hookcall then
				gamemode.Call(hookcall, ply, entity)
			end
		end

		instance:checkCpu()
		return owrap(entity)
	end
end

--- Checks if a user can spawn anymore props.
-- @server
-- @return boolean True if user can spawn props, False if not.
function props_library.canSpawn()
	if not SF.Permissions.hasAccess(instance, nil, "prop.create") then return false end
	return entList:check(instance.player) > 0 and plyPropBurst:check(instance.player) >= 1
end

--- Checks how many props can be spawned
-- @server
-- @return number Number of props able to be spawned
function props_library.propsLeft()
	if not SF.Permissions.hasAccess(instance,  nil, "prop.create") then return 0 end
	return math.min(entList:check(instance.player), plyPropBurst:check(instance.player))
end

--- Returns how many props per second the user can spawn
-- @server
-- @return number Number of props per second the user can spawn
function props_library.spawnRate()
	return plyPropBurst.rate
end

--- Returns whether the chip will automatically remove created props when the chip is removed
-- @server
-- @return boolean Determines whether props will be cleaned
function props_library.getPropClean()
    return propConfig.clean
end

--- Returns whether the props are undo-able
-- @server
-- @return boolean Determines whether props are undo-able
function props_library.getPropUndo()
    return propConfig.undo
end

--- Sets whether the chip should remove created props when the chip is removed
-- @server
-- @param boolean on Whether the props should be cleaned or not
function props_library.setPropClean(on)
	checkluatype(on, TYPE_BOOL)
	propConfig.clean = on
end

--- Sets whether the props should be undo-able
-- @server
-- @param boolean on Whether the props should be undo-able
function props_library.setPropUndo(on)
	checkluatype(on, TYPE_BOOL)
	propConfig.undo = on
end

end
