--[[
| 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/
--]]

local FrameTime = FrameTime
local CurTime = CurTime
local NULL = NULL
local Vector = Vector
local util = util
local SysTime = SysTime

local BUILDER, PART = pac.PartTemplate("base")

PART.ClassName = "event"

PART.ThinkTime = 0
PART.AlwaysThink = true
PART.Icon = 'icon16/clock.png'

BUILDER:StartStorableVars()
	BUILDER:GetSet("Event", "", {enums = function(part)
		local output = {}

		for i, event in pairs(part.Events) do
			if not event.IsAvailable or event:IsAvailable(part) then
				output[i] = event
			end
		end

		return output
	end})
	BUILDER:GetSet("Operator", "find simple", {enums = function(part) local tbl = {} for i,v in ipairs(part.Operators) do tbl[v] = v end return tbl end})
	BUILDER:GetSet("Arguments", "", {hidden = true})
	BUILDER:GetSet("Invert", true)
	BUILDER:GetSet("RootOwner", true)
	BUILDER:GetSet("AffectChildrenOnly", false)
	BUILDER:GetSet("ZeroEyePitch", false)
	BUILDER:GetSetPart("TargetPart")
BUILDER:EndStorableVars()

function PART:SetEvent(event)
	local reset = (self.Arguments == "") or
	(self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event)

	self.Event = event
	self:SetWarning()
	self:GetDynamicProperties(reset)
end

local function get_default(typ)
	if typ == "string" then
		return ""
	elseif typ == "number" then
		return 0
	elseif typ == "boolean" then
		return false
	end
end

local function string_to_type(typ, val)
	if typ == "number" then
		return tonumber(val) or 0
	elseif typ == "boolean" then
		return tobool(val) or false
	end
	return val
end

local function cast(typ, val)
	if val == nil then
		val = get_default(typ)
	end

	return string_to_type(typ, val)
end

function PART:GetDynamicProperties(reset_to_default)
	local data = self.Events[self.Event]
	if not data then return end

	local tbl = {}
	for pos, arg in ipairs(data:GetArguments()) do
		local key, typ, udata = unpack(arg)
		udata = udata or {}
		udata.group = udata.group or "arguments"

		tbl[key] = {
			key = key,
			get = function()
				local args = {self:GetParsedArguments(data)}
				return cast(typ, args[pos])
			end,
			set = function(val)
				local args = {self:GetParsedArguments(data)}
				args[pos] = val
				self:ParseArguments(unpack(args))
			end,
			udata = udata,
		}

		local arg = tbl[key]
		if arg.get() == nil or reset_to_default then
			if udata.default then
				arg.set(udata.default)
			else
				arg.set(nil)
			end
		end
	end

	return tbl
end

local function convert_angles(self, ang)
	if self.ZeroEyePitch then
		ang.p = 0
	end

	return ang
end

local function calc_velocity(part)
	if not part.GetWorldPosition then
		return vector_origin
	end

	local diff = part:GetWorldPosition() - (part.last_pos or Vector(0, 0, 0))
	part.last_pos = part:GetWorldPosition()

	part.last_vel_smooth = part.last_vel_smooth or Vector(0, 0, 0)
	part.last_vel_smooth = part.last_vel_smooth + (part:GetWorldPosition() - part.last_vel_smooth) * FrameTime() * 4

	return part.last_vel_smooth
end

local function try_viewmodel(ent)
	return ent == pac.LocalViewModel and pac.LocalPlayer or ent
end

local function get_owner(self)
	if self.RootOwner then
		return try_viewmodel(self:GetRootPart():GetOwner())
	else
		return try_viewmodel(self:GetOwner())
	end
end

local movetypes = {}

for k,v in pairs(_G) do
	if isstring(k) and isnumber(v) and k:sub(0,9) == "MOVETYPE_" then
		movetypes[v] = k:sub(10):lower()
	end
end

