--[[
Datastream Module
By Janorkie and Deco da Man
]]--

require "glon"

function string.split(str,d)
	local t = {}
	local len = str:len()
	local i = 0
	while i*d < len do
		t[i+1] = str:sub(i*d+1,(i+1)*d)
		i=i+1
	end
	return t
end

local function MergeChanges(dest,changes)
	for k,v in pairs(changes) do
		if v == "\1\2\1" then -- Delete code
			dest[k] = nil
		else
			dest[k] = v
		end
	end
end

if SERVER then

umsg.PoolString "DSStart"
umsg.PoolString "DSPacket"
umsg.PoolString "DSEnd"
umsg.PoolString "DSRequestResponse"
umsg.PoolString "DSPacketReceived"
umsg.PoolString "DSConfirmEnd"

local table = table
local string = string
local umsg = umsg
local concommand = concommand
local util = util
local hook = hook
local math = math
local type = type
local pairs = pairs
local tostring = tostring
local tonumber = tonumber
local RecipientFilter = RecipientFilter
local pcall = pcall
local error = error
local glon = glon
local iterations = CreateConVar( "gmod_datastream_iterations", "1", { FCVAR_ARCHIVE } )
local rawget = rawget
local rawset = rawset
local setmetatable = setmetatable
local _R = _R
local print = print
local ErrorNoHalt = ErrorNoHalt
local IsValid = IsValid

module "datastream"
local _outgoing = {}
local _incoming = {}
local _hooks = {}

function Hook(h,f)
	local hk = {}
	hk.handler = h
	hk.func = f
	_hooks[h] = hk
end

function StreamToClients( rcp, h, d, cb )
	local o = {}
	-- Recipient information
	local rt = type(rcp)
	if rt == "CRecipientFilter" then
		o.rf = rcp
	elseif rt == "table" then
		o.rf = RecipientFilter()
		for k,v in pairs(rcp) do
			o.rf:AddPlayer(v)
		end
	elseif rt == "Player" then
		o.rf = rcp
	else
		error(("Invalid type %q given to datastream.StreamToClients for recipients!"):format(type(rcp)))
	end
	
	-- Data information
	o.decdata = d
	o.encdata = ""
	o.sent = 0
	o.size = 0
	-- Operation state
	o.block = 1
	o.state = 1
	-- Operation handlers
	o.handler = h
	o.callback = cb
	-- Put the operation into the queue and finish up
	o.id = table.insert(_outgoing,o)
	return o.id
end

function DownstreamActive() return #_incoming > 0 end

function GetProgress(id)
	if _outgoing[id] then return _outgoing[id].sent end
end

local function CallStreamHook(pl,h,id,encdat,decdat)
	if not _hooks[h] then
		ErrorNoHalt(("DataStreamServer: Unhandled stream %q!"):format(h))
	return end
	if hook.Call("CompletedIncomingStream",GAMEMODE,pl,h,id,encdat,decdat) then return end
	_hooks[h].func(pl,h,id,encdat,decdat)
end

