--[[
	UPCOMMING ADDITIONS
	AUDP - advance udp. Ensures packets arrive and handles late packets.
	P2P - peer to peer (Server to set up initial connection)
	Relay - offput server load (locally)
	Threading - Simple threading ~~(UDP/AUDP Only)~~ Thanks to an updated multi library we can thread with ease
	Priority handling
]]
--[[
	TODO: Finish stuff for Priority handling
]]
function table.merge(t1, t2)
    for k,v in pairs(t2) do
    	if type(v) == 'table' then
    		if type(t1[k] or false) == 'table' then
    			table.merge(t1[k] or {}, t2[k] or {})
    		else
    			t1[k] = v
    		end
    	else
    		t1[k] = v
    	end
    end
    return t1
end
function string.trim(s)
	local from = s:match"^%s*()"
	return from > #s and "" or s:match(".*%S", from)
end
socket=require("socket")
http=require("socket.http")
mime=require("mime")
net={}
net.Version={2,0,1} -- This will probably stay this version for quite a while... The modules on the otherhand will be more inconsistant
net._VERSION="2.0.1"
net.OnServerCreated=multi:newConnection()
net.OnClientCreated=multi:newConnection()
net.loadedModules={}
net.autoInit=true
net.sParams={
	mode = "server",
	protocol = "sslv3",
	key = "./certs/serverAkey.pem",
	certificate = "./certs/serverA.pem",
	cafile = "./certs/rootA.pem",
	verify = {"peer", "fail_if_no_peer_cert"},
	options = {"all", "no_sslv2"},
}
net.cParams={
	mode = "client",
	protocol = "sslv3",
	key = "./certs/clientAkey.pem",
	certificate = "./certs/clientA.pem",
	cafile = "./certs/rootA.pem",
	verify = {"peer", "fail_if_no_peer_cert"},
	options = {"all", "no_sslv2"},
}
function net.normalize(input)
	local enc=mime.b64(input)
	return enc
end
function net.denormalize(input)
	local unenc=mime.unb64(input)
	return unenc
end
function net.getLocalIP()
	local someRandomIP = "192.168.1.122"
	local someRandomPort = "3102"
	local mySocket = socket.udp()
	mySocket:setpeername(someRandomIP,someRandomPort)
	local dat = (mySocket:getsockname())
	mySocket:close()
	return dat
end
function net.getExternalIP()
	local data=http.request("http://whatismyip.org/")
	return data:match("600;\">(%d-.%d-.%d-.%d-)</span>")
end
function net:registerModule(mod,version)
	if net[mod] then
		error("Module by the name: "..mod.." has already been registered! Remember some modules are internal and use certain names!")
	end
	table.insert(self.loadedModules,mod)
	net[mod]={}
	if version then
		net[mod].Version=version
		net[mod]._VERSION=version[1].."."..version[2].."."..version[3]
	else
		net[mod].Version={1,0,0}
		net[mod]._VERSION={1,0,0}
	end
	return {Version=version,_VERSION=version[1].."."..version[2].."."..version[3]}
end
function net.getModuleVersion(ext)
	if not ext then
		return string.format("%d.%d.%d",net.Version[1],net.Version[2],net.Version[3])
	end
	return string.format("%d.%d.%d",net[ext].Version[1],net[ext].Version[2],net[ext].Version[3])
end
function net.resolveID(obj)
	local num=math.random(10000000,99999999)
	if obj[tostring(num)] then
		return net.resolveID(obj)
	end
	obj.ids[tostring(num)]=true
	return tostring(num)
end
function net.inList(list,dat)
	for i,v in pairs(list) do
		if v==dat then
			return true
		end
	end
	return false
end
function net.setTrigger(funcW,funcE)
	multi:newTrigger(func)
end
net:registerModule("net",net.Version)
-- Client broadcast
function net:newCastedClient(name) -- connects to the broadcasted server
	local listen = socket.udp() -- make a new socket
	listen:setsockname(net.getLocalIP(), 11111)
	listen:settimeout(0)
	local timer=multi:newTimer()
	while true do
		local data, ip, port = listen:receivefrom()
		if timer:Get()>3 then
			error("Timeout! Server by the name: "..name.." has not been found!")
		end
		if data then
			local n,tp,ip,port,secure=data:match("(%S-)|(%S-)|(%S-):(%d-)|(.)")
			if n:match(name) then
				print("Found Server!",n,tp,ip,port)
				if tp=="tcp" then
					return net:newTCPClient(ip,tonumber(port),secure=="1")
				else
					return net:newClient(ip,tonumber(port),secure=="1")
				end
			end
		end
	end