PART.Events = {}
PART.OldEvents = {
	random = {
		arguments = {{compare = "number"}},
		callback = function(self, ent, compare)
			return self:NumberOperator(math.random(), compare)
		end,
	},

	randint = {
		arguments = {{compare = "number"}, {min = "number"}, {max = "number"}},
		callback = function(self, ent, compare, min, max)
			min = min or 0
			max = max or 1
			if min > max then return 0 end
			return self:NumberOperator(math.random(min,max), compare)
		end,
	},

	random_timer = {
		arguments = {{min = "number"}, {max = "number"}, {holdtime = "number"}},
		callback = function(self, ent, min, max, holdtime)

			holdtime = holdtime or 0.1
			min = min or 0
			max = max or 1

			if min > max then return false end

			if self.RndTime == nil then
				self.RndTime = 0
			end

			if not self.SetRandom then
				self.RndTime = CurTime() + math.random(min,max)
				self.SetRandom = true
			elseif self.SetRandom then

				if CurTime() > self.RndTime then
					if CurTime() < self.RndTime + holdtime then
						return true
					end

					self.SetRandom = false
					return false
				end

			end

			return false
		end,
	},

	timerx = {
		arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}, {synced_time = "boolean"}},
		nice = function(self, ent, seconds)
			return "timerx: " .. ("%.2f"):format(self.number or 0, 2) .. " " .. self:GetOperator() .. " " .. seconds .. " seconds?"
		end,
		callback = function(self, ent, seconds, reset_on_hide, synced_time)
			local time = synced_time and CurTime() or RealTime()

			self.time = self.time or time
			self.timerx_reset = reset_on_hide

			if self.AffectChildrenOnly and self:IsHiddenBySomethingElse() then
				return false
			end
			self.number = time - self.time

			return self:NumberOperator(self.number, seconds)
		end,
	},

	timersys = {
		arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}},

		callback = function(self, ent, seconds, reset_on_hide)
			local time = SysTime()

			self.time = self.time or time
			self.timerx_reset = reset_on_hide

			if self.AffectChildrenOnly and self:IsHiddenBySomethingElse() then
				return false
			end
			return self:NumberOperator(time - self.time, seconds)
		end,
	},

	map_name = {
		arguments = {{find = "string"}},
		callback = function(self, ent, find)
			return self:StringOperator(game.GetMap(), find)
		end,
	},

	fov = {
		arguments = {{fov = "number"}},
		callback = function(self, ent, fov)
			ent = try_viewmodel(ent)

			if ent.GetFOV then
				return self:NumberOperator(ent:GetFOV(), fov)
			end

			return 0
		end,
	},
	health_lost = {
		arguments = {{amount = "number"}},
		callback = function(self, ent, amount)

			ent = try_viewmodel(ent)

			if ent.Health then

				local dmg = self.pac_lastdamage or 0

				if self.dmgCD == nil then
					self.dmgCD = 0
				end

				if not self.pac_wasdmg then

					local dmgDone = dmg - ent:Health()
					self.pac_lastdamage = ent:Health()

					if dmgDone == 0 then return false end

					if self:NumberOperator(dmgDone,amount) then
						self.pac_wasdmg = true
						self.dmgCD = pac.RealTime + 0.2
					end

				else

					if self.pac_wasdmg and pac.RealTime > self.dmgCD then
						self.pac_wasdmg = false
					end

				end

				return self.pac_wasdmg
			end

			return false
		end,
	},

	holdtype = {
		arguments = {{find = "string"}},
		callback = function(self, ent, find)
			ent = try_viewmodel(ent)
			local wep = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL
			if wep:IsValid() and self:StringOperator(wep:GetHoldType(), find) then
				return true
			end
		end,
	},

	is_crouching = {
		callback = function(self, ent)
			ent = try_viewmodel(ent)
			return ent.Crouching and ent:Crouching()
		end,
	},

	is_typing = {
		callback = function(self, ent)
			ent = self:GetPlayerOwner()
			return ent.IsTyping and ent:IsTyping()
		end,
	},

	using_physgun = {
		callback = function(self, ent)
			ent = self:GetPlayerOwner()
			local pac_drawphysgun_event_part = ent.pac_drawphysgun_event_part
			if not pac_drawphysgun_event_part then
				pac_drawphysgun_event_part = {}
				ent.pac_drawphysgun_event_part = pac_drawphysgun_event_part
			end
			pac_drawphysgun_event_part[self] = true
			return ent.pac_drawphysgun_event ~= nil
		end,
	},

	eyetrace_entity_class = {
		arguments = {{class = "string"}},
		callback = function(self, ent, find)
			if ent.GetEyeTrace then
				ent = ent:GetEyeTrace().Entity
				if self:StringOperator(ent:GetClass(), find) then
					return true
				end
			end
		end,
	},

	owner_health = {
		arguments = {{health = "number"}},
		callback = function(self, ent, num)
			ent = try_viewmodel(ent)
			if ent.Health then
				return self:NumberOperator(ent:Health(), num)
			end

			return 0
		end,
	},
	owner_max_health = {
		arguments = {{health = "number"}},
		callback = function(self, ent, num)
			ent = try_viewmodel(ent)
			if ent.GetMaxHealth then
				return self:NumberOperator(ent:GetMaxHealth(), num)
			end

			return 0
		end,
	},
	owner_alive = {
		callback = function(self, ent)
			ent = try_viewmodel(ent)
			if ent.Alive then
				return ent:Alive()
			end
			return 0
		end,
	},
	owner_armor = {
		arguments = {{armor = "number"}},
		callback = function(self, ent, num)
			ent = try_viewmodel(ent)
			if ent.Armor then
				return self:NumberOperator(ent:Armor(), num)
			end

			return 0
		end,
	},

	owner_scale_x = {
		arguments = {{scale = "number"}},
		callback = function(self, ent, num)
			ent = try_viewmodel(ent)

				return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.x or (ent.GetModelScale and ent:GetModelScale()) or 1, num)
		end,
	},
	owner_scale_y = {
		arguments = {{scale = "number"}},
		callback = function(self, ent, num)
			ent = try_viewmodel(ent)

				return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.y or (ent.GetModelScale and ent:GetModelScale()) or 1, num)
		end,
	},
	owner_scale_z = {
		arguments = {{scale = "number"}},
		callback = function(self, ent, num)
			ent = try_viewmodel(ent)

				return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.z or (ent.GetModelScale and ent:GetModelScale()) or 1, num)
		end,
	},

	pose_parameter = {
		arguments = {{name = "string"}, {num = "number"}},
		callback = function(self, ent, name, num)
			ent = try_viewmodel(ent)
			return self:NumberOperator(ent:GetPoseParameter(name), num)
		end,
	},

	speed = {
		arguments = {{speed = "number"}},
		callback = function(self, ent, num)
			ent = try_viewmodel(ent)
			return self:NumberOperator(ent:GetVelocity():Length(), num)
		end,
	},

	is_under_water = {
		arguments = {{level = "number"}},
		callback = function(self, ent, num)
			ent = try_viewmodel(ent)
			return self:NumberOperator(ent:WaterLevel(), num)
		end,
	},

	is_on_fire = {
		callback = function(self, ent)
			ent = try_viewmodel(ent)
			return ent:IsOnFire()
		end,
	},

	client_spawned = {
		arguments = {{time = "number"}},
		callback = function(self, ent, time)
			time = time or 0.1
			ent = try_viewmodel(ent)
			if ent.pac_playerspawn and ent.pac_playerspawn + time > pac.RealTime then
				return true
			end
		end,
	},

	is_client = {
		callback = function(self, ent)
			ent = try_viewmodel(ent)
			return self:GetPlayerOwner() == ent
		end,
	},

	is_flashlight_on = {
		callback = function(self, ent)
			ent = try_viewmodel(ent)
			return ent.FlashlightIsOn and ent:FlashlightIsOn()
		end,
	},

	collide = {
		callback = function(self, ent)
			ent.pac_event_collide_callback = ent.pac_event_collide_callback or ent:AddCallback("PhysicsCollide", function(ent, data)
				ent.pac_event_collision_data = data
			end)

			if ent.pac_event_collision_data then
				local data = ent.pac_event_collision_data
				ent.pac_event_collision_data = nil
				return true
			end

			return false
		end,
	},

	ranger = {
		arguments = {{distance = "number"}, {compare = "number"}, {npcs_and_players_only = "boolean"}},
		userdata = {{editor_panel = "ranger", ranger_property = "distance"}, {editor_panel = "ranger", ranger_property = "compare"}},
		callback = function(self, ent, distance, compare, npcs_and_players_only)
			local parent = self:GetParentEx()

			if parent:IsValid() and parent.GetWorldPosition then
				self:SetWarning()
				distance = distance or 1
				compare = compare or 0

				local res = util.TraceLine({
					start = parent:GetWorldPosition(),
					endpos = parent:GetWorldPosition() + parent:GetWorldAngles():Forward() * distance,
					filter = ent,
				})

				if npcs_and_players_only and (not res.Entity:IsPlayer() and not res.Entity:IsNPC()) then
					return false
				end

				return self:NumberOperator(res.Fraction * distance, compare)
			else
				local classname = parent:GetNiceName()
				local name = parent:GetName()
				self:SetWarning(("ranger doesn't work on [%s] %s"):format(classname, classname ~= name and "(" .. name .. ")" or ""))
			end
		end,
	},

	is_on_ground = {
		arguments = {{exclude_noclip = "boolean"}},
		callback = function(self, ent, exclude_noclip)
			ent = try_viewmodel(ent)
			if exclude_noclip and ent:GetMoveType() == MOVETYPE_NOCLIP then return false end
			--return ent.IsOnGround and ent:IsOnGround()

			local rad = ent:BoundingRadius() / 2
			local times = 2

			for x = -times, times do
				for y = -times, times do
					local xy = Vector(x/times,y/times,0) * rad
					local res = util.TraceLine({
						start = ent:GetPos() + xy + Vector(0,0,2.5),
						endpos = ent:GetPos() + xy/1.25 + Vector(0,0,-10),
						--mins = ent:OBBMins(),
						--maxs = ent:OBBMaxs(),
						filter = ent,
						--mask = MASK_SOLID_BRUSHONLY,
					})
					if res.Hit and math.abs(res.HitNormal.z) > 0.70 then return true end
				end
			end

			return false
		end,
	},

	is_touching = {
		arguments = {{extra_radius = "number"}},
		userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius"}},
		callback = function(self, ent, extra_radius)
			extra_radius = extra_radius or 0

			local radius =  ent:BoundingRadius()

			if radius == 0 and IsValid(ent.pac_projectile) then
				radius = ent.pac_projectile:GetRadius()
			end

			radius = math.max(radius + extra_radius + 1, 1)

			local mins = Vector(-1,-1,-1)
			local maxs = Vector(1,1,1)
			local startpos = ent:WorldSpaceCenter()
			mins = mins * radius
			maxs = maxs * radius

			local tr = util.TraceHull( {
				start = startpos,
				endpos = startpos,
				maxs = maxs,
				mins = mins,
				filter = ent
			} )
			return tr.Hit
		end,
	},

	is_in_noclip = {
		callback = function(self, ent)
			ent = try_viewmodel(ent)
			return ent:GetMoveType() == MOVETYPE_NOCLIP and (not ent.GetVehicle or not ent:GetVehicle():IsValid())
		end,
	},

	is_voice_chatting = {
		callback = function(self, ent)
			ent = try_viewmodel(ent)
			return ent.IsSpeaking and ent:IsSpeaking()
		end,
	},

	ammo = {
		arguments = {{primary = "boolean"}, {amount = "number"}},
		userdata = {{editor_onchange = function(part, num) return math.Round(num) end}},
		callback = function(self, ent, primary, amount)
			ent = try_viewmodel(ent)
			ent = ent.GetActiveWeapon and ent:GetActiveWeapon() or ent

			if ent:IsValid() then
				return self:NumberOperator(ent.Clip1 and (primary and ent:Clip1() or ent:Clip2()) or 0, amount)
			end
		end,
	},
	total_ammo = {
		arguments = {{ammo_id = "string"}, {amount = "number"}},
		callback = function(self, ent, ammo_id, amount)
			if ent.GetAmmoCount then
				ammo_id = tonumber(ammo_id) or ammo_id:lower()
				if ammo_id == "primary" then
					local wep = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL
					return self:NumberOperator(wep:IsValid() and ent:GetAmmoCount(wep:GetPrimaryAmmoType()) or 0, amount)
				elseif ammo_id == "secondary" then
					local wep = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL
					return self:NumberOperator(wep:IsValid() and ent:GetAmmoCount(wep:GetSecondaryAmmoType()) or 0, amount)
				else
					return self:NumberOperator(ent:GetAmmoCount(ammo_id), amount)
				end
			end
		end,
	},

	clipsize = {
		arguments = {{primary = "boolean"}, {amount = "number"}},
		callback = function(self, ent, primary, amount)
			ent = try_viewmodel(ent)
			ent = ent.GetActiveWeapon and ent:GetActiveWeapon() or ent

			if ent:IsValid() then
				return self:NumberOperator(ent.GetMaxClip1 and (primary and ent:GetMaxClip1() or ent:GetMaxClip2()) or 0, amount)
			end
		end,
	},

	vehicle_class = {
		arguments = {{find = "string"}},
		callback = function(self, ent, find)
			ent = try_viewmodel(ent)
			ent = ent.GetVehicle and ent:GetVehicle() or NULL

			if ent:IsValid() then
				return self:StringOperator(ent:GetClass(), find)
			end
		end,
	},

	vehicle_model = {
		arguments = {{find = "string"}},
		callback = function(self, ent, find)
			ent = try_viewmodel(ent)
			ent = ent.GetVehicle and ent:GetVehicle() or NULL

			if ent:IsValid() and ent:GetModel() then
				return self:StringOperator(ent:GetModel():lower(), find)
			end
		end,
	},

	driver_name = {
		arguments = {{find = "string"}},
		callback = function(self, ent, find)
			ent = ent.GetDriver and ent:GetDriver() or NULL

			if ent:IsValid() then
				return self:StringOperator(ent:GetName(), find)
			end
		end,
	},

	entity_class = {
		arguments = {{find = "string"}},
		callback = function(self, ent, find)
			return self:StringOperator(ent:GetClass(), find)
		end,
	},

	weapon_class = {
		arguments = {{find = "string"}, {hide = "boolean"}},
		callback = function(self, ent, find, hide)
			ent = try_viewmodel(ent)

			local wep = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL

			if wep:IsValid() then
				local class_name = wep:GetClass()
				local found = self:StringOperator(class_name, find)

				if class_name == "hands" and not found then
					found = self:StringOperator("none", find)
				end

				if found then
					wep:SetNoDraw(hide)
					wep.pac_weapon_class = true
					return true
				end
			end
		end,
	},

	has_weapon = {
		arguments = {{find = "string"}},
		callback = function(self, ent, find)
			ent = try_viewmodel(ent)
			local tbl = ent.GetWeapons and ent:GetWeapons()
			if tbl then
				for _, val in pairs(tbl) do
					val = val:GetClass()
					if self:StringOperator(val, find) then
						return true
					end
				end
			end
		end,
	},

	model_name = {
		arguments = {{find = "string"}},
		callback = function(self, ent, find)
			return self:StringOperator(ent:GetModel(), find)
		end,
	},

	sequence_name = {
		arguments = {{find = "string"}},
		nice = function(self, ent)
			return self.sequence_name or "invalid sequence"
		end,
		callback = function(self, ent, find)
			ent = get_owner(self)

			self.sequence_name = ent:GetSequenceName(ent:GetSequence())

			return self:StringOperator(self.sequence_name, find)
		end,
	},

	timer = {
		arguments = {{interval = "number"}, {offset = "number"}},
		callback = function(self, ent, interval, offset)
			interval = interval or 1
			offset = offset or 0

			if interval == 0 or interval < FrameTime() then
				self.timer_hack = not self.timer_hack
				return self.timer_hack
			end

			return (CurTime() + offset) % interval > (interval / 2)
		end,
	},

	animation_event = {
		arguments = {{find = "string"}, {time = "number"}},
		nice = function(self)
			return self.anim_name or ""
		end,
		callback = function(self, ent, find, time)
			time = time or 0.1

			ent = get_owner(self)

			local data = ent.pac_anim_event
			local b = false

			if data and (self:StringOperator(data.name, find) and (time == 0 or data.time + time > pac.RealTime)) then
				data.reset = false
				b = true
			end

			if b then
				self.anim_name = data.name
			else
				self.anim_name = nil
			end

			return b
		end,
	},

	fire_bullets = {
		arguments = {{find_ammo = "string"}, {time = "number"}},
		callback = function(self, ent, find, time)
			time = time or 0.1

			ent = try_viewmodel(ent)

			local data = ent.pac_fire_bullets
			local b = false

			if data and (self:StringOperator(data.name, find) and (time == 0 or data.time + time > pac.RealTime)) then
				data.reset = false
				b = true
			end

			return b
		end,
	},

	emit_sound = {
		arguments = {{find_sound = "string"}, {time = "number"}, {mute = "boolean"}},
		callback = function(self, ent, find, time, mute)
			time = time or 0.1

			ent = try_viewmodel(ent)

			local data = ent.pac_emit_sound
			local b = false

			if data and (self:StringOperator(data.name, find) and (time == 0 or data.time + time > pac.RealTime)) then
				data.reset = false
				b = true
				if mute then
					data.mute_me = true
				end
			end

			return b
		end,
	},

	command = {
		arguments = {{find = "string"}, {time = "number"}, {hide_in_eventwheel = "boolean"}},
		userdata = {
			{default = "change_me", editor_friendly = "CommandName"},
			{default = 0.1, editor_friendly = "EventDuration"},
			{default = false, group = "event wheel", editor_friendly = "HideInEventWheel"}
		},
		nice = function(self, ent, find, time)
			find = find or "?"
			time = time or "?"
			return "command: " .. find .. " | " .. "duration: " .. time
		end,
		callback = function(self, ent, find, time)
			time = time or 0.1

			local ply = self:GetPlayerOwner()

			local events = ply.pac_command_events

			if events then
				local found = nil
				for _, data in pairs(events) do
					if self:StringOperator(data.name, find) then
						if data.on > 0 then
							found = data.on == 1
						elseif data.time + time > pac.RealTime then
							found = true
						end
					end
				end
				return found
			end
		end,
	},

	say = {
		arguments = {{find = "string"}, {time = "number"}, {all_players = "boolean"}},
		callback = function(self, ent, find, time, all_players)
			time = time or 0.1

			ent = try_viewmodel(ent)

			if all_players then
				for _, ply in ipairs(player.GetAll()) do
					local data = ply.pac_say_event

					if data and self:StringOperator(data.str, find) and data.time + time > pac.RealTime then
						return true
					end
				end
			else
				local owner = self:GetRootPart():GetOwner()
				if owner:IsValid() then
					local data = owner.pac_say_event

					if data and self:StringOperator(data.str, find) and data.time + time > pac.RealTime then
						return true
					end
				end
			end
		end,
	},

	-- outfit owner
	owner_velocity_length = {
		arguments = {{speed = "number"}},
		callback = function(self, ent, speed)
			local parent = self:GetParentEx()
			ent = try_viewmodel(ent)

			if parent:IsValid() and ent:IsValid() then
				return self:NumberOperator(get_owner(parent):GetVelocity():Length(), speed)
			end

			return 0
		end,
	},
	owner_velocity_forward = {
		arguments = {{speed = "number"}},
		callback = function(self, ent, speed)
			ent = try_viewmodel(ent)

			if ent:IsValid() then
				return self:NumberOperator(convert_angles(self, ent:EyeAngles()):Forward():Dot(ent:GetVelocity()), speed)
			end

			return 0
		end,
	},
	owner_velocity_right = {
		arguments = {{speed = "number"}},
		callback = function(self, ent, speed)
			ent = try_viewmodel(ent)

			if ent:IsValid() then
				return self:NumberOperator(convert_angles(self, ent:EyeAngles()):Right():Dot(ent:GetVelocity()), speed)
			end

			return 0
		end,
	},
	owner_velocity_up = {
		arguments = {{speed = "number"}},
		callback = function(self, ent, speed)
			ent = try_viewmodel(ent)

			if ent:IsValid() then
				return self:NumberOperator(convert_angles(self, ent:EyeAngles()):Up():Dot(ent:GetVelocity()), speed)
			end

			return 0
		end,
	},
	owner_velocity_world_forward = {
		arguments = {{speed = "number"}},
		callback = function(self, ent, speed)
			ent = try_viewmodel(ent)

			if owner:IsValid() then
				return self:NumberOperator(ent:GetVelocity()[1], speed)
			end

			return 0
		end,
	},
	owner_velocity_world_right = {
		arguments = {{speed = "number"}},
		callback = function(self, ent, speed)
			ent = try_viewmodel(ent)

			if ent:IsValid() then
				return self:NumberOperator(ent:GetVelocity()[2], speed)
			end

			return 0
		end,
	},
	owner_velocity_world_up = {
		arguments = {{speed = "number"}},
		callback = function(self, ent, speed)
			ent = try_viewmodel(ent)

			if ent:IsValid() then
				return self:NumberOperator(ent:GetVelocity()[3], speed)
			end

			return 0
		end,
	},

	-- parent part
	parent_velocity_length = {
		arguments = {{speed = "number"}},
		callback = function(self, ent, speed)
			local parent = self:GetParentEx()

			if not self.TargetPart:IsValid() and parent:HasParent() then
				parent = parent:GetParent()
			end

			if parent:IsValid() then
				return self:NumberOperator(calc_velocity(parent):Length(), speed)
			end

			return 0
		end,
	},
	parent_velocity_forward = {
		arguments = {{speed = "number"}},
		callback = function(self, ent, speed)
			local parent = self:GetParentEx()

			if not self.TargetPart:IsValid() and parent:HasParent() then
				parent = parent:GetParent()
			end

			if parent:IsValid() and parent.GetWorldAngles then
				return self:NumberOperator(parent:GetWorldAngles():Forward():Dot(calc_velocity(parent)), speed)
			end

			return 0
		end,
	},
	parent_velocity_right = {
		arguments = {{speed = "number"}},
		callback = function(self, ent, speed)
			local parent = self:GetParentEx()

			if not self.TargetPart:IsValid() and parent:HasParent() then
				parent = parent:GetParent()
			end

			if parent:IsValid() and parent.GetWorldAngles then
				return self:NumberOperator(parent:GetWorldAngles():Right():Dot(calc_velocity(parent)), speed)
			end

			return 0
		end,
	},
	parent_velocity_up = {
		arguments = {{speed = "number"}},
		callback = function(self, ent, speed)
			local parent = self:GetParentEx()

			if not self.TargetPart:IsValid() and  parent:HasParent() then
				parent = parent:GetParent()
			end

			if parent:IsValid() and parent.GetWorldAngles then
				return self:NumberOperator(parent:GetWorldAngles():Up():Dot(calc_velocity(parent)), speed)
			end

			return 0
		end,
	},

	parent_scale_x = {
		arguments = {{scale = "number"}},
		callback = function(self, ent, num)
			local parent = self:GetParentEx()

			if not self.TargetPart:IsValid() and parent:HasParent() then
				parent = parent:GetParent()
			end

			if parent:IsValid() then
				return self:NumberOperator((parent.Type == "part" and parent.Scale and parent.Scale.x * parent.Size) or (parent.pac_model_scale and parent.pac_model_scale.x) or (parent.GetModelScale and parent:GetModelScale()) or 1, num)
			end

			return 1
		end,
	},
	parent_scale_y = {
		arguments = {{scale = "number"}},
		callback = function(self, ent, num)
			local parent = self:GetParentEx()

			if not self.TargetPart:IsValid() and parent:HasParent() then
				parent = parent:GetParent()
			end

			if parent:IsValid() then
				return self:NumberOperator((parent.Type == "part" and parent.Scale and parent.Scale.y * parent.Size) or (parent.pac_model_scale and parent.pac_model_scale.y) or (parent.GetModelScale and parent:GetModelScale()) or 1, num)
			end

			return 1
		end,
	},
	parent_scale_z = {
		arguments = {{scale = "number"}},
		callback = function(self, ent, num)
			local parent = self:GetParentEx()

			if not self.TargetPart:IsValid() and parent:HasParent() then
				parent = parent:GetParent()
			end

			if parent:IsValid() then
				return self:NumberOperator((parent.Type == "part" and parent.Scale and parent.Scale.z * parent.Size) or (parent.pac_model_scale and parent.pac_model_scale.z) or (parent.GetModelScale and parent:GetModelScale()) or 1, num)
			end

			return 1
		end,
	},

	gravitygun_punt = {
		arguments = {{time = "number"}},
		callback = function(self, ent, time)
			time = time or 0.1

			ent = try_viewmodel(ent)

			local punted = ent.pac_gravgun_punt

			if punted and punted + time > pac.RealTime then
				return true
			end
		end,
	},

	movetype = {
		arguments = {{find = "string"}},
		callback = function(self, ent, find)
			local mt = ent:GetMoveType()
			if movetypes[mt] then
				return self:StringOperator(movetypes[mt], find)
			end
		end,
	},

	dot_forward = {
		arguments = {{normal = "number"}},
		callback = function(self, ent, normal)

			local owner = self:GetRootPart():GetOwner()

			if owner:IsValid() then
				local ang = owner:EyeAngles()
				ang.p = 0
				return self:NumberOperator(pac.EyeAng:Forward():Dot(ang:Forward()), normal)
			end

			return 0
		end,
	},

	dot_right = {
		arguments = {{normal = "number"}},
		callback = function(self, ent, normal)

			local owner = self:GetRootPart():GetOwner()

			if owner:IsValid() then
				local ang = owner:EyeAngles()
				ang.p = 0
				return self:NumberOperator(pac.EyeAng:Right():Dot(ang:Forward()), normal)
			end

			return 0
		end,
	},

	flat_dot_forward = {
		arguments = {{normal = "number"}},
		callback = function(self, ent, normal)
			local owner = self:GetRootPart():GetOwner()

			if owner:IsValid() then
				local ang = owner:EyeAngles()
				ang.p = 0
				ang.r = 0
				local dir = pac.EyePos - owner:EyePos()
				dir[3] = 0
				dir:Normalize()
				return self:NumberOperator(dir:Dot(ang:Forward()), normal)
			end

			return 0
		end
	},

	flat_dot_right = {
		arguments = {{normal = "number"}},
		callback = function(self, ent, normal)
			local owner = self:GetRootPart():GetOwner()

			if owner:IsValid() then
				local ang = owner:EyeAngles()
				ang.p = 0
				ang.r = 0
				local dir = pac.EyePos - owner:EyePos()
				dir[3] = 0
				dir:Normalize()
				return self:NumberOperator(dir:Dot(ang:Right()), normal)
			end

			return 0
		end
	}
}

