-- DISABLE_CODE_RESULT
local type { Entity, ClientsideModel, CompileString, isentity, istable, isstring } = import("~/nattlua/definitions/glua.nlua")

local type CLIENT = false
local type SERVER = true
local luadev = {}
type luadev.@Name = "luadev"
local Tag = "luadev"

--net_retdata = Tag..'_retdata'
if SERVER then
	util.AddNetworkString(Tag)
--util.AddNetworkString(net_retdata)
end

do -- enums
	local enums = {
		TO_CLIENTS = 1,
		TO_CLIENT = 2,
		TO_SERVER = 3,
		TO_SHARED = 4,
	}
	local revenums = {} -- lookup
	luadev.revenums = revenums

	for k, v in pairs(enums) do
		luadev[k] = v
		revenums[v] = k
	end

	luadev.STAGE_PREPROCESS = 1
	luadev.STAGE_COMPILED = 2
	luadev.STAGE_POST = 3
	luadev.STAGE_PREPROCESSING = 4
	luadev.STAGE_PRE = 5
end

do -- helpers
	function luadev.MakeExtras(pl, tbl)
		tbl = tbl or {}

		if pl and isentity(pl) and pl:IsPlayer() then tbl.ply = pl end

		return tbl
	end

	function luadev.TransmitHook(stage, ...)
		return hook.Run("LuaDevTransmit", stage, ...)
	end

	function luadev.IsOneLiner(script)
		return script and not script:find("\n", 1, true)
	end

	function luadev.GiveFileContent(fullpath, searchpath)
		--luadev.Print("Reading: "..tostring(fullpath))
		if fullpath == nil or fullpath == "" then return false end

		local content = file.Read(fullpath, searchpath or "MOD")

		if content == 0 then return false end

		return content
	end

	function luadev.TableToString(tbl)
		return string.Implode(" ", tbl)
	end

	function luadev.Print(...)
		Msg("[Luadev" .. (SERVER and " Server" or "") .. "] ")
		print(...)
	end

	if CLIENT then
		local store = CreateClientConVar("luadev_store", "1", true)

		function luadev.ShouldStore()
			return store:GetBool()
		end
	end

	if CLIENT then
		luadev.verbose = CreateClientConVar("luadev.verbose", "1", true)
	else
		luadev.verbose = CreateConVar("luadev.verbose", "0", {FCVAR_NOTIFY, FCVAR_ARCHIVE})
	end

	function luadev.Verbose(lev)
		return (luadev.verbose:GetInt() or 99) >= (lev or 1)
	end

	function luadev.FindPlayer(plyid)
		if not plyid or not isstring(plyid) then return end

		local cl

		for _, v in pairs(player.GetAll()) do
			if v:SteamID() == plyid or v:UniqueID() == plyid or tostring(v:UserID()) == plyid then
				cl = v

				break
			end
		end

		if not cl then
			for _, v in pairs(player.GetAll()) do
				if v:Name():lower():find(plyid:lower(), 1, true) == 1 then
					cl = v

					break
				end
			end
		end

		if not cl then
			for _, v in pairs(player.GetAll()) do
				if string.find(v:Name(), plyid) then
					cl = v

					break
				end
			end
		end

		if not cl then
			for _, v in pairs(player.GetAll()) do
				if v:Name():lower():find(plyid:lower(), 1, true) then
					cl = v

					break
				end
			end
		end

		return IsValid(cl) and cl or nil
	end
end

function luadev.Compress(data)
	return util.Compress(data)
end

function luadev.Decompress(data)
	return util.Decompress(data)
end

function luadev.WriteCompressed(data)
	if #data == 0 then
		net.WriteUInt(0, 24)
		return false
	end

	local compressed = luadev.Compress(data)
	local len = compressed:len()
	net.WriteUInt(len, 24)
	net.WriteData(compressed, len)
	return compressed
end

function luadev.ReadCompressed()
	local len = net.ReadUInt(24)

	if len == 0 then return "" end

	return luadev.Decompress(net.ReadData(len))
end

-- Compiler / runner
function luadev.ValidScript(src, chunkname)
	local ret = CompileString(src, chunkname or "", false)

	if type(ret) == "string" then return nil, ret end

	return ret or true
end

local mt = {
	__tostring = function(self)
		return self[1]
	end,
	__index = {
		set = function(self, what)
			self[1] = what
		end,
		get = function(self)
			return self[1]
		end,
	},
--__newindex=function(self,what) rawset(self,1,what) end,
}
local strobj = setmetatable({""}, mt)