end
-- UDP Stuff
function net:newServer(port,servercode)
	local c={}
	c.udp=assert(socket.udp())
	c.udp:settimeout(0)
	c.udp:setsockname("*", port)
	c.ips={}
	c.Type="udp"
	c.port=port
	c.ids={}
	c.servercode=servercode
	c.bannedIPs={}
	c.bannedCIDs={}
	c.autoNormalization=false
	function c:setUpdateRate(n)
		print("Not needed in a udp server!")
	end
	function c:banCID(cid)
		table.insert(self.bannedCIDs,cid)
	end
	function c:banIP(ip)
		table.insert(self.bannedIPs,cid)
	end
	c.broad=socket.udp()
	c.hostip=net.getLocalIP()
	function c:broadcast(name)
		local loop=multi:newTLoop(function(loop,dt)
			self.broad:setoption('broadcast',true)
			self.broad:sendto(name.."|"..self.Type.."|"..self.hostip..":"..self.port.."|"..({[true]="1",[false]="0"})[secure], "255.255.255.255", 11111)
			self.broad:setoption('broadcast',false)
		end,1)
	end
	function c:send(ip,data,port,cid)
		if self.autoNormalization then
			data=net.normalize(data)
		end
		if self.servercode then
			cid=cid or self:CIDFrom(ip,port)
			if not self.ips[cid] then
				print("Can't determine cid from client... sending the client a new one!")
				local cid=net.resolveID(self)
				print("Sending unique cid to client: "..cid)
				self.ips[cid]={ip,port,0,self.servercode==nil}
				print(ip)
				self.udp:sendto("I!"..cid,ip,port)
				if self.servercode then
					self.udp:sendto("S!",ip,port)
				end
				return
			end
			if net.inList(self.bannedIPs,ip) or net.inList(self.bannedCIDs,cid) then
				self.udp:sendto("BANNED CLIENT", ip, port or self.port)
			elseif self.ips[cid][4] then
				self.udp:sendto(data, ip, port or self.port)
			elseif self.ips[cid][4]==false then
				self.udp:sendto("Make sure your server code is correct!", ip, port)
			end
		else
			self.udp:sendto(data, ip, port or self.port)
		end
	end
	function c:pollClientModules(ip,port)
		self:send(ip,"L!",port)
	end
	function c:CIDFrom(ip,port)
		for i,v in pairs(self.ips) do
			if(ip==v[1] and v[2]==port) then
				return i
			end
		end
	end
	function c:sendAll(data)
		for i,v in pairs(self.ips) do
			self:send(v[1],data,v[2],i)
		end
	end
	function c:sendAllBut(data,cid)
		for i,v in pairs(self.ips) do
			if i~=cid then
				self:send(v[1],data,v[2],i)
			end
		end
	end
	function c:clientRegistered(cid)
		return self.ips[cid]
	end
	function c:clientLoggedIn(cid)
		if not self.clientRegistered(cid) then
			return nil
		end
		return self.ips[cid][4]
	end
	function c:update()
		local data,ip,port=self.udp:receivefrom()
		if net.inList(self.bannedIPs,ip) or net.inList(self.bannedCIDs,cid) then
			print("We will ingore data from a banned client!")
			return
		end
		if data then
			if self.autoNormalization then
				data=net.denormalize(data)
			end
			if data:sub(1,4)=="pong" then
				--print("Recieved pong from: "..data:sub(5,-1))
				self.ips[data:sub(5,-1)][3]=os.clock()
			elseif data:sub(1,2)=="S!" then
				local cid=self:CIDFrom(ip,port)
				if data:sub(3,-1)==self.servercode then
					print("Servercode Accepted: "..self.servercode)
					if self.ips[cid] then
						self.ips[cid][4]=true
					else
						print("Server can't keep up! CID: "..cid.." has been skipped! Sending new CID to the client!")
						local cid=net.resolveID(self)
						print("Sending unique cid to client: "..cid)
						self.ips[cid]={ip,port,0,self.servercode==nil}
						print(ip)
						self.udp:sendto("I!"..cid,ip,port)
						if self.servercode then
							self.udp:sendto("S!",ip,port)
						end
					end
				else
					self.udp:sendto("Make sure your server code is correct!", ip, port)
				end
			elseif data:sub(1,2)=="C!" then
				local hook=(data:sub(11,-1)):match("!(.-)!")
				self.OnDataRecieved:getConnection(hook):Fire(self,data:sub(11,-1),data:sub(3,10),ip,port)
			elseif data:sub(1,2)=="E!" then
				self.ips[data:sub(3,10)]=nil
				obj.ids[data:sub(3,10)]=false
				self.OnClientClosed:Fire(self,"Client Closed Connection!",data:sub(3,10),ip,port)
			elseif data=="I!" then
				local cid=net.resolveID(self)
				print("Sending unique cid to client: "..cid)
				self.ips[cid]={ip,port,os.clock(),self.servercode==nil}
				print(ip)
				self.udp:sendto("I!"..cid,ip,port)
				if self.servercode then
					self.udp:sendto("S!",ip,port)
				end
				self.OnClientConnected:Fire(self,cid,ip,port)
			elseif data:sub(1,2)=="L!" then
				cid,cList=data:sub(3,10),data:sub(11,-1)
				local list={}
				for m,v in cList:gmatch("(%S-):(%S-)|") do
					list[m]=v
				end
				self.OnClientsModulesList:Fire(list,cid,ip,port)
			end
		end
		for cid,dat in pairs(self.ips) do
			if not((os.clock()-dat[3])<65) then
				self.ips[cid]=nil
				self.OnClientClosed:Fire(self,"Client lost Connection: ping timeout",cid,ip,port)
			end
		end
	end
	c.OnClientsModulesList=multi:newConnection()
	c.OnDataRecieved=multi:newConnection()
	c.OnClientClosed=multi:newConnection()
	c.OnClientConnected=multi:newConnection()
	c.connectiontest=multi:newAlarm(30)
	c.connectiontest.link=c
	c.connectiontest:OnRing(function(alarm)
		--print("pinging clients!")
		alarm.link:sendAll("ping")
		alarm:Reset()
	end)
	multi:newLoop(function()
		c:update()
	end)
	net.OnServerCreated:Fire(c)
	return c
