/*
	Stargate Eventhorizon for GarrysMod10
	Copyright (C) 2007  aVoN

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

--################# HEADER #################
ENT.CDSIgnore = true; -- CDS Immunity
function ENT:gcbt_breakactions() end; ENT.hasdamagecase = true; -- GCombat invulnarability!

--################# Include
AddCSLuaFile("cl_init.lua");
AddCSLuaFile("shared.lua");
AddCSLuaFile("modules/bullets.lua");
include("shared.lua");
include("modules/teleport.lua");
include("modules/bullets.lua");

--################# Defines
ENT.IgnoreTouch = true; -- This tells the physical objects like drones or staff not to collide with the eventhorizon (= no explode on them)
ENT.CDSIgnore = true; -- Fixes Combat Damage System destroying this entity

ENT.Model = "models/zup/stargate/stargate_horizon.mdl"
ENT.Sounds = {
	Idle=Sound("stargate/wormhole_loop.wav"),
	Teleport=Sound("stargate/teleport.mp3"),
	Shutdown=Sound("stargate/gate_close.mp3"),
	Open=Sound("stargate/gate_open.mp3"), -- You can manually change this later when you spawn the event horizon in the SpawnFunction!
}
-- These entities are immune against autoclose and should also never avoid an autoclose event if to near to a gate
ENT.AutocloseImmunity = {
	"npc_grenade_frag",
	"rpg_missile",
	"grenade_ar2",
	"crossbow_bolt",
	"npc_satchel",
	"prop_combine_ball",
	"hunter_flechette",
	"grenade_helicopter",
	"weapon_striderbuster",
	"grenade_spit", -- Antlion poison blasts
	"cloaking",
	"shield",
	"stargate_iris",
	"gmod_ghost", -- AdvDupe/Dupe ghostpreview - For some obvious reason, it's having a physics object...
}
util.PrecacheModel(ENT.Model);
--################# SENT CODE ###############

--################# Init @aVoN
function ENT:Initialize()
	-- We need a "fresh" copy of sounds for this entity. Otherwise "SGA-opening-sounds" might overwrite "SG1-opening-sounds", because ENT.Sounds is global!
	self.Sounds = table.Copy(self.Sounds);
	self.Entity:SetModel(self.Model);
	self.Entity:PhysicsInit(SOLID_VPHYSICS);
	self.Entity:SetMoveType(MOVETYPE_NONE);
	self.Entity:SetTrigger(true); -- The most important thing: Makes the EH trigger Touch() events, even when it's not solid
	self.Entity:SetNotSolid(true);
	self.Entity:DrawShadow(false);
	-- Do not draw the model now (Alpha must be 1 because for some mystical reason, setting this to 0 makes effects not draw on it. Seems like Valva/garry added stuff not to draw things which actually have alpha = 0 to save performance)
	self.Entity:SetColor(255,255,255,1);
	self.Created = CurTime();
	self.AutoClose = StarGate.CFG:Get("stargate","autoclose",true);
	self.HorizonRadius = 150; -- Just needed for effects
	self.OpeningDelay = 0.8; -- This time decides when the script starts it's opening effect after it played the opening sound
	self.OpenTime = 2.2;
	self.Holding = {}; -- A player holds this Entity: We are not going to teleport it until he stopped holding it!
	self.DoNotDestroy = {}; -- A contraption just got teleported to this event horizon but hasn't exited it yet. This prevents the receiving EH from destroying this contraption!
	self:Open(); -- Let us open :D
	local phys = self.Entity:GetPhysicsObject();
	if(phys:IsValid()) then
		phys:EnableCollisions(false);
	end
end

--################# Prevent PVS bug/drop of all networkes vars (Let's hope, it works) @aVoN
function ENT:UpdateTransmitState() return TRANSMIT_ALWAYS end;

--################# OnRemove @aVoN
function ENT:OnRemove()
	-- Kill wormhole idle sound
	if(self.IdleSound) then
		self.IdleSound:Stop();
	end
	-- Remove no-collide from our gate
	local parent = self.Entity:GetParent();
	if(ValidEntity(parent)) then
		parent:SetCollisionGroup(COLLISION_GROUP_NONE);
	end
end

--################# Set the target GATE (not Event Horizon!!!!!) we are linked to. We need to retrieve the target Horizon later manually
function ENT:SetTarget(e)
	self.TargetGate = e;
end

--################# Are we opened? @aVoN
function ENT:IsOpen()
	if(not self.Opened and self.Created + self.OpenTime + self.OpeningDelay > CurTime()) then 
		return false; -- We havent opened yet;
	end
	self.Opened = true; -- Do not calculate this above. This saves performance, even when it's really less. But coding performance saving is always good
	return true;
end

-- Just for DEBUG so I can spawn it from the SENT menu
-- FIXME: Remove this function later
function ENT:SpawnFunction(p,t)
	if (not t.Hit) then return end
	local e = ents.Create("event_horizon");
	e:SetPos(t.HitPos+Vector(0,0,90));
	e:SetAngles(Angle(0,math.GetVectorYawFromCenter(p:GetAimVector()),0));
	e:Spawn();
	e:Activate();
	return e;
end

--################# The openeing effect @aVoN
function ENT:Open()
	self.Attached = {}; -- These entities won't be dissolveable when the EH openes (Welded/Attached props)
	local parent = self.Entity:GetParent();
	if(ValidEntity(parent)) then
		self.Attached[parent] = true;
		self.Attached[parent.Target] = true;
		self.Attached[self.Entity] = true;
		for _,v in pairs(StarGate.GetConstrainedEnts(parent,10) or {}) do
			self.Attached[v] = true; -- Instead of doing a table.HasValue() everytime we do this - Much faster!
		end
	end
	local e = self.Entity;
	--##### This effect simply is the eventhorizon creation effect. It's NOT the kawoosh!
	local fx = EffectData();
	fx:SetEntity(e);
	fx:SetOrigin(e:GetPos());
	fx:SetScale(self.OpeningDelay+0.1); -- The additional delay stops the effect from stopping to early so the other (below) is getting started to late (Noticabel by a short flicker)
	util.Effect("eventhorizon_open",fx,true,true);
	self.Entity:EmitSound(self.Sounds.Open,90,math.random(98,102));
	timer.Create("EventHorizonOpening"..self.Entity:EntIndex(),self.OpeningDelay,1,
		function()
			if(ValidEntity(e)) then
				local blocked = false;
				if(ValidEntity(parent) and parent.IsStargate) then
					blocked = parent:IsBlocked();
				end
				--##### Now, we will FIRST OF ALL get the corresponding event horizon!
				if(not ValidEntity(self.Target) and ValidEntity(self.TargetGate)) then
					self.Target = self.TargetGate.EventHorizon;
				end
				--##### Start opening
				e:SetNWEntity("Target",self.Target); -- Tell clientside what our target is (Necessary for the ENT:GetTeleportedVector function, if used clientside)
				local pos = e:GetPos();
				e:SetColor(255,255,255,255); -- Draw model now
				-- May fixes this bug where the EH is invisible
				timer.Simple(e.OpenTime+0.3, -- Compensates up to a ping of 300 ms
					function()
						if(ValidEntity(e)) then
							e:SetColor(255,255,255,255); -- Draw model (just to be sure)
						end
					end
				);
				e:SetNWBool("activate_lights",true); -- Tell the event horizon to activate his dynamic lights!
				util.ScreenShake(pos,2,2.5,3,1000); -- Add the earth quake when the gate openes!
				-- The lovely idle sound
				e.IdleSound = CreateSound(e,e.Sounds.Idle);
				e.IdleSound:Play();
				--##### Do the "gate stabilize" effect. When you watch stargate, youll always see a quite brighter overlay just shortly after the EH has been established
				local fx = EffectData();
				fx:SetEntity(e);
				fx:SetOrigin(pos);
				fx:SetScale(e.OpenTime); -- Time in seconds until it dies
				util.Effect("eventhorizon_stabilize",fx,true,true);
				--##### The Kawoosh (Yeah, that thing which comes out the gate and kills peopl/stuff)
				if(not blocked) then -- gate is not blocked - Add kawoosh
					local fx = EffectData();
					fx:SetEntity(e);
					util.Effect("stargate_kawoosh",fx,true,true);
					-- This will kill people
					local pos = self.Entity:GetPos();
					local normal = self.Entity:GetForward();
					local radius = self.Entity:BoundingRadius()*(1/3);
					self:Dissolve(pos+radius*normal,radius);
					self:Dissolve(pos+3*radius*normal,radius);
					self:Dissolve(pos+5*radius*normal,radius);
				end
				--##### Refract to the event horizon (Because people complained it's to less)
				local fx = EffectData();
				fx:SetEntity(e);
				fx:SetOrigin(pos);
				util.Effect("eventhorizon_refract",fx,true,true);
			end
		end
	);
end

--################# The dissolve function of the kawoosh. MOST LEET FUNCTION EVER! @TetaBonita & aVoN
-- Some sort of inspired by TetaBonitas Strider Cannon: His dissolving way is much cuter than my crappy "Lasers". This thing isn't a complete copy and paste
-- The way he does it is already the only way to do it. So I mentioned him "for being the first" who has done this.
function ENT:Dissolve(pos,radius)
	-- The hurt will actually kill things and give credits to the stargate as killer
	local e = ents.Create("point_hurt");
	e:SetOwner(self.Entity);
	e:SetKeyValue("Damage",10000);
	e:SetKeyValue("DamageRadius",radius);
	e:SetKeyValue("DamageType",DMG_DISSOLVE | DMG_BLAST);
	e:SetPos(pos);
	e:SetParent(self.Entity);
	e:Spawn();
	for i=0,10 do
		e:Fire("hurt","",0.12*i);
	end
	e:Fire("kill","",1.5);
	-- Start dissolving the crap!
	if(StarGate.CFG:Get("stargate","disintegrate",true)) then
		timer.Simple(0.01,self.DissolveEntities,self,pos,radius);
	end
end

--################# Dissolving entities @TetaBonita & aVoN
function ENT:DissolveEntities(pos,radius)
	local name = "EH_DISSOLVE_"..self:EntIndex();
	for _,v in pairs(ents.FindInSphere(pos,radius)) do
		if(not (self.Attached[v] or v.GateSpawnerSpawned or v.NoDissolve)) then
			if(v:GetMoveType() == 6 and not self.Attached[v:GetParent()] and not self.Attached[v:GetDerive()]) then
				local phys = v:GetPhysicsObject();
				local class = v:GetClass();
				local mdl = v:GetModel(); -- We are searching e.g. for map entities (func_brush etc): Models with an "*" in it's modelname are such things
				-- Do not dissolve crap!
				if(phys:IsValid() and (class == "phys_magnet" or not class:find("phys_[^m]")) and mdl ~= "" and not mdl:find("*")) then
					phys:EnableMotion();
					v:SetKeyValue("targetname",name);
					for bone=0,v:GetPhysicsObjectCount()-1 do 
						local phys = v:GetPhysicsObjectNum(bone);
						if(phys:IsValid()) then
							phys:SetVelocity(phys:GetVelocity()*0.04);
							phys:EnableGravity(false);
						end
					end
				end
			end
		end
	end
	-- Start the real cool dissolving effect
	local e = ents.Create("env_entity_dissolver");
	e:SetKeyValue("dissolvetype",3);
	e:SetKeyValue("magnitude",0);
	e:SetPos(pos);
	e:SetKeyValue("target",name);
	e:Spawn();
	e:Fire("Dissolve",name,0);
	e:Fire("kill","",0.1);
end

--################# Shutting down the event horizon @aVoN
function ENT:Shutdown(override)
	if(self.ShuttingDown or (not self:IsOpen() and not override)) then return end; -- Aready shutting down or not opened yet
	timer.Remove("EventHorizonOpening"..self.Entity:EntIndex());
	self.ShuttingDown = true;
	local fx = EffectData();
	fx:SetEntity(self.Entity);
	fx:SetOrigin(self.Entity:GetPos());
	util.Effect("eventhorizon_collapse",fx,true,true);
	if(ValidEntity(self.Target)) then
		self.Target:Shutdown(override);
	end
	self.IdleSound:Stop();
	self.Entity:EmitSound(self.Sounds.Shutdown,90,math.random(97,103));
	local e = self.Entity;
	-- Let the light fade away!
	timer.Simple(2,
		function()
			if(ValidEntity(e)) then
				e:SetNWBool("activate_lights",false);
			end
		end
	);
	-- Make sure, it will be deleted!
	timer.Simple(4,
		function()
			if(ValidEntity(e)) then
				e:Remove();
			end
		end
	);
end

--################# Hit @aVoN
function ENT:Hit()
	if(self.ShuttingDown) then return end;
	-- This basically is the same as the establish effect until someone else does a better one
	local fx = EffectData();
	fx:SetEntity(self.Entity);
	fx:SetOrigin(self.Entity:GetPos());
	fx:SetScale(0.5); -- Time in seconds until it dies
	util.Effect("eventhorizon_establish",fx);
end

--################# Draws an entering effect on the event horizon @aVoN
function ENT:EnterEffect(pos,size)
	local diff = self:WorldToLocal(pos); diff.x = 0;
	local thresold = (self.HorizonRadius - diff:Length()/1.3);
	local effect_radius = size*2;
	effect_radius = math.Clamp(effect_radius,0,thresold);
	-- This effect isnt perfect or looks like the original one. Sometimes you event can't see it. But I dont mind.
	local fx = EffectData();
	-- This is, so the effect will be drawn always on the frontside of the event horizon
	local dist = self:WorldToLocal(self:NearestPoint(pos)); 
	fx:SetOrigin(self:LocalToWorld(dist)); -- I love this freaking "NearestPoint" function
	fx:SetEntity(self);
	fx:SetScale(effect_radius);
	util.Effect("gate_enter",fx,true,true);
end

--################# This is a wrapper for entities entering the EH @aVoN
function ENT:EnterEffectEntity(e)
	local pos = e:LocalToWorld(e:OBBCenter());
	local radius = e:BoundingRadius();
	self:EnterEffect(pos,radius);
end

--################# The most important part - Recognizes entering props and teleports them @aVoN
function ENT:StartTouch(e)
	if(self.Attached[e]) then return end; -- Attached props
	if(self.ShuttingDown) then return end; -- We are shutting down
	if(not ValidEntity(e)) then return end; -- Not valid
	if(e:IsPlayer() and (ValidEntity(e:GetParent()) or ValidEntity(e:GetScriptedVehicle()))) then return end; -- No teleport/kill of parented players
	if(e.NotTeleportable) then return end; -- Does not want to be teleported!
	local class = e:GetClass();
	if(class:find("stargate")) then return end;
	local parent = self.Entity:GetParent();
	--################# This fixes props, which not have exited the receiving EH yet are getting deleted
	if(e.__StargateTeleport and e.__StargateTeleport.__TARGET == self.Entity) then
		-- Start the check
		local add = false;
		if(e.__StargateTeleport.__LastTeleport + 0.5 > CurTime()) then -- GraceTime says: "Add this entity to DoNotDestroy list!"
			add = true;
		else -- GraceTime is over. But do we still have entities in our DoNotDestroy list which belong to this entity? If so, DO NOT DESTROY!
			for k,_ in pairs(self.DoNotDestroy) do
				if(e.__StargateTeleport[k]) then 
					add = true;
					break;
				end
			end
		end
		if(add) then
			-- Nocollide the gate as long as something is in it!
			if(ValidEntity(parent)) then
				-- If a nocollide timer has been started, stop it so it won't interfere
				timer.Remove("StarGate.SetReceivingGateCollide"..parent:EntIndex());
				parent:SetCollisionGroup(COLLISION_GROUP_WORLD);
			end
			self.DoNotDestroy[e] = true;
			return;
		end
	end
	-- Makes the prop getting teleported as soon as the player stops holding it
	if(e:IsPlayerHolding()) then
		self.Holding[e] = true;
	else
		--################# Is our receiving gate blocked?
		local block = false;
		local target_gate; -- Used in here and a bit lower later (for the iris hit sounds)
		if(not self:IsOpen()) then block = true end; -- We havent opened yet. Kill people!
		-- Are we entering the eventhorizon from the wrong side? Kill us!
		local dir = (self.Entity:GetVelocity()-e:GetVelocity()):Normalize();
		if(self.Entity:GetForward():DotProduct(dir) < 0) then 
			block = true
		else
			-- Our iris prvents us from getting in "from the front" (but not from the back aka "die"
			if(ValidEntity(parent) and parent:IsBlocked(true)) then
				self.DoNotDestroy[e] = true;
				return;
			end
		end
		if(not block and not ValidEntity(self.Target)) then block = true end; -- Do we have a valid target? If not, we are inbound
		-- Not yet getting killed? So check, if the otherside has an iris activated
		if(not block) then
			target_gate = self.Target:GetParent();
			if(ValidEntity(target_gate) and target_gate.IsStargate) then
				block = target_gate:IsBlocked();
			end
		end
		--################# Get attachments, mainly for the hook below but we need it anyway
		local attached = self:GetEntitiesForTeleport(e);
		if(attached) then
			local allow_teleport = hook.Call("StarGate.Teleport",GAMEMODE,e,self.Entity,attached,blocked);
			if(allow_teleport ~= nil and not allow_teleport) then return end; -- Someone does not want to teleport us! Shame on him
			--################# Just for the show - The "gulping" effect
			-- Before we teleport, draw the entering effect on out event horizon
			self:EnterEffectEntity(e);
			self.Entity:EmitSound(self.Sounds.Teleport,90,math.random(90,110));
			--################# Teleport us
			self:Teleport(e,block,attached);
			--################# Blocked or not? Either make iris play the "blocked" sound or draw the gulping at the other end
			if(block) then
				-- Iris blocked us. Make hut-noise
				if(ValidEntity(target_gate) and ValidEntity(target_gate.Iris) and target_gate.Iris.IsActivated) then
					target_gate.Iris:HitIris(self:GetTeleportedVector(e:GetPos(),e:GetVelocity())); -- Tell that we hit and where and how fast
				end
			else
				-- Needs to be delayed, or you wont hear the teleporting gulp if your a player
				local t = self.Target
				timer.Simple(0.05,
					function()
						if(ValidEntity(t) and ValidEntity(e)) then
							t:EmitSound(t.Sounds.Teleport,90,math.random(90,110));
							-- Draw the effect on the other eventhorizon
							t:EnterEffectEntity(e);
						end
					end
				);
				if(
					self.AutoClose and -- Disabled by config - Overrides every other setting
					not (
						e.NoAutoClose or -- Disabled by SENT Writer
						table.HasValue(self.AutocloseImmunity,class) or -- Disabled by me
						(ValidEntity(parent) and util.tobool(parent:GetWire("Disable Autoclose",0))) -- Wire forbids it
					)
				) then
					--################# Autoclose the gate after a delay
					self.DoAutoClose = true;
					self.Entity:NextThink(CurTime()+3); -- Trigger autoclose in the next 3 seconds
				end
			end
		end
	end
end

--################# Checks, if the player stopped holding the prop and teleports it @aVoN
function ENT:Touch(e)
	if(self.Holding[e]) then
		if(not ValidEntity(e)) then
			self.Holding[e] = nil;
			return;
		end
		if(not e:IsPlayerHolding()) then
			self.Holding[e] = nil;
			self:StartTouch(e);
		end
	end
end

--################# Stops being in the eventhorizon @aVoN
function ENT:EndTouch(e)
	self.DoNotDestroy[e] = nil;
	if(e.__StargateTeleport) then
		e.__StargateTeleport.__LastTeleport = CurTime(); -- Avoids a bug where parts of a contraption has holes between each other. This results into not destroying the object.
	end
	if(table.Count(self.DoNotDestroy) == 0) then
		-- Remove the nocollide again (but delayed)
		local parent = self.Entity:GetParent();
		timer.Create("StarGate.SetReceivingGateCollide"..parent:EntIndex(),1,1,
			function()
				if(ValidEntity(parent)) then
					parent:SetCollisionGroup(COLLISION_GROUP_NONE);
				end
			end
		);
	end
	self.Holding[e] = nil;
end

--################# For the autoclose @aVoN
function ENT:Think()
	if(self.DoAutoClose) then
		-- FIXME: Add config for the autoclose
		local gate = self.Entity:GetParent();
		if(ValidEntity(gate)) then
			-- Prevents some bugs
			if(not ValidEntity(self.Target)) then
				gate:DeactivateStargate();
				return;
			end
			local close = true;
			local g = {self.Entity,self.Target};
			for _,e in pairs({self.Entity,self.Target}) do
				if(close) then
					local parent = e:GetParent();
					if(ValidEntity(parent)) then
						local constrained = (StarGate.GetConstrainedEnts(parent,3) or {}); -- Constrained props to a stargate SHOULD NEVER keep it open!
						for _,v in pairs(ents.FindInSphere(e:GetPos(),e:BoundingRadius())) do
							local class = v:GetClass();
							if(not table.HasValue(constrained,v) and not table.HasValue(self.AutocloseImmunity,class)) then
								local vp = v:GetParent(); -- Parents of the object we found
								local phys = v:GetPhysicsObject();
								if(
									(phys:IsValid() and phys:IsMoveable()) and -- Has to be unfrozen/Movable
									(v ~= e and v ~= parent and not v.IsStargate and not v.IsDHD and not v.GateSpawnerSpawned) and -- General entities
									(not ValidEntity(vp) or (vp ~= parent and not vp.IsDHD and not vp.IsStargate and not vp.GateSpawnerSpawned))
								) then
									-- Keep the EH open!
									if(not v:IsPlayer() or v:Alive()) then -- Only if it's a player who is alive
										close = false;
										break;
									end
								end
							end
						end
					end
				end
			end
			if(close) then
				gate:DeactivateStargate();
			else
				-- Check a bit more frequently now!
				self.Entity:NextThink(CurTime()+0.5);
				return true;
			end
		end
	end
	self.Entity:NextThink(CurTime()+3);
	return true;
end
