local type = type
local error = error
local pairs = pairs
local rawget = rawget
local tonumber = tonumber
local getmetatable = getmetatable
local setmetatable = setmetatable

local debug = debug
local debug_getinfo = debug.getinfo

local table_pack = table.pack
local table_unpack = table.unpack
local table_insert = table.insert

local coroutine_yield = coroutine.yield
local coroutine_running = coroutine.running

--[[ Custom extensions --]]
local msgpack = msgpack
local msgpack_pack = msgpack.pack
local msgpack_unpack = msgpack.unpack
local msgpack_pack_args = msgpack.pack_args

local Citizen = Citizen
local Citizen_InvokeFunctionReference = Citizen.InvokeFunctionReference

local isDuplicityVersion = IsDuplicityVersion()

-- setup msgpack compat
msgpack.set_string('string_compat')
msgpack.set_integer('unsigned')
msgpack.set_array('without_hole')
msgpack.setoption('empty_table_as_array', true)

-- setup json compat
json.version = json._VERSION -- Version compatibility
json.setoption("empty_table_as_array", true)
json.setoption('with_hole', true)

-- temp
local _in = Citizen.InvokeNative

local function FormatStackTrace()
	return _in(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString())
end

local boundaryIdx = 1

local function dummyUseBoundary(idx)
	return nil
end

local function getBoundaryFunc(bfn, bid)
	return function(fn, ...)
		local boundary = bid
		if not boundary then
			boundary = boundaryIdx + 1
			boundaryIdx = boundary
		end

		bfn(boundary, coroutine_running())

		local wrap = function(...)
			dummyUseBoundary(boundary)

			local v = table_pack(fn(...))
			return table_unpack(v)
		end

		local v = table_pack(wrap(...))

		bfn(boundary, nil)

		return table_unpack(v)
	end
end

local runWithBoundaryStart = getBoundaryFunc(Citizen.SubmitBoundaryStart)
local runWithBoundaryEnd = getBoundaryFunc(Citizen.SubmitBoundaryEnd)

local AwaitSentinel = Citizen.AwaitSentinel()
Citizen.AwaitSentinel = nil

function Citizen.Await(promise)
	local coro = coroutine_running()
	assert(coro, "Current execution context is not in the scheduler, you should use CreateThread / SetTimeout or Event system (AddEventHandler) to be able to Await")

	if promise.state == 0 then
		local reattach = coroutine_yield(AwaitSentinel)
		promise:next(reattach, reattach)
		coroutine_yield()
	end

	if promise.state == 2 or promise.state == 4 then
		error(promise.value, 2)
	end

	return promise.value
end

Citizen.SetBoundaryRoutine(function(f)
	boundaryIdx = boundaryIdx + 1

	local bid = boundaryIdx
	return bid, function()
		return runWithBoundaryStart(f, bid)
	end
end)