do
	local enums = {}
	local enums2 = {}
	for key, val in pairs(_G) do
		if isstring(key) and isnumber(val) then
			if key:sub(0,4) == "KEY_" and not key:find("_LAST$") and not key:find("_FIRST$")  and not key:find("_COUNT$")  then
				enums[val] = key:sub(5):lower()
				enums2[enums[val]] = val
			elseif (key:sub(0,6) == "MOUSE_" or key:sub(0,9) == "JOYSTICK_") and not key:find("_LAST$") and not key:find("_FIRST$")  and not key:find("_COUNT$")  then
				enums[val] = key:lower()
				enums2[enums[val]] = val
			end
		end
	end

	pac.key_enums = enums

	--TODO: Rate limit!!!
	net.Receive("pac.BroadcastPlayerButton", function()
		local ply = net.ReadEntity()

		if not ply:IsValid() then return end

		if ply == pac.LocalPlayer and (pace and pace.IsFocused() or gui.IsConsoleVisible()) then return end

		local key = net.ReadUInt(8)
		local down = net.ReadBool()

		key = pac.key_enums[key] or key

		ply.pac_buttons = ply.pac_buttons or {}
		ply.pac_buttons[key] = down
	end)

	PART.OldEvents.button = {
		arguments = {{button = "string"}},
		userdata = {{enums = function()
			return enums
		end}},
		nice = function(self, ent, button)
			local ply = self:GetPlayerOwner()

			local active = {}
			if ply.pac_buttons then
				for k,v in pairs(ply.pac_buttons) do
					if v then
						table.insert(active, "\"" .. tostring(k) .. "\"")
					end
				end
			end
			active = table.concat(active, " or ")

			if active == "" then
				active = "-"
			end

			return self:GetOperator() .. " \"" .. button .. "\"" .. " in (" .. active .. ")"
		end,
		callback = function(self, ent, button)
			local ply = self:GetPlayerOwner()

			if ply == pac.LocalPlayer then
				ply.pac_broadcast_buttons = ply.pac_broadcast_buttons or {}
				if not ply.pac_broadcast_buttons[button] then
					local val = enums2[button:lower()]
					if val then
						net.Start("pac.AllowPlayerButtons")
						net.WriteUInt(val, 8)
						net.SendToServer()
					end
					ply.pac_broadcast_buttons[button] = true
				end
			end

			local buttons = ply.pac_buttons

			if buttons then
				return buttons[button]
			end
		end,
	}