function luadev.Run(script, chunkname, extra)
	chunkname = chunkname or "??ANONYMOUS??"
	extra = extra or {}
	extra.args = extra.args or {}

	do -- luadev.STAGE_PREPROCESS
		local ret, new_chunkname = hook.Run("LuaDevProcess", luadev.STAGE_PREPROCESS, script, chunkname, extra)

		if ret == false then return true end

		if ret ~= nil and ret ~= true then script = ret end

		if new_chunkname then chunkname = new_chunkname end
	end

	do -- luadev.STAGE_PREPROCESSING
		rawset(strobj, 1, script)
		hook.Run("LuaDevProcess", luadev.STAGE_PREPROCESSING, script, chunkname, extra)
		script = rawget(strobj, 1)

		if not script then return false, "no script" end
	end

	-- Compiling
	local func = CompileString(script, chunkname, false)

	if not func or isstring(func) then
		luadev.compileerr = func or true
		func = false
	end

	do -- luadev.STAGE_COMPILED
		local ret = hook.Run("LuaDevProcess", luadev.STAGE_COMPILED, script, chunkname, extra, func)

		-- replace function
		if ret == false then return true end

		if ret ~= nil and isfunction(ret) then
			func = ret
			luadev.compileerr = false
		end
	end

	if not func then
		if luadev.compileerr then
			return false, "Syntax error: " .. tostring(luadev.compileerr)
		end
	end

	luadev.lastextra = extra
	luadev.lastinfo = chunkname
	luadev.lastscript = script
	luadev.lastfunc = func
	-- luadev.Run the stuff
	-- because garry's runstring has social engineer sexploits and such
	local errormessage

	local function LUADEV_TRACEBACK(errmsg)
		errormessage = errmsg
		local tracestr = debug.traceback(errmsg, 2)
		-- Tidy up the damn long trace
		local p1 = tracestr:find("LUADEV_EXECUTE_FUNCTION", 1, true)

		if p1 then
			local p2 = 0

			while p2 and p2 < p1 do
				local new = tracestr:find("\n", p2 + 1, true)

				if new and new > p1 then
					tracestr = tracestr:sub(1, new)

					break
				end

				p2 = new
			end
		end

		ErrorNoHalt("[ERROR] " .. tracestr) --   ..'\n')
	end

	hook.Run("LuaDevProcess", luadev.STAGE_PRE, script, chunkname, extra, func)
	local LUADEV_EXECUTE_FUNCTION = xpcall
	local returnvals = {LUADEV_EXECUTE_FUNCTION(func, LUADEV_TRACEBACK, unpack(extra.args))}
	local ok = returnvals[1]
	table.remove(returnvals, 1)
	-- luadev.STAGE_POST
	hook.Run(
		"LuaDevProcess",
		luadev.STAGE_POST,
		script,
		chunkname,
		extra,
		func,
		args,
		ok,
		returnvals
	)

	if not ok then return false, errormessage end

	return ok, returnvals
end

function luadev.RealFilePath(name)
	local searchpath = "MOD"
	local RelativePath = "lua/" .. name

	if name:find("^lua/") then -- search cache
		name = name:gsub("^lua/", "")
		RelativePath = name
		searchpath = "LUA"
	elseif name:find("^%.%./") then -- whole shit
		name = name:gsub("^%.%./", "")
		RelativePath = name
	elseif name:find("^data/") then -- whatever
		name = name:gsub("^data/", "")
		RelativePath = "data/" .. name
	end

	if not file.Exists(RelativePath, searchpath) then return nil end

	return RelativePath, searchpath
end

