if not SF.Require("socket.core") then
	return function() end
end

-- LuaSocket helper module
-- Author: Diego Nehab

-- Exported auxiliar functions
function socket.connect4(address, port, laddress, lport)
	return socket.connect(address, port, laddress, lport, "inet")
end

function socket.connect6(address, port, laddress, lport)
	return socket.connect(address, port, laddress, lport, "inet6")
end

function socket.bind(host, port, backlog)
	if host == "*" then
		host = "0.0.0.0"
	end
	local addrinfo, err = socket.dns.getaddrinfo(host)
	if not addrinfo then
		return nil, err
	end
	local sock, res
	err = "no info on address"
	for i, alt in ipairs(addrinfo) do
		if alt.family == "inet" then
			sock, err = socket.tcp4()
		else
			sock, err = socket.tcp6()
		end
		if not sock then
			return nil, err
		end
		sock:setoption("reuseaddr", true)
		res, err = sock:bind(alt.addr, port)
		if not res then
			sock:close()
		else
			res, err = sock:listen(backlog)
			if not res then
				sock:close()
			else
				return sock
			end
		end
	end
	return nil, err
end

socket.try = socket.newtry()

function socket.choose(table)
	return function(name, opt1, opt2)
		if type(name) ~= "string" then
			name, opt1, opt2 = "default", name, opt1
		end
		local f = table[name or "nil"]
		if not f then
			error("unknown key (" .. tostring(name) .. ")", 3)
		else
			return f(opt1, opt2)
		end
	end
end

-- Socket sources and sinks, conforming to LTN12
-- create namespaces inside LuaSocket namespace
local sourcet, sinkt = {}, {}
socket.sourcet = sourcet
socket.sinkt = sinkt

socket.BLOCKSIZE = 2048

sinkt["close-when-done"] = function(sock)
	return setmetatable({
		getfd = function()
			return sock:getfd()
		end,
		dirty = function()
			return sock:dirty()
		end,
	}, {
		__call = function(self, chunk, err)
			if not chunk then
				sock:close()
				return 1
			else
				return sock:send(chunk)
			end
		end,
	})
end

sinkt["keep-open"] = function(sock)
	return setmetatable({
		getfd = function()
			return sock:getfd()
		end,
		dirty = function()
			return sock:dirty()
		end,
	}, {
		__call = function(self, chunk, err)
			if chunk then
				return sock:send(chunk)
			else
				return 1
			end
		end,
	})
end

sinkt["default"] = sinkt["keep-open"]

socket.sink = socket.choose(sinkt)

sourcet["by-length"] = function(sock, length)
	return setmetatable({
		getfd = function()
			return sock:getfd()
		end,
		dirty = function()
			return sock:dirty()
		end,
	}, {
		__call = function()
			if length <= 0 then
				return nil
			end
			local size = math.min(socket.BLOCKSIZE, length)
			local chunk, err = sock:receive(size)
			if err then
				return nil, err
			end
			length = length - string.len(chunk)
			return chunk
		end,
	})
end

sourcet["until-closed"] = function(sock)
	local done
	return setmetatable({
		getfd = function()
			return sock:getfd()
		end,
		dirty = function()
			return sock:dirty()
		end,
	}, {
		__call = function()
			if done then
				return nil
			end
			local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
			if not err then
				return chunk
			elseif err == "closed" then
				sock:close()
				done = 1
				return partial
			else
				return nil, err
			end
		end,
	})
end

sourcet["default"] = sourcet["until-closed"]

socket.source = socket.choose(sourcet)

--- Socket library. Only usable by owner of starfall.
-- See the WebSocket type for a version of this that doesn't require a DLL, and supports secure websockets (wss)
-- Beware "Blocking" functions; they will freeze the game. See http://w3.impa.br/~diego/software/luasocket/socket.html
-- Install the gmcl_socket.core_*.dll binary file to lua/bin and create a 'gm_socket_whitelist.txt' file in steamapps/common
-- Each line in the whitelist will allow luasocket to access the specified domain and port. They are formatted as 'domain:port' e.g. 'garrysmod.com:80', '*.com:80' '95.123.12.22:27015'
-- @name socket
-- @class library
-- @libtbl socket_library
SF.RegisterLibrary("socket")

-- Socket lib doc sourced from: https://defold.com/ref/stable/socket

