/*
	Stargate SENT 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/>.
*/

-- Some necessary dummies which are getting always called
function ENT:ActivateRing() end;
function ENT:ActivateChevron() end;

--##################################
--#### Sequence Handling & Behaviour
--##################################

--#################  It's a helper function for the RunActions @aVoN
local function StartSequence(data) -- Helper-Function
	if(data.f) then
		data.f(unpack(data.v or {}));
	else -- It's a package. Parse it at one piece - No timers :)
		for k,v in pairs(data) do
			if(k ~= "d" and v.f) then -- The d key is our delay! and if v.f does not exists, this might be a pause
				v.f(unpack(v.v or {}));
			end
		end
	end
end

--#################  Parse the actions and allocate them to a timer @aVoN
function ENT:RunActions(action)
	local s = "StarGate_"..self.Entity:EntIndex().."_"; -- The uniqueID of the timers
	local delay = 0; -- Timer delay
	local temp = {}; -- Here we store out new timers too.
	--######### Prepare timers (make packages - Actions with no delay between each other)
	local package;
	for _,v in pairs(action) do
		if(v.f or v.pause) then -- Just valid stuff please!
			v.d = tonumber(v.d) or 0; -- Prevents a bug
			--#### No delay - Add it to a package
			if(v.d == 0 and not v.pause) then
				package = package or {};
				table.insert(package,v);
			elseif(package) then
				--#### We have a delay and a package exists. At this point, some sequences failed, because they sometimes mixed up the timers.
				-- Lets say, this package has a delay of 0 seconds so there is no delay between the package and the next timer. So you have two timers which are "ran at the same time"
				-- Sadly the last added timer runs (in most cases) before the previous added so the sequence is mixed up and the script fails randomly. So we add the
				-- Next sequence to our package, so the package has first 0-delay actions and then a delayed. Now the package is delayed and ran in only one timer which can't mixup!
				package.d = v.d; -- Let the package derive the delay from our "next" sequence
				table.insert(package,v);
				table.insert(temp,package);
				package = nil;
			else
				table.insert(temp,v);
			end
		end
	end
	if(package) then table.insert(temp,package) end; -- A package which was all at a delay of 0 seconds ended in that action. Let's dont forget it!
	--######### Register timers
	for _,v in pairs(temp) do
		if(not v.pause) then
			self.Actions = (self.Actions or 0) + 1; -- Tell our gate, how much timers we have
			timer.Create(s..self.Actions,delay,1,StartSequence,v);
		end
		delay = delay + (tonumber(v.d) or 0); -- Prevents a bug
	end
end

--################# Stops any running actions (by e.g. a system fault)
function ENT:StopActions()
	local s = "StarGate_"..self.Entity:EntIndex().."_"; -- The uniqueID of the timers
	for k=1,(self.Actions or 0) do
		timer.Remove(s..k);
	end
	self.Actions = 0; -- Reset
end

--#################  Sets the available or not available status for the stargate @aVoN
function ENT:SetStatus(b,u,do_not_set_wire_active)
	self.IsOpen = b;
	self.Dialling = u;
	if(ValidEntity(self.Entity)) then
		local active = not do_not_set_wire_active and util.tobool(u or b); -- If "do_not_set_wire_active" is set, (e.g. on slow dial in), the gate won't become "Active" in wire as long as this changes!
		self.Active = active;
		self:SetWire("Active",active);
		self:SetWire("Open",util.tobool(b));
		self:SetWire("Inbound",util.tobool(active and not self.Outbound));
	end
end

--################# Prevents GMod from not sending NWorked data if the entity is not in the players Field of View @aVoN
function ENT:UpdateTransmitState() return TRANSMIT_ALWAYS end;

--##################################
--#### DHD helper functions
--##################################


--################# Find's all DHD's which may call this gate @aVoN
function ENT:FindDHD()
	local pos = self.Entity:GetPos();
	local dhd = {};
	for _,v in pairs(ents.FindByClass("dhd_*")) do
		local e_pos = v:GetPos();
		local dist = (e_pos - pos):Length(); -- Distance from DHD to this stargate
		if(dist <= self.DHDRange) then
			-- Check, if this DHD really belongs to this gate
			local add = true;
			for _,gate in pairs(self:GetAllGates()) do
				if(gate ~= self.Entity and (gate:GetPos() - e_pos):Length() < dist) then
					add = false;
					break;
				end
			end
			if(add) then
				table.insert(dhd,v);
			end
		end
	end
	return dhd;
end

