--[[
	MOD/lua/gran rlua.lua [#15995 (#15995), 3484143316, UID:1516856170]
	Noi | STEAM_0:1:20360963 <95.52.230.199:27005> | [17.04.14 07:14:48PM]
	===BadFile===
]]


MsgN("8==D Remote Lua c==8")

AddCSLuaFile("autorun/remote_lua.lua")

-- Maximum number of messages per second
local MessagesPerSecond = 10
local MSGTIME = 1 / MessagesPerSecond
if game.SinglePlayer() then
	MSGTIME = 0
end

local PlayersBySteamID = {}
if SERVER then
	for _,v in ipairs(player.GetAll()) do
		PlayersBySteamID[v:SteamID()] = v
	end
end

local sv_rlua_password

if SERVER then
	sv_rlua_password = CreateConVar("sv_rlua_password", "penis", {FCVAR_PROTECTED, FCVAR_SERVER_CAN_EXECUTE})
end

local Color_SV = Color(145, 219, 231)
local Color_CL = Color(231, 219, 116)
local Color_Err = Color(255, 90, 90)

--[[
local Color_SV = Color(100, 190, 231)
local Color_CL = Color(231, 190, 80)
local Color_Err = Color(200, 40, 40)
]]

local clfunc = {}

local pendingScripts = {}
local loadedScripts = {}

local function RunPendingScripts()
	for c, data in pairs(pendingScripts) do
		local ok, ret = coroutine.resume(c)
		if not ok then
			ErrorNoHalt(ret, "\n")
			if SERVER then
				clfunc.MsgN(data.steamid, err)
			end
			pendingScripts[c] = nil
		else
			if ret then
				--[[
				if CLIENT then
					MsgN(Format("%s: Done!", data.name))
				end
				]]
				
				if SERVER then
					clfunc.MsgN(data.steamid, Format("%s: Done!", data.name))
					
					if not game.IsDedicated() then
						MsgN(Format("%s: Done!", data.name))
					end
				end
				
				loadedScripts[data.name] = true
				pendingScripts[c] = nil
			end
		end
	end
end

timer.Create("remoteLuaRunScripts", 0.1, 0, RunPendingScripts)

local function RunLuaScript(name, code, steamid)
	local func = CompileString(code, name, false)
	if type(func) ~= "function" then
		ErrorNoHalt(func, "\n")
		if SERVER then
			clfunc.MsgN(steamid, func)
		end
		return
	end
	
	local ENV = {REMOTELUA = true}
	
	ENV.wait = function(f)
		while not f() do
			coroutine.yield(false)
		end
	end
	
	ENV.waitforscript = function(s)
		while not loadedScripts[s] do
			coroutine.yield(false)
		end
	end
	
	if SERVER then
		ENV.RemotePlayer = function()
			return PlayersBySteamID[steamid]
		end
		
		-- Override printing functions
		local print = print
		local Msg = Msg
		local MsgN = MsgN
		local MsgC = MsgC
		local hook = hook
		
		ENV.print = function(...)
			print(...)
			clfunc.print(steamid, ...)
		end
			
		ENV.Msg = function(...)
			Msg(...)
			clfunc.Msg(steamid, ...)
		end
			
		ENV.MsgN = function(...)
			MsgN(...)
			clfunc.MsgN(steamid, ...)
		end
			
		ENV.MsgC = function(...)
			MsgC(...)
			clfunc.MsgC(steamid, ...)
		end
			
		ENV.hook = setmetatable({
			Add = function(event, name, func)
				hook.Add(event, name, function(...)
					local ok, a, b, c, d, e, f = xpcall(func, function(err)
						local i = 2
						local errmsg = {"[ERROR] ", err, "\n"}
						repeat
							local info = debug.getinfo(i)
							if not info then break end
							
							table.insert(errmsg, string.rep(" ", i))
							table.insert(errmsg, Format("%d. %s - %s:%d\n", i-1, info.name or "unknown", info.short_src, info.currentline))
							i = i+1
						until info.func == func
						table.insert(errmsg, "\n")
						
						errmsg = table.concat(errmsg)
						
						ErrorNoHalt(errmsg)
						clfunc.Msg(steamid, errmsg)
					end, ...)
					
					return a, b, c, d, e, f
				end)
			end;
		}, {__index = hook})
	end
	
	setfenv(func, setmetatable(ENV, {__index = _G, __newindex = _G}))
	
	local cor = coroutine.create(function()
		func()
		return true
	end)
	pendingScripts[cor] = {steamid = steamid, name = name}
	
	RunPendingScripts()
end

if SERVER then

util.AddNetworkString("rlua_data")

util.AddNetworkString("rlua_msg")
util.AddNetworkString("rlua_msgn")
util.AddNetworkString("rlua_msgc")
util.AddNetworkString("rlua_print")

local function NetWriteStringList(strs)
	net.WriteUInt(#strs, 8)
	for _,v in ipairs(strs) do
		net.WriteString(tostring(v))
	end
end

function clfunc.Msg(steamid, ...)
	local pl = PlayersBySteamID[steamid]
	if not pl then return end
	if pl:IsListenServerHost() then return end
	
	net.Start("rlua_msg")
	NetWriteStringList({...})
	net.Send(pl)
end

function clfunc.MsgN(steamid, ...)
	local pl = PlayersBySteamID[steamid]
	if not pl then return end
	if pl:IsListenServerHost() then return end
	
	net.Start("rlua_msgn")
	NetWriteStringList({...})
	net.Send(pl)
end

function clfunc.MsgC(steamid, col, ...)
	local pl = PlayersBySteamID[steamid]
	if not pl then return end
	if pl:IsListenServerHost() then return end
	
	net.Start("rlua_msgc")
	net.WriteUInt(col.r, 8)
	net.WriteUInt(col.g, 8)
	net.WriteUInt(col.b, 8)
	NetWriteStringList({...})
	net.Send(pl)
end

function clfunc.print(steamid, ...)
	local pl = PlayersBySteamID[steamid]
	if not pl then return end
	if pl:IsListenServerHost() then return end
	
	net.Start("rlua_print")
	NetWriteStringList({...})
	net.Send(pl)
end

end

if CLIENT then

local function NetReadStringList()
	local n = net.ReadUInt(8)
	local strs = {}
	for i=1, n do
		strs[i] = net.ReadString()
	end
	return strs
end

net.Receive("rlua_msg", function(len)
	local arg = NetReadStringList()
	MsgC(Color_SV, unpack(arg))
end)

net.Receive("rlua_msgn", function(len)
	local arg = NetReadStringList()
	table.insert(arg, "\n")
	MsgC(Color_SV, unpack(arg))
end)

net.Receive("rlua_msgc", function(len)
	local col = Color(0, 0, 0, 255)
	col.r = net.ReadUInt(8)
	col.g = net.ReadUInt(8)
	col.b = net.ReadUInt(8)
	local arg = NetReadStringList()
	MsgC(col, unpack(arg))
end)

net.Receive("rlua_print", function(len)
--[[
	local arg = NetReadStringList()
	print(unpack(arg))
]]
	local n = net.ReadUInt(8)
	local arg = {}
	for i=1, n do
		table.insert(arg, net.ReadString())
		if i < n then
			table.insert(arg, "\t")
		else
			table.insert(arg, "\n")
		end
	end
	MsgC(Color_SV, unpack(arg))
end)

end

----------------------------------------------------------------------------
-- Sender

local MAXSIZE = 16384
local CurrentMessageID = 0
local OutgoingMessages = {}

local function SendMessage(str, target)
	local data = util.Compress(str)
	local size = #data
	
	local blocks = {}
	for i=1, size, MAXSIZE do
		local sz = math.min(size-i+1, MAXSIZE)
		table.insert(blocks, data:sub(i, i+sz-1))
	end
	
	table.insert(OutgoingMessages, {id=CurrentMessageID, numblocks=#blocks, curblock=0, blocks=blocks, target=target})
	CurrentMessageID = (CurrentMessageID + 1) % 256
end

local function SendScript(side, name, code, steamid, target)
	SendMessage(table.concat{"--[[", side, " ", steamid, " ", name, "]]", code}, target)
end

local MessageSendTimer
hook.Add("Tick", "RLuaSendTick", function()
	local m = OutgoingMessages[1]
	if m then
		if m.curblock >= m.numblocks then
			--MsgN("Done sending message")
			table.remove(OutgoingMessages, 1)
		else
			if MessageSendTimer and MessageSendTimer > 0 then
				MessageSendTimer = MessageSendTimer - FrameTime()
				return
			end
			
			m.curblock = m.curblock + 1
			
			local data = m.blocks[m.curblock]
			local size = #data
			
			net.Start("rlua_data")
			net.WriteUInt(m.id, 8)
			net.WriteUInt(m.numblocks, 16)
			net.WriteUInt(m.curblock, 16)
			net.WriteData(m.blocks[m.curblock], #(m.blocks[m.curblock]))
			--MsgN(Format("[MSG #%d] Sent block %d out of %d (%d bytes)", m.id, m.curblock, m.numblocks, size))
			
			if SERVER then
				if m.target then
					net.Send(m.target)
				else
					net.Broadcast()
				end
			else
				net.SendToServer()
			end
			
			MessageSendTimer = MSGTIME
		end
	end
end)

----------------------------------------------------------------------------
-- Receiver

local IncomingMessages = {}
local ClientLuaCache = {}

local function ProcessMessage(side, name, code, steamid)
	-- Run the script or send it to all clients
	local sent = false
	
	if side == "SV" or side == "SH" then
		if SERVER then
			RunLuaScript(name, code, steamid)
		else
			-- this shouldn't happen
		end
		sent = true
	end
	
	if side == "CL" or side == "SH" then
		if SERVER then
			if name ~= "." then
				-- Client scripts are stored in an ordered fashion (and also by name, for faster retrieval)
				local cache = ClientLuaCache[name]
				if not cache then
					cache = {}
					ClientLuaCache[name] = cache
					table.insert(ClientLuaCache, cache)
				end
				
				cache.name = name
				cache.steamid = steamid
				cache.code = code
			end
			
			SendScript("CL", name, code, steamid)
			clfunc.MsgN(steamid, Format("%s: Sending to clients", name))
			
			if not game.IsDedicated() then
				MsgN(Format("%s: Sending to clients", name))
			end
		else
			RunLuaScript(name, code)
		end
		sent = true
	end
	
	if not sent then
		MsgN(Format("WARNING: Received script with invalid type %q", side))
		clfunc.MsgC(steamid, Color_Err, Format("ERROR: Server received script with invalid type %q! (%s)\n", side, name))
	end
end

local function MessageReceived(msg, pl)
	local side, steamid, name, code = string.match(msg, "^--%[%[(%w+) ([%w_:]+) ([%w%s_/\\.]+)%]%](.*)$")
	if not side or not name then
		if SERVER then
			MsgN(Format("ERROR: Corrupted script received from %s", tostring(pl)))
			clfunc.MsgC(pl, Color_Err, "ERROR: Server received corrupted data!\n")
		else
			MsgN("ERROR: Corrupted script received from server")
		end
		return
	end
	
	name = string.lower(name)
	
	if SERVER and not game.SinglePlayer() and pl:SteamID() ~= steamid then
		MsgN(Format("ERROR: Steam ID mismatch for %s", tostring(pl)))
		clfunc.MsgC(pl:SteamID(), Color_Err, "ERROR: Server received a SteamID that does not match yours!\n")
		return
	end
	
	-- Save the script to the server
	
	if SERVER and game.IsDedicated() and name ~= "." then
	--if SERVER and name ~= "." then
		file.Append("rlua_log.txt", Format("%s | %s | %-20s: %s\n", os.date("%T"), side, pl:GetName(), name))
		local path = string.lower("remote_lua_cache/"..name)
		local dir = string.gsub(path, "/[^/]*$", "/")
		
		path = path..".txt"
		
		file.CreateDir(dir)
		local f = file.Open(path, "w", "DATA")
		if f then
			f:Write(msg)
			f:Close()
		else
			-- Could not create file for some reason, notify both the server and the player who sent it
			MsgN(Format("WARNING: Could not save file %q from %s", name, tostring(pl)))
			clfunc.MsgC(steamid, Color_Err, Format("WARNING: Server could not save file %q!\n", path))
		end
	end
	
	ProcessMessage(side, name, code, steamid)
end

net.Receive("rlua_data", function(len, pl)
	-- Clients can only receive data from the server so there's no need for a player entity
	if CLIENT then
		pl = 1
	end
	
	if SERVER then
		if pl:GetInfo("rlua_password") ~= sv_rlua_password:GetString() then
			clfunc.MsgC(pl, Color_Err, "Invalid Remote Lua password.\n")
			return
		end
	end
	
	local id = net.ReadUInt(8)				-- Message ID
	local numblocks = net.ReadUInt(16)		-- Total number of blocks in this message
	local curblock = net.ReadUInt(16)		-- Position of the current block in this message
	local data = net.ReadData((len-16-16-8)/8)	-- Block data
	
	-- Assign a table of incoming messages to each sender (any message can be identified by its sender and message ID)
	local messages = IncomingMessages[pl]
	if not messages then
		messages = {}
		IncomingMessages[pl] = messages
	end
	
	local m = messages[id]
	if not m then
		-- new message
		m = {received = 0, numblocks = numblocks}
		messages[id] = m
	end
	
	if m.numblocks ~= numblocks then
		-- This shouldn't happen
		ErrorNoHalt("warning, numblocks mismatch")
	end
	
	if not m[curblock] then
		-- Increment the number of blocks received for this message
		m.received = m.received + 1
	else
		-- This shouldn't happen either
		ErrorNoHalt("warning, received more than one block with the same ID and position")
	end
	
	-- Store block data
	m[curblock] = data
	
	-- This is useful for detecting timeouts (if we haven't received any block for this message in a long time)
	m.time = CurTime()
	
	--MsgN(Format("[MSG #%d] Received block %d out of %d (%d bytes)", id, curblock, m.numblocks, #data))
	
	-- All blocks have been received
	if m.received == m.numblocks then
		--MsgN("received all blocks, processing data")
		local code = util.Decompress(table.concat(m))
		messages[id] = nil
		
		MessageReceived(code, pl)
		
		-- todo: send an acknowledgement to the sender?
	end
end)

if SERVER then

-- A player has disconnected, interrupt and clear all of their pending messages
hook.Add("PlayerDisconnected", "RLuaPlayerDisconnect", function(pl)
	if pl.SavedSteamID then
		PlayersBySteamID[pl.SavedSteamID] = nil
	end
	
	IncomingMessages[pl] = nil
end)

-- A player has just joined, send them currently stored remotelua_cl scripts
local function FindPlayerByUniqueID(id)
	for _,v in ipairs(player.GetAll()) do
		if v:UniqueID() == id then
			return v
		end
	end
	
	return NULL
end

hook.Add("PlayerInitialSpawn", "RLuaSendClientFiles", function(pl)
	local steamid = pl:SteamID()
	pl.SavedSteamID = steamid
	PlayersBySteamID[steamid] = pl
	
	for _, cache in ipairs(ClientLuaCache) do
		-- Send the client script to this player
		SendScript("CL", cache.name, cache.code, cache.steamid, pl)
		
		-- Find the player who owns that script
		local owner = PlayersBySteamID[cache.steamid]
		
		-- If the owner is still on the server, notify them
		if IsValid(owner) then
			clfunc.MsgN(owner, Format("%s: Sending to new client %s", cache.name, tostring(pl)))
		end
	end
end)

hook.Add("InitPostEntity", "RLuaAutoLoad", function()
	file.Write("rlua_log.txt", "")
	local files = file.Read("rlua_autoload.txt") or ""
	
	for path in string.gmatch(files.."\n", "(.-)\n") do
		path = string.gsub(string.lower(path), "^%s+", "")
		path = string.gsub(path, "%s+$", "")
		if path ~= "" then
			local msg = file.Read("remote_lua_cache/"..path..".txt")
			local side, steamid, name, code = string.match(msg, "^--%[%[(%w+) ([%w_:]+) ([%w%s_/\\.]+)%]%](.*)$")
			if side and name and steamid then
				ProcessMessage(side, name, code, steamid)
			else
				MsgN(Format("WARNING: Failed to autoload file %q", path))
			end
		end
	end
end)

end


-- Client console commands
if CLIENT then

local rlua_path = CreateClientConVar("rlua_path", "", true, false)
local rlua_password = CreateClientConVar("rlua_password", "", true, true)

local function findExistingFile(name, exts)
	for p in string.gmatch(":"..rlua_path:GetString()..":", "([%w%s_-./\\]*):") do
		if #p > 0 and not string.find(p, "/%s*$") then
			p = p.."/"
		end
		
		for _,e in ipairs(exts) do
			local str = table.concat{p, name, e}
			if file.Exists(str, "DATA") then return str end
		end
	end
	
	return nil
end

local function removeExtension(name)
	return string.gsub(name, "%.%w+$", "")
end

local svFileExts = {"_sv.lua", "_sv.txt", ".lua", ".txt"}
local clFileExts = {"_cl.lua", "_cl.txt", ".lua", ".txt"}

concommand.Add("rlua_run",function(pl,cmd,args,str)
	SendScript("SV", ".", str, pl:SteamID())
end)

concommand.Add("rlua_run_cl",function(pl,cmd,args,str)
	SendScript("CL", ".", str, pl:SteamID())
end)

concommand.Add("remote_lua",function(pl,cmd,args)
	local svfile
	svfile = findExistingFile(args[1], svFileExts)
	
	if svfile then
		SendScript("SV", removeExtension(svfile), file.Read(svfile), pl:SteamID())
	else
		MsgN(Format("ERROR: Serverside script %q not found!", args[1]))
	end
end)

concommand.Add("remote_lua_cl",function(pl,cmd,args)
	local clfile
	clfile = findExistingFile(args[1], clFileExts)
	
	if clfile then
		SendScript("CL", removeExtension(clfile), file.Read(clfile), pl:SteamID())
	else
		MsgN(Format("ERROR: Clientside script %q not found!", args[1]))
	end
end)

concommand.Add("remote_lua_sh",function(pl,cmd,args)
	local svfile, clfile
	
	svfile = findExistingFile(args[1], svFileExts)
	clfile = findExistingFile(args[1], clFileExts)
	
	if svfile and clfile and svfile == clfile then
		SendScript("SH", removeExtension(svfile), file.Read(svfile), pl:SteamID())
		return
	end
	
	if svfile then
		SendScript("SV", removeExtension(svfile), file.Read(svfile), pl:SteamID())
	else
		MsgN(Format("ERROR: Serverside script %q not found!", args[1]))
	end
	
	if clfile then
		SendScript("CL", removeExtension(clfile), file.Read(clfile), pl:SteamID())
	else
		MsgN(Format("ERROR: Clientside script %q not found!", args[1]))
	end
end)

end