-- root-level alias (to prevent people from calling the game's function accidentally)
Wait = Citizen.Wait
CreateThread = Citizen.CreateThread
SetTimeout = Citizen.SetTimeout
ClearTimeout = Citizen.ClearTimeout

--[[

	Event handling

]]

local eventHandlers = {}

Citizen.SetEventRoutine(function(eventName, eventPayload, eventSource)
	-- set the event source
	local lastSource = _G.source
	_G.source = eventSource

	-- try finding an event handler for the event
	local eventHandlerEntry = eventHandlers[eventName]

	-- deserialize the event structure (so that we end up adding references to delete later on)
	local data = msgpack_unpack(eventPayload)

	if eventHandlerEntry and eventHandlerEntry.handlers then
		-- if this is a net event and we don't allow this event to be triggered from the network, return
		if eventSource:sub(1, 3) == 'net' then
			if not eventHandlerEntry.safeForNet then
				Citizen.Trace('event ' .. eventName .. " was not safe for net\n")

				_G.source = lastSource
				return
			end

			_G.source = tonumber(eventSource:sub(5))
		elseif isDuplicityVersion and eventSource:sub(1, 12) == 'internal-net' then
			_G.source = tonumber(eventSource:sub(14))
		end

		-- return an empty table if the data is nil
		if not data then
			data = {}
		end


		-- if this is a table...
		if type(data) == 'table' then
			-- loop through all the event handlers
			for _, handler in pairs(eventHandlerEntry.handlers) do
				local handlerFn = handler
				local handlerMT = getmetatable(handlerFn)

				if handlerMT and handlerMT.__call then
					handlerFn = handlerMT.__call
				end

				if type(handlerFn) == 'function' then
					local di = debug_getinfo(handlerFn)

					Citizen.CreateThreadNow(function()
						handler(table_unpack(data))
					end, ('event %s [%s[%d..%d]]'):format(eventName, di.short_src, di.linedefined, di.lastlinedefined))
				end
			end
		end
	end

	_G.source = lastSource
end)

local stackTraceBoundaryIdx

Citizen.SetStackTraceRoutine(function(bs, ts, be, te)
	if not ts then
		ts = runningThread
	end

	local t
	local n = 0

	local frames = {}
	local skip = false

	if bs then
		skip = true
	end

	repeat
		if ts then
			t = debug_getinfo(ts, n, 'nlfS')
		else
			t = debug_getinfo(n + 1, 'nlfS')
		end

		if t then
			if t.name == 'wrap' and t.source == '@citizen:/scripting/lua/scheduler.lua' then
				if not stackTraceBoundaryIdx then
					local b
					local u = 1

					repeat
						b = debug.getupvalue(t.func, u)

						if b == 'boundary' then
							break
						end

						u = u + 1
					until not b

					stackTraceBoundaryIdx = u
				end

				local _, boundary = debug.getupvalue(t.func, stackTraceBoundaryIdx)

				if boundary == bs then
					skip = false
				end

				if boundary == be then
					break
				end
			end

			if not skip then
				if t.source and t.source:sub(1, 1) ~= '=' and t.source:sub(1, 10) ~= '@citizen:/' then
					table_insert(frames, {
						file = t.source:sub(2),
						line = t.currentline,
						name = t.name or '[global chunk]'
					})
				end
			end

			n = n + 1
		end
	until not t

	return msgpack_pack(frames)
end)

local eventKey = 10

function AddEventHandler(eventName, eventRoutine)
	local tableEntry = eventHandlers[eventName]

	if not tableEntry then
		tableEntry = { }

		eventHandlers[eventName] = tableEntry
	end

	if not tableEntry.handlers then
		tableEntry.handlers = { }
	end

	eventKey = eventKey + 1
	tableEntry.handlers[eventKey] = eventRoutine

	RegisterResourceAsEventHandler(eventName)

	return {
		key = eventKey,
		name = eventName
	}
end

function RemoveEventHandler(eventData)
	if not eventData or not eventData.key or not eventData.name then
		error('Invalid event data passed to RemoveEventHandler()', 2)
	end

	-- remove the entry
	eventHandlers[eventData.name].handlers[eventData.key] = nil
end

local ignoreNetEvent = {
	['__cfx_internal:commandFallback'] = true,
}

function RegisterNetEvent(eventName, cb)
	if not ignoreNetEvent[eventName] then
		local tableEntry = eventHandlers[eventName]

		if not tableEntry then
			tableEntry = { }

			eventHandlers[eventName] = tableEntry
		end

		tableEntry.safeForNet = true
	end

	if cb then
		return AddEventHandler(eventName, cb)
	end
end

function TriggerEvent(eventName, ...)
	local payload = msgpack_pack_args(...)

	return runWithBoundaryEnd(function()
		return TriggerEventInternal(eventName, payload, payload:len())
	end)
end

if isDuplicityVersion then
	function TriggerClientEvent(eventName, playerId, ...)
		local payload = msgpack_pack_args(...)

		return TriggerClientEventInternal(eventName, playerId, payload, payload:len())
	end

	function TriggerLatentClientEvent(eventName, playerId, bps, ...)
		local payload = msgpack_pack_args(...)

		return TriggerLatentClientEventInternal(eventName, playerId, payload, payload:len(), tonumber(bps))
	end

	RegisterServerEvent = RegisterNetEvent
	RconPrint = Citizen.Trace
	GetPlayerEP = GetPlayerEndpoint
	RconLog = function() end

	function GetPlayerIdentifiers(player)
		local numIds = GetNumPlayerIdentifiers(player)
		local t = {}

		for i = 0, numIds - 1 do
			table_insert(t, GetPlayerIdentifier(player, i))
		end

		return t
	end

	function GetPlayerTokens(player)
		local numIds = GetNumPlayerTokens(player)
		local t = {}

		for i = 0, numIds - 1 do
			table_insert(t, GetPlayerToken(player, i))
		end

		return t
	end

	function GetPlayers()
		local num = GetNumPlayerIndices()
		local t = {}

		for i = 0, num - 1 do
			table_insert(t, GetPlayerFromIndex(i))
		end

		return t
	end

	local httpDispatch = {}
	AddEventHandler('__cfx_internal:httpResponse', function(token, status, body, headers, errorData)
		if httpDispatch[token] then
			local userCallback = httpDispatch[token]
			httpDispatch[token] = nil
			userCallback(status, body, headers, errorData)
		end
	end)

	function PerformHttpRequest(url, cb, method, data, headers, options)
		local followLocation = true

		if options and options.followLocation ~= nil then
			followLocation = options.followLocation
		end

		local t = {
			url = url,
			method = method or 'GET',
			data = data or '',
			headers = headers or {},
			followLocation = followLocation
		}

		local id = PerformHttpRequestInternalEx(t)

		if id ~= -1 then
			httpDispatch[id] = cb
		else
			cb(0, nil, {}, 'Failure handling HTTP request')
		end
	end

	function PerformHttpRequestAwait(url, method, data, headers, options)
		local p = promise.new()
		PerformHttpRequest(url, function(...)
			p:resolve({...})
		end, method, data, headers, options)

		Citizen.Await(p)
		return table.unpack(p.value)
	end
else
	function TriggerServerEvent(eventName, ...)
		local payload = msgpack_pack_args(...)

		return TriggerServerEventInternal(eventName, payload, payload:len())
	end

	function TriggerLatentServerEvent(eventName, bps, ...)
		local payload = msgpack_pack_args(...)

		return TriggerLatentServerEventInternal(eventName, payload, payload:len(), tonumber(bps))
	end
end

local funcRefs = {}
local funcRefIdx = 0

local function MakeFunctionReference(func)
	local thisIdx = funcRefIdx

	funcRefs[thisIdx] = {
		func = func,
		refs = 0
	}

	funcRefIdx = funcRefIdx + 1

	local refStr = Citizen.CanonicalizeRef(thisIdx)
	return refStr
end

function Citizen.GetFunctionReference(func)
	if type(func) == 'function' then
		return MakeFunctionReference(func)
	elseif type(func) == 'table' and rawget(func, '__cfx_functionReference') then
		return MakeFunctionReference(function(...)
			return func(...)
		end)
	end

	return nil
end

local function doStackFormat(err)
	local fst = FormatStackTrace()

	-- already recovering from an error
	if not fst then
		return nil
	end

	return string.format('^1SCRIPT ERROR: %s^7\n%s', err or '', fst)
end

Citizen.SetCallRefRoutine(function(refId, argsSerialized)
	local refPtr = funcRefs[refId]

	if not refPtr then
		Citizen.Trace('Invalid ref call attempt: ' .. refId .. "\n")

		return msgpack_pack(nil)
	end

	local ref = refPtr.func

	local err
	local retvals = false
	local cb = {}

	local di = debug_getinfo(ref)

	local waited = Citizen.CreateThreadNow(function()
		local status, result = xpcall(function()
			retvals = { ref(table_unpack(msgpack_unpack(argsSerialized))) }
		end, doStackFormat)

		if not status then
			err = result or ''
		end

		if cb.cb then
			cb.cb(retvals, err)
		elseif err then
			Citizen.Trace(err)
		end
	end, ('ref call [%s[%d..%d]]'):format(di.short_src, di.linedefined, di.lastlinedefined))

	if not waited then
		if err then
			return msgpack_pack(nil)
		end

		return msgpack_pack(retvals)
	else
		return msgpack_pack({{
			__cfx_async_retval = function(rvcb)
				cb.cb = rvcb
			end
		}})
	end
end)

Citizen.SetDuplicateRefRoutine(function(refId)
	local ref = funcRefs[refId]

	if ref then
		ref.refs = ref.refs + 1

		return refId
	end

	return -1
end)

Citizen.SetDeleteRefRoutine(function(refId)
	local ref = funcRefs[refId]

	if ref then
		ref.refs = ref.refs - 1

		if ref.refs <= 0 then
			funcRefs[refId] = nil
		end
	end
end)

local EXT_FUNCREF = 10
local EXT_LOCALFUNCREF = 11

msgpack.extend_clear(EXT_FUNCREF, EXT_LOCALFUNCREF)

local funcref_mt = nil

funcref_mt = msgpack.extend({
	__gc = function(t)
		DeleteFunctionReference(rawget(t, '__cfx_functionReference'))
	end,

	__index = function(t, k)
		error('Cannot index a funcref', 2)
	end,

	__newindex = function(t, k, v)
		error('Cannot set indexes on a funcref', 2)
	end,

	__call = function(t, ...)
		local ref = rawget(t, '__cfx_functionReference')

		local args = msgpack_pack_args(...)

		-- as Lua doesn't allow directly getting lengths from a data buffer, and _s will zero-terminate, we have a wrapper in the game itself
		local rv = runWithBoundaryEnd(function()
			return Citizen_InvokeFunctionReference(ref, args)
		end)
		local rvs = msgpack_unpack(rv)

		-- handle async retvals from refs
		if rvs and type(rvs[1]) == 'table' and rawget(rvs[1], '__cfx_async_retval') and coroutine_running() then
			local p = promise.new()

			rvs[1].__cfx_async_retval(function(r, e)
				if r then
					p:resolve(r)
				elseif e then
					p:reject(e)
				end
			end)

			return table_unpack(Citizen.Await(p))
		end

		if not rvs then
			error()
		end

		return table_unpack(rvs)
	end,

	__ext = EXT_FUNCREF,

	__pack = function(self, tag)
		local refstr = Citizen.GetFunctionReference(self)
		if refstr then
			return refstr
		else
			error(("Unknown funcref type: %d %s"):format(tag, type(self)), 2)
		end
	end,

	__unpack = function(data, tag)
		local ref = data

		-- add a reference
		DuplicateFunctionReference(ref)

		local tbl = {
			__cfx_functionReference = ref
		}

		tbl = setmetatable(tbl, funcref_mt)

		return tbl
	end,
})

--[[ Also initialize unpackers for local function references --]]
msgpack.extend({
	__ext = EXT_LOCALFUNCREF,
	__pack = funcref_mt.__pack,
	__unpack = funcref_mt.__unpack,
})

msgpack.settype("function", EXT_FUNCREF)

-- exports compatibility
local function getExportEventName(resource, name)
	if resource == "txAdmin" then
		resource = "monitor"
	end
	return string.format('__cfx_export_%s_%s', resource, name)
end

-- callback cache to avoid extra call to serialization / deserialization process at each time getting an export
local exportsCallbackCache = {}

local exportKey = (isDuplicityVersion and 'server_export' or 'export')

do
	local resource = GetCurrentResourceName()

	local numMetaData = GetNumResourceMetadata(resource, exportKey) or 0

	for i = 0, numMetaData-1 do
		local exportName = GetResourceMetadata(resource, exportKey, i)

		AddEventHandler(getExportEventName(resource, exportName), function(setCB)
			-- get the entry from *our* global table and invoke the set callback
			if _G[exportName] then
				setCB(_G[exportName])
			end
		end)
	end
end

-- Remove cache when resource stop to avoid calling unexisting exports
local function lazyEventHandler() -- lazy initializer so we don't add an event we don't need
	AddEventHandler(('on%sResourceStop'):format(isDuplicityVersion and 'Server' or 'Client'), function(resource)
		exportsCallbackCache[resource] = {}
	end)

	lazyEventHandler = function() end
end

-- Helper for newlines in nested error message
local function prefixNewlines(str, prefix)
	str = tostring(str)

	if #str == 0 then
		return str
	end

	return prefix .. str:gsub("\n(.)", "\n" .. prefix .. "%1")
end

-- Handle an export with multiple return values.
local function exportProcessResult(resource, exportName, status, ...)
	if not status then
		local result = tostring(select(1, ...))
		if result:len() > 2048 then
			result = result:sub(1, 1024) .. '\n... [large output partially truncated] ...\n' .. result:sub(-1024)
		end
		error(('\n^5 An error occurred while calling export `%s` in resource `%s`:\n%s\n^5 ---'):format(exportName, resource, prefixNewlines(result, '  ')), 2)
	end
	return ...
end

-- invocation bit
exports = {}

setmetatable(exports, {
	__index = function(t, k)
		local resource = k

		return setmetatable({}, {
			__index = function(t2, k2)
				lazyEventHandler()

				if not k2 or type(k2) ~= 'string' then
					error('Invalid export name: ' .. tostring(k2), 2)
				end

				if not exportsCallbackCache[resource] then
					exportsCallbackCache[resource] = {}
				end

				if not exportsCallbackCache[resource][k2] then
					TriggerEvent(getExportEventName(resource, k2), function(exportData)
						exportsCallbackCache[resource][k2] = exportData
					end)

					if not exportsCallbackCache[resource][k2] then
						error('No such export ' .. k2 .. ' in resource ' .. resource, 2)
					end
				end

				return function(self, ...) -- TAILCALL
					return exportProcessResult(resource, k2, pcall(exportsCallbackCache[resource][k2], ...))
				end
			end,

			__newindex = function(t, k, v)
				error('cannot set values on an export resource', 2)
			end
		})
	end,

	__newindex = function(t, k, v)
		error('cannot set values on exports', 2)
	end,

	__call = function(t, exportName, func)
		AddEventHandler(getExportEventName(GetCurrentResourceName(), exportName), function(setCB)
			setCB(func)
		end)
	end
})

-- NUI callbacks
if not isDuplicityVersion then
	local origRegisterNuiCallback = RegisterNuiCallback

	local cbHandler

--[==[
	local cbHandler = load([[
		-- Lua 5.4: Create a to-be-closed variable to monitor the NUI callback handle.
		local callback, body, resultCallback = ...

		local hasCallback = false
		local _ <close> = defer(function()
			if not hasCallback then
				local di = debug.getinfo(callback, 'S')
				local name = ('function %s[%d..%d]'):format(di.short_src, di.linedefined, di.lastlinedefined)
				warn(("No NUI callback captured: %s"):format(name))
			end
		end)

		local status, err = pcall(function()
			callback(body, function(...)
				hasCallback = true
				resultCallback(...)
			end)
		end)

		return status, err
	]], '@citizen:/scripting/lua/scheduler.lua#nui')]==]

	if not cbHandler then
		cbHandler = load([[
			local callback, body, resultCallback = ...

			local status, err = pcall(function()
				callback(body, resultCallback)
			end)

			return status, err
		]], '@citizen:/scripting/lua/scheduler.lua#nui')
	end

	-- wrap RegisterNuiCallback to handle errors (and 'missed' callbacks)
	function RegisterNuiCallback(type, callback)
		origRegisterNuiCallback(type, function(body, resultCallback)
			local _, err = cbHandler(callback, body, resultCallback)

			if err then
				Citizen.Trace("error during NUI callback " .. type .. ": " .. tostring(err) .. "\n")
			end
		end)
	end

	-- 'old' function (uses events for compatibility, as people may have relied on this implementation detail)
	function RegisterNUICallback(type, callback)
		RegisterNuiCallbackType(type)

		AddEventHandler('__cfx_nui:' .. type, function(body, resultCallback)
			local _, err = cbHandler(callback, body, resultCallback)

			if err then
				Citizen.Trace("error during NUI callback " .. type .. ": " .. tostring(err) .. "\n")
			end
		end)
	end

	local _sendNuiMessage = SendNuiMessage

	function SendNUIMessage(message)
		_sendNuiMessage(json.encode(message))
	end
end

-- entity helpers
local EXT_ENTITY = 41
local EXT_PLAYER = 42

msgpack.extend_clear(EXT_ENTITY, EXT_PLAYER)

local function NewStateBag(es)
	return setmetatable({}, {
		__index = function(_, s)
			if s == 'set' then
				return function(_, s2, v2, r2)
					local payload = msgpack_pack(v2)
					SetStateBagValue(es, s2, payload, payload:len(), r2)
				end
			end

			return GetStateBagValue(es, s)
		end,

		__newindex = function(_, s, v)
			local payload = msgpack_pack(v)
			SetStateBagValue(es, s, payload, payload:len(), isDuplicityVersion)
		end
	})
end

GlobalState = NewStateBag('global')

local function GetEntityStateBagId(entityGuid)
	if isDuplicityVersion or NetworkGetEntityIsNetworked(entityGuid) then
		return ('entity:%d'):format(NetworkGetNetworkIdFromEntity(entityGuid))
	else
		EnsureEntityStateBag(entityGuid)
		return ('localEntity:%d'):format(entityGuid)
	end
end

local entityMT
entityMT = {
	__index = function(t, s)
		if s == 'state' then
			local es = GetEntityStateBagId(t.__data)

			if isDuplicityVersion then
				EnsureEntityStateBag(t.__data)
			end

			return NewStateBag(es)
		end

		return nil
	end,

	__newindex = function()
		error('Setting values on Entity is not supported at this time.', 2)
	end,

	__ext = EXT_ENTITY,

	__pack = function(self, t)
		return tostring(NetworkGetNetworkIdFromEntity(self.__data))
	end,

	__unpack = function(data, t)
		local ref = NetworkGetEntityFromNetworkId(tonumber(data))

		return setmetatable({
			__data = ref
		}, entityMT)
	end
}

msgpack.extend(entityMT)

local playerMT
playerMT = {
	__index = function(t, s)
		if s == 'state' then
			local pid = t.__data

			if pid == -1 then
				pid = GetPlayerServerId(PlayerId())
			end

			local es = ('player:%d'):format(pid)

			return NewStateBag(es)
		end

		return nil
	end,

	__newindex = function()
		error('Setting values on Player is not supported at this time.', 2)
	end,

	__ext = EXT_PLAYER,

	__pack = function(self, t)
		return tostring(self.__data)
	end,

	__unpack = function(data, t)
		local ref = tonumber(data)

		return setmetatable({
			__data = ref
		}, playerMT)
	end
}

msgpack.extend(playerMT)

function Entity(ent)
	if type(ent) == 'number' then
		return setmetatable({
			__data = ent
		}, entityMT)
	end

	return ent
end

function Player(ent)
	if type(ent) == 'number' or type(ent) == 'string' then
		return setmetatable({
			__data = tonumber(ent)
		}, playerMT)
	end

	return ent
end

if not isDuplicityVersion then
	LocalPlayer = Player(-1)
end