function luadev.AutoComplete(_, commandName, args)
	local name = string.Explode(" ", args)
	name = name[#name] or ""
	local path = string.GetPathFromFilename(name)
	local searchpath = "MOD"
	local RelativePath = "lua/" .. (name or "")

	if name:find("^lua/") then -- search cache
		name = name:gsub("^lua/", "")
		RelativePath = name
		searchpath = "LUA"
	elseif name:find("^%.%./") then -- whole shit
		name = name:gsub("^%.%./", "")
		RelativePath = name
	elseif name:find("^data/") then -- whatever
		name = name:gsub("^data/", "")
		RelativePath = "data/" .. name
	end

	local searchstr = RelativePath .. "*"
	local files, folders = file.Find(searchstr, searchpath or "MOD")
	files = files or {}
	folders = folders or {}

	for _, v in pairs(folders) do
		table.insert(files, v)
	end

	local candidates = files
	candidates = candidates or {}

	for i, _ in pairs(candidates) do
		candidates[i] = commandName .. " " .. path .. candidates[i]
	end

	return candidates
end

local sv_allowcslua = GetConVar("sv_allowcslua")

function luadev.CanLuaDev(ply, script, command, target, target_ply, extra)
	local ret, x = hook.Run("luadev.CanLuaDev", ply, script, command, target, target_ply, extra)

	if ret ~= nil then return ret, x end

	ret, x = hook.Run("LuaDevIsPlayerAllowed", ply, script or "")

	if ret ~= nil then return ret, x end

	if ply:IsSuperAdmin() then return true end

	if
		target == luadev.TO_CLIENT and
		(
			target_ply == ply or
			(
				target_ply and
				istable(target_ply) and
				target_ply[1] == ply and
				table.Count(target_ply) == 1
			)
		)
	then
		if sv_allowcslua:GetBool() then return true end
	end
end

if SERVER then
	function luadev.RejectCommand(pl, x)
		luadev.S2C(pl, "No Access" .. (x and (": " .. tostring(x)) or ""))
	end
end

function luadev.COMMAND(str, func, complete)
	if SERVER then
		if not game.IsDedicated() then return end

		concommand.Add("lua_" .. str, function(pl: Entity, command: string, cmds: {[number] = string}, strcmd: string)
			local id = pl

			if IsValid(pl) then
				local ok, err = luadev.CanLuaDev(pl, strcmd, command, nil, nil, nil)

				if not ok then return luadev.RejectCommand(pl, err or command) end

				id = luadev.GetPlayerIdentifier(pl, str) or pl
			else
				pl = "Console"
				id = pl
			end

			func(pl, cmds, strcmd, id)
		end)
	else
		concommand.Add("lua_" .. str, function(pl, _, cmds, strcmd)
			func(pl, cmds, strcmd, str)
		end) --,(not complete and function(...) return luadev.AutoComplete(str,...) end) or nil) TODO
	end
end

