-- this file is meant to be run before client init.lua (the very first lua file run)
-- so we can properly detour all functions we need
-- if u do it after init.lua, you are instantly detected by anticheats.

local gc_start = collectgarbage()
local gc_end

local rawget = _G.rawget
local rawset = _G.rawset

local tableCopy
tableCopy = (table and isfunction(table.Copy) and table.Copy) or function( t, lookup_table )
	if t == nil then return nil end
	lookup_table = lookup_table or {}

	local copy = {}
	setmetatable( copy, debug.getmetatable( t ) )

	lookup_table[t] = copy
	for i, v in pairs( t ) do
		if ( not istable( v ) ) then
			copy[ i ] = v
		else
			if ( lookup_table[ v ] ) then
				copy[ i ] = lookup_table[ v ] -- we already copied this table. reuse the copy.
			else
				copy[ i ] = tableCopy( v, lookup_table ) -- not yet copied. copy it.
			end
		end
	end

	return copy
end

local G = {}
G._G = _G
G.G = G

-- now nothing exists in _G unless I do _G.Var = Value
setmetatable(G, {
	__index = function(t,k)
		local v = rawget(t, k)
		if v ~= nil then
			return v end

		v = rawget(_G, k)
		if v ~= nil then
			-- cache in G after initial get
			t[k] = v

			return v end
	end,
	__newindex = function(t,k,v)
		rawset(t,k,v)
	end
})
setfenv(1, G)

g_GCStart = gc_start

_R = debug.getregistry()
Format = string.format

file = tableCopy(file)
debug = tableCopy(debug)
string = tableCopy(string)

bak = {}
bak.setfenv = setfenv
bak.getfenv = getfenv
bak.CompileString = CompileString
bak.getmetatable = getmetatable
bak.setmetatable = setmetatable
bak.CompileFile = CompileFile
bak.RunString = RunString
bak.string = string
bak.debug = debug
bak.file = file

function file.Read(filename, path)
	if ( path == true ) then path = "GAME" end
	if ( path == nil or path == false ) then path = "DATA" end

	local f = file.Open( filename, "rb", path )
	if ( not f ) then return end

	local str = f:Read( f:Size() )

	f:Close()

	if ( not str ) then str = "" end
	return str
end

-- define in fenv
detours = _G.hn_detours or {
	bak = bak,

	detours_to_original = {},
	detours_to_ours = {},
}

-- Color() doesnt exist
function Color(r,g,b,a)
	if not isnumber(r) then r = 255 end
	if not isnumber(g) then g = 255 end
	if not isnumber(b) then b = 255 end
	if not isnumber(a) then a = 255 end

	return {
		r = r,
		g = g,
		b = b,
		a = a,
	}
end

-- IsValid() doesnt exist
function IsValid( object )
	if not object then return false end

	local isvalid = object.IsValid
	if not isvalid then return false end

	return isvalid(object)
end

-- local & (not always) global
local detours = detours
_G.hn_detours = detours

function detours:Add(original_func, our_detour_func)
	if not (isfunction(original_func) and isfunction(our_detour_func)) then return original_func end

	-- if detouring func is already detoured
	if self.detours_to_ours[original_func] then
		original_func = self.detours_to_original[original_func]
		print("RE DETOUR!?!?!")
	end

	-- if we haven't detoured this already
	local function our_detour_wrapper(...)
		--setfenv(original_func, getfenv(1))
		return our_detour_func(...)
	end

	self.detours_to_original[our_detour_wrapper] = original_func
	self.detours_to_ours[original_func] = our_detour_wrapper

	setfenv(our_detour_wrapper, getfenv(original_func))
	setfenv(our_detour_func, setmetatable({fnOriginal = original_func}, {
		__index = function(t,k)
			local v = rawget(t, k)
			if v ~= nil then
				return v end

			v = rawget(G, k)
			if v ~= nil then
				return v end

			v = rawget(_G, k)
			if v ~= nil then
				return v end
		end,
		__newindex = G
	}))

	return self.detours_to_ours[original_func]
end

