local silly = require "silly"
local net = require "silly.net"
local logger = require "silly.logger"
local ns = require "silly.netstream"
local type = type
local assert = assert

--when luaVM destroyed, all process will be exit
--so no need to clear socket connection
---@type table<integer, silly.net.tcp>
local socket_pool = {}

---@class silly.net.tcp
---@field fd integer
---@field delim boolean|string|integer|nil
---@field co thread|nil
---@field err string|nil
---@field sbuffer any
---@field disp async fun(fd:integer, addr:string)?
local socket = {}

---@param fd integer
local function new_socket(fd)
	local s = {
		fd = fd,
		delim = false,
		---@type thread|nil
		co = nil,
		err = nil,
		sbuffer = ns.new(fd),
	}
	assert(not socket_pool[fd])
	socket_pool[fd] = s
end

local function del_socket(s)
	ns.free(s.sbuffer)
	socket_pool[s.fd] = nil
end

---@param s silly.net.tcp
---@return string?, string? error
local function suspend(s)
	assert(not s.co)
	local co = silly.running()
	s.co = co
	local dat = silly.wait()
	if not dat then
		return nil, s.err
	end
	return dat, nil
end

---@param dat string?
local function wakeup(s, dat)
	local co = s.co
	s.co = nil
	silly.wakeup(co, dat)
end

local EVENT = {

accept = function(fd, listenid, addr)
	local lc = socket_pool[listenid];
	new_socket(fd)
	local ok, err = silly.pcall(lc.disp, fd, addr)
	if not ok then
		logger.error(err)
		socket.close(fd)
	end
end,

close = function(fd, errno)
	local s = socket_pool[fd]
	if s == nil then
		return
	end
	s.err = errno
	if s.co then
		wakeup(s, nil)
	end
end,

data = function(fd, ptr, chunk_size)
	local s = socket_pool[fd]
	if not s then
		return
	end
	local sbuffer = s.sbuffer
	local size = ns.push(sbuffer, ptr, chunk_size)
	local delim = s.delim
	local typ = type(delim)
	if typ == "number" then
		if size >= delim then
			local dat = ns.read(sbuffer, delim)
			s.delim = false
			wakeup(s, dat)
		end
	else
		if typ == "string" then
			local dat = ns.readline(sbuffer, delim)
			if dat then
				s.delim = false
				wakeup(s, dat)
				return
			end
		end
	end
end
}

---@param addr string
---@param disp async fun(fd:integer, addr:string)
---@param backlog integer|nil
---@return integer|nil, string|nil
function socket.listen(addr, disp, backlog)
	assert(addr)
	assert(disp)
	local listenid, err = net.tcplisten(addr, EVENT, backlog)
	if listenid then
		socket_pool[listenid] = {
			fd = listenid,
			disp = disp,
			co = nil,
			err = nil,
			delim = nil
		}
	end
	return listenid, err
end

---@async
---@param ip string
---@param bind string|nil
---@return integer|nil, string? error
function socket.connect(ip, bind)
	local fd, err = net.tcpconnect(ip, EVENT, bind)
	if fd then
		assert(fd >= 0)
		new_socket(fd)
	end
	return fd, err
end

---@param fd integer
---@param limit integer
---@return integer|boolean
function socket.limit(fd, limit)
	local s = socket_pool[fd]
	if s == nil then
		return false
	end
	return ns.limit(s.sbuffer, limit)
end

---@param fd integer
---@return boolean, string? error
function socket.close(fd)
	local s = socket_pool[fd]
	if s == nil then
		return false, "socket closed"
	end
	if s.co then
		s.err = "active closed"
		wakeup(s, nil)
	end
	del_socket(s)
	net.close(fd)
	return true, nil
end

---@async
---@param fd integer
---@param n integer
---@return string?, string? error
function socket.read(fd, n)
	local s = socket_pool[fd]
	if not s then
		return nil, "socket closed"
	end
	local r = ns.read(s.sbuffer, n)
	if r then
		return r, nil
	end
	local err = s.err
	if err then
		return nil, err
	end
	s.delim = n
	return suspend(s)
end

---@param fd integer
---@param max integer|nil
---@return string?, string? error
function socket.readall(fd, max)
	local s = socket_pool[fd]
	if not s then
		return nil, "socket closed"
	end
	local r = ns.readall(s.sbuffer, max)
	if r == "" and s.err then
		return nil, s.err
	end
	return r, nil
end

---@async
---@param fd integer
---@param delim string|nil
---@return string?, string? error
function socket.readline(fd, delim)
	delim = delim or "\n"
	local s = socket_pool[fd]
	if not s then
		return nil, "socket closed"
	end
	local r = ns.readline(s.sbuffer, delim)
	if r then
		return r, nil
	end
	if s.err then
		return nil, s.err
	end
	s.delim = delim
	return suspend(s)
end

---@param fd integer
---@return integer
function socket.recvsize(fd)
	local s = socket_pool[fd]
	if not s then
		return 0
	end
	return ns.size(s.sbuffer)
end

socket.write = net.tcpsend
socket.sendsize = net.sendsize

---@param fd integer
---@return boolean
function socket.isalive(fd)
	local s = socket_pool[fd]
	return s and not s.err
end

return socket