end

function net:newClient(host,port,servercode,nonluaServer)
	local c={}
	c.ip=assert(socket.dns.toip(host))
	c.udp=assert(socket.udp())
	c.udp:settimeout(0)
	c.udp:setpeername(c.ip, port)
	c.cid="NIL"
	c.lastPing=0
	c.Type="udp"
	c.servercode=servercode
	c.autoReconnect=true
	c.autoNormalization=false
	function c:pollPing(n)
		return not((os.clock()-self.lastPing)<(n or 60))
	end
	function c:send(data)
		if self.autoNormalization then
			data=net.normalize(data)
		end
		self.udp:send("C!"..self.cid..data)
	end
	function c:sendRaw(data)
		if self.autoNormalization then
			data=net.normalize(data)
		end
		self.udp:send(data)
	end
	function c:getCID()
		if self:IDAssigned() then
			return self.cid
		end
	end
	function c:close()
		self:send("E!")
	end
	function c:IDAssigned()
		return self.cid~="NIL"
	end
	function c:update()
		local data=self.udp:receive()
		if data then
			if self.autoNormalization then
				data=net.denormalize(data)
			end
			if data:sub(1,2)=="I!" then
				self.cid=data:sub(3,-1)
				self.OnClientReady:Fire(self)
			elseif data=="S!" then
				self.udp:send("S!"..(self.servercode or ""))
			elseif data=="L!" then
				local mods=""
				local m=""
				for i=1,#net.loadedModules do
					m=net.loadedModules[i]
					mods=mods..m..":"..net.getModuleVersion(m).."|"
				end
				self.udp:send("L!"..self.cid..mods)
			elseif data=="ping" then
				self.lastPing=os.clock()
				self.OnPingRecieved:Fire(self)
				self.udp:send("pong"..self.cid)
			else
				local hook=data:match("!(.-)!")
				self.OnDataRecieved:getConnection(hook):Fire(self,data)
			end
		end
	end
	function c:reconnect()
		if not nonluaServer then
			self.cid="NIL"
			c.udp:send("I!")
		end
		self.OnConnectionRegained:Fire(self)
	end
	c.pingEvent=multi:newEvent(function(self) return self.link:pollPing() end)
	c.pingEvent:OnEvent(function(self)
		if self.link.autoReconnect then
			self.link.OnServerNotAvailable:Fire("Connection to server lost: ping timeout! Attempting to reconnect...")
			self.link.OnClientDisconnected:Fire(self,"closed")
			self.link:reconnect()
		else
			self.link.OnServerNotAvailable:Fire("Connection to server lost: ping timeout!")
			self.link.OnClientDisconnected:Fire(self,"closed")
		end
	end)
	c.pingEvent.link=c
	c.OnPingRecieved=multi:newConnection()
	c.OnDataRecieved=multi:newConnection()
	c.OnServerNotAvailable=multi:newConnection()
	c.OnClientReady=multi:newConnection()
	c.OnClientDisconnected=multi:newConnection()
	c.OnConnectionRegained=multi:newConnection()
	c.notConnected=multi:newFunction(function(self)
		self:hold(3)
		if self.link:IDAssigned()==false then
			self.link.OnServerNotAvailable:Fire("Can't connect to the server: no response from server")
		end
	end)
	c.notConnected.link=c
	if not nonluaServer then
		c.udp:send("I!")
	end
	multi:newLoop(function()
		c:update()
	end)
	multi:newJob(function() c.notConnected() end)
	net.OnClientCreated:Fire(c)
	return c