end

do
	local eventMeta = {}

	eventMeta.__name = 'undefined'
	AccessorFunc(eventMeta, '__name', 'Name')
	AccessorFunc(eventMeta, '__name', 'EventName')
	AccessorFunc(eventMeta, '__name', 'Nick')

	function eventMeta:IsValid(event)
		return true
	end

	function eventMeta:IsAvailable(eventPart)
		return true
	end

	function eventMeta:GetArguments()
		self.__registeredArguments = self.__registeredArguments or {}
		return self.__registeredArguments
	end

	function eventMeta:AppendArgument(keyName, keyType, userdata)
		self.__registeredArguments = self.__registeredArguments or {}
		if not keyType then
			error('No Type of argument was specified!')
		end

		if keyType ~= 'number' and keyType ~= 'string' and keyType ~= 'boolean' then
			error('Invalid Type of argument was passed. Valids are number, string or boolean')
		end

		for i, data in ipairs(self.__registeredArguments) do
			if data[1] == keyName then
				error('Argument with key ' .. keyName .. ' already exists!')
			end
		end

		self.__registeredArguments = self.__registeredArguments or {}
		table.insert(self.__registeredArguments, {keyName, keyType, userdata})
	end

	function eventMeta:PopArgument(keyName)
		for i, data in ipairs(self.__registeredArguments) do
			if data[1] == keyName then
				return true, i, table.remove(self.__registeredArguments, i)
			end
		end

		return false
	end

	eventMeta.RemoveArgument = eventMeta.PopArgument
	eventMeta.SpliceArgument = eventMeta.PopArgument

	function eventMeta:GetClass()
		return self.__classname
	end

	function eventMeta:Think(event, ent, ...)
		return false
	end

	function eventMeta:GetNiceName(part, ent)
		if self.extra_nice_name then
			return self.extra_nice_name(part, ent, part:GetParsedArgumentsForObject(self))
		end

		local str = part:GetEvent()

		if part:GetArguments() ~= "" then
			local args = part:GetArguments():gsub(";", " or ")

			if not tonumber(args) then
				args = [["]] .. args .. [["]]
			end
			str = str .. " " .. part:GetOperator() .. " " .. args
		end

		return pac.PrettifyName(str)
	end

	local eventMetaTable = {
		__index = function(self, key)
			if key == '__class' or key == '__classname' then
				return rawget(getmetatable(self), '__classname')
			end

			if rawget(self, key) ~= nil then
				return rawget(self, key)
			end

			return eventMeta[key]
		end,

		__call = function(self)
			local newObj = pac.CreateEvent(self:GetClass())

			for k, v in pairs(self) do
				if not istable(v) then
					newObj[k] = v
				else
					newObj[k] = table.Copy(v)
				end
			end

			return newObj
		end,

		-- __newindex = function(self, key, val)
		--  rawset(self, key, val)
		-- end
	}

	function pac.GetEventMetatable()
		return eventMeta
	end

	function pac.CreateEvent(nClassName, defArguments)
		if not nClassName then error('No classname was specified!') end

		local newObj = setmetatable({}, {
			__index = eventMetaTable.__index,
			__call = eventMetaTable.__call,
			__classname = nClassName
		})

		newObj.__registeredArguments = {}
		newObj:SetName(nClassName)

		if defArguments then
			for i, data in pairs(defArguments) do
				newObj:AppendArgument(data[1], data[2], data[3])
			end
		end

		return newObj
	end

	function pac.RegisterEvent(nRegister)
		local classname = nRegister:GetClass()

		if PART.Events[classname] then
			pac.Message('WARN: Registering event with already existing classname!: '.. classname)
		end

		PART.Events[classname] = nRegister
	end

	for classname, data in pairs(PART.OldEvents) do
		local arguments = data.arguments
		local think = data.callback
		local eventObject = pac.CreateEvent(classname)

		if arguments then
			for i, data2 in ipairs(arguments) do
				local key, Type = next(data2)
				eventObject:AppendArgument(key, Type, data.userdata and data.userdata[i] or nil)
			end
		end

		eventObject.extra_nice_name = data.nice

		function eventObject:Think(event, ent, ...)
			return think(event, ent, ...)
		end

		pac.RegisterEvent(eventObject)
	end

	timer.Simple(0, function() -- After all addons has loaded
		hook.Call('PAC3RegisterEvents', nil, pac.CreateEvent, pac.RegisterEvent)
	end)