local function StreamTick(rep)
	if not rep and iterations:GetInt() > 0 then
		for i=1,iterations:GetInt() do
			StreamTick(true)
		end
	end
	for id,o in pairs(_outgoing) do
		if type(o.rf) != "CRecipientFilter" and !IsValid(o.rf) then
			-- Player disconnected
			_outgoing[o.id] = nil
			return
		end
		if o.state == 1 then -- Processing stage.
			if o.decdata then
				local enc = ""
				local b,err = pcall(glon.encode, o.decdata)
				if b then enc = err else ErrorNoHalt(("DataStreamServer Encoding Error: %s (operation %s)"):format(err, id)) _outgoing[o.id] = nil return end
				o.encdata = string.split(enc,128)
				o.state = 2
			else
				o.state = 3
			end
			umsg.Start("DSStart",o.rf)
				umsg.Long(o.id)
				umsg.String(o.handler)
				umsg.Bool(type(o.encdata) == "table" and #o.encdata <= 1)
				if type(o.encdata) == "table" and #o.encdata <= 1 then
					umsg.String(o.encdata[1] or "")
					if o.callback then o.callback(o.id) end
					_outgoing[o.id] = nil
				end
			umsg.End()
			return
		elseif o.state == 2 then -- Sending stage.
			umsg.Start("DSPacket",o.rf)
				umsg.Long(o.id)
				umsg.String(o.encdata[o.block])
			umsg.End()
			o.block = o.block+1
			if o.block > #o.encdata then o.state = 3 end
			return
		elseif o.state == 3 then -- Ending stage
			umsg.Start("DSEnd",o.rf)
				umsg.Long(o.id)
			umsg.End()
			if o.callback then o.callback(o.id) end
			_outgoing[o.id] = nil
			return
		end
	end
end
hook.Add("Tick","DatastreamTick",StreamTick)

-- Default stuff that will be put in base gamemode
local function AcceptStream()
	return true
end
hook.Add("AcceptStream","AcceptStream",AcceptStream)

-- Messages from the client
local function DSRequest(pl,cmd,args)
	if args[1] ~= "\1" then return end
	local h = args[2]
	local id = tonumber(args[3])
	if hook.Call("AcceptStream",GAMEMODE,pl,h,id) then
		local o = {
			pl = pl,
			handler = h,
			buffer = "",
		}
		o.id = table.insert(_incoming,o)
		umsg.Start("DSRequestResponse",pl)
			umsg.Long(id)
			umsg.Bool(true)
			umsg.Long(o.id)
		umsg.End()
	else
		umsg.Start("DSRequestResponse",pl)
			umsg.Long(id)
			umsg.Bool(false)
		umsg.End()
	end
end
concommand.Add("__dsr",DSRequest)

local function DSPacket(pl,cmd,args)
	if args[1] ~= "\1" then return end
	local id = tonumber(args[3])
	if not _incoming[id] then return end
	local data = args[2]
	_incoming[id].buffer = _incoming[id].buffer..data
	umsg.Start("DSPacketReceived",pl)
		umsg.Long(id)
		umsg.Long(_incoming[id].buffer:len())
	umsg.End()
end
concommand.Add("__dsp",DSPacket)

local function DSEnd(pl, cmd, args)
	if args[1] ~= "\1" then return end
	local id = tonumber(args[2])
	if not _incoming[id] then return end
	local reptab = {["\\"]="\\",["n"]="\n"}
	local encdat = string.gsub(_incoming[id].buffer,"\\(.)",reptab)
	local decdat = ""
	local b,err = pcall(glon.decode,encdat)
	if b then decdat=err else ErrorNoHalt("DataStreamServer Decoding Error: "..err.." (operation "..id.." from "..pl:Name().."("..pl:SteamID().."))") return end
	CallStreamHook(pl,_incoming[id].handler,id,encdat,decdat)
	_incoming[id] = nil
	umsg.Start("DSConfirmEnd",pl)
		umsg.Long(id)
	umsg.End()
end
concommand.Add("__dse",DSEnd)

function _R.Player:SendData(handler, data, callback)
	StreamToClients(self, handler, data, callback)
end

function _R.Player:CreateSharedTable(name,data,clientchanges)
	CreateSharedTable(self,name,data,clientchanges)
end

function _R.Player:GetSharedTable(name)
	return GetSharedTable(self,name)
end

--SharedTable
_R.SharedTable = {
	__index = function(self, key)
		return rawget(self, "__realtable")[key]
	end,
	__newindex = function(self, key, value)
		rawget(self,"__realtable")[key] = value
		if value == nil then
			rawget(self,"__changes")[key] = "\1\2\1" -- Our delete code.
		else
			rawget(self,"__changes")[key] = value
		end
		rawset(self,"__changed",true)
	end,
	__call = function(self, ...)
		-- Think of something l33t to do here
	end,
}
local supported_types = {
	--table			=	true, -- not supported yet
	string			=	true,
	number			=	true,
	boolean			=	true,
	number			=	true,
	Vector			=	true,
	Angle			=	true,
	Entity			=	true,
	Player			=	true,
	CEffectData		=	true,
	ConVar			=	true,
	Color			=	true,
}

local SharedTables = {}
function CreateSharedTable(pl,name,data,clientchanges)
	for k,v in pairs(data) do
		if not supported_types[type(k)] then
			error(("Invalid type %q for key %q!"):format(type(k), tostring(k)))
		elseif not supported_types[type(v)] then
			error(("Invalid type %q for value %q (of key %q)!"):format(type(v), tostring(v), tostring(k)))	
		end
	end
	local t = setmetatable({
		__pl = pl,
		__realtable = data,
		__changes = {},
		__changed = false,
		__clientchanges = clientchanges
	}, _R.SharedTable)
	if not SharedTables[pl] then SharedTables[pl] = {} end
	SharedTables[pl][name] = t
	StreamToClients(pl, "SharedTableCreate", {name = name, data = data, clientchanges = clientchanges})
	return t
end
local function OnTick()
	for k,v in pairs(SharedTables) do
		for tk,tv in pairs(v) do
			local b, err = pcall(
			function( )
				if rawget(tv,"__changed") then
					StreamToClients(rawget(tv,"__pl"), "SharedTableUpdate", {name = tk,changes = rawget(tv,"__changes")})
					rawset(tv,"__changed",false)
					rawset(tv,"__changes",{})
				end
			end)
			if not b then ErrorNoHalt("DatastreamServer STUError: " .. err ) end
		end
	end
end
hook.Add("Tick", "SharedTableTick", OnTick)
function GetSharedTable(pl,name)
	if SharedTables[pl] then
		return SharedTables[pl][name]
	end
	return nil
end

local function SharedTableUpdate(pl,handler,id,encoded,decoded)
	local st = GetSharedTable(pl,decoded.name)
	if not st then return end
	if rawget(st,"__clientchanges") then
		MergeChanges(rawget(st,"__realtable"),decoded.changes)
	else
		pl:PrintMessage(HUD_PRINTCONSOLE, "DatastreamClient STError: Not allowed to change this table!")
	end
end
Hook("SharedTableUpdate",SharedTableUpdate)

elseif CLIENT then
local table = table
local string = string
local usermessage = usermessage
local hook = hook
local math = math
local os = os
local util = util
local RunConsoleCommand = RunConsoleCommand
local LocalPlayer = LocalPlayer
local type = type
local pairs = pairs
local tostring = tostring
local RecipientFilter = RecipientFilter
local pcall = pcall
local error = error
local setmetatable = setmetatable
local glon = glon
local _R = _R
local print = print
local PrintTable = PrintTable
local ErrorNoHalt = ErrorNoHalt
local rawget = rawget
local rawset = rawset

module "datastream"

local _outgoing = {}
local _incoming = {}
local _tempout = {}
local _hooks = {}

function Hook(h,f)
	local hk = {}
	hk.handler = h
	hk.func = f
	_hooks[h] = hk
end

function StreamToServer(h,d,cb,acb)
	local o = {
		-- Data information
		decdata = d,
		encdata = "",
		sent = 0,
		size = 0,
		-- Operation state
		block = 1,
		state = 1,
		-- Operation handlers
		handler = h,
		callback = cb,
		acallback = acb,
	}
	o.id = table.insert(_tempout,o)
	RunConsoleCommand("__dsr","\1",h,o.id)
	return o.id
end

function DownstreamActive() return #_incoming > 0 end

function GetProgress(id)
	if _outgoing[id] then return _outgoing[id].sent end
end

local function CallStreamHook(h,id,encdat,decdat)
	if not _hooks[h] then
		ErrorNoHalt("DataStreamClient: Unhandled stream "..h.."!")
	return end
	hook.Call("CompletedIncomingStream",GAMEMODE,h,id,encdat,decdat)
	_hooks[h].func(h,id,encdat,decdat)
end

local function StreamTick()
	for id,o in pairs(_outgoing) do
		if o.state == 1 then -- Processing stage.
			if o.decdata then
				local enc = ""
				local b,err = pcall(glon.encode, o.decdata)
				if b then enc=err else ErrorNoHalt("DataStreamClient Encoding Error: "..err.." (operation "..o.id..")\n") _outgoing[o.id] = nil return end
				o.encdata = string.split(string.gsub(string.gsub(enc,"\\","\\\\"),"\n","\\n"),128)
				o.state = 2
			else
				o.state = 3
			end
			return
		elseif o.state == 2 then -- Sending stage.
			RunConsoleCommand("__dsp","\1",o.encdata[o.block],o.id)
			o.block = o.block+1
			if o.block > #o.encdata then o.state = 3 end
			return
		elseif o.state == 3 then -- Ending stage
			RunConsoleCommand("__dse","\1",o.id)
			o.state = 4
			return
		end
	end
end
hook.Add("Tick","DatastreamTick",StreamTick)

-- Usermessages from the server
local function DSStart(data)
	local id = data:ReadLong()
	local handler = data:ReadString()
	local o = {
	id = id,
	handler = handler,
	buffer = "",
	}
	_incoming[id] = o
	if data:ReadBool() then
		local encdat = data:ReadString()
		local decdat = ""
		local b,err = pcall(glon.decode,encdat)
		if b then decdat=err else ErrorNoHalt("DataStreamClient Decoding Error: "..err.." (operation "..id..")\n") return end
		CallStreamHook(_incoming[id].handler,id,encdat,decdat)
		_incoming[id] = nil
	end
end
usermessage.Hook("DSStart",DSStart)

local function DSPacket(data)
	local id = data:ReadLong()
	if not _incoming[id] then return end
	local data = data:ReadString()
	_incoming[id].buffer = _incoming[id].buffer..data
end
usermessage.Hook("DSPacket",DSPacket)

local function DSEnd(data)
	local id = data:ReadLong()
	if not _incoming[id] then return end
	local encdat = _incoming[id].buffer
	local decdat = ""
	local b,err = pcall(glon.decode,encdat)
	if b then decdat=err else ErrorNoHalt("DataStreamClient Decoding Error: "..err.." (operation "..id..")\n") return end
	CallStreamHook(_incoming[id].handler,id,encdat,decdat)
	_incoming[id] = nil
end
usermessage.Hook("DSEnd",DSEnd)

local function DSConfirmEnd(data)
	local id = data:ReadLong()
	local o = _outgoing[id]
	if not o then return end
	if o.callback then
		o.callback(o.id)
	end
	_outgoing[o.id] = nil
end
usermessage.Hook("DSConfirmEnd",DSConfirmEnd)

-- Accept/Deny Handlers

local function DSRequestResponse(data)
	local tempid = data:ReadLong()
	local o = _tempout[tempid]
	local accept = data:ReadBool()
	if not o then return end
	_tempout[tempid] = nil
	if accept then
		o.id = data:ReadLong()
		_outgoing[o.id] = o
		if o.acallback then
			o.acallback(true,tempid,o.id)
		end
	else
		if o.acallback then
			o.acallback(false,tempid)
		end
	end
end
usermessage.Hook("DSRequestResponse",DSRequestResponse)

-- Packet Confirmation Message
local function DSPacketReceived(data)
	local id = data:ReadLong()
	if not _outgoing[id] then return end
	_outgoing[id].sent = data:ReadLong()
end
usermessage.Hook("DSPacketReceived",DSPacketReceived)

--SharedTable
_R.SharedTable = {
	__index = function(self, key)
		return rawget(self, "__realtable")[key]
	end,
	__newindex = function(self, key, value)
		if not rawget(self,"__clientchanges") then ErrorNoHalt("DatastreamClient STError: Not allowed to change this table!") return end
		rawget(self,"__realtable")[key] = value
		if value == nil then
			rawget(self,"__changes")[key] = "\1\2\1" -- Our delete code.
		else
			rawget(self,"__changes")[key] = value
		end
		rawset(self,"__changed",true)
	end,
	__call = function(self, ...)
		-- Think of something l33t to do here
	end,
}

local SharedTables = {}

local function OnTick()
	for k,v in pairs(SharedTables) do
		local b, err = pcall(
		function( )
			if rawget(v,"__changed") and rawget(v,"__clientchanges") then
				StreamToServer("SharedTableUpdate", {name = k,changes = rawget(v,"__changes")})
				rawset(v,"__changed",false)
				rawset(v,"__changes",{})
			end
		end)
		if not b then ErrorNoHalt( "DatastreamClient STUError: "..err.."\n" ) end
	end
end
hook.Add("Tick", "SharedTableTick", OnTick)
function GetSharedTable(name)
	return SharedTables[name] or nil
end

local function SharedTableCreate(handler,id,encoded,decoded)
	local t = setmetatable({
		__realtable = decoded.data,
		__changes = {},
		__changed = false,
		__clientchanges = decoded.clientchanges
	}, _R.SharedTable)
	SharedTables[decoded.name] = t
end
Hook("SharedTableCreate",SharedTableCreate)

local function SharedTableUpdate(handler,id,encoded,decoded)
	local st = GetSharedTable(decoded.name)
	if not st then return end
	MergeChanges(st,decoded.changes)
end
Hook("SharedTableUpdate",SharedTableUpdate)

end