end
--TCP Stuff
function net:newTCPServer(port,secure)
	local c={}
	c.tcp=assert(socket.bind("*", port))
	c.tcp:settimeout(0)
	c.ip,c.port=c.tcp:getsockname()
	c.ips={}
	c.port=port
	c.ids={}
	c.bannedIPs={}
	c.Type="tcp"
	c.rMode="*l"
	c.sMode="*l"
	c.updaterRate=1
	c.autoNormalization=false
	c.updates={}
	c.links={}
	c.broad=socket.udp()
	c.hostip=net.getLocalIP()
	function c:broadcast(name)
		local loop=multi:newTLoop(function(loop,dt)
			self.broad:setoption('broadcast',true)
			self.broad:sendto(name.."|"..self.Type.."|"..self.hostip..":"..self.port.."|"..({[true]="1",[false]="0"})[secure], "255.255.255.255", 11111)
			self.broad:setoption('broadcast',false)
		end,1)
	end
	function c:setUpdateRate(n)
		self.updaterRate=n
	end
	function c:setReceiveMode(mode)
		self.rMode=mode
	end
	function c:setSendMode(mode)
		self.rMode=mode
	end
	function c:banCID(cid)
		print("Function not supported on a tcp server!")
	end
	function c:banIP(ip)
		table.insert(self.bannedIPs,cid)
	end
	function c:send(handle,data)
		if self.autoNormalization then
			data=net.normalize(data)
		end
		if self.sMode=="*l" then
			handle:send(data.."\n")
		else
			handle:send(data)
		end
	end
	function c:sendAllData(handle,data)
		if self.autoNormalization then
			data=net.normalize(data)
		end
		handle:send(data)
	end
	function c:pollClientModules(ip,port)
		self:send(ip,"L!",port)
	end
	function c:CIDFrom(ip,port)
		print("Method not supported when using a TCP Server!")
		return "CIDs in TCP work differently!"
	end
	function c:sendAll(data)
		for i,v in pairs(self.ips) do
			self:send(v,data)
		end
	end
	function c:sendAllBut(data,cid)
		for i,v in pairs(self.ips) do
			if not(cid==i) then
				self:send(v,data)
			end
		end
	end
	function c:clientRegistered(cid)
		return self.ips[cid]
	end
	function c:clientLoggedIn(cid)
		return self.ips[cid]
	end
	function c:getUpdater(cid)
		return self.updates[cid]
	end
	function c:update()
		local client = self.tcp:accept(self.rMode)
		if not client then return end
		ip,port=client:getpeername()
		if secure then
			if not net.ssl then
				net.ssl=require("ssl")
			end
			client = net.ssl.wrap(client, net.sParams)
			client:dohandshake()
		else
			client:settimeout(0)
			client:setoption('keepalive', true)
		end
		table.insert(self.ips,client)
		if ip and port then
			print("Got connection from: ",ip,port)
			local updater=multi:newUpdater(skip)
			self.updates[client]=updater
			self.OnClientConnected:Fire(self,self.client,self.client,ip)
			updater:OnUpdate(function(self)
				local data, err = self.client:receive(self.rMode or self.Link.rMode)
				if err=="closed" then
					for i=1,#self.Link.ips do
						if self.Link.ips[i]==self.client then
							table.remove(self.Link.ips,i)
						end
					end
					self.Link.OnClientClosed:Fire(self.Link,"Client Closed Connection!",self.client,self.client,ip)
					self.Link.links[self.client]=nil -- lets clean up
					self:Destroy()
				end
				if data then
					if self.autoNormalization then
						data=net.denormalize(data)
					end
					if net.inList(self.Link.bannedIPs,ip) then
						print("We will ingore data from a banned client!")
						return
					end
					local hook=data:match("!(.-)!")
					self.Link.OnDataRecieved:getConnection(hook):Fire(self.Link,data,self.client,self.client,ip,self)
					if data:sub(1,2)=="L!" then
						cList=data
						local list={}
						for m,v in cList:gmatch("(%S-):(%S-)|") do
							list[m]=v
						end
						self.Link.OnClientsModulesList:Fire(list,self.client,self.client,ip)
					end
				end
			end)
			updater:setSkip(self.updaterRate)
			updater.client=client
			updater.Link=self
			function updater:setReceiveMode(mode)
				self.rMode=mode
			end
			self.links[client]=updater
		end
	end
	c.OnClientsModulesList=multi:newConnection()
	c.OnDataRecieved=multi:newConnection()
	c.OnClientClosed=multi:newConnection()
	c.OnClientConnected=multi:newConnection()
	multi:newLoop(function()
		c:update()
	end)
	net.OnServerCreated:Fire(c)
	return c
