dofile("packets.lua")

Network = inherited("Network", State)

Network.nextClassIds = 1
Network.classIds = {}

function Network.registerClass(classRef)
	Network.classIds[Network.nextClassIds] = classRef
	Network.classIds[classRef] = Network.nextClassIds
	Network.nextClassIds = Network.nextClassIds + 1
end

function Network.getIdFromClass(classRef)
	return Network.classIds[classRef]
end

function Network.getClassFromId(id)
	return Network.classIds[id]
end

function Network.isRegistered(instance)
	return Network.classIds[getClassTableOf(instance)]
end

function Network:getDevice()
	return self.device or RaknetInstance()
end

function Network:setCallback(c)
	self.callback = c
end

function Network:replaceDevice(d)
	self.device = d
	self.hosted = true
	self:setGuid(self.device:getMyGuid())
	self:onMessage("Replaced device, new guid "..self:getGuid())
end

function Network:new(gameString, versionString, packets)
	local g = instance(self)
	g.device = g:getDevice()
	g.gameString = gameString
	g.versionString = versionString
	if packets then
		g.packets = packets
		for ind, pdef in ipairs(packets) do
			packets[pdef.name] = pdef
			pdef.id = ind
		end
	end
	g.connections = {}
	return g
end

function Network:seek(startPort, endPort, seekDelay)
	self.lanHosts = {}
	self.startPort = startPort
	self.endPort = endPort or startPort
	self.seekPort = startPort
	self.seeking = true
	self.seekTimer = 0
	self.seekDelay = seekDelay or 2
end

function Network:host(port, maxConnections)
	if self:isOffline() then
		
		self.port = port
		self.maxConnections = maxConnections or 8
		
		local result, message = self.device:host(self.port, self.maxConnections)
		if result then
			self.hosted = true
			self:setGuid(self.device:getMyGuid())
			self:updatePongResponse()
			self:onMessage("Hosted on port:" .. self.port .. ", guid:" .. self.guid.."!")
			return true
		else
			self:onError("Hosting on port:"..self.port.." failed: "..tostring(message))
			return false, message
		end
	else
		self:onError("Already "..self:getCurrentStateString())
	end
end



function Network:join(ip, port)
	if self:isOffline() then
		
		self.ip = ip
		self.port = port
		
		local result, message = self.device:connect(ip, port)
		
		if result then
			self.connected = true
			self:onMessage("Connecting to "..ip..":"..port.."..." )
			return true
		else
			self:onError("Connecting to "..ip..":"..port.. " failed: "..tostring(message))
			return false, message
		end
	else
		self:onError("Already "..self:getCurrentStateString())
	end
end

function Network:disconnect()
	if self.device then
		self.device = nil
	end
end