return function(instance)
	if LocalPlayer() ~= instance.player then
		return
	end

	local socket_list = setmetatable({}, { __mode = "k" })

	local function create_proxy_function(original_function)
		return function(...)
			local sock, err = original_function(...)
			if sock ~= nil then
				socket_list[sock] = true
			end
			return sock, err
		end
	end

	instance:AddHook("deinitialize", function()
		for socket in pairs(socket_list) do
			socket:close()
		end
	end)

	local socket_proxy = {}
	setmetatable(socket_proxy, { __index = socket })

	--- Creates and returns an IPv4 TCP master object.
	-- A master object can be transformed into a server object with the method listen (after a call to bind) or into a client object with the method connect.
	-- The only other method supported by a master object is the close method.
	-- @name socket_library.tcp
	-- @class function
	-- @return table New IPv4 TCP Master Object, or nil if error
	-- @return string? The error message, or nil if no error
	socket_proxy.tcp = create_proxy_function(socket.tcp)

	--- Creates and returns an IPv4 TCP master object.
	-- A master object can be transformed into a server object with the method listen (after a call to bind) or into a client object with the method connect.
	-- The only other method supported by a master object is the close method.
	-- @name socket_library.tcp4
	-- @class function
	-- @return table New IPv4 TCP Master Object, or nil if error
	-- @return string? The error message, or nil if no error
	socket_proxy.tcp4 = create_proxy_function(socket.tcp4)

	--- Creates and returns an IPv6 TCP master object.
	-- A master object can be transformed into a server object with the method listen (after a call to bind) or into a client object with the method connect.
	-- The only other method supported by a master object is the close method.
	-- Note: The TCP object returned will have the option "ipv6-v6only" set to true.
	-- @name socket_library.tcp6
	-- @class function
	-- @return table New IPv6 TCP Master Object, or nil if error
	-- @return string? The error message, or nil if no error
	socket_proxy.tcp6 = create_proxy_function(socket.tcp6)

	--- Creates and returns an unconnected IPv4 UDP object.
	-- Unconnected objects support the sendto, receive, receivefrom, getoption, getsockname, setoption, settimeout, setpeername, setsockname, and close methods.
	-- The setpeername method is used to connect the object.
	-- @name socket_library.udp
	-- @class function
	-- @return table New IPv4 TCP master object, or nil in case of error.
	-- @return string? The error string if errored, else nil
	socket_proxy.udp = create_proxy_function(socket.udp)

	--- Creates and returns an unconnected IPv4 UDP object.
	-- Unconnected objects support the sendto, receive, receivefrom, getoption, getsockname, setoption, settimeout, setpeername, setsockname, and close methods.
	-- The setpeername method is used to connect the object.
	-- @name socket_library.udp4
	-- @class function
	-- @return table New IPv4 UDP master object, or nil in case of error.
	-- @return string? The error string if errored, else nil
	socket_proxy.udp4 = create_proxy_function(socket.udp4)

	--- Creates and returns an unconnected IPv4 UDP object.
	-- Unconnected objects support the sendto, receive, receivefrom, getoption, getsockname, setoption, settimeout, setpeername, setsockname, and close methods.
	-- The setpeername method is used to connect the object.
	-- Note: The UDP object returned will have the option "ipv6-v6only" set to true.
	-- @name socket_library.udp6
	-- @class function
	-- @return table New IPv6 UDP master object, or nil in case of error.
	-- @return string? The error string if errored, else nil
	socket_proxy.udp6 = create_proxy_function(socket.udp6)

	--- This function is a shortcut that creates and returns a TCP client object connected to a remote address at a given port.
	-- Optionally, the user can also specify the local address and port to bind (locaddr and locport), or restrict the socket family to "inet" or "inet6".
	-- Without specifying family to connect, whether a tcp or tcp6 connection is created depends on your system configuration.
	-- @name socket_library.connect
	-- @class function
	-- @param number addr Address to connect to
	-- @param number port Port to connect to
	-- @param number? laddr Local address to bind to
	-- @param number? lport Local port to bind to
	-- @param string? family Socket family, either "inet" or "inet6".
	-- @return table client TCPClient object. Nil if error
	-- @return string? error Error string if the previous return was nil, else nil
	socket_proxy.connect = create_proxy_function(socket.connect)

	--- This function is a shortcut that creates and returns a TCP client object connected to a remote address at a given port.
	-- Optionally, the user can also specify the local address and port to bind (locaddr and locport)
	-- @name socket_library.connect4
	-- @class function
	-- @param number addr Address to connect to
	-- @param number port Port to connect to
	-- @param number? laddr Local address to bind to
	-- @param number? lport Local port to bind to
	-- @return table client TCPClient object. Nil if error
	-- @return string? error Error string if the previous return was nil, else nil
	socket_proxy.connect4 = create_proxy_function(socket.connect4)

	--- This function is a shortcut that creates and returns a TCP client object connected to a remote address at a given port.
	-- Optionally, the user can also specify the local address and port to bind (locaddr and locport)
	-- @name socket_library.connect6
	-- @class function
	-- @param number addr Address to connect to
	-- @param number port Port to connect to
	-- @param number? laddr Local address to bind to
	-- @param number? lport Local port to bind to
	-- @return table client TCPClient object. Nil if error
	-- @return string? error Error string if the previous return was nil, else nil
	socket_proxy.connect6 = create_proxy_function(socket.connect6)

	-- TODO: Docs for this. is this supposed to be the raw metamethod?
	socket_proxy.bind = create_proxy_function(socket.bind)

	instance.env.socket = socket_proxy
end
