--love 1
-- This is the main code file for the 8608 architecture emulator.
-- It is meant to be run with the Love2d game engine: https://love2d.org
-- This file deals mostly with the user interface, controls, and high-level functions.
-- Emulation of the CPU internals is done by the 8608emulator.dll library. See 8608emulator.c for details.

-- Number of frames to highlight memory and instructions when they're accessed
local MainHighlightTime = 8
-- If true, char display renders in default print fallback mode; otherwise, terminal print mode is used
local PrintMode = true
-- Maximum number of CPU cycles per frame
local MaxTicksPerFrame = 555

require("colorset")
local ffi = require("ffi")

local lg = love.graphics
local li = love.image
local le = love.event
local lt = love.timer
local lk = love.keyboard
local la = love.audio

local PlaySound
local CPURequestInterrupt
local TimerSchedule
local TickCPU

local jsrInstrs = {0x20, 0x2C, 0x0C}

----
local function InitColorset()
	for i = 0, 63 do
		local c = ColorSet[i+1]
		ColorSet[i] = {
			c[1]/255,
			c[2]/255,
			c[3]/255,
		}
		ColorSet[i+1] = nil
	end
end

local eventTypes = {}
local function registerEvent(mem, addrFirst, addrLast, read, write, func)
	table.insert(eventTypes, func)
	local id = #eventTypes
	for addr = addrFirst, addrLast do
		if read  then mem.c.onread [addr] = id end
		if write then mem.c.onwrite[addr] = id end
	end
end
local function handleEvents(cpu, mem)
	for i = 0, mem.c.numevents-1 do
		local event = mem.c.events[i]
		eventTypes[event.id](event.addr, cpu, mem)
	end
	mem.c.numevents = 0
end

local RegDisplay = {
	scrX = 384+16, scrY = 32+16+8,
	width = 128, height = 192,
	fontWidth = 8, fontHeight = 12,
	registers = {
		{ name = "A"  , idx = "a"    , x=8,y=8      ,w=2},
		{ name = "B"  , idx = "b"    , x=8,y=8+16   ,w=2},
		{ name = "C"  , idx = "c"    , x=8,y=8+16* 2,w=2},
		{ name = "U"  , idx = "u"    , x=8,y=8+16* 3,w=2},
		{ name = "T"  , idx = "t"    , x=8,y=8+16* 4,w=2},
		{ name = "X"  , idx = "p"    , x=8,y=8+16* 5,w=4},
		{ name = "Y"  , idx = "q"    , x=8,y=8+16* 6,w=4},
		{ name = "S"  , idx = "s"    , x=8,y=8+16* 7,w=2},
		{ name = "V"  , idx = "v"    , x=8,y=8+16* 8,w=4},
		{ name = "I"  , idx = "i"    , x=8,y=8+16* 9,w=4},
		{ name = "Ins", idx = "instr", x=8,y=8+16*10,w=2},
		{ name = "Cyc", idx = "cycle", x=64+8-16,y=8+16*10,w=2},
	},
	flags = {
		{ name = "CF" , idx = "cf" , x=64+8,y=8     },
		{ name = "NZ" , idx = "nz" , x=64+8,y=8+16  },
		{ name = "IRq", idx = "irq", x=64+8,y=8+16*2},
		{ name = "IEn", idx = "ien", x=64+8,y=8+16*5},
		{ name = "Int", idx = "ifg", x=64+8,y=8+16*3},
		{ name = "Run", idx = "rfg", x=64+8,y=8+16*4},
	},
}
local function InitRegDisplay(rd)
	lg.print("Registers", rd.scrX, rd.scrY-12)
	lg.rectangle("line", rd.scrX, rd.scrY, rd.width, rd.height+1)
	for i, reg in ipairs(rd.registers) do
		lg.rectangle("line", rd.scrX+reg.x+(rd.fontWidth*(4-reg.w)), rd.scrY+reg.y, rd.fontWidth*reg.w+1, rd.fontHeight+1)
		lg.print(reg.name, rd.scrX+reg.x+32+4, reg.y + rd.scrY)
	end
	for i, flg in pairs(rd.flags) do
		lg.rectangle("line", rd.scrX+flg.x, rd.scrY+flg.y, rd.fontHeight+1, rd.fontHeight+1)
		lg.print(flg.name, rd.scrX+flg.x+rd.fontHeight+4, flg.y+rd.scrY)
	end
end
local function printValue(val, w, x, y, fw)
	for i = 1, w do
		local v = math.floor(val/math.pow(16,i-1))%16
		lg.print(string.format("%01X", v), x+(fw*(4-i)), y+1)
	end
end
local function RedrawRegDisplay(rd, cpu, mem)
	for i, reg in ipairs(rd.registers) do
		lg.setColor(0,0,0)
		lg.rectangle("fill", rd.scrX+reg.x+(rd.fontWidth*(4-reg.w)), rd.scrY+reg.y, rd.fontWidth*reg.w, rd.fontHeight)
		lg.setColor(1,1,1)
		local val = cpu.c[reg.idx]%(math.pow(16, reg.w))
		printValue(val, reg.w, rd.scrX+reg.x, rd.scrY+reg.y, rd.fontWidth)
	end
	for i, flg in pairs(rd.flags) do
		local val = cpu.c[flg.idx]
		if val~=0 then
			lg.setColor(1,1,1)
		else
			lg.setColor(0,0,0)
		end
		lg.rectangle("fill", rd.scrX+flg.x, rd.scrY+flg.y, rd.fontHeight, rd.fontHeight)
	end
	lg.setColor(1,1,1)