-- hiding certain objects and placing a substitute in its place
do
	detours.substituting_objects = {}
	function detours:SubstituteObject(obj, sub)
		sub = sub or {}
		self.substituting_objects[obj] = sub
	end
	function detours:IsSubstitutingObject(obj)
		return self.substituting_objects[obj] ~= nil
	end
	function detours:GetObjectSubstitute(obj)
		return self.substituting_objects[obj]
	end

	--detours:SubstituteObject(G, _G)
	--detours:SubstituteObject(detours, {})
end

detours.hooks = {}
function detours:AddHook(event, id, func)
	if not isfunction(func) then return end
	if not isstring(event) then return end

	self.hooks[event] = self.hooks[event] or {}
	self.hooks[event][id] = func
end
function detours:RemoveHook(event, id)
	if not isstring(event) then return end
	if not self.hooks[event] then return end

	self.hooks[event][id] = nil
end

detours.concommands = {}
detours.concommands_auto = {}
function detours:AddConCommand(name, func, autocomplete_func, help, flags)
	local LowerName = string.lower(name)
	self.concommands[LowerName] = func
	self.concommands_auto[LowerName] = autocomplete_func
	AddConsoleCommand(name, help, flags)
end

COLOR_CLIENT = Color(255, 222, 102)
COLOR_WHITE = Color(255,255,255)
COLOR_BLACK = Color(0,0,0)
COLOR_PRINT = Color(237, 145, 255)
function detours:Msg(...)
	MsgC(COLOR_WHITE, "[", COLOR_PRINT, "HN_DETOURS", COLOR_WHITE, "] ", COLOR_WHITE)
	MsgN(...)
end

DETOURS_NOTIFICATION_FONT = "TargetIDSmall"
DETOURS_NOTIFICATION_SPAWN_TIME = 1
DETOURS_NOTIFICATION_FADE_TIME = 2
DETOURS_NOTIFICATION_TIME = 20
DETOURS_NOTIFICATION_MAX_DRAW = math.huge

detours.notifications = {}
function detours:Notify(text)
	table.insert(self.notifications, {
		text = text,
		time = DETOURS_NOTIFICATION_TIME,
		start_time = RealTime(),
		isrendering = false,
	})
end

local text_cache = {}
function GetTextSizeCached(text)
	if not text_cache[text] then
		surface.SetFont(DETOURS_NOTIFICATION_FONT)
		local w,h = surface.GetTextSize(text)

		text_cache[text] = {w,h}
	end

	return text_cache[text][1], text_cache[text][2]
end

detours:AddHook("HUDPaint", "draw_notifs", function()
	if #detours.notifications < 1 then return end

	local time = RealTime()

	for i = 1, DETOURS_NOTIFICATION_MAX_DRAW do
		local notif = detours.notifications[i]
		if not notif then break end

		if not notif.isrendering then
			notif.isrendering = true
			notif.start_time = time
		end

		surface.SetFont(DETOURS_NOTIFICATION_FONT)
		local iTextW, iTextH = GetTextSizeCached(notif.text)

		local w = iTextW + 30
		local h = iTextH + 8

		local x = 0
		local y = (i-1) * h + ((i - 1) * 3)

		local time_decaying = _G.math.Clamp(time - (notif.start_time + DETOURS_NOTIFICATION_TIME), 0, DETOURS_NOTIFICATION_FADE_TIME)
		local time_spawning = _G.math.Clamp((notif.start_time + DETOURS_NOTIFICATION_SPAWN_TIME) - time, 0, DETOURS_NOTIFICATION_SPAWN_TIME)

		local flDecayPercentage = (time_decaying / DETOURS_NOTIFICATION_FADE_TIME)
		local flSpawnPercentage = (time_spawning / DETOURS_NOTIFICATION_SPAWN_TIME)
		local alpha = 255

		-- time logic
		do
			if flDecayPercentage > 0 then
				alpha = _G.math.Remap(1 - flDecayPercentage, 0, 1, 0, 255)
				x = x - (w * math.ease.OutQuint(flDecayPercentage))
			elseif flSpawnPercentage > 0 then
				x = x - (w * math.ease.InQuint(flSpawnPercentage))
			end
		end

		-- draw
		do
			-- background draw
			surface.SetAlphaMultiplier(alpha / 255)

			surface.SetDrawColor(255,128,128)
			surface.DrawRect(x, y, w, h)

			local bg_w = 8 + (w * math.ease.InOutQuart(flSpawnPercentage))
			surface.SetDrawColor(0,255,0)
			surface.DrawRect(x + w - bg_w, y, bg_w, h)

			surface.SetDrawColor(32,32,32)
			surface.DrawOutlinedRect(x, y, w, h, 1)

			-- draw text
			surface.SetTextColor(255,255,255)
			surface.SetTextPos(x + 16 - bg_w, y + (h / 2) - (iTextH / 2))
			surface.DrawText(notif.text)

			surface.SetAlphaMultiplier(1)
			surface.SetDrawColor(255,255,255)
		end

		if notif.isrendering and alpha <= 1 then
			table.remove(detours.notifications, i)
		end
	end
end)