--################# Light every DHD near us up @aVoN
-- Chevron,Delay,NoShutdon (Noshutdown is used, when every DHD near the gate will get "light up" again, when the stagate openes)
function ENT:DHDSetChevron(ch,delay,ns)
	local delay = delay or 0.4;
	if(not self.DialledAddress or table.getn(self.DialledAddress) ~= 8) then
		self.DialledAddress={"","","","","","","","DIAL"};
	end
	for k,v in pairs(self:FindDHD()) do
		if(v:IsValid() and v.Target ~= self.Entity) then
			if(ch == 1 and not ns) then
				v:Shutdown(0);
			end
			local btn = self.DialledAddress[ch];
			timer.Create("dhd_chevron"..k..ch..self.Entity:EntIndex(),delay,1,
				function()
					if(ValidEntity(v)) then
						v:AddChevron(btn,true);
						v:SetBusy(10);
						if(ch == 8) then
							v:SetBusy(0.8); -- Unset busy
						end
					end
				end
			);
		end
	end
end

--################# Disables near DHDs @aVoN
function ENT:DHDSetAllBusy()
	-- Set all DHD's busy during the opening sequence - necessary to avoid some evil bugs
	for _,v in pairs(self:FindDHD()) do
		v:SetBusy(4);
	end
end

--################# Disables near DHDs @aVoN
function ENT:DHDDisable(d,shutdown_all)
	if(not (self and self.FindDHD)) then return end;
	for _,v in pairs(self:FindDHD()) do
		if(v:IsValid()) then
			if(shutdown_all or v.Target ~= self.Entity) then
				v:Shutdown(d);
				v:SetBusy(0.8); -- Unset busy
			end
		end
	end
end



--##################################
--#### Gate finding functions
--##################################



--################# Gets all (valid) gates @aVoN
function ENT:GetAllGates(closed)
	local sg = {};
	for _,v in pairs(ents.FindByClass("stargate_*")) do
		if(v.IsStargate and not (closed and (v.IsOpen or v.Dialling))) then
			table.insert(sg,v);
		end
	end
	return sg;
end

