-- Arcan- specific debugger to cover both the needs of stepping arcan/arcan_lwa
-- via the --monitor command, and through arcan-net to attach to a running
-- controller and debugging an appl.
--
-- The only 'special' thing versus DAP, other than a lot of the
-- functions matching raw memory doesn't make sense, is that we
-- have the .lua snapshot format to create a view for.
--
-- Since everything is single threaded, the thread interface makes more sense
-- to be used for when we can attach to an appl-controller and step/interface
-- with that. Then we map that as thread 2 .. n.
--
-- Todo:
--   [ ] - support varargs
--   [ ] - modify local
--   [ ] - completion helper for arcan launch
--   [ ] - view for showing snapshots and snapshot deltas
--   [ ] - copy table to spreadsheet
--   [ ] - move reads/writes to thread specific inp/outp
--   [ ] - port interface to arcan-net directory
--   [ ] - port interface to afsrv_terminal:lua
--
return
function(cat9, mode, args, target)

-- This table is generated by a simple appl enumerating _G, then we use that on the
-- global table to ignore entires that already exist in documentation. A possible
-- change would be to mark functions that are intercepted or use it to build tracesets
-- of functions (just eval(
-- 	local ot = {}; ot[sym] = sym;
-- 	sym = function(...) ot[sym] trigger_breakpoint(); return deb(...) end
-- )
--
local global_filter =
	loadfile(string.format("%s/cat9/dev/support/arcan_globals.lua", lash.scriptdir))()
local gen_local

local errors = {
	no_frame = "missing requested frame %d",
	missing_source = "append target missing",
	missing_appl = "launch >target< missing"
}

local synch_frame =
-- Arcan- specific debugger to cover both the needs of stepping
function(thread)
	if thread.state ~= "stopped" then
		return
	end

	thread.stack = {}
end

local function invalidate_threads(dbg)
	for k,thread in ipairs(dbg.data.threads) do
		for i=#thread.handlers.invalidated,1,-1 do
			thread.handlers.invalidated[i](thread)
		end
	end
end

local function ensure_thread(dbg, id)
	if dbg.data.threads[id] then
		return dbg.data.threads[id]
	end

	local th
	th = {
		id = id,
		state = "unknown",
		dbg = dbg,
		write = function(_, ...)
			local args = {...}
			dbg.job.inp:write(...)
		end,
		vmstack = {
			locals =
			function(_, cb)
				cb({locals = { variables = th.vmstack }})
			end
		},
		synch_frames = synch_frame,
		step = function(th)
			th:write("stepnext\n")
			th.stack = {}
			th.state = "running"
		end,
		stepin = function(th)
			th:write("stepcall\n")
			th.stack = {}
			th.state = "running"
		end,
		stepout = function(th)
			th:write("stepend\n")
			th.stack = {}
			th.state = "running"
		end,
		freerun = function(th, mode, granularity)
		end,
		stepi = function(th)
			th:write("stepinstruction\n")
			th.stack = {}
			th.state = "running"
		end,
		locals = function(th, fid, cb)
			local frame = th:frame(fid)
			if not frame then
				cb({error = string.format(errors.no_frame, fid or -1)})
			else
				frame:locals(cb)
			end
		end,
		frame = function(th, fid)
			if fid == -1 then
				return th.vmstack
			end

			for i=1,#th.stack do
				if th.stack[i].id == fid then
					return th.stack[i]
				end
			end
		end,
		stack = {},
		handlers = {
			invalidated = {}
		}
	}
	dbg.data.threads[id] = th
	return th
end

local Debugger = {}
function Debugger:get_suggestions(prefix, closure)
end

function Debugger:break_at(source, line)
	self.job.inp:write(string.format(
		"breakpoint %s:%s\n", source, tostring(line)))
end

function Debugger:break_on(func)
end

function Debugger:break_addr(addr)
end

function Debugger:disassemble(addr, ofs, count, closure)
end

function Debugger:source(ref, closure)
	self.job.inp:write("source " .. ref .. "\n")
	self.source_closure = closure
-- source references are just files
end

function Debugger:read_memory(base, length, closure)
end

function Debugger:watch_memory(base, length, closure)
end

function Debugger:eval(expression, context, closure)
	if self.job and self.job.inp then
		self.job.inp:write("eval " .. expression .. "\n")
	end
end

function Debugger:update_signal(signo, state, closure)
end

function Debugger:continue(id)
	local th = ensure_thread(self, id)
	th.state = "running"
	th.stack = {}
	th:write("continue\n")
end

function Debugger:thread(id)
	return ensure_thread(self, id)
end

function Debugger:pause(id)
	local th = ensure_thread(self, id)

	if self.job.pid then
		lash.root:psignal(self.job.pid, "user1");
	end

	th:write("dumpkeys\n");
	th:write("backtrace\n");
end

function Debugger:restart()
	if self.job.pid then
		lash.root:psignal(self.job.pid, "user1");
	end

-- need to send to all threads
	self.job.inp:write("reload\n");
end

function Debugger:set_log(login, logout)
	self.log = login
	self.log_out = logout
end

local function get_frame_locals(debug, frame, cb)
	cb(
		{
			locals = { variables = frame.locals_tbl },
			globals = { variables = frame.globals_tbl }
		}
	)
end

local function frame_filter(frame, var, scope)
	if not frame.filters or not frame.filters[scope] then
		return
	end

	return frame.filters[scope][var.name] ~= nil
end

local function handler_for_tbl(debug, frame, var, cb, scope)
-- having a scope filter in presentation would be useful also outside global
--     global_filter[shmif.tblkey]
-- in order to highlight new/changed keys
	return
	function(key)
		var.variables = {}

-- we have table:index:tblkey:type:value | name
		for i,v in ipairs(key) do
			local shmif = string.unpack_shmif_argstr(v)
			local repack =
			{
				index = shmif.index,
				vartype = shmif.vartype,
				value = shmif.value,
				parent = val,
				name = shmif.tblkey,
				length = shmif.length,
				keys = shmif.keys,
				source = shmif.source,
				line = shmif.line and tonumber(shmif.line) or nil,
				line_end = shmif.line_end and tonumber(shmif.line_end) or nil
			}

-- for C functions we could just check name against the doc and get the
-- reference from there
			if shmif.vartype == "func" then
				repack.value = shmif.source or "function(C)"
			end

-- append accessor functions and let us recurse into hierarchies
			if not frame_filter(frame, repack, scope) then
				table.insert(
					var.variables, gen_local(debug, frame, repack, var, scope))
			end
		end
		cb(var)
	end
end

gen_local =
function(dbg, frame, shmif, parent, scope)
	local tbl =
	{
		ref = tonumber(shmif.index),
		type = shmif.vartype and shmif.vartype or "nil",
		value = shmif.value,
		parent = parent,
		name = shmif.name or "(missing)",
		modify =
		function(var, val)
			print("set", frame.id, shmif.index, val)
		end,
		fetch =
		function(var, cb)
			if var.type ~= "table" then
				cb(var.value or "nil")
			end

-- walk parents and build forward- list of indices
			table.insert(
				dbg.queue, {"TABLE", handler_for_tbl(dbg, frame, var, cb, scope)}
			)

			local tree = {tonumber(shmif.index)}

			local cv = var.parent
			while cv do
				table.insert(tree, 1, tostring(cv.ref))
				cv = cv.parent
			end

-- for global scope we don't want the id of the outmost variable as it is
-- just the container reference, and we want a filter-set that excludes the
-- known default arcan functions (with some caveat that if a function has
-- a non-C reference we want to see it as it means it is overwritten and
-- can be used as a possible breakpoint)
			local line
			if scope == "global" then
				line = "table g"
				if var.parent then
					table.remove(tree, 1)
					line = line .. " " .. table.concat(tree, " ")
				end
			elseif scope == "stack" then
				line =
					string.format("table s %s", table.concat(tree, " "))
			else
				line = string.format(
					"table l %d %s", frame.id, table.concat(tree, " "))
			end

		dbg.job.inp:write(line)
		dbg.job.inp:write("\n")
		end
	}

	if shmif.vartype == "table" then
		tbl.namedVariables = tonumber(shmif.length) + tonumber(shmif.keys)
	elseif not tbl.value then
		tbl.value = "nil"
	end

	return tbl
end

local function add_globals_wrapper(debug)
	return
		gen_local(
			debug, {id = -1, filters = {global = global_filter}},
			{
				index = 1,
				vartype = "table",
				name = "_G",
-- count isn't actually used in variables_view.lua
				length = 1,
				keys = 1,
			}, nil, "global"
		)
end

local key_handlers = {}
function key_handlers.BACKTRACE(debug, args, thread)
	local stack = thread.stack
	stack.entrypoint = "(unknown)"

	for i,v in ipairs(args) do
		local frame = {
			id = i,
			line = 0,
			line_end = 0,
			column = 0,
			pc = 0,
			thread = 0,
			name = "",
			locals = function(...)
				return get_frame_locals(debug, ...)
			end,
			filters = {
				global = global_filter
			},
			locals_tbl = {},
			globals_tbl = {add_globals_wrapper(debug)},
			source = "nop",
			path = "/tmp",
		}
		local shmif = string.unpack_shmif_argstr(v)
		if not shmif then
			debug.errors:add_line("Broken backtrace: " .. v)
			return
		end

-- some frames of anonymous inner functions we should resolve the outer name
-- afterwards as a fixup and propagate onwards
		if shmif.type == "stacktrace" then
			frame.path = shmif.source
			frame.source = frame.path
			frame.block_start = tonumber(shmif.start)
			frame.block_end = tonumber(shmif["end"])
			frame.line = tonumber(shmif.current)
			frame.line_end = current
			frame.name = shmif.name
			frame.id = tonumber(shmif.frame) or i
			table.insert(stack, frame)

		elseif shmif.type == "entrypoint" then
			stack.entrypoint = shmif.kind

		elseif shmif.type == "local" then
			table.insert(
				stack[#stack].locals_tbl,
				gen_local(debug, stack[#stack], shmif, nil, shmif.type))
		end
	end

-- fixup stack in reverse, propagate name into (null) named ones
	local lastname = stack.entrypoint
	for i=#stack,1,-1 do
		if stack[i].name == "(null)" then
			stack[i].name = lastname
		end
		lastname = stack[i].name
	end
end

function key_handlers.STACK(debug, args, thread)
	for i=#thread.vmstack, 1, -1 do
		table.remove(thread.vmstack, i)
	end

	for i,v in ipairs(args) do
		local shmif = string.unpack_shmif_argstr(v)
		if not shmif then
			debug.errors:add_line("Broken VM stack: " .. v)
			return
		end
		table.insert(
			thread.vmstack,
			gen_local(debug, {id = -1}, shmif, nil, "stack")
		)
	end
end

function key_handlers.ERROR(debug, args, thread)
	for _,v in ipairs(args) do
		local source, line, msg = string.match(v, "%[string%s(.+)%]%:(%d+):(.+)")
		if not source or not line or not msg then
			debug.errors:add_line(debug, "couldn't parse error message, raw:")
			debug.errors:add_line(debug, v)
		else
-- can get string.sub 2,-2 from source for the full filename
			debug.errors:add_line(debug, msg)
		end
	end
end

function key_handlers.SOURCE(debug, args, thread)
	debug.key.path = table.remove(debug.key, 1)
	debug.key.sourceReference = 0

	if debug.source_closure then
		debug.source_closure(table.concat(debug.key, "\n"))
		debug.source_closure = nil
	end
end

function key_handlers.BREAK(debug, args, thread)
	thread.breakpoints = {}

	for i,v in ipairs(args) do
		local shmif = string.unpack_shmif_argstr(v)
		if shmif and shmif.file and shmif.line then
			table.insert(thread.breakpoints, {
				id = i,
				verified = true,
				source = shmif.file,
				line = {tonumber(shmif.line)},
				path = shmif.file
			}
			);
		end
	end

-- merge into active set, this needs to extract / sort threads later
	debug.data.breakpoints = {}
	for i,v in ipairs(thread.breakpoints) do
		table.insert(debug.data.breakpoints, v)
	end
end

function process_key(debug, thread)
-- priority if the key matches a queued one
	if debug.queue[1] and debug.queue[1][1] == debug.key.name then
		local ent = table.remove(debug.queue, thread)
		ent[2](debug.key)
		return
	end

-- BEGINKV is special as its contents will depend on if we ran dumpstate or
-- dumpkeys, this is because how arcan-net does it based on exit status
		if key_handlers[debug.key.name] then
		key_handlers[debug.key.name](debug, debug.key, debug.data.threads[thread])
		return
	end

	print("unhandled key", debug.key.name)
end

function Debugger:append(source)
	if not source or #source == 0 then
		return false, errors.missing_source
	end

-- check if we have an a12 tag (@tag) or if it's another launch job that we
-- should mask as a new thread.
--
-- find a free thread id in self.data.thread
--  replace the write function with something for our new job
--  modify break_at to do the same
--  modify source to do the same
--  modify eval to do the same
--  modify restart to do the same
--  modify terminate to do the same
--  modify gen_local to take thread into account
--
-- for pause we need something else.
--
end

function Debugger:terminate(hard)
	for k,v in pairs(self.data.threads) do
		v:write("detach\n")
	end
end

local function add_tbl_line(tbl, dbg, line)
-- presenting the number as a timeline gives weird interactions with the default
-- crop view, track the counter as linear between the buffers but don't add it to
-- the explicit data for now
	line = string.trim(line)
	if #line == 0 then
		return
	end

	table.insert(tbl, line)
	tbl.linecount = tbl.linecount + 1
	tbl.bytecount = tbl.bytecount + #line
end

local function thread_data_parser(debug, thid)
return
function(line, _, _)
	local th = ensure_thread(debug, thid)
	if debug.log then
		debug.log(line)
	end

	line = string.sub(line, 1, -2) -- trailing linefeed

-- special cases: WAITING, FINISHED
	if line == "#WAITING" then
		if th.state ~= "stopped" then
			th.state = "stopped"
		end
		invalidate_threads(debug)
	elseif string.sub(line, 1, 5) == "#PID " then

-- set the pid and switch to polling
		if not debug.job.pid then
			debug.job.pid = tonumber(string.sub(line, 6))
		end

-- process key tags with the frame ID, so when we have remote threads as well
-- their IDs should be swapped accordingly and we add it as hidden
	elseif string.sub(line, 1, 4) == "#END" then
		if debug.key and string.sub(line, 5) == debug.key.name then
			process_key(debug, 1)
			debug.key = nil
		else
			table.insert(debug.key, line)
		end

-- new key?
	elseif string.sub(line, 1, 6) == "#BEGIN" then
		if not debug.key then
			debug.key = {
				name = string.sub(line, 7)
			}
		else
			table.insert(debug.key, line)
		end

-- just buffer? if so, drop escaped \# (and unescape \n)
	elseif debug.key then
		if string.sub(line, 1, 1) == "\\" and string.sub(line, 2, 2) == "#" then
			table.insert(debug.key, string.sub(line, 2))
		else
			local line = string.gsub(line, "\\n", "\n")
			table.insert(debug.key, line)
		end

-- print output from the debugee?
	elseif th.state ~= "stopped" then
		local shmif = string.unpack_shmif_argstr(line)
		if shmif and shmif.type and shmif.value then
			debug.stdout:add_line(debug,
				string.format("%s: %s", shmif.type, shmif.value))
		else
			print("unexpected", line)
		end
	end
end
end

local debug = setmetatable(
{
	data = {
		threads = {},
		files = {},
		functions = {},
		modules = {},
		capabilities = {},
		breakpoints = {},
		sources = {}
	},
	queue = {},
	stdout = {bytecount = 0, linecount = 0, add_line = add_tbl_line},
	output = {bytecount = 0, linecount = 0, add_line = add_tbl_line},
	errors = {bytecount = 0, linecount = 0, add_line = add_tbl_line},
	features = {
		launch = true,
	},
	job = job,
}, {__index = Debugger})

if mode == "attach" then
	local job = {
		raw = "Arcan:Debug",
		short = "Arcan:Debug",
		block_buffer = true,
	}
	if not target[1] then
		return false, "missing output FIFO argument"
	end

	job.inp = lash.root:fopen(target[1], "w")
	if not job.inp then
		return false, "couldn't open " .. target[1]
	end

	local mon_outf = "/tmp/arcan.debug"
	job.out = lash.root:fopen(mon_outf, "fifo-in")

	if not job.out then
		job.inp:close()
		return false, "couldn't create output FIFO"
	end
	job.inp:write("output " .. mon_outf .. "\n")

-- since job doesn't have a pid yet it won't get processed in the normal job
-- dispatch, mark as deferred before importing and when the pid is set, unmark.
	cat9.import_job(job)
	debug.job = job
	local th = ensure_thread(debug, 1)
	th.state = "stopped"

	job.out:data_handler(
		function()
			local _, alive = cat9.flush_job(job, false, 100)
			if not alive then
				debug.log("closing attach handler")
				job.out:close()
			end
			return alive
		end
	)

	table.insert(job.hooks.on_data, thread_data_parser(debug, 1))
	table.insert(job.hooks.on_destroy, function()
		lash.root:funlink(mon_outf)
	end)

	return debug
end

if not target[2] then
	return false, errors.missing_appl
end

cat9.shmif_handover(
	args.arcan_default_mode, -- creation
	"rwe", -- streams wanted
	"/usr/bin/arcan_lwa", -- binary
	cat9.env,
	{
		string.format("arcan(debug:%s)", applname), -- actual name
		"-O", -- monitor through stdout
		"LOGFD:1",
		"-C", "-", -- accept commands through stdin
		target[2]
	},
	{
	block_wnd = true,
	closure =
	function(job)
		debug.job = job
		job.raw = "Arcan:Debug"
		job.short = "Arcan:Debug"
		job.block_buffer = true
		debug.stderr = job.err_buffer

--
-- uses #TAG and #ENDTAG to group data,
-- \# to escape initial # and \\n to escape linefeed
--
-- #TAG / #ENDTAG can be nested, buffer those
--
		table.insert(job.hooks.on_data, thread_data_parser(debug, 1))
		local th = ensure_thread(debug, 1)
		th.state = "stopped"
	end
	}
)

return debug
end