end

local ReadMemory
local WriteMemory
local StackDisplay = {
	scrX = 8+384+32+6, scrY = 32+192+32-4+16,
	lines = 16, fontHeight = 12,
}
local function InitStackDisplay(sd)
	sd.width = 96+1
	sd.height = 12+(sd.lines*sd.fontHeight)
	lg.print("Stack", sd.scrX, sd.scrY-12)
	lg.rectangle("line", sd.scrX, sd.scrY, sd.width+1, sd.height+1)
end
local function RedrawStackDisplay(sd, cpu, mem)
	lg.setColor(0,0,0)
	lg.rectangle("fill", sd.scrX, sd.scrY, sd.width, sd.height)
	lg.setColor(1,1,1)
	for i = 1, sd.lines do
		local addr = ((cpu.c.s%256)+256-i)%65536
		local val = ReadMemory(mem, addr)
		if addr>=0x0100 then
			lg.print(string.format("%04X -%2i %02X", addr, i, val), sd.scrX+8, sd.scrY+8+(12*(i-1)))
		end
	end
end

local MemoryDisplays = {
	{
		scrX = 8, scrY = 32+192+32-4+16,
		columns = 16, columnSpace = 4, rows = 16,
		fontHeight = 12,
		showAscii = false,
		addr = 0x0000,
		highlightTime = MainHighlightTime,
	},
}
local function InitMemoryDisplay(md)
	local cw = 7
	md.mainCols = 9+md.columns*3+md.columnSpace-1
	md.width = cw*(md.mainCols)+cw*(md.showAscii and (md.columns+5) or 0)-8
	md.height = 12+md.fontHeight*md.rows
	lg.print("Memory", md.scrX, md.scrY-12)
	lg.rectangle("line", md.scrX, md.scrY, md.width+1, md.height+1)
end
local function RedrawMemoryDisplay(md, cpu, mem)
	lg.setColor(0,0,0)
	lg.rectangle("fill", md.scrX, md.scrY, md.width, md.height)
	lg.setColor(1,1,1)
	
	local highlightAddrs = {}
	highlightAddrs[cpu.c.p] = "X"
	highlightAddrs[cpu.c.q] = "Y"
	highlightAddrs[(cpu.c.s%256)+256] = "S"
	highlightAddrs[cpu.c.v] = "V"
	highlightAddrs[(cpu.c.i-1)%65536] = "I"
	
	local cw = 7
	
	local tickDraw = cpu.c.frame-md.highlightTime
	
	for l = 1, md.rows do
		local addr = md.addr + (l-1)*md.columns
		local lx = md.scrX+8
		local ly = md.scrY+8+(md.fontHeight*(l-1))
		lg.print(string.format("%04X", addr).." | ", lx, ly)
		lx = lx+(cw*7)
		for x = 1, md.columns do
			local a = addr + (x-1)
			local v = ReadMemory(mem, a)
			if highlightAddrs[a] then
				lg.line(lx, ly, lx, ly+11)
				lg.line(lx-1, ly, lx+15, ly)
				lg.line(lx-1, ly+11, lx+15, ly+11)
				lg.print(highlightAddrs[a], lx-cw, ly)
			end
			if mem.c.writes[a] > tickDraw then
				lg.rectangle("fill",lx-1,ly,cw*2+2,11)
				lg.setColor(0,0,0)
			end
			lg.print(string.format("%02X", v), lx, ly)
			lg.setColor(1,1,1)
			if mem.c.reads[a] > tickDraw then
				lg.rectangle("line", lx-1, ly, cw*2+2+1, 11+1)
			end
			lx = lx+(cw*3)
			if x%md.columnSpace==0 then lx = lx+cw end
		end
		if md.showAscii then
			lg.print("|", lx, ly)
			lx = lx+cw
		end
		if md.showAscii then
			for x = 1, md.columns do
				local a = addr + (x-1)
				local v = ReadMemory(mem, a)
				if v>=128 then v = 0 end
				local c = string.char(v)
				lg.print(c, md.scrX+lx+((x-1)*cw), md.scrY+8+(12*(l-1)))
			end
		end
	end
end

local ProgramDisplay = {
	scrX = 8+384+8+128+8, scrY = 32+8+16,
	fontHeight = 12,
	numLines = 34,
	highlightTime = MainHighlightTime,
	maxChars = 30,
}
local function toPrintableChar(v)
	if v>=0x20 and v<=0x7E then return string.char(v)
	else return "\\x"..string.format("%02X", v) end
