-- license:BSD-3-Clause
-- copyright-holders: Carl
local exports = {
	name = "gdbstub",
	version = "0.0.1",
	description = "GDB stub plugin",
	license = "BSD-3-Clause",
	author = { name = "Carl" } }

local gdbstub = exports

-- percpu mapping of mame registers to gdb register order
local regmaps = {
	i386 = {
		togdb = {
			EAX = 1, ECX = 2, EDX = 3, EBX = 4, ESP = 5, EBP = 6, ESI = 7, EDI = 8, EIP = 9, EFLAGS = 10, CS = 11, SS = 12,
			DS = 13, ES = 14, FS = 15, GS = 16 },
		fromgdb = {
			"EAX", "ECX", "EDX", "EBX", "ESP", "EBP", "ESI", "EDI", "EIP", "EFLAGS", "CS", "SS", "DS", "ES", "FS", "GS" },
		regsize = 4,
		addrsize = 4,
		pcreg = "EIP"
	}
}
regmaps.i486 = regmaps.i386
regmaps.pentium = regmaps.i386

local reset_subscription, stop_subscription

function gdbstub.startplugin()
	local debugger
	local debug
	local cpu
	local breaks
	local watches
	local consolelog
	local consolelast
	local running

	reset_subscription = emu.add_machine_reset_notifier(function ()
		debugger = manager.machine.debugger
		if not debugger then
			print("gdbstub: debugger not enabled")
			return
		end
		cpu = manager.machine.devices[":maincpu"]
		if not cpu then
			print("gdbstub: maincpu not found")
		end
		if not regmaps[cpu.shortname] then
			print("gdbstub: no register map for cpu " .. cpu.shortname)
			cpu = nil
		end
		consolelog = debugger.consolelog
		consolelast = 0
		breaks = {byaddr = {}, byidx = {}}
		watches = {byaddr = {}, byidx = {}}
		running = false
	end)

	stop_subscription = emu.add_machine_stop_notifier(function ()
		consolelog = nil
		cpu = nil
		debug = nil
	end)

	local socket = emu.file("", 7)
	local connected = false
	socket:open("socket.127.0.0.1:2159")

	emu.register_periodic(function ()
		if not cpu then
			return
		end

		if running and debugger.execution_state == "stop" then
			socket:write("$S05#B8")
			running = false
			return
		elseif debugger.execution_state == "run" then
			running = true
		end

		local function chksum(str)
			local sum = 0
			str:gsub(".", function(s) sum = sum + s:byte() end)
			return string.format("%.2x", sum & 0xff)
		end

		local function makebestr(val, len)
			local str = ""
			for count = 0, len - 1 do
				str = str .. string.format("%.2x", (val >> (count * 8)) & 0xff)
			end
			return str
		end

		local last = consolelast
		local msg = consolelog[#consolelog]
		consolelast = #consolelog
		if #consolelog > last and msg:find("Stopped at", 1, true) then
			local point = tonumber(msg:match("Stopped at breakpoint ([0-9]+)"))
			local map = regmaps[cpu.shortname]
			running = false
			if not point then
				point = tonumber(msg:match("Stopped at watchpoint ([0-9]+"))
				if not point then
					return -- ??
				end
				local wp = watches.byidx[point]
				if wp then
					local reply = "T05" .. wp.type .. ":" .. makebestr(wp.addr, map.addrsize)
					socket:write("$" .. reply .. "#" .. chksum(reply))
				else
					socket:write("$S05#B8")
				end
				return
			else
				local bp = breaks.byidx[point]
				if bp then
					local reply = "T05hwbreak:" .. makebestr(cpu.state[map.pcreg].value, map.regsize)
					socket:write("$" .. reply .. "#" .. chksum(reply))
				else
					socket:write("$S05#B8")
				end
				return
			end
		end

		if running and debugger.execution_state == "stop" then
			socket:write("$S05#B8")
			running = false
			return
		elseif debugger.execution_state == "run" then
			running = true
		end

		local data = ""

		repeat
			local read = socket:read(100)
			data = data .. read
		until #read == 0
		if #data == 0 then
			return
		end
		if data == "\x03" then
			debugger.execution_state = "stop"
			socket:write("$S05#B8")
			running = false
			return
		end
		local packet, checksum = data:match("%$([^#]+)#(%x%x)")
		if packet then
			packet:gsub("}(.)", function(s) return string.char(string.byte(s) ~ 0x20) end)
			local cmd = packet:sub(1, 1)
			local map = regmaps[cpu.shortname]
			if cmd == "g" then
				local regs = {}
				for reg, idx in pairs(map.togdb) do
					regs[idx] = makebestr(cpu.state[reg].value, map.regsize)
				end
				local data = table.concat(regs)
				socket:write("+$" .. data .. "#" .. chksum(data))
			elseif cmd == "G" then
				local count = 0
				packet:sub(2):gsub(string.rep("%x", map.regsize * 2), function(s)
						count = count + 1
						cpu.state[map.fromgdb[count]].value = tonumber(s,16)
					end)
				socket:write("+$OK#9a")
			elseif cmd == "m" then
				local addr, len = packet:match("m(%x+),(%x+)")
				if addr and len then
					addr = tonumber(addr, 16)
					len = tonumber(len, 16)
					local data = ""
					local space = cpu.spaces["program"]
					for count = 1, len do
						data = data .. string.format("%.2x", space:readv_u8(addr))
						addr = addr + 1
					end
					socket:write("+$" .. data .. "#" .. chksum(data))
				else
					socket:write("+$E00#a5") -- fix error
				end
			elseif cmd == "M" then
				local count = 0
				local addr, len, data = packet:match("M(%x+),(%x+),(%x+)")
				if addr and len and data then
					addr = tonumber(addr, 16)
					local space = cpu.spaces["program"]
					data:gsub("%x%x", function(s) space:writev_u8(addr + count, tonumber(s, 16)) count = count + 1 end)
					socket:write("+$OK#9a")
				else
					socket:write("+$E00#a5")
				end
			elseif cmd == "s" then
				if #packet == 1 then
					cpu.debug:step()
					socket:write("+$OK#9a")
					socket:write("$S05#B8")
					running = false
				else
					socket:write("+$E00#a5")
				end
			elseif cmd == "c" then
				if #packet == 1 then
					cpu.debug:go()
					socket:write("+$OK#9a")
				else
					socket:write("+$E00#a5")
				end
			elseif cmd == "Z" then
				local btype, addr, kind = packet:match("Z([0-4]),(%x+),(.*)")
				addr = tonumber(addr, 16)
				if btype == "0" then
					socket:write("") -- is machine dependant
				elseif btype == "1" then
					if breaks.byaddr[addr] then
						socket:write("+$E00#a5")
						return
					end
					local idx = cpu.debug:bpset(addr)
					breaks.byaddr[addr] = idx
					breaks.byidx[idx] = addr
					socket:write("+$OK#9a")
				elseif btype == "2" then
					if watches.byaddr[addr] then
						socket:write("+$E00#a5")
						return
					end
					local idx = cpu.debug:wpset(cpu.spaces["program"], "w", addr, 1)
					watches.byaddr[addr] = idx
					watches.byidx[idx] = {addr = addr, type = "watch"}
					socket:write("+$OK#9a")
				elseif btype == "3" then
					if watches.byaddr[addr] then
						socket:write("+$E00#a5")
						return
					end
					local idx = cpu.debug:wpset(cpu.spaces["program"], "r", addr, 1)
					watches.byaddr[addr] = idx
					watches.byidx[idx] = {addr = addr, type = "rwatch"}
					socket:write("+$OK#9a")
				elseif btype == "4" then
					if watches.byaddr[addr] then
						socket:write("+$E00#a5")
						return
					end
					local idx = cpu.debug:wpset(cpu.spaces["program"], "rw", addr, 1)
					watches.byaddr[addr] = idx
					watches.byidx[idx] = {addr = addr, type = "awatch"}
					socket:write("+$OK#9a")
				end
			elseif cmd == "z" then
				local btype, addr, kind = packet:match("z([0-4]),(%x+),(.*)")
				addr = tonumber(addr, 16)
				if btype == "0" then
					socket:write("") -- is machine dependent
				elseif btype == "1" then
					if not breaks.byaddr[addr] then
						socket:write("+$E00#a5")
						return
					end
					local idx = breaks.byaddr[addr]
					cpu.debug:bpclr(idx)
					breaks.byaddr[addr] = nil
					breaks.byidx[idx] = nil
					socket:write("+$OK#9a")
				elseif btype == "2" or btype == "3" or btype == "4" then
					if not watches.byaddr[addr] then
						socket:write("+$E00#a5")
						return
					end
					local idx = watches.byaddr[addr]
					cpu.debug:wpclr(idx)
					watches.byaddr[addr] = nil
					watches.byidx[idx] = nil
					socket:write("+$OK#9a")
				end
			elseif cmd == "?" then
				socket:write("+$S05#B8")
			else
				socket:write("+$#00")
			end
		end
	end)
end

return exports