if SERVER then
	function luadev.S2C(cl, msg)
		if cl and cl:IsValid() and cl:IsPlayer() then
			cl:ChatPrint("[LuaDev] " .. tostring(msg))
		end
	end

	function luadev.RunOnClients(script, chunkname, extra)
		local data = {
			--src=script,
			chunkname = chunkname,
			extra = extra,
		}
		net.Start(Tag)
		luadev.WriteCompressed(script)
		net.WriteTable(data)

		if net.BytesWritten() == 65536 then return nil, "too big" end

		net.Broadcast()
		return true
	end

	function luadev.RunOnClient(script, targets, chunkname, extra)
		local data = {
			chunkname = chunkname,
			extra = extra,
		}

		if not istable(targets) then targets = {targets} end

		for i = #targets, 1, -1 do
			if not IsValid(targets[i]) then table.remove(targets, i) end
		end

		if table.Count(targets) == 0 then return false, "no players" end

		net.Start(Tag)
		luadev.WriteCompressed(script)
		net.WriteTable(data)

		if net.BytesWritten() == 65536 then return false, "too big" end

		net.Send(targets)
		return #targets, targets
	end

	function luadev.RunOnServer(script: string, chunkname: string, extra: any)
		return luadev.Run(script, chunkname, extra)
	end

	function luadev.RunOnSelf(script: string, chunkname: string, extra: any)
		return luadev.RunOnServer(script, chunkname, extra)
	end

	function luadev.RunOnShared(...)
		luadev.RunOnClients(...)
		return luadev.RunOnServer(...)
	end

	function luadev.GetPlayerIdentifier(ply, extrainfo)
		if type(ply) == "Player" then
			local info = ply:Name()

			if luadev.Verbose(3) then
				local sid = ply:SteamID():gsub("^STEAM_", "")
				info = ("<%s|%s>"):format(sid, info:sub(1, 24))
			elseif luadev.Verbose(2) then
				info = ply:SteamID():gsub("^STEAM_", "")
			end

			if extrainfo then info = ("%s<%s>"):format(info, tostring(extrainfo)) end

			info = info:gsub("%]", "}"):gsub("%[", "{"):gsub("%z", "_") -- GMod bug
			return info
		else
			return "??" .. tostring(ply)
		end
	end

	local function log_script(script, ply, where, identifier, targets)
		if IsEntity(targets) then targets = {targets} end

		hook.Run("LuaDevRunScript", script, ply, where, identifier, targets)
		luadev.Print(ply, "running", identifier, "on", where)

		if luadev.Verbose() then
			luadev.Print(script)
			luadev.Print("\n\n")
			luadev.Print("#" .. #script:Split("\n") .. "lines")
			luadev.Print("#" .. #script .. "bytes")
		end

		if targets then for _, v in pairs(targets) do
			luadev.Print(v)
		end end
	end

	function luadev._ReceivedData(_, ply)
		local script = luadev.ReadCompressed()
		local decoded = net.ReadTable()
		decoded.chunkname = decoded.chunkname or "no chunkname"
		local extra = decoded.extra or {}
		extra.sender = extra.sender or ply

		if not luadev.CanLuaDev(ply, script, nil, target, decoded.dst_ply, extra) then
			return luadev.RejectCommand(ply)
		end

		local identifier = luadev.GetPlayerIdentifier(ply, decoded.chunkname)
		local where
		local ok, err

		if decoded.dst == luadev.TO_SERVER then
			ok, err = luadev.RunOnServer(script, identifier, extra)
			where = "server"
		elseif decoded.dst == luadev.TO_CLIENT then
			ok, err = luadev.RunOnClient(script, decoded.dst_ply, identifier, extra)
			where = "client"
		elseif decoded.dst == luadev.TO_CLIENTS then
			ok, err = luadev.RunOnClients(script, identifier, extra)
			where = "clients"
		elseif decoded.dst == luadev.TO_SHARED then
			ok, err = luadev.RunOnShared(script, identifier, extra)
			where = "shared"
		else
			luadev.S2C(ply, "Unknown target")
		end

		log_script(script, ply, where, identifier, decoded.dst_ply)

		-- no callback system yet
		if not ok then ErrorNoHalt(tostring(err) .. "\n") end
	end

	net.Receive(Tag, function(...)
		luadev._ReceivedData(...)
	end)
end

function luadev.RepeatLastCommand()
	if luadev.last_run then luadev.last_run.func(unpack(luadev.last_run.args)) end
end

function luadev.GetLastRunPath()
	if luadev.last_run then
		return luadev.last_run.path, luadev.last_run.searchpath
	end
end

function luadev.AddCommands()
	local function CMD(chunkname)
		return CLIENT and "CMD" or chunkname or "CMD"
	end

	luadev.COMMAND(
		"run_sv",
		function(ply, _, script, chunkname)
			chunkname = CMD(chunkname)
			luadev.RunOnServer(script, chunkname, luadev.MakeExtras(ply))
		end,
		true
	)

	luadev.COMMAND(
		"run_sh",
		function(ply, _, script, chunkname)
			chunkname = CMD(chunkname)
			luadev.RunOnShared(script, chunkname, luadev.MakeExtras(ply))
		end,
		true
	)

	luadev.COMMAND(
		"run_clients",
		function(ply, _, script, chunkname)
			chunkname = CMD(chunkname)
			luadev.RunOnClients(script, chunkname, luadev.MakeExtras(ply))
		end,
		true
	)

	luadev.COMMAND(
		"run_self",
		function(ply, _, script, chunkname)
			chunkname = CMD(chunkname)
			luadev.RunOnSelf(script, chunkname, luadev.MakeExtras(ply))
		end,
		true
	)

	luadev.COMMAND("run_client", function(ply, tbl, script, chunkname)
		if not tbl[1] or not tbl[2] then
			luadev.Print("Syntax: lua_run_client (steamid/userid/uniqueid/part of name) script")
			return
		end

		local cl = luadev.FindPlayer(tbl[1])

		if not cl then
			luadev.Print("Client not found!\n")
			return
		end

		if CLIENT then luadev.Print("Running script on " .. tostring(cl:Name())) end

		local _, e = script:find("^%s*\"[^\"]+")

		if e then
			script = script:sub(e + 2)
		else
			local _, e = script:find("^%s*[^%s]+%s")

			if not e then
				luadev.Print("Invalid Command syntax.")
				return
			end

			script = script:sub(e)
		end

		script = script:Trim()
		luadev.RunOnClient(script, cl, CMD(chunkname), luadev.MakeExtras(ply))
	end)

	luadev.COMMAND("send_cl", function(ply, tbl, _, chunkname)
		if not tbl[1] or not tbl[2] then
			luadev.Print("Syntax: lua_send_cl (steamid/userid/uniqueid/part of name) \"path\"")
			return
		end

		local cl = luadev.FindPlayer(tbl[1])

		if not cl then
			luadev.Print("Client not found!\n")
			return
		end

		luadev.Print("Running script on " .. tostring(cl:Name()))
		table.remove(tbl, 1)
		local path = luadev.TableToString(tbl)
		local Path, searchpath = luadev.RealFilePath(path)

		if not Path then
			luadev.Print("Could not find the file\n")
			return
		end

		local content = Path and luadev.GiveFileContent(Path, searchpath)

		if not content then
			luadev.Print("Could not read the file\n")
			return
		end

		chunkname = chunkname or CMD(chunkname)
		luadev.RunOnClient(content, cl, chunkname, luadev.MakeExtras(ply))
	end)

	local function handle(
		ply,
		c,
		func: function=(script: string, chunkname: string, extra: any)>(boolean, nil | string)
	)
		local Path, searchpath = luadev.RealFilePath(c[2] and luadev.TableToString(c) or c[1])

		if not Path then
			luadev.Print("Could not find the file\n")
			return
		end

		local content = Path and luadev.GiveFileContent(Path, searchpath)

		if not content then
			luadev.Print("Could not read the file\n")
			return
		end

		local chunkname = string.GetFileFromFilename(Path)
		chunkname = chunkname or CMD(chunkname)

		if content then
			luadev.last_run = {func = handle, args = {ply, c, func}, path = Path, searchpath = searchpath}
		end

		func(content, chunkname, luadev.MakeExtras(ply))
	end

	luadev.COMMAND("send_sv", function(ply, c)
		handle(ply, c, luadev.RunOnServer)
	end)

	luadev.COMMAND("send_clients", function(ply, c)
		handle(ply, c, luadev.RunOnClients)
	end)

	luadev.COMMAND("send_sh", function(ply, c)
		handle(ply, c, luadev.RunOnShared)
	end)

	luadev.COMMAND("send_self", function(ply, c)
		handle(ply, c, luadev.RunOnSelf)
	end)

	luadev.COMMAND("send_self_now", function(ply, c)
		handle(ply, c, luadev.RunOnSelfNow)
	end)
end

if CLIENT then
	net.Receive(Tag, function(...)
		luadev._ReceivedData(...)
	end)

	function luadev._ReceivedData()
		local script = luadev.ReadCompressed()
		local decoded = net.ReadTable()
		local ok, ret = luadev.Run(script, decoded.chunkname, decoded.extra)

		if not ok then ErrorNoHalt(tostring(ret) .. "\n") end
	end

	function luadev.CheckStore(src: string)
		if not luadev.ShouldStore() then return end

		local crc = util.CRC(src)
		local path = "luadev_hist/" .. crc .. ".txt"

		if file.Exists(path, "DATA") then return end

		if not file.IsDir("luadev_hist", "DATA") then
			file.CreateDir("luadev_hist", "DATA")
		end

		file.Write(path, tostring(src), "DATA")
	end

	function luadev.ToServer(data)
		if luadev.TransmitHook(data) ~= nil then return false end

		luadev.CheckStore(data.src)
		net.Start(Tag)
		luadev.WriteCompressed(data.src or "")
		net.WriteTable(data)

		if net.BytesWritten() == 65536 then
			luadev.Print("Unable to send lua code (too big)\n")
			return nil, "Unable to send lua code (too big)"
		end

		net.SendToServer()
		return true
	end

	function luadev.RunOnClients(script: string, chunkname: string, extra)
		local data = {
			src = script,
			dst = luadev.TO_CLIENTS,
			chunkname = chunkname,
			extra = extra,
		}
		return luadev.ToServer(data)
	end

	function luadev.RunOnSelf(script: string, chunkname: string, extra)
		return luadev.RunOnClient(script, LocalPlayer(), chunkname, extra)
	end

	function luadev.RunOnSelfNow(script: string, chunkname: string, extra)
		return luadev.Run(script, chunkname, extra)
	end

	function luadev.RunOnClient(script: string, targets: {[number] = Entity}, chunkname: string, extra)
		local data = {
			src = script,
			dst = luadev.TO_CLIENT,
			dst_ply = targets,
			chunkname = chunkname,
			extra = extra,
		}
		return luadev.ToServer(data)
	end

	function luadev.RunOnServer(script: string, chunkname: string, extra)
		local data = {
			src = script,
			dst = luadev.TO_SERVER,
			--dst_ply=pl
			chunkname = chunkname,
			extra = extra,
		}
		return luadev.ToServer(data)
	end

	function luadev.RunOnShared(script: string, chunkname: string, extra)
		local data = {
			src = script,
			dst = luadev.TO_SHARED,
			--dst_ply=pl
			chunkname = chunkname,
			extra = extra,
		}
		return luadev.ToServer(data)
	end
end

return luadev