end

-- custom animation event
do
	local animations = pac.animations
	local event = {
		name = "custom_animation_frame",
		nice = function(self, ent, animation)
			if animation == "" then self:SetWarning("no animation selected") return "no animation" end
			local part = pac.GetLocalPart(animation)
			if not IsValid(part) then self:SetError("invalid animation selected") return "invalid animation" end
			self:SetWarning()
			return part:GetName()
		end,
		args = {
			{"animation", "string", {
				enums = function(part)
					local output = {}
					local parts = pac.GetLocalParts()

					for i, part in pairs(parts) do
						if part.ClassName == "custom_animation" then
							output[i] = part
						end
					end

					return output
				end
			}},
			{"frame_start", "number", {
				editor_onchange = function(self, num)
					local anim = pace.current_part:GetProperty("animation")
					if anim ~= "" then
						local part = pac.GetLocalPart(anim)
						-- GetAnimationDuration only works while editor is active for some reason
						local data = util.JSONToTable(part:GetData())
						return math.Clamp(math.ceil(num), 1, #data.FrameData)
					end
				end
			}},
			{"frame_end", "number", {
				editor_onchange = function(self, num)
					local anim = pace.current_part:GetProperty("animation")
					local start = pace.current_part:GetProperty("frame_start")
					if anim ~= "" then
						local part = pac.GetLocalPart(anim)
						-- GetAnimationDuration only works while editor is active for some reason
						local data = util.JSONToTable(part:GetData())
						return math.Clamp(math.ceil(num), start, #data.FrameData)
					end
				end
			}},
			--{"framedelta", "number", {editor_clamp = {0,1}, editor_sensitivity = 0.15}}
		},
		available = function(self, eventPart)
			return next(animations.registered) and true or false
		end,
		func = function (self, eventPart, ent, animation, frame_start, frame_end)
			local frame_start = frame_start or 1
			local frame_end = frame_end or 1
			if not animation or animation == "" then return end
			if not IsValid(ent) then return end
			if not next(animations.playing) then return end
			for i,v in ipairs(animations.playing) do
				if v == ent then
					local part = pac.GetPartFromUniqueID(pac.Hash(ent), animation)
					if not IsValid(part) then return end
					local frame, delta = animations.GetEntityAnimationFrame(ent, part:GetAnimID())
					if not frame or not delta then return end -- different animation part is playing
					return frame >= frame_start and frame <= frame_end
				end
			end
		end
	}

	local eventObject = pac.CreateEvent(event.name, event.args)
	eventObject.Think = event.func
	eventObject.IsAvailable = event.available
	eventObject.extra_nice_name = event.nice

	pac.RegisterEvent(eventObject)
end

-- DarkRP default events
do
	local plyMeta = FindMetaTable('Player')
	local gamemode = engine.ActiveGamemode
	local isDarkRP = function() return gamemode() == 'darkrp' end

	local events = {
		{
			name = 'is_arrested',
			args = {},
			available = function() return plyMeta.isArrested ~= nil end,
			func = function(self, eventPart, ent)
				ent = try_viewmodel(ent)
				return ent.isArrested and ent:isArrested() or false
			end
		},

		{
			name = 'is_wanted',
			args = {},
			available = function() return plyMeta.isWanted ~= nil end,
			func = function(self, eventPart, ent)
				ent = try_viewmodel(ent)
				return ent.isWanted and ent:isWanted() or false
			end
		},

		{
			name = 'is_police',
			args = {},
			available = function() return plyMeta.isCP ~= nil end,
			func = function(self, eventPart, ent)
				ent = try_viewmodel(ent)
				return ent.isCP and ent:isCP() or false
			end
		},

		{
			name = 'wanted_reason',
			args = {{'find', 'string'}},
			available = function() return plyMeta.getWantedReason ~= nil and plyMeta.isWanted ~= nil end,
			func = function(self, eventPart, ent, find)
				ent = try_viewmodel(ent)
				return eventPart:StringOperator(ent.isWanted and ent.getWantedReason and ent:isWanted() and ent:getWantedReason() or '', find)
			end
		},

		{
			name = 'is_cook',
			args = {},
			available = function() return plyMeta.isCook ~= nil end,
			func = function(self, eventPart, ent)
				ent = try_viewmodel(ent)
				return ent.isCook and ent:isCook() or false
			end
		},

		{
			name = 'is_hitman',
			args = {},
			available = function() return plyMeta.isHitman ~= nil end,
			func = function(self, eventPart, ent)
				ent = try_viewmodel(ent)
				return ent.isHitman and ent:isHitman() or false
			end
		},

		{
			name = 'has_hit',
			args = {},
			available = function() return plyMeta.hasHit ~= nil end,
			func = function(self, eventPart, ent)
				ent = try_viewmodel(ent)
				return ent.hasHit and ent:hasHit() or false
			end
		},

		{
			name = 'hit_price',
			args = {{'amount', 'number'}},
			available = function() return plyMeta.getHitPrice ~= nil end,
			func = function(self, eventPart, ent, amount)
				ent = try_viewmodel(ent)
				return eventPart:NumberOperator(ent.getHitPrice and ent:getHitPrice() or 0, amount)
			end
		},
	}

	for k, v in ipairs(events) do
		local available = v.available
		local eventObject = pac.CreateEvent(v.name, v.args)
		eventObject.Think = v.func

		function eventObject:IsAvailable()
			return isDarkRP() and available()
		end

		pac.RegisterEvent(eventObject)
	end
end

function PART:GetParentEx()
	local parent = self:GetTargetPart()

	if parent:IsValid() then
		return parent
	end

	return self:GetParent()
end

function PART:GetNiceName()
	local event_name = self:GetEvent()

	if not PART.Events[event_name] then return "unknown event" end

	return PART.Events[event_name]:GetNiceName(self, get_owner(self))
end

local function is_hidden_by_something_else(part, ignored_part)
	if part.active_events_ref_count > 0 and not part.active_events[ignored_part] then
		return true
	end

	return part.Hide
end

function PART:IsHiddenBySomethingElse(only_self)
	if is_hidden_by_something_else(self, self) then
		return true
	end

	if only_self then return false end

	for _, parent in ipairs(self:GetParentList()) do
		if is_hidden_by_something_else(parent, self) then
			return true
		end
	end

	return false
end

local function should_trigger(self, ent, eventObject)
	if not eventObject:IsAvailable(self) then
		return true
	end

	local b = false
	if eventObject.ParseArguments then
		b = eventObject:Think(self, ent, eventObject:ParseArguments(self)) or false
	else
		b = eventObject:Think(self, ent, self:GetParsedArgumentsForObject(eventObject)) or false
	end

	if self.Invert then
		b = not b
	end

	if is_hidden_by_something_else(self, self) then
		b = self.Invert
	end

	self.is_active = b

	return b
end

PART.last_event_triggered = false

function PART:OnThink()
	local ent = get_owner(self)
	if not ent:IsValid() then return end

	local data = PART.Events[self.Event]
	if not data then return end

	self:TriggerEvent(should_trigger(self, ent, data))

	if pace and pace.IsActive() and self.Name == "" then
		if self.pace_properties and self.pace_properties["Name"] and self.pace_properties["Name"]:IsValid() then
			self.pace_properties["Name"]:SetText(self:GetNiceName())
		end
	end

end

function PART:TriggerEvent(b)
	self.event_triggered = b -- event_triggered is just used for the editor

	if self.AffectChildrenOnly then
		for _, child in ipairs(self:GetChildren()) do
			child:SetEventTrigger(self, b)
		end
	else
		local parent = self:GetParent()
		if parent:IsValid() then
			parent:SetEventTrigger(self, b)
		end
	end
end

PART.Operators = {
	"equal",
	"not equal",
	"above",
	"equal or above",
	"below",
	"equal or below",

	"find",
	"find simple",

	"maybe",
}

pac.EventArgumentCache = {}

function PART:ParseArguments(...)
	local str = ""
	local args = {...}

	for key, val in pairs(args) do
		local T = type(val)

		if T == "boolean" then
			val = val and "1" or "0"
		elseif T == "string" then
			val = tostring(val) or ""
		elseif T == "number" then
			val = tostring(val) or "0"
		end

		if key == #args then
			str = str .. val
		else
			str = str .. val .. "@@"
		end
	end

	self.Arguments = str
end

function PART:GetParsedArguments(eventObject)
	if not eventObject then return end

	if eventObject.ParseArguments then
		return eventObject:ParseArguments(self)
	end

	return self:GetParsedArgumentsForObject(eventObject)
end

function PART:GetParsedArgumentsForObject(eventObject)
	if not eventObject then return end

	local line = self.Arguments
	local hash = line .. tostring(eventObject)

	if pac.EventArgumentCache[hash] then
		return unpack(pac.EventArgumentCache[hash])
	end

	local args = line:Split("@@")

	for i, argData in pairs(eventObject:GetArguments()) do
		local typ = argData[2]

		if args[i] ~= nil then
			if typ == "boolean" then
				args[i] = tonumber(args[i]) ~= 0
			elseif typ == "number" then
				args[i] = tonumber(args[i]) or 0
			elseif typ == "string" then
				args[i] = tostring(args[i]) or ""
			end
		end
	end

	pac.EventArgumentCache[hash] = args

	return unpack(args)
end

local cache = {}

local function CompareBTable(a, btbl, func, ...)
	for _, b in pairs(btbl) do
		if func(a, b, ...) then
			return true
		end
	end

	return false
end

function PART:StringOperator(a, b)

	local args = cache[b]

	if not args then
		args = b:Split(";")
		cache[b] = args
	end

	if not self.Operator or not a or not b then
		return false
	elseif self.Operator == "equal" then
		return CompareBTable(a, args, function(x, y) return x == y end)
	elseif self.Operator == "not equal" then
		return CompareBTable(a, args, function(x, y) return x ~= y end)
	elseif self.Operator == "find" then
		return CompareBTable(a, args, pac.StringFind)
	elseif self.Operator == "find simple" then
		return CompareBTable(a, args, pac.StringFind, true)
	elseif self.Operator == "changed" then
		if a ~= self.changed_last_a then
			self.changed_last_a = a

			return true
		end
	elseif self.Operator == "maybe" then
		return math.random() > 0.5
	end
end

function PART:NumberOperator(a, b)
	if not self.Operator or not a or not b then
		return false
	elseif self.Operator == "equal" then
		return a == b
	elseif self.Operator == "not equal" then
		return a ~= b
	elseif self.Operator == "above" then
		return a > b
	elseif self.Operator == "equal or above" then
		return a >= b
	elseif self.Operator == "below" then
		return a < b
	elseif self.Operator == "equal or below" then
		return a <= b
	elseif self.Operator == "maybe" then
		return math.random() > 0.5
	end
end

function PART:OnHide()
	if self.timerx_reset then
		self.time = nil
		self.number = 0
	end

	if self.Event == "weapon_class" then
		local ent = self:GetOwner()
		if ent:IsValid() then
			ent = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL
			if ent:IsValid() then
				ent.pac_weapon_class = nil
				ent:SetNoDraw(false)
			end
		end
	end
end

function PART:OnShow()
	if self.timerx_reset then
		self.time = nil
		self.number = 0
	end
end

function PART:OnAnimationEvent(ent)
	if self.Event == "animation_event" then
		self:GetParent():CallRecursive("Think")
	end
end

function PART:OnFireBullets()
	if self.Event == "fire_bullets" then
		self:GetParent():CallRecursive("Think")
	end
end

function PART:OnEmitSound(ent)
	if self.Event == "emit_sound" then
		self:GetParent():CallRecursive("Think")

		if ent.pac_emit_sound.mute_me then
			return false
		end
	end
end

BUILDER:Register()

do
	local enums = {}

	for key, val in pairs(_G) do
		if isstring(key) and key:find("PLAYERANIMEVENT_", nil, true) then
			enums[val] = key:gsub("PLAYERANIMEVENT_", ""):gsub("_", " "):lower()
		end
	end

	pac.AddHook("DoAnimationEvent", "animation_events", function(ply, event, data)
		-- update all parts once so OnShow and OnHide are updated properly for animation events
		if ply.pac_has_parts then
			ply.pac_anim_event = {name = enums[event], time = pac.RealTime, reset = true}

			pac.CallRecursiveOnAllParts("OnAnimationEvent")
		end
	end)
end


pac.AddHook("EntityEmitSound", "emit_sound", function(data)
	if pac.playing_sound then return end
	local ent = data.Entity

	if not ent:IsValid() or not ent.pac_has_parts then return end

	ent.pac_emit_sound = {name = data.SoundName, time = pac.RealTime, reset = true, mute_me = ent.pac_emit_sound and ent.pac_emit_sound.mute_me or false}

	if pac.CallRecursiveOnAllParts("OnEmitSound", ent) == false then
		return false
	end

	if ent.pac_mute_sounds then
		return false
	end
end)

pac.AddHook("EntityFireBullets", "firebullets", function(ent, data)
	if not ent:IsValid() or not ent.pac_has_parts then return end
	ent.pac_fire_bullets = {name = data.AmmoType, time = pac.RealTime, reset = true}

	pac.CallRecursiveOnAllParts("OnFireBullets")

	if ent.pac_hide_bullets then
		return false
	end
end)

net.Receive("pac_event", function(umr)
	local ply = net.ReadEntity()
	local str = net.ReadString()
	local on = net.ReadInt(8)

	-- ^ resets all other events
	if str:find("^", 0, true) then
		ply.pac_command_events = {}
	end

	if ply:IsValid() then
		ply.pac_command_events = ply.pac_command_events or {}
		ply.pac_command_events[str] = {name = str, time = pac.RealTime, on = on}
	end
end)

pac.AddHook("OnPlayerChat", "say_event", function(ply, str)
	if ply:IsValid() then
		ply.pac_say_event = {str = str, time = pac.RealTime}
	end
end)

pac.AddHook("GravGunOnPickedUp", "gravgun_event", function(ply, ent)
	if ply:IsValid() then
		ply.pac_gravgun_ent = ent
	end
end)

pac.AddHook("GravGunOnDropped", "gravgun_event", function(ply, ent)
	if ply:IsValid() then
		ply.pac_gravgun_ent = ent
	end
end)
-- ####

pac.AddHook("GravGunPunt", "gravgun_event", function(ply, ent)
	if ply:IsValid() then
		ply.pac_gravgun_ent = ent
		ply.pac_gravgun_punt = pac.RealTime
	end
end)

--[[
attack primary
swim
flinch rightleg
flinch leftarm
flinch head
cancel
attack secondary
flinch rightarm
jump
snap yaw
attack grenade
custom
cancel reload
reload loop
custom gesture sequence
custom sequence
spawn
doublejump
flinch leftleg
flinch chest
die
reload end
reload
custom gesture
--]]


-- Custom event selector wheel
do
	local function get_events()
		local available = {}

		for k,v in pairs(pac.GetLocalParts()) do
			if v.ClassName == "event" then
				local e = v:GetEvent()
				if e == "command" then
					local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command)
					if hide then continue end

					available[cmd] = {type = e, time = time}
				end
			end
		end

		local list = {}
		for k,v in pairs(available) do
			v.trigger = k
			table.insert(list, v)
		end

		table.sort(list, function(a, b) return a.trigger > b.trigger end)

		return list
	end

	local selectorBg = Material("sgm/playercircle")
	local selected

	function pac.openEventSelectionWheel()
		gui.EnableScreenClicker(true)

		local scrw, scrh = ScrW(), ScrH()
		local scrw2, scrh2 = scrw*0.5, scrh*0.5
		local color_red = Color(255,0,0)
		local R = 48

		local events = get_events()
		local nevents = #events

		-- Theta size of each wedge
		local thetadiff = math.pi*2 / nevents
		-- Used to compare the dot product
		local coslimit = math.cos(thetadiff * 0.5)
		-- Keeps the circles R units from each others' center
		local radius
		if nevents < 3 then
			radius = R
		else
			radius = R/math.cos((nevents - 2)*math.pi*0.5/nevents)
		end

		-- Scale down to keep from going out of the screen
		local gScale
		if radius+R > scrh2 then
			gScale = scrh2 / (radius+R)
		else
			gScale = 1
		end

		local selections = {}
		for k, v in ipairs(events) do
			local theta = (k-1)*thetadiff
			selections[k] = {
				grow = 0,
				name = v.trigger,
				event = v,
				x = math.sin(theta),
				y = -math.cos(theta),
			}
		end

		local function draw_circle(self, x, y)
			local dot = self.x*x + self.y*y
			local grow
			if dot > coslimit then
				selected = self
				grow = 0.1
			else
				grow = 0
			end
			self.grow = self.grow*0.9 + grow -- Framerate will affect this effect's speed but oh well

			local scale = gScale*(1 + self.grow*0.2)
			local m = Matrix()
			m:SetTranslation(Vector(scrw2, scrh2, 0))
			m:Scale(Vector(scale, scale, scale))
			cam.PushModelMatrix(m)

			local x, y = self.x*radius, self.y*radius


			local ply = pac.LocalPlayer
			local data = ply.pac_command_events and ply.pac_command_events[self.event.trigger] and ply.pac_command_events[self.event.trigger]
			if data then
				local is_oneshot = self.event.time and self.event.time > 0

				if is_oneshot then
					local f = (pac.RealTime - data.time) / self.event.time
					local s = Lerp(math.Clamp(f,0,1), 1, 0)
					local v = Lerp(math.Clamp(f,0,1), 0.55, 0.15)
					surface.SetDrawColor(HSVToColor(210,s,v))
				else
					if data.on == 1 then
						surface.SetDrawColor(HSVToColor(210,1,0.55))
					else
						surface.SetDrawColor(HSVToColor(210,0,0.15))
					end
				end
			else
				surface.SetDrawColor(HSVToColor(210,0,0.15))
			end

			surface.DrawTexturedRect(x-48, y-48, 96, 96)
			draw.SimpleText(self.name, "DermaDefault", x, y, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)

			cam.PopModelMatrix()
		end

		pac.AddHook("HUDPaint","custom_event_selector",function()
			-- Right clicking cancels
			if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionWheel(true) return end

			-- Normalize mouse vector from center of screen
			local x, y = input.GetCursorPos()
			x = x - scrw2
			y = y - scrh2
			if x==0 and y==0 then x = 1 y = 0 else
				local l = math.sqrt(x^2+y^2)
				x = x/l
				y = y/l
			end

			DisableClipping(true)
			render.PushFilterMag(TEXFILTER.ANISOTROPIC)
			render.PushFilterMin(TEXFILTER.ANISOTROPIC)

			surface.SetMaterial(selectorBg)

			draw.SimpleText("Right click to cancel", "DermaDefault", scrw2, scrh2+radius+R, color_red, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP)
			for _, v in ipairs(selections) do draw_circle(v, x, y) end

			render.PopFilterMag()
			render.PopFilterMin()
			DisableClipping(false)
		end)
	end

	function pac.closeEventSelectionWheel(cancel)
		gui.EnableScreenClicker(false)
		pac.RemoveHook("HUDPaint","custom_event_selector")

		if selected and cancel ~= true then
			if not selected.event.time then
				RunConsoleCommand("pac_event", selected.event.trigger, "toggle")
			elseif selected.event.time > 0 then
				RunConsoleCommand("pac_event", selected.event.trigger)
			else
				local ply = pac.LocalPlayer

				if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then
					RunConsoleCommand("pac_event", selected.event.trigger, "0")
				else
					RunConsoleCommand("pac_event", selected.event.trigger, "1")
				end
			end
		end
		selected = nil
	end

	concommand.Add("+pac_events", pac.openEventSelectionWheel)
	concommand.Add("-pac_events", pac.closeEventSelectionWheel)
end