end
function net:newTCPClient(host,port,secure)
	local c={}
	c.ip=assert(socket.dns.toip(host))
	c.port=port
	if secure then
		if not net.ssl then
			net.ssl=require("ssl")
		end
		c.tcp=socket.tcp()
		c.tcp:connect(c.ip,port)
		if not c.tcp then
			return false,"Can't connect to the server: no response from server"
		end
		c.tcp=net.ssl.wrap(c.tcp, net.cParams)
		c.tcp:dohandshake()
		c.ready=true
	else
		c.tcp=socket.connect(c.ip,port)
		c.tcp:settimeout(0)
		c.tcp:setoption('keepalive', true)
	end
	if not c.tcp then
		return false,"Can't connect to the server: no response from server"
	end
	c.Type="tcp"
	c.autoReconnect=true
	c.rMode="*l"
	c.sMode="*l"
	c.ready=false
	c.autoNormalization=false
	function c:setReceiveMode(mode)
		self.rMode=mode
	end
	function c:setSendMode(mode)
		self.sMode=mode
	end
	function c:send(data)
		if self.autoNormalization then
			data=net.normalize(data)
		end
		if self.sMode=="*l" then
			ind,err=self.tcp:send(data.."\n")
		else
			ind,err=self.tcp:send(data)
		end
		if err=="closed" then
			self.OnClientDisconnected:Fire(self,err)
		elseif err=="timeout" then
			self.OnClientDisconnected:Fire(self,err)
		elseif err then
			print(err)
		end
	end
	function c:sendRaw(data)
		if self.autoNormalization then
			data=net.normalize(data)
		end
		self.tcp:send(data)
	end
	function c:getCID()
		return "No Cid on a tcp client!"
	end
	function c:close()
		self.tcp:close()
	end
	function c:IDAssigned()
		return self.ready
	end
	function c:update()
		if not self.tcp then return end
		local data,err=self.tcp:receive()
		if err=="closed" then
			self.OnClientDisconnected:Fire(self,err)
		elseif err=="timeout" then
			self.OnClientDisconnected:Fire(self,err)
		elseif err then
			print(err)
		end
		if data then
			if self.autoNormalization then
				data=net.denormalize(data)
			end
			local hook=data:match("!(.-)!")
			self.OnDataRecieved:getConnection(hook):Fire(self,data)
		end
	end
	function c:reconnect()
		multi:newFunction(function(func)
			self.tcp=socket.connect(self.ip,self.port)
			if self.tcp==nil then
				print("Can't connect to the server: No response from server!")
				func:hold(3)
				self:reconnect()
				return
			end
			self.OnConnectionRegained:Fire(self)
			self.tcp:settimeout(0)
			--self.tcp:setoption('tcp-nodelay', true)
			self.tcp:setoption('keepalive', true)
		end)
	end
	c.event=multi:newEvent(function(event)
		return event.link:IDAssigned()
	end)
	c.event:OnEvent(function(event)
		event.link.OnClientReady:Fire(event.link)
	end)
	c.event.link=c
	c.OnClientReady=multi:newConnection()
	c.OnClientDisconnected=multi:newConnection()
	c.OnDataRecieved=multi:newConnection()
	c.OnConnectionRegained=multi:newConnection()
	multi:newLoop(function()
		c:update()
	end)
	net.OnClientCreated:Fire(c)
	return c
end