-- recycling my event listener code from my cheat lol
do
	detours.EventListeners = {}
	detours.m_iCurrentListener = nil
	detours.m_strCurrentEvent = nil
	function detours:On(event, callback)
		self.EventListeners[event] = self.EventListeners[event] or {}
		if not isfunction(callback) then
			print(debug.traceback(1, "event listener callback is not a function!", 2))
			return
		end

		return table.insert(self.EventListeners[event], callback)
	end
	function detours:Emit(event, ...)
		if not self.EventListeners[event] or next(self.EventListeners[event]) == nil then return end

		local rets = {}

		local i = 1
		repeat
			local f = self.EventListeners[event][i]
			self.m_bRemovedEvent = false

			self.m_iCurrentListener = i
			self.m_strCurrentEvent = event

			rets = {xpcall(f, debug.traceback, ...)}

			local bOk = table.remove(rets, 1)
			if not bOk then
				local why = table.remove(rets, 1)
				self:Msg("callback error!", why)
			end

			self.m_iCurrentListener = nil
			self.m_strCurrentEvent = nil

			if self.m_bRemovedEvent then
				continue end

			i = i + 1
		until i > #self.EventListeners[event]

		return unpack(rets)
	end

	-- called inside detours:On() callback
	function detours:RemoveCurrentListener()
		if not self.m_iCurrentListener then return false end

		local tEventListeners = self.EventListeners[self.m_strCurrentEvent]

		if not tEventListeners then return false end
		if not tEventListeners[self.m_iCurrentListener] then return false end

		table.remove(tEventListeners, self.m_iCurrentListener)
		self.m_bRemovedEvent = true
		return true
	end
end