end
local function pdLinesFromDasm(dasm, maxChars)
	local div = "|"
	local lines, addrLines, lineAddrs = {}, {}, {}
	local function addDasmLine(addr, data, text)
		text = text:sub(1, maxChars - #div*2 - 8)
		local dataStr = ""
		if data then
			local dt = {}
			for _, v in ipairs(data) do
				table.insert(dt, string.format("%02X", v))
			end
			dataStr = table.concat(dt, " ")
		end
		dataStr = dataStr .. (" "):rep(8 - #dataStr)
		if (not data) or #data==0 then
			table.insert(lines, (" "):rep(4+#div+8) .. div .. text)
		else
			table.insert(lines,
				string.format("%04X", addr) .. div ..
				dataStr .. div ..
				text
			)
		end
		local la = {}
		if data then
			for i = 1, #data do
				addrLines[addr+i-1] = #lines
				table.insert(la, addr+i-1)
			end
		end
		lineAddrs[#lines] = la
	end
	
	for line in dasm:gmatch("[^\r\n]+") do
		if line~="" and not line:find("^ *outp | addr | data %(base 16%)$") then
			local addrh, datah, rest = line:match("^ *[0-9a-fA-F:]+ | *([0-9a-fA-F]+) | ([0-9a-fA-F ]*) ; +(.+)$")
			local text = rest
			local label = nil
			if rest:find(":") then label, text = rest:match("^(.+): *(.*)$") end
			local addr = tonumber(addrh, 16)
			
			if label then
				addDasmLine(addr, nil, label..":")
			else
				local len = 0
				local maxlen = 3
				local data = {}
				local datastr = {}
				for v in datah:gfind("[0-9a-fA-F][0-9a-fA-F]") do
					if #data>=maxlen then
						addDasmLine(addr+len-#data, data, "\""..table.concat(datastr).."\"")
						data = {}
						datastr = {}
					end
					table.insert(data, tonumber(v, 16))
					table.insert(datastr, toPrintableChar(tonumber(v, 16)))
					len = len+1
				end
				if len<=maxlen then
					addDasmLine(addr, data, text)
				elseif #data>0 then
					addDasmLine(addr+len-#data, data, "\""..table.concat(datastr).."\"")
				end
			end
		end
	end
	return lines, addrLines, lineAddrs
end
local function InitProgramDisplay(pd, dasmText)
	pd.width = 256
	pd.height = 12+pd.fontHeight*pd.numLines-3
	lg.print("Program", pd.scrX, pd.scrY-12)
	lg.rectangle("line", pd.scrX, pd.scrY, pd.width, pd.height)
	pd.firstLine = 1
	pd.lines, pd.addrLines, pd.lineAddrs = pdLinesFromDasm(dasmText, pd.maxChars)
	pd.midLine = math.floor(pd.numLines/2)
	pd.init = true
end
local function RedrawProgramDisplay(pd, cpu, mem)
	if not pd.init then return end
	lg.setColor(0,0,0)
	lg.rectangle("fill", pd.scrX, pd.scrY, pd.width-1, pd.height-1)
	lg.setColor(1,1,1)
	
	local rectwidth = pd.width-18
	local rectheight = pd.fontHeight-1
	local tickDraw = cpu.c.frame-pd.highlightTime
	
	local instrAddr = (cpu.c.i-1)%65536
	local instrLine = pd.addrLines[instrAddr]
	if instrLine then
		if pd.firstLine > instrLine then pd.firstLine = math.max(instrLine-pd.midLine, 1)
		elseif pd.firstLine+pd.numLines-1 < instrLine then pd.firstLine = math.min(instrLine-pd.numLines+pd.midLine, #pd.lines-pd.numLines+1)
		end
	end
	local screenlineidx = 0
	for lineidx = pd.firstLine, pd.firstLine+pd.numLines-1 do
		local line = pd.lines[lineidx]
		local lineaddrs = pd.lineAddrs[lineidx]
		if line then
			local x, y = pd.scrX+8, pd.scrY+4+screenlineidx*pd.fontHeight
			if lineaddrs then
				for _, addr in ipairs(lineaddrs) do
					if mem.c.reads[addr] > tickDraw then
						lg.rectangle("line", x, y, rectwidth, rectheight)
					end
				end
			end
			if instrLine==lineidx then
				lg.rectangle("fill", x, y, rectwidth, rectheight)
				lg.setColor(0,0,0)
			end
			lg.print(line, x, y)
			lg.setColor(1,1,1)
		end
		screenlineidx = screenlineidx + 1
	end
end

--local VideoDisplay = {
--	width = 256, height = 128,
--	scrX = 8, scrY = 256,
--	addr = 0x8000,
--}
--local function InitVideoDisplay(vd)
--	lg.print("Video Display", vd.scrX, vd.scrY-12)
--	vd.imageData = li.newImageData(vd.width, vd.height)
--	lg.setColor(1,1,1)
--	lg.rectangle("line", vd.scrX, vd.scrY, vd.width+1, vd.height+1)
--end
local CharDisplay = {
	width = 64*6, height = 16*12,
	rows = 16, cols = 64,
	fontWidth = 6, fontHeight = 12,
	scrX = 8, scrY = 32+8+16,
	addrChar = 0xC000,
	addrColor = 0xC800,
}
local function InitCharDisplay(cd)
	lg.print("Char Display", cd.scrX, cd.scrY-12)
	cd.font = lg.newFont("content/consola.ttf", cd.fontHeight-1, "mono")
end

local Keyboard = {
	addrRange = {0xF100, 0xF1FF},
	queueSize = 16,
	queue = {},
	interrupts = false,
	queueEmpty = true,
}
local function kbSetNext(kb, cpu, mem)
	local newval = kb.queue[1] or 0
	if mem.c.data[kb.addrRange[1]] ~= newval then
		for a = kb.addrRange[1], kb.addrRange[2] do
			mem.c.data[a] = newval
		end
	end
end
local function KeyboardOnRead(addr, cpu, mem, kb)
	table.remove(kb.queue, 1)
	kbSetNext(kb, cpu, mem)
end
local function KeyboardOnWrite(addr, cpu, mem, kb)
	local val = mem.c.data[addr]
	kb.interrupts = val~=0
	mem.c.data[addr] = kb.queue[1] or 0
end
local keycodes = require("keycodes")
local function KeyboardOnKey(kb, key, press, cpu, mem)
	local code = keycodes[key] or keycodes["invalid"]
	if code==0x7F then print("invalid key: "..key) end
	table.insert(kb.queue, code + (press and 128 or 0))
	if #kb.queue > kb.queueSize then table.remove(kb.queue, 1) end
	kb.queueEmpty = false
	kbSetNext(kb, cpu, mem)
	if kb.interrupts then CPURequestInterrupt(cpu) end
end

local function RedrawFPSCounter(x, y)
	lg.setColor(0,0,0)
	lg.rectangle("fill",x,y,64,12)
	lg.setColor(1,1,1)
	lg.print("FPS: "..lt.getFPS(), x,y)
end
local function printHighlight(s, o, h, x, y)
	x = x+o*7
	local w = 7*#s
	lg.setColor(1,1,1)
	if h then
		lg.rectangle("fill", x, y, w, 12)
		lg.setColor(0,0,0)
	end
	lg.print(s, x, y)
end
local function RedrawKeyInfo(x, y, uk, run)
	lg.setColor(0,0,0)
	lg.rectangle("fill",x,y,768,12+16)
	lg.setColor(1,1,1)
		printHighlight("[F4] Toggle keyboard", 0, lk.isDown("f4"), x, y)
	lg.setColor(1,1,1)
	if uk then
		printHighlight("Keystrokes passed to device", 23, false, x, y)
	else
		printHighlight("[R] "..(run and "Stop" or "Run "), 23, lk.isDown("r"), x, y)
		if not run then
		printHighlight("[S] Step"                        , 33, lk.isDown("s"), x, y)
	--	printHighlight("[T] Tick once"                   , 48, lk.isDown("t"), x, y)
		printHighlight("[X] Step Over"                   , 43, lk.isDown("x"), x, y)
		end
		printHighlight("[I] Interrupt"                   , 58, lk.isDown("i"), x, y)
		printHighlight("[Q] Quit"                        , 73, lk.isDown("q"), x, y)
	end
end

local GPIO = {}
local function InitGPIO(gpio)
	gpio.mulLeft = 0
	gpio.mulRight = 0
	gpio.divLeft = 0
	gpio.divRight = 0
	gpio.timerCount = 0
end
local function UpdateGPIO(gpio, cpu, mem)
	if gpio.timerCount>0 then
		gpio.timerCount = gpio.timerCount-1
		if gpio.timerCount<=0 then
			CPURequestInterrupt(cpu)
		end
	end
end
local function gpioSetValue(name, func)
	return function(addr, cpu, mem, gpio)
		gpio[name] = ReadMemory(mem, addr)
		func(addr, cpu, mem, gpio)
	end
end
local function gpioMul(addr, cpu, mem, gpio)
	local base = math.floor(addr/256)*256
	local res = gpio.mulLeft*gpio.mulRight
	WriteMemory(mem, base+0x00, math.floor(res/256))
	WriteMemory(mem, base+0x01, res%256)
end
local function gpioDiv(addr, cpu, mem, gpio)
	local base = math.floor(addr/256)*256
	WriteMemory(mem, base+0x02, math.floor(gpio.divLeft/gpio.divRight))
	WriteMemory(mem, base+0x03, gpio.divLeft%gpio.divRight)
end
local function gpioPopcount(v)
	return 0 -- todo
end
local gpioFunctions = {
	[0x00] = gpioSetValue("mulLeft" , gpioMul),
	[0x01] = gpioSetValue("mulRight", gpioMul),
	[0x02] = gpioSetValue("divLeft" , gpioDiv),
	[0x03] = gpioSetValue("divRight", gpioDiv),
	[0x04] = function(addr, cpu, mem, gpio) WriteMemory(mem, addr, gpioPopcount(ReadMemory(mem, addr))) end,
	[0x05] = function(addr, cpu, mem, gpio) gpio.timerCount = 60/10; WriteMemory(mem, addr, 0); end
}
local function GPIOOnWrite(addr, cpu, mem, gpio)
	local offset = addr%256
	local func = gpioFunctions[offset]
	if func then
		func(addr, cpu, mem, gpio)
	end
end

local peripherals = {
	CharDisplay  = { range = {0xC000, 0xCFFF}, write = true  },
	BootROM      = { range = {0xFC00, 0xFFFF}, write = false },
	MainRAM      = { range = {0x0000, 0x1FFF}, write = true  },
--	VideoDisplay = { range = {0x8000, 0xFFFF}, write = true  },
	Keyboard     = { range = {0xF100, 0xF1FF}, write = true,
		onread  = function(addr, cpu, mem) KeyboardOnRead (addr, cpu, mem, Keyboard) end,
		onwrite = function(addr, cpu, mem) KeyboardOnWrite(addr, cpu, mem, Keyboard) end,
	},
	GPIO         = { range = {0xF000, 0xF0FF}, write = true,
		onwrite = function(addr, cpu, mem) GPIOOnWrite(addr, cpu, mem, GPIO) end,
	},
}

----
-- Console
local Console = {
	scrX = 8, scrY = 20,
	width = 512+8, height = 20,
	maxChars = 72,
	blinkFrames = 60,
}
local function InitConsole(con)
	con.blinkFrame = 0
	con.input = ""
	con.prevInputs = {}
	con.prevInputIdx = 0
	con.tempError = nil
end
local function RedrawConsole(con, kb)
	lg.setColor(0,0,0)
	lg.rectangle("fill", con.scrX, con.scrY, con.width, con.height)
	lg.setColor(1,1,1)
	lg.rectangle("line", con.scrX, con.scrY, con.width, con.height)
	if not kb then
		if con.blinkFrame < con.blinkFrames/2 then
			local curx, cury = con.scrX + #con.input*7 + 2, con.scrY+3
			lg.rectangle("fill", curx, cury, 8, 12)
		end
		con.blinkFrame = con.blinkFrame + 1
		if con.blinkFrame >= con.blinkFrames then con.blinkFrame = 0 end
		if #con.input==0 then
			lg.setColor(0.5,0.5,0.5)
			lg.print(con.tempError or "Enter Command (>ticks | !addr | @addr | addr=byte | addr=byte1 byte2...)", con.scrX+2+8, con.scrY+4)
		else
			lg.print(con.input, con.scrX+2, con.scrY+4)
		end
	end
end
local function ConsoleError(con, err)
	con.tempError = err
end
local function conWriteDataBlock(con, mem, addr, data)
	local writeTime = 2
	ConsoleError(con, "Writing")
	for i, byte in ipairs(data) do
		TimerSchedule((i-1)*writeTime, function()
			if con.tempError ~= nil then
				con.tempError = con.tempError .. "."
			end
			mem.c.data[(addr+i-1)%65536] = byte
			PlaySound("write")
		end)
	end
	TimerSchedule((#data-1)*writeTime, function()
		PlaySound("writeDone")
		PlaySound("success")
		ConsoleError(con, "Wrote " .. #data .. " byte" .. (#data>1 and "s" or "") .. " to address $" .. string.format("%04X", addr))
	end)
end
local function tickCpuOnce(cpu, mem, byInstr)
	TickCPU(cpu, mem, 1, byInstr, nil)
end
local function tickCpuUntil(con, cpu, mem, contfunc, strfunc, donestr, step)
	con.tempError = ""
	local tickTime = 1
	local tickStep, byInstr
	if step then
		tickStep = step
		byInstr = true
	else
		tickStep = MaxTicksPerFrame
		byInstr = false
	end
	local i = 1
	local recFunc
	recFunc = function()
		for j = 1, tickStep do
			if contfunc(i) then
				tickCpuOnce(cpu, mem, byInstr)
			else
				PlaySound("writeDone")
				PlaySound("success")
				ConsoleError(con, donestr)
				return
			end
			i = i+1
		end
		if con.tempError ~= nil then
			con.tempError = strfunc(i)
		end
		PlaySound("write")
		TimerSchedule(tickTime, recFunc)
	end
	recFunc()
end
local function tickCpuStepOver(con, cpu, mem, pd)
	local stepOver = false
	for _, v in ipairs(jsrInstrs) do
		if cpu.c.instr == v then
			stepOver = true
		end
	end
	if not stepOver then
		tickCpuOnce(cpu, mem, true)
		return
	end
	
	local line = pd.addrLines and pd.addrLines[(cpu.c.i-1)%65536]
	if not line then
		tickCpuOnce(cpu, mem, true)
		return
	end
	local addr
	while not addr do
		line = line+1
		local addrs = pd.lineAddrs[line]
		if not addrs then
			tickCpuOnce(cpu, mem, true)
			return
		end
		addr = addrs[1]
	end
	--print(string.format("%04X", addr))
	tickCpuUntil(con, cpu, mem,
		--function() return (cpu.c.i ~= (addr+1)%65536) and (cpu.c.rfg==1) end,
		function() return (cpu.c.i ~= (addr+1)%65536) end,
		function() return "Running to address $" .. string.format("%04X", addr) end,
		nil,
		nil
	)
end
local function ConsoleExec(con, cpu, mem)
	PlaySound("enter")
	if con.input=="" then
		PlaySound("error")
		con.tempError = nil
		return
	end
	
	local ip = con.input
	con.input = ""
	print("exec \""..ip.."\"")
	
	table.insert(con.prevInputs, 1, ip)
	con.prevInputIdx = 0
	
	if ip:sub(1, 1)=="@" then
		local rest = ip:sub(2, #ip)
		local addr = tonumber(rest, 16)
		if not addr then
			ConsoleError(con, "Error: Expected a hex number")
			PlaySound("error")
			return
		end
		if addr<0 then addr = 0 end
		if addr>65536-256 then addr = 65536-256 end
		MemoryDisplays[1].addr = addr
		ConsoleError(con, "Memory display set to address $" .. string.format("%04X", addr))
		PlaySound("success")
	elseif ip:sub(1, 1)=="!" then
		if RunCPU then
			ConsoleError(con, "CPU must be paused to change execution")
			PlaySound("error")
			return
		end
		local rest = ip:sub(2, #ip)
		local addr = tonumber(rest, 16)
		if not addr then
			ConsoleError(con, "Error: Expected a hex number")
			PlaySound("error")
			return
		end
		if addr<0 then addr = 0 end
		if addr>65535 then addr = 65535 end
		cpu.c.i = (addr+1)%65536
		cpu.c.cycle = 0
		cpu.c.instr = ReadMemory(mem, addr)
		ConsoleError(con, "CPU instruction pointer set to $" .. string.format("%04X", addr))
		PlaySound("success")
	elseif ip:sub(1, 2)==">>" then
		if RunCPU then
			ConsoleError(con, "CPU must be paused to step")
			PlaySound("error")
			return
		end
		local rest = ip:sub(3, #ip)
		local addr = tonumber(rest, 16)
		if not addr then
			ConsoleError(con, "Error: Expected a hex number")
			PlaySound("error")
			return
		end
		if addr<0 then addr = 0 end
		if addr>65535 then addr = 65535 end
		tickCpuUntil(con, cpu, mem,
			function() return cpu.c.i ~= (addr+1)%65536 end,
			function() return "Running to address $" .. string.format("%04X", addr) end,
			"Ran to address " .. string.format("%04X", addr),
			nil
		)
	elseif ip:sub(1, 1)==">" then
		if RunCPU then
			ConsoleError(con, "CPU must be paused to single-step")
			PlaySound("error")
			return
		end
		local rest = ip:sub(2, #ip)
		local steps = tonumber(rest, 16)
		if rest=="" then steps = 1 end
		if not steps then
			ConsoleError(con, "Error: Expected a hex number")
			PlaySound("error")
			return
		end
		if steps<1 then steps = 1 end
		if steps>65535 then steps = 65535 end
		tickCpuUntil(con, cpu, mem,
			function(i) return i<=steps; end,
			function(i) return "Step $"..string.format("%X", i).."/$"..string.format("%X", steps) end,
			"Ran $" .. string.format("%02X", steps) .. " steps",
			steps<(MaxTicksPerFrame/4) and steps or nil
		)
	elseif ip:find("=") then
		local addrS, rest = ip:match("^ *([0-9a-fA-F]+) *= *([0-9a-fA-F ]+)$")
		local addr = tonumber(addrS or "", 16)
		if not (addr and rest) then
			ConsoleError(con, "Error: Expected [hex number] = [hex numbers]")
			PlaySound("error")
			return
		end
		local data = {}
		for byteS in rest:gmatch("[0-9a-fA-F]+") do
			local byte = tonumber(byteS, 16)
			if byte>255 then
				ConsoleError(con, "Error: Data byte \"" .. byteS .. "\" is greater than $FF")
				PlaySound("error")
				return
			end
			table.insert(data, byte)
		end
		
		conWriteDataBlock(con, mem, addr, data)
	else
		ConsoleError(con, "Error: Unknown command")
		PlaySound("error")
	end
end
local function shiftDown()
	return lk.isDown("lshift") or lk.isDown("rshift")
end
local function ConsoleKey(con, k, cpu, mem)
	local add
	if k=="backspace" then con.input = con.input:sub(1, #con.input-1)
	elseif shiftDown() and k=="1" then add = "!"
	elseif shiftDown() and k=="2" then add = "@"
	elseif shiftDown() and k=="." then add = ">"
	elseif k>="0" and k<="9" then add = k
	elseif #k==1 and k>="a" and k<="f" then add = k:upper()
	elseif k=="=" then add = "="
	elseif k=="space" then add = " "
	elseif k=="return" then ConsoleExec(con, cpu, mem)
	elseif k=="up" and con.prevInputs[con.prevInputIdx+1] then
		if con.prevInputIdx==0 then con.prevInputs[0] = con.input end
		con.prevInputIdx = con.prevInputIdx + 1
		con.input = con.prevInputs[con.prevInputIdx]
	elseif k=="down" then
		if con.prevInputIdx>0 then
			con.prevInputIdx = con.prevInputIdx - 1
			con.input = con.prevInputs[con.prevInputIdx]
		else
			if con.input ~= "" then table.insert(con.prevInputs, 1, con.input) end
			con.input = ""
		end
	else return end
	if add and #con.input < con.maxChars then con.input = con.input .. add; con.tempError = nil; end
	con.blinkFrame = 0
end
local function DrawHelp()
	
end

----
-- Sound

local Sounds = {}
local soundNames = {
	"enter","error",
	"interrupt",
	"keyOff","keyOn",
	"runOff","runOn",
	"start",
	"step",
	"success",
	"write","writeDone",
}
local soundCounts = {
	--["step"] = 100,
	["write"] = 100,
}
local function InitSound()
	for _, name in ipairs(soundNames) do
		local sound = {
			name = name,
			sources = {},
			lastPlayed = 0,
		}
		local fn = "content/" .. name .. ".wav"
		for i = 1, soundCounts[name] or 4 do
			table.insert(sound.sources, la.newSource(fn, "static"))
		end
		Sounds[name] = sound
	end
end
PlaySound = function(name)
	local sound = Sounds[name] or error("no sound with name: " .. name)
	sound.lastPlayed = (sound.lastPlayed % #sound.sources) + 1
	sound.sources[sound.lastPlayed]:play()
end

----
-- Timer

local TimerFrame = 0
local TimerSchedules = {}
TimerSchedule = function(time, func)
	local frame = TimerFrame + time
	TimerSchedules[frame] = TimerSchedules[frame] or {}
	table.insert(TimerSchedules[frame], func)
end
local function UpdateTimer()
	if TimerSchedules[TimerFrame] then
		for _, func in ipairs(TimerSchedules[TimerFrame]) do
			func()
		end
		TimerSchedules[TimerFrame] = nil
	end
	TimerFrame = TimerFrame + 1
end


----
-- CPU Interface

ffi.cdef [[
struct Event {
	int id;
	int addr;
};
struct Memory {
	int data[65536];
	int canwrite[65536];
	int writes[65536];
	int reads[65536];
	int onread[65536];
	int onwrite[65536];
	struct Event events[4096];
	int numevents;
};
]]
local Memory = {
	c = ffi.new("struct Memory"),
}
local function InitMemory(mem, pers)
	for i = 0, 65535 do
		mem.c.data[i] = 0
		mem.c.canwrite[i] = 0
		mem.c.writes[i] = 0
		mem.c.reads[i] = 0
		mem.c.onread[i] = 0
		mem.c.onwrite[i] = 0
	end
	for k, per in pairs(pers) do
		if per.onread  then registerEvent(mem, per.range[1], per.range[2], true , false, per.onread ) end
		if per.onwrite then registerEvent(mem, per.range[1], per.range[2], false, true , per.onwrite) end
		for a = per.range[1], per.range[2] do
			mem.c.canwrite[a] = (per.write and 1 or 0)
		end
	end
end
ReadMemory = function(mem, addr)
	return mem.c.data[addr%65536]%256
end
WriteMemory = function(mem, addr, val)
	if mem.c.canwrite[addr%65536]~=0 then
		mem.c.data[addr%65536] = val%256
	end
end
--local function AssembleToMemory(mem, fn, arch)
--	local data, code, symbols = asm.assembleFile(fn)
--	for addr = 0, 65535 do
--		if data[addr] then
--			mem.c.data[addr] = data[addr]
--		end
--	end
--	return data, code, symbols
--end
local function CopyBinToMemory(mem, binText)
	for i = 1, #binText do
		local c = binText:sub(i, i)
		local v = string.byte(c)
		local addr = i-1
		mem.c.data[addr] = v
	end
end
local function CopyHexToMemory(mem, hexText)
	for i = 1, #hexText, 2 do
		local c = hexText:sub(i, i+1)
		local v = tonumber(c, 16) or error("CopyHexToMemory: Invalid hex pair \"" .. c .. "\"")
		local addr = (i-1)/2
		mem.c.data[addr] = v
	end
end

ffi.cdef [[
struct CPU {
	int a;
	int b;
	int c;
	int u;
	int t;
	int p;
	int q;
	int s;
	int v;
	int i;
	int cf;
	int nz;
	int irq;
	int ifg;
	int rfg;
	int ien;
	int instr;
	int cycle;
	int instrpre;
	int frame;
};
int TickCPU(struct CPU* const cpu, struct Memory* const mem, const int count, const int countinstrs, const int breakaddr);
]]
local CPU = {
	c = ffi.new("struct CPU"),
}
local cpuDll = ffi.load("8608emulator.dll")
TickCPU = function(cpu, mem, count, countinstrs, breakaddr)
	local countleft = count
	while countleft>0 do
		countleft = cpuDll.TickCPU(cpu.c, mem.c, countleft, countinstrs and 1 or 0, breakaddr or 0xFFFFFFFF)
		handleEvents(cpu, mem)
	end
end
local function InitCPU(cpu)
	cpu.c.rfg = 1
	cpu.c.instr = 0x41 -- reset instruction
end
CPURequestInterrupt = function(cpu)
	cpu.c.irq = 1;
end

----

--local function RedrawVideoDisplay(vd, mem)
--	local vd = VideoDisplay
--	for y = 0, vd.height-1 do
--		for x = 0, vd.width-1 do
--			local a = vd.addr + y*vd.width + x
--			local colorid = ReadMemory(mem, a)%64
--			local color = ColorSet[colorid]
--			vd.imageData:setPixel(x, y, color[1], color[2], color[3])
--		end
--	end
--	local img = lg.newImage(vd.imageData)
--	lg.setColor(1,1,1)
--	lg.draw(img, vd.scrX, vd.scrY)
--end
local function RedrawCharDisplay(cd, mem)
	lg.rectangle("line", cd.scrX, cd.scrY, cd.width+1, cd.height+1)
	lg.setColor(0,0,0)
	lg.rectangle("fill", cd.scrX, cd.scrY, cd.width, cd.height)
	lg.setFont(cd.font)
	local cd = CharDisplay
	for cy = 0, cd.rows-1 do
		for cx = 0, cd.cols-1 do
			local abase = cy*cd.cols + cx
			local achar = cd.addrChar + abase
			local acolor = cd.addrColor + abase
			local colormem = ReadMemory(mem, acolor)
			local colorid = colormem%64
			local highlight = PrintMode or colormem>=128
			lg.setColor(ColorSet[colorid])
			local scrx = cd.scrX + cx*cd.fontWidth
			local scry = cd.scrY + cy*cd.fontHeight
			local scrw = cd.fontWidth
			local scrh = cd.fontHeight
			if highlight then
				lg.rectangle("fill", scrx, scry, scrw, scrh)
				if PrintMode then lg.setColor(1,1,1) else lg.setColor(0,0,0) end
			end
			local val = ReadMemory(mem, achar)%128
			if val>=0x20 and val<=0x7F then -- printable ascii
				local char = string.char(val)
				lg.print(char, scrx, scry)
			elseif val>=0x10 and val<=0x17 then -- solid color
				local r, g, b = math.floor(val/4)%2, math.floor(val/2)%2, val%2
				lg.setColor(r, g, b)
				lg.rectangle("fill", scrx, scry, scrw, scrh)
			elseif val>=0x80 and val<=0x8F then -- 2x2 pixels
				lg.setColor(0,0,0)
				if            val   %2==1 then lg.rectangle("fill", scrx+scrw/2, scry+scrh/2, scrw/2, scrh/2) end -- bottom right
				if math.floor(val/2)%2==1 then lg.rectangle("fill", scrx       , scry+scrh/2, scrw/2, scrh/2) end -- bottom left
				if math.floor(val/4)%2==1 then lg.rectangle("fill", scrx+scrw/2, scry       , scrw/2, scrh/2) end -- top right
				if math.floor(val/4)%2==1 then lg.rectangle("fill", scrx       , scry       , scrw/2, scrh/2) end -- top left
			elseif val>=0xA0 and val<=0xDF then -- kana
				-- todo
			end
		end
	end
	lg.setColor(1,1,1)
	lg.setFont(InfoFont)
end
local function InitWindowCanvas()
	WindowCanvas = lg.newCanvas(WindowX, WindowY)
	lg.setCanvas(WindowCanvas)
	lg.setColor(1,1,1)
	lg.setFont(InfoFont)
	lg.print("8608 Emulator", 4, 4)
end
local function RedrawWindow(usekeyboard, runcpu)
	lg.setCanvas(WindowCanvas)
	lg.setFont(InfoFont)
	RedrawCharDisplay(CharDisplay, Memory)
	--RedrawVideoDisplay(VideoDisplay, Memory)
	RedrawRegDisplay(RegDisplay, CPU, Memory)
	RedrawStackDisplay(StackDisplay, CPU, Memory)
	RedrawProgramDisplay(ProgramDisplay, CPU, Memory)
	for _, md in ipairs(MemoryDisplays) do RedrawMemoryDisplay(md, CPU, Memory) end
	RedrawFPSCounter(128, 4)
	RedrawKeyInfo(128+64+16, 4, usekeyboard, runcpu)
	RedrawConsole(Console, usekeyboard)
	
	lg.setCanvas()
end

----

local function readFile(fn)
	local fi = io.open(fn, "rb")
	if not fi then print("Could not open file: \""..fn.."\"") return end
	local data = fi:read("*a")
	fi:close()
	return data
end
local function fileExt(fn)
	return fn:match("%.([^%.]+)$")..""
end
local function changeFileExt(fn, ext)
	return fn:gsub("%.[^%.]+$", "."..ext)
end
local function fileExists(fn)
	local fi = io.open(fn, "rb")
	if fi then
		fi:close()
		return true
	else
		return false
	end
end
local function importListing(fn)
	if not fileExists(fn) then return false end
	local text = readFile(fn)
	InitProgramDisplay(ProgramDisplay, text)
end
local function importProgram(fn)
	local ext = fileExt(fn)
	local data = readFile(fn)
	if not data then return end
	if ext=="bin" then
		CopyBinToMemory(Memory, data)
	elseif ext=="hex" then
		CopyHexToMemory(Memory, data)
	else
		print("importProgram: Invalid file extension: \""..ext.."\"")
		return
	end
	if not importListing(changeFileExt(fn, "lis")) then importListing(changeFileExt(fn, "txt")) end
end
function love.load()
	InitColorset()
	lg.setDefaultFilter("nearest", "nearest")
	lg.setLineWidth(1)
	lg.setLineStyle("rough")
	InfoFont = lg.newFont("content/consola.ttf", 12, "mono")
	InitMemory(Memory, peripherals)
	InitWindowCanvas()
	InitCPU(CPU)
	InitGPIO(GPIO)
	--InitVideoDisplay(VideoDisplay)
	InitCharDisplay(CharDisplay)
	InitRegDisplay(RegDisplay)
	InitStackDisplay(StackDisplay)
	InitConsole(Console)
	InitSound()
	
	if arg[2] then
		importProgram(arg[2])
	end
	
	for _, md in ipairs(MemoryDisplays) do InitMemoryDisplay(md) end
	RedrawWindow()
	lg.setCanvas()
	
	PlaySound("start")
end

local targetFrameRate = 60
local targetFrameTime = (1/targetFrameRate)-0.002
local frameStartTime = 0
local lastFrameTime = 0
local function startFrame()
	frameStartTime = lt.getTime()
end
local function endFrame()
	local frameTime = lt.getTime() - frameStartTime
	if frameTime < targetFrameTime then
		lt.sleep(targetFrameTime - frameTime)
		lastFrameTime = frameTime
	end
end

local RunCPU = false
local CPUSpeed = MaxTicksPerFrame
local UseKeyboard = false
function love.draw()
	startFrame()
	
	UpdateTimer()
	
	UpdateGPIO(GPIO, CPU, Memory)
	
	CPU.c.frame = CPU.c.frame + 1
	if RunCPU then
		TickCPU(CPU, Memory, CPUSpeed, false, nil)
	end
	
	RedrawWindow(UseKeyboard, RunCPU)
	
	lg.setColor(1,1,1)
	lg.draw(WindowCanvas, 0, 0, 0, 2, 2)
	
	endFrame()
end

function love.keypressed(k)
	if k=="f4" then
		UseKeyboard = not UseKeyboard; PlaySound(UseKeyboard and "keyOn" or "keyOff");
	else
		if UseKeyboard then
			KeyboardOnKey(Keyboard, k, true, CPU, Memory)
		else
			    if k=="q" then le.quit()
			elseif k=="s" and (not RunCPU) then tickCpuOnce(CPU, Memory, true); PlaySound("step");
			elseif k=="x" and (not RunCPU) then tickCpuStepOver(Console, CPU, Memory, ProgramDisplay); PlaySound("step");
			--elseif k=="t" then TickCPU(CPU, Memory, 1, false, nil)
			elseif k=="r" then RunCPU = not RunCPU; PlaySound(RunCPU and "runOn" or "runOff");
			elseif k=="i" then CPU.c.irq = 1; PlaySound("interrupt");
			--elseif k=="u" then CPU.c.rfg = 1
			else
				ConsoleKey(Console, k, CPU, Memory)
			end
		end
	end
end
function love.keyreleased(k)
	if k~="f4" and UseKeyboard then KeyboardOnKey(Keyboard, k, false, CPU, Memory) end
end