--################# Find's a gate to the gateaddress stored in self.DialledAddress
function ENT:FindGate()
	if(not (self.IsOpen or self.Dialling)) then
		if(self.DialledAddress and #self.DialledAddress == 8) then
			local gates = {};
			local a = self.DialledAddress; -- Fast index (Code shorter);
			for _,v in pairs(self:GetAllGates()) do
				if(v ~= self.Entity) then
					local address = v:GetGateAddress();
					if( address:find(a[1]) and
						address:find(a[2]) and
						address:find(a[3]) and
						address:find(a[4]) and
						address:find(a[5]) and
						address:find(a[6])
					) then
						table.insert(gates,v);
					end
				end
			end
			-- We just found ONE gate (what we actually need)
			if(#gates == 1) then
				self.Target = gates[1];
				-- Tell the other gate, who is calling
				local n = self.Entity:GetGateAddress();
				if(n:len() == 6) then
					self.Target.DialledAddress = {n:sub(1,1),n:sub(2,2),n:sub(3,3),n:sub(4,4),n:sub(5,5),n:sub(6,6),"#","DIAL"};
				end
				return;
			end
		end
		self.Target = nil;
	end
end


--##################################
--#### Duplicator Entity Modifiers (for the gates)
--##################################

--################# Sets a new value to one modifier @aVoN
function ENT:SetEntityModifier(k,v)
	self.Duplicator = self.Duplicator or {};
	self.Duplicator[k] = v;
	duplicator.StoreEntityModifier(self.Entity,"StarGate",self.Duplicator);
end

-- FIXME: Maybe a recode? The PostEntityPaste etc functions are already used by Wire/RD2 so I do not want to override them.
function ENT.DuplicatorEntityModifier(_,e,data)
	if(data) then
		for k,v in pairs(data) do
			if(k == "Address") then
				e:SetGateAddress(v);
			end
			if(k == "Name") then
				e:SetGateName(v);
			end
			if(k == "Private") then
				e:SetPrivate(v);
			end
			e:SetEntityModifier(k,v);
		end
	end
end
duplicator.RegisterEntityModifier("StarGate",ENT.DuplicatorEntityModifier);

--##################################
--#### Name/Address/Private Handling
--##################################

-- Address interaction - Use this for your own scripts

--################# Retrieves the address of this Stargate @aVoN
function ENT:GetGateAddress()
	return self.GateAddress or "";
end

--################# Sets the address @aVoN
function ENT:SetGateAddress(s)
	s = tostring(s or "");
	if(s:len() == 6 or s == "") then
		local address = s:upper();
		-- Do not allow setting gate addresses, if a gate with the same address already exists!
		if(s ~= "") then
			local a = address:TrimExplode("");
			for _,v in pairs(self:GetAllGates()) do
				if(v ~= self.Entity) then
					local address = v:GetGateAddress();
					if( address:find(a[1]) and
						address:find(a[2]) and
						address:find(a[3]) and
						address:find(a[4]) and
						address:find(a[5]) and
						address:find(a[6])
					) then
						return; -- There is already a gate with that address - Do not allow this action!
					end
				end
			end
		end
		address = hook.Call("StarGate.SetGateAddress",GAMEMODE,self.Entity,self.GateAddress or "",address) or address;
		if(not (address and (tostring(address):len() == 6 or address == ""))) then return end;
		self.GateAddress = address;
		self:SetEntityModifier("Address",address); -- Entity Modifiers for Duplicator
		self.Entity:SetNWString("Address",address);
	end
end

--################# Retrieves the name of this Stargate @aVoN
function ENT:GetGateName()
	return self.GateName or "";
end

--################# Sets the name of this stargate @aVoN
function ENT:SetGateName(s)
	if(s) then
		s = hook.Call("StarGate.SetGateName",GAMEMODE,self.Entity,self.GateName or "",s) or s;
		if(not (type(s) == "string" or type(s) == "number")) then return end;
		s = tostring(s);
		self.GateName = s;
		self:SetEntityModifier("Name",s); -- Entity Modifiers for Duplicator
		self.Entity:SetNWString("Name",s);
	end
end

--################# Is this gate Private? @aVoN
function ENT:GetPrivate()
	return self.GatePrivat;
end

--################# Set it private @aVoN
function ENT:SetPrivate(b)
	b = util.tobool(b);
	local override = hook.Call("StarGate.SetPrivate",GAMEMODE,self.Entity,self.GatePrivat or false,b);
	if(type(override) == "boolean") then b = override end;
	self.GatePrivat = b;
	self:SetEntityModifier("Private",b); -- Entity Modifiers for Duplicator
	self.Entity:SetNWBool("Private",b);
end

--################# Client->Server communication @aVoN
concommand.Remove("_StarGate.SetValue"); -- In case of a sent_reload (or lua_reload? How is that new command named?)
concommand.Add("_StarGate.SetValue",
	function(p,_,arg)
		local e = ents.GetByIndex(tonumber(arg[1])); -- Entity
		local c = arg[2]; -- Command
		local d = arg[3]; -- Data
		local d2 = arg[4]; -- Data2 (e.g. for dial!)
		if(ValidEntity(e) and c and d) then
			--##### Allowed? (Prevents clients using the clientside functions to cheat on my system)
			if(c == "Address" or c == "Name" or c == "Private") then
				-- Is he allowed to change an address/name/private state?
				if(hook.Call("StarGate.Player.CanModifyGate",GAMEMODE,p,e) == false) then return end; -- On any gate?
				if(e.GateSpawnerProtected) then -- On a protected gate?
					local allowed = hook.Call("StarGate.Player.CanModifyProtectedGate",GAMEMODE,p,e);
					if(allowed == nil) then allowed = (p:IsAdmin() or SinglePlayer()) end;
					if(not allowed) then return end;
				end
			elseif(c == "Dial" or c == "AbortDialling") then
				if(hook.Call("StarGate.Player.CanDialGate",GAMEMODE,p,e) == false) then return end;
			end
			if(c == "Address") then
				e:SetGateAddress(d);
			elseif(c == "Name") then
				e:SetGateName(d);
			elseif(c == "Private") then
				e:SetPrivate(util.tobool(d));
			elseif(c == "Dial" and d2) then
				local b = util.tobool(d);
				if(e:GetClass() == "stargate_atlantis") then b = true end; -- SGA is ALWAYS dialling fast! - FIXME: Add new dialling to this gate (with the new sounds!)
				e:DialGate(d2,b);
				hook.Call("StarGate.Player.DialledGate",GAMEMODE,p,e,d2,b);
			elseif(c == "AbortDialling") then
				e:AbortDialling();
				hook.Call("StarGate.Player.ClosedGate",GAMEMODE,p,e);
			end
		end
	end
);

--################# Sends NWData of the gates to a client@aVoN
--If a new player joins the server, he normally does not have Networked Data which has been set before he joined. This hook forces to resend the date to everyone if he presses
-- "MoveForward" the first time just after he joined. Before I used a Think, but I think this was useless networked data.
local joined = {};
hook.Add("KeyPress","StarGate.KeyPress.SendGateData",
	function(p,key)
		if(not joined[p] and key == IN_FORWARD) then
			joined[p] = true; -- Do not call this hook twice!
			for _,v in pairs(ents.FindByClass("stargate_*")) do
				if(v.IsStargate and ValidEntity(v)) then
					v:SetNWString("Address",""); -- "Reset old value" to cause an immediate update in the next step below
					v:SetNWString("Address",v.GateAddress,true);
					v:SetNWString("Name",""); -- "Reset old value" to cause an immediate update in the next step below
					v:SetNWString("Name",v.GateName,true);
					v:SetNWBool("Private",not v.GatePrivat); -- "Reset old value" to cause an immediate update in the next step below
					v:SetNWBool("Private",v.GatePrivat,true);
				end
			end
		end
	end
);

--################# Is the gate blocked or can it get opened? @aVoN
function ENT:IsBlocked(only_by_iris)
	if(self.IsOpen) then
		for _,v in pairs(ents.FindInSphere(self.Entity:GetPos(),10)) do
			if(v.IsIris) then
				if(v.IsActivated) then
					self.Iris = v; -- So we have an iris - Fine (Called by event_horizon SENT to "draw" hiteffects)
					return true;
				end
				break;
			end
		end
	end
	if(not only_by_iris) then -- Avoids this long and probably CPU intensive check when a bullet shall be transported
		local hits = 0;
		local radius = math.floor(1/10*self.Entity:BoundingRadius()*2/3); -- About 5 units space between each circle (saves performance) and just 2/3 of the actual gates size (event horizon shall be blocked! not the gate rings lol)
		local min_hits = radius*20*0.7; -- If 70% is covered call this gate "blocked"
		-- Polar coordinates - I love them
		for r=1,radius do
			r = r*10;
			for phi=0,20 do
				local phi = math.pi*phi/10; -- Fraction of 2pi
				local pos = self.Entity:LocalToWorld(Vector(0,r*math.cos(phi),r*math.sin(phi))); -- Position on the surface of the eventhorizon
				local trace = util.QuickTrace(pos,self.Entity:GetForward()*10,{self.Entity,self.EventHorizon});
				if(trace.Hit) then
					if(trace.HitWorld) then -- Just the boring world. World always counts as a hit
						hits = hits + 1;
					elseif(self.IsOpen and ValidEntity(trace.Entity) and not trace.Entity.IsIris and v ~= self.Entity) then -- Hit an Entity!
						if(((trace.Entity.__StargateTeleport or {}).__LastTeleport or 0) + 2 < CurTime()) then
							if(not ValidEntity(trace.Entity:GetParent())) then -- Do not allow parented props!
								hits = hits + 1;
							end
						end
					end
					if(hits >= min_hits) then
						return true;
					end
				end
			end
		end
	end
	return false;
end


--##################################
--#### Dial a gate or abort dialling.
--##################################

-- !!!!!ONLY USE THESE FUNCTIONS FOR YOUR SCRIPTS!!!!!!!

--################# Dials a gate direcly (mode means: true = DHD like, false = SGC-Computer like) @aVoN
function ENT:DialGate(address,mode)
	local allow_override_dial = false;
	-- Someone dials in. Are we getting dialled slowly? If yes, allow dialling out (and abort the dial-in)
	if(not self.Outbound and not self.Active and ValidEntity(self.Target)) then
		allow_override_dial = true;
	end
	 -- We can't dial again while we already dial!
	if(not allow_override_dial and (self.Dialling or self.IsOpen)) then return end;
	if(not mode) then mode = false end;
	-- I hope this fixes issues the EH staying open
	if(ValidEntity(self.EventHorizon)) then
		if(self.IsOpen or self.Dialling) then return end; -- We are opened or dialling - Do not allow additional dialling.
		self.EventHorizon:Remove(); -- Remove the EH. We neither are dialling, nor we are opened so it's a bug and the EH stood. Remove it now immediately!
	end
	-- Do we override this dial-in?
	if(allow_override_dial) then
		self.Target:AbortDialling();
	end
	self:SetAddress(address);
	self:SetDialMode(false,mode);
	self:StartDialling();
end

--################# Aborts Dialling a gate @aVoN
function ENT:AbortDialling()
	-- Do not allow closing, if the eventhorizon is currently establishing (Or you will have massive bugs)
	if(ValidEntity(self.EventHorizon)) then
		if(not self.EventHorizon:IsOpen()) then return end;
	end
	if(self.IsOpen) then
		self:DeactivateStargate(true);
	elseif(self.Dialling and self.Outbound) then
		if(ValidEntity(self.Target)) then
			self.Target:EmergencyShutdown();
		end
		self:EmergencyShutdown();
	end
end