detours:On("NetLoaded", function()
	--MsgC({r = 237,g = 145,b = 255}, "[DETOURS] ", {r = 255,b = 255,g = 255}, "extensions/net.lua LOADED!!!", _G.net.Incoming, "\n")
	--print("NET HAS LOADED!!!!!!", _G.net.Incoming)
	--_G.net.Incoming = detours:Add(_G.net.Incoming, function(...)
	--	MsgC({r = 237,g = 145,b = 255}, "[DETOURS] ", {r = 255,b = 255,g = 255}, "net.Incoming() called\n")
	--	local bytes_left, _bits_left = net.BytesLeft()
	--
	--	local buffer = net.ReadData(bytes_left)
	--
	--	MsgC({r = 237,g = 145,b = 255}, "[DETOURS] ", {r = 255,b = 255,g = 255}, "NET BUFFER = ", buffer, #buffer / 4, "\n")
	--
	--	return fnOriginal(...)
	--end)
end)
detours:On("HookLoaded", function()
	detours:Notify("detoured hook library")
	Hooks = _G.hook.GetTable()

	_G.hook.Add = detours:Add(_G.hook.Add, function(event, id, callback)
		--if isstring(id) and not id:find("ULib") then
		--	detours:Notify(Format("added hook %s.%s", event,id))
		--	print("hook.add", event,id,callback)
		--end

		fnOriginal(event,id,callback)
	end)
	_G.hook.Call = detours:Add(_G.hook.Call, function(name, gm, ...)
		-- return values
		local a, b, c, d, e, f

		-- Run detour's hooks
		local HookTable = detours.hooks[name]
		if HookTable ~= nil then
			for k, v in next, HookTable do
				if isstring( k ) then
					-- If it's a string, it's cool
					a, b, c, d, e, f = v( ... )

				else
					-- If the key isn't a string - we assume it to be an entity
					-- Or panel, or something else that IsValid works on.
					if IsValid( k ) then
						-- If the object is valid - pass it as the first argument (self)
						a, b, c, d, e, f = v( k, ... )
					else
						-- If the object has become invalid - remove it
						HookTable[ k ] = nil
					end
				end

				-- Hook returned a value - it overrides the gamemode function
				if a ~= nil then
					return a, b, c, d, e, f
				end
			end
		end

		-- Run default hooks
		HookTable = Hooks[ name ]
		if HookTable ~= nil then
			for k, v in next, HookTable do
				if isstring( k ) then
					-- If it's a string, it's cool
					a, b, c, d, e, f = v( ... )
				else
					-- If the key isn't a string - we assume it to be an entity
					-- Or panel, or something else that IsValid works on.
					if IsValid( k ) then
						-- If the object is valid - pass it as the first argument (self)
						a, b, c, d, e, f = v( k, ... )
					else
						-- If the object has become invalid - remove it
						HookTable[ k ] = nil
					end
				end

				-- Hook returned a value - it overrides the gamemode function
				if a ~= nil then
					return a, b, c, d, e, f
				end
			end
		end

		-- Call the gamemode function
		if not gm then return end

		local GamemodeFunction = gm[ name ]
		if GamemodeFunction == nil then return end

		return GamemodeFunction( gm, ... )
	end)
end)
detours:On("ConCommandLoaded", function()
	detours:Notify("detoured concommand library")
	ConCommands, CommandsAutoCompleteList = _G.concommand.GetTable()

	_G.concommand.Run = detours:Add(_G.concommand.Add, function(ply, command, arguments, args)
		local LowerCommand = string.lower( command )

		if detours.concommands[LowerCommand] ~= nil then
			detours.concommands[LowerCommand](ply, command, arguments, args)
		end

		if ConCommands[LowerCommand] ~= nil then
			ConCommands[LowerCommand](ply, command, arguments, args)
			return true
		end

		Msg("Unknown command: " .. command .. "\n")

		return false
	end)
	_G.concommand.AutoComplete = detours:Add(_G.concommand.AutoComplete, function(command, arguments )
		local LowerCommand = string.lower( command )

		if detours.concommands_auto[LowerCommand] ~= nil then
			return detours.concommands_auto[LowerCommand](command, arguments)
		end

		if CommandsAutoCompleteList[ LowerCommand ] ~= nil then
			return CommandsAutoCompleteList[LowerCommand](command, arguments)
		end

	end)
end)
detours:On("SwiftACLoaded", function()
	print("LOADED SWIFT AC")
	timer.Simple(1, function()
		local obj = hook.GetTable().Think.k
		print("does it exist?!?!", obj ~= nil)

		if not obj then
			print("late remove")
			timer.Simple(0, function()
				hook.Remove("Think", "k")
			end)
		else
			print("on time remove")
			hook.Remove("Think", "k")
		end
	end)
end)

-- prevent leaking our internals
--[=[do
	_G.getfenv = detours:Add(_G.getfenv, function(func_or_level)
		return fnOriginal(func_or_level)
		-- prevent leaking our sexy fenv
		--if isnumber(func_or_level) then
		--	if func_or_level <= 0 then
		--		func_or_level = 1
		--	end
		--
		--	func_or_level = 2 + func_or_level
		--end
		--
		---- getfenv on our stored detour functions
		--if detours.detours_to_original[func_or_level] then
		--	return fnOriginal(detours.detours_to_original[func_or_level])
		--end
		--
		--local fenv = fnOriginal(func_or_level)
		--
		--if detours:IsSubstitutingObject(fenv) then
		--	return detours:GetObjectSubstitute(fenv)
		--end
		--
		--return fenv
	end)
	_G.setfenv = detours:Add(_G.setfenv, function(func_or_level, fenv)
		return fnOriginal(func_or_level, fenv)
	end)
end]=]

MsgC(COLOR_PRINT, "spectre loaded!\n")

local files,_ = file.Find("lua/spectre/*", "GAME")
for i, f in ipairs(files) do
	local content = file.Read(Format("lua/spectre/%s", f), "GAME") or ""
	local func = CompileString(content, f, false)

	if isfunction(func) then
		setfenv(func, G)
		local ok, why = pcall(func)
		if not ok then
			print("run err", why)
		else
			print("load", f)
		end
	else
		print("load err", func)
	end
end

gc_end = collectgarbage("count")
g_GCEnd = gc_end