function Network:writePacketData(pdef, data)
	if DEBUG.showDebug and DEBUG.showGraphs then
		debug.addToGraph("packet", 1)
	end
	local structure = pdef.structure
	local structureLen = #structure
	local len = structureLen
	if pdef.variable then
		self.device:writeInt(#data)	
		len = math.max(#data, len)	
	end
	
	for i=1, len do
		local m = structure[i]
		if string.len(m) <= 0 then
			if pdef.tail then
				local ti = ((i-structureLen)%string.len(pdef.tail))+1
				m = string.sub(pdef.tail, ti,ti)
			else
				self:onError(pdef.name.. " variable tail missing in write")
				break
			end
		end
		if packets.writers[m] then
			local errorMessage = packets.writers[m](self.device, data[i], self.callback)
			if errorMessage then
				self:onError("writer "..m.." failed for "..pdef.name..":"..errorMessage)
			end
		else
			self:onError(tostring(m).. " unhandled packet writer searching in ("..tostring(table.concat(structure))..":"..tostring(pdef.tail)..") at ".. i )
			local cand = ""
			for index, writer in pairs(packets.writers) do
				cand = cand..index..","
			end
			self:onError("writer candidates: "..cand)
		end
	end
end

function Network:readPacketData(pdef)
	if DEBUG.showDebug and DEBUG.showGraphs then
		debug.addToGraph("packet", 1)
	end
	local structure = pdef.structure
	local structureLen = #structure
	local len = pdef.variable and self.device:readInt() or structureLen
	local data = {}
	
	for i=1,len do
		local m = structure[i]
		if string.len(m) <= 0 then
			if pdef.tail then
				local ti = ((i-structureLen)%string.len(pdef.tail))+1
				m = string.sub(pdef.tail, ti,ti)
			else
				self:onError(pdef.name.. " variable tail missing in read")
				break
			end
		end
		if packets.readers[m] then
			local read = packets.readers[m](self.device, self.callback)
			if read ~= nil then
				table.insert(data, read)
			end
		else
			self:onError(tostring(m).. " unhandled packet reader")
		end
	end
	return data
end

function Network:handlePacketData(pdef, guid, timeDiff, ...)
	if self.callback then
		if pdef.call then
			if self.callback[pdef.call] then
				self.callback[pdef.call](self.callback, ...)
			else
				self:onError(pdef.call.. " missing from callback")
			end
		end
		if pdef.relayCall then
			if self.callback[pdef.relayCall] then
				if pdef.timeDiff then
					self.callback[pdef.relayCall](self.callback, guid, timeDiff, ...)
				else
					self.callback[pdef.relayCall](self.callback, guid, ...)
				end
			else
				self:onError(pdef.relayCall.. " missing from callback")
			end
		end
	end
end

function Network:handlePacket(packetId, guid, timeDiff)
	local pdef = self.packets[packetId]
	if pdef then
		if pdef.host and self.hosted then
			self:onError("Client "..guid.." tried to call protected packet: "..pdef.name)
			return
		end
		if pdef.connected and self.client then
			self:onError("Server tried to call client only packet: "..pdef.name)
			return
		end
		local data = self:readPacketData(pdef)
		
		if DEBUG.showData then
			self:onData("RECIEVE "..pdef.name)
			for index, val in pairs(data) do
				self:onData(">> "..tostring(val))
			end
		end
		self:handlePacketData(pdef, guid, timeDiff, unpack(data))
	end
end

function Network:sendPacketTo(guid, packetType, ...)
	if DEBUG.showDebug and DEBUG.showGraphs then
		debug.addToGraph("send", 1)
	end
	local pdef = self.packets[packetType]
	if self.hosted then

		if pdef.setup then
			if self.callback[pdef.setup] then
				self.callback[pdef.setup](self.callback, guid, ...)
			else
				self:onError(pdef.setup.. " missing from callback")
			end
		end

		if self:isGuidConnection(guid) then
			self.device:createPacket(pdef.id + packets.baseIndex)
			self:writePacketData(pdef, {...})
			self.device:send(guid, pdef.prio, pdef.mode)
		elseif guid == self.guid then
			self:handlePacketData(pdef, guid, 0, ...)
		end
	else
		debug.printtrace()
		self:onError("cannot send to client as client")
	end
	
	if DEBUG.showData then
		self:onData("SENDTO "..packetType.. " ("..guid..")")
		for index, val in pairs({...}) do
			self:onData(">> "..tostring(val))
		end
	end
end

function Network:sendPacket(packetType, ...)
	if DEBUG.showDebug and DEBUG.showGraphs then
		debug.addToGraph("send", 1)
	end
	local pdef = self.packets[packetType]
	if pdef then
		if self.hosted then
			if pdef.relayCall then
				self:handlePacketData(pdef, self:getGuid(), 0, ...)
			end
			if not pdef.relayCall or pdef.broadcast then
				for guid, connection in pairs(self.connections) do
					if guid ~= self.guid then
						self.device:createPacket(pdef.id + packets.baseIndex)
						self:writePacketData(pdef, {...})
						self.device:send(guid, pdef.prio, pdef.mode)
					end
				end
			end
		elseif self.connected then
			self.device:createPacket(pdef.id + packets.baseIndex)
			self:writePacketData(pdef, {...})
			self.device:send(self:getServerGuid(), pdef.prio, pdef.mode)
		end
		if pdef.callSelf then
			self:handlePacketData(pdef, self:getGuid(), 0, ...)
		end
	else
		self:onError(packetType.. " is not a valid packet")
	end
	
	if DEBUG.showData then
		self:onData("SEND "..packetType)
		for index, val in pairs({...}) do
			self:onData(">> "..tostring(val))
		end
	end
end

function Network:isOffline()
	return not self.hosted and not self.connected
end

function Network:isOnline()
	return not self:isOffline()
end

function Network:getCurrentStateString()
	if self.hosted then
		return "hosted"	
	elseif self.connected then
		return "connected"
	end
end

function Network:getPongString()
	return self.gameString.."_"..self.versionString
end

function Network:setPongDataString(str)
	self.pongDataString = str
	if self:isOnline() then
		self:updatePongResponse()
	end
end

function Network:updatePongResponse()
	if self.pongDataString then
		self.device:setUnconnectedPingResponse(self:getPongString().."|"..self:getPongDataString())	
	end
end

function Network:getPongDataString()
	return self.pongDataString or ""
end



function Network:update(time)
	if self.device then
		self:poll()
		
		if self.seeking then
			if self.seekTimer <= 0 then
				self:broadcastPing(self.seekPort)
				self.seekPort = self.seekPort +1
				if self.seekPort > self.endPort then
					self.seekPort = self.startPort
				end
				self.seekTimer = self.seekDelay
			else
				self.seekTimer = self.seekTimer - time
			end
			
			if #self.lanHosts > 0 then
				for index, host in pairs(self.lanHosts) do
					host.age = host.age + time
					if host.age > self.seekDelay + 1 then
						host._remove = true
					end
				end
				table.removeCompletelyIf(self.lanHosts)
			end
		end
	end
end

function Network:getNumberOfConnections()
	local num = 0
	for index, con in pairs(self.connections) do
		num = num +1
	end
	return num
end

function Network:addConnection(guid)
	self.connections[guid] = true
end

function Network:removeConnection(guid)
	self.connections[guid] = nil
end

function Network:isGuidConnection(guid)
	return self.connections[guid] ~= nil
end

function Network:isGuidServer(guid)
	return self.serverGuid == guid
end

function Network:setGuid(guid)
	self.guid = guid
end

function Network:setServerGuid(guid)
	self.serverGuid = guid	
end

function Network:getServerGuid(guid)
	return self.serverGuid
end

function Network:getGuid()
	return self.guid
end

function Network:broadcastPing(port)
	self.device:sendPing("255.255.255.255", port, false)
end

function Network:removeListed(ip, port)
	for index, host in pairs(self.lanHosts) do
		if host.ip == ip and host.port == port then
			table.remove(self.lanHosts, index)
			self.updated = true
			break
		end
	end
end

function Network:addLanHost(ip, port, data)
	local found = false
	for index, host in pairs(self.lanHosts) do
		if host.ip == ip and host.port == port then
			host.age = 0
			for var, val in pairs(data) do
				host[var] = val
			end
			found = true
		end
	end
	if not found then
		local newHost =  {lan = true, ip = ip, port = port, age = 0}
		for var, val in pairs(data) do
			newHost[var] = val
		end
		table.insert(self.lanHosts,newHost)
	end
	self.updated = true
end

function Network:isLanListUpdated()
	if self.updated then
		self.updated = false
		return true
	end
end

function Network:getLanHosts()
	return self.lanHosts
end

function Network:poll()
	local packetType, sourceGuid, length, timeDiff = self.device:nextPacket()
	while packetType ~= nil do
		if packetType > packets.baseIndex then
			if (self.hosted and self:isGuidConnection(sourceGuid)) or (self.connected and self:isGuidServer(sourceGuid)) then
				self:handlePacket(packetType - packets.baseIndex, sourceGuid, timeDiff)
			else
				self:onError("Received packet " .. packetType .. " from unexpected guid " .. sourceGuid)
			end
		else -- RakNet packet
			local name = self.device:getPacketName(packetType)
			if name == "ID_CONNECTION_REQUEST_ACCEPTED" then
				-- we connected to a server successfully
				self:onMessage("Server accepted our connection. Guid: " .. sourceGuid)
				self:setGuid(self.device:getMyGuid())
				self:setServerGuid(sourceGuid)
				if self.callback and self.callback.onConnectedToServer then
					self.callback:onConnectedToServer(self.device:getMyGuid())
				end
			elseif name == "ID_NEW_INCOMING_CONNECTION" and self.hosted then
				-- someone wants to connect to the server
				self:onMessage("SERVER: Client has connected to us. GUID: " .. sourceGuid)
				self:addConnection(sourceGuid)
				if self.callback and self.callback.onClientConnected then
					self.callback:onClientConnected(sourceGuid)
				end
			elseif name == "ID_DISCONNECTION_NOTIFICATION" then
				-- someone disconnected from the server or the server disconnected us
				if self.hosted then
					self:onMessage("SERVER: Client has disconnected from us. GUID: " .. sourceGuid)
					self:removeConnection(sourceGuid)
					if self.callback and self.callback.onClientDisconnected then
						self.callback:onClientDisconnected(sourceGuid)
					end
				elseif self.connected then
					self:onMessage("Disconnected from the server!")
					self.connected = false
					self:setServerGuid()
					if self.callback and self.callback.onDisconnected then
						self.callback:onDisconnected()
					end
				end
			elseif name == "ID_UNCONNECTED_PONG" then
				if not self.connected then
					self.device:readInt()
					local pongResponse = self.device:readString()
					local pongData = string.split(pongResponse, "|")
					if pongData[1] == self:getPongString() then
						local data = {}
						if pongData[2] then
							table.remove(pongData, 1)
							local layout = string.split(pongData[1], ",")
							table.remove(pongData, 1)
							
							for rawIndex, key in pairs(layout) do
								data[key] = pongData[rawIndex]
							end
							local ip, port = string.splitParameters(self.device:getPacketSenderAddress(), "|")
							self:addLanHost(ip, port, data)
							self:onMessage("Received Lan Pong from " .. ip .. ":" .. port)
						else
							self:onError("Pong missing meta info")
						end
					end
				end
			end
		end
		if self.device then
			packetType, sourceGuid, length, timeDiff = self.device:nextPacket()
		else
			break
		end
	end
end
