--[[
can I make a NES ROM to NuMo9 converter?  "transpiler"? "emulator"?  "is not emulator"?
using this to learn:
https://www.patater.com/gbaguy/day2n.htm
https://yizhang82.dev/nes-emu-overview
https://www.nesdev.org/wiki/6502_instructions
https://www.nesdev.org/wiki/CPU_unofficial_opcodes
https://www.nesdev.org/wiki/PPU_memory_map
https://www.nesdev.org/wiki/PPU_programmer_reference
https://www.nesdev.org/obelisk-6502-guide/reference.html
https://www.nesdev.org/6502_cpu.txt
https://github.com/skilldrick/easy6502
https://github.com/EtienneMaire37/Simple-NES/tree/main/src
https://gist.github.com/1wErt3r/4048722 <- smb dis but without addresses
https://6502disassembly.com/nes-smb/SuperMarioBros.html	<- with addresses
https://web.archive.org/web/20200129081101/http://users.telenet.be:80/kim1-6502/6502/proman.html#910
https://www.pagetable.com/?p=410
https://fms.komkon.org/EMUL8/NES.html
https://zserge.com/posts/6502/

registers:
A = 8bit
X = 8bit
Y = 8bit
P = 16bit
S = 8bit
PC = 8bit
... 2kb onboard RAM
PPU: 256x240 8x8 tiles, up to 64 8x8 or 8x16 sprites
APU: 2 pulse channels, 1 triangle, 1 noise, 1 delta-modulation
$0000-$07ff = RAM
$2000-$2007 = PPU regs
$4000-$401f = APU
$6000-$ffff = cart space
$8000 = bank0 code starts at address
bank1 interrupts start at $fffa
bank2 sprites start at $0000

key mapping in NES: A B Start Select Up Down Left Right
--]]

-- these convert to cdata, which make some other tests screw up
-- TODO tonumber here or in the NuMo9 API?
u8=|v| tonumber(uint8_t(v))
s8=|v| tonumber(int8_t(v))
u16=|v| tonumber(uint16_t(v))
s16=|v| tonumber(int16_t(v))
u32=|v| tonumber(uint32_t(v))
s32=|v| tonumber(int32_t(v))

mode(1)	-- 256x256 8bpp indexed

local hex=|i,n| (('%0'..(n or 2)..'X'):format(tonumber(i)))

PPUNameTableMirrors = 0	-- 1: CIRAM A10 = PPU A11, 2: CIRAM A10 = PPU A10
PPUNameTableAltLayout = false

local nes_header = blobaddr('data', 2)
assert.ge(blobsize('data', 2), 0x10)	-- 0x10 or 0x210 if trainer present

-- read the header:
-- https://www.nesdev.org/wiki/INES
assert.eq(peekl(nes_header), 0x1A53454E)	-- "NES\n"
local nes_PRG_ROM_size = peek(nes_header+4) << 14
local nes_CHR_ROM_size = peek(nes_header+5) << 13
local nes_header_6 = peek(nes_header+6)
-- bit 01 = nametable arrangement
PPUNameTableMirrors = nes_header_6 & 3
-- bit 2 = PRG RAM present at $6000-7FFF
if (nes_header_6 >> 3) & 1 == 1 then
	trace'has trainer'
end	-- trainer
PPUNameTableAltLayout = (nes_header_6 >> 4) & 1 == 1	-- bit 4 = alt nametable layout
local nes_mapper = nes_header_6 >> 4
local nes_header_7 = peek(nes_header+7)
-- bit 0 = VS unisystem
-- bit 1 = playchoice-10 (8 KB f hint screen afer CHR data)
-- bit 2:3 = if equal 10b then we use NES 2.0 format
nes_mapper |= nes_header_7 & 0xF0
trace('nes_mapper', nes_mapper)
local PRG_RAM_size = peek(nes_header+8)
local NTSC_vs_PAL = peek(nes_header+9) & 1	-- and assert the rest of the bits are zero ...
local nes_header_10 = peek(nes_header+10)
local TV = nes_header_10 & 3	-- 0 = NTSC, 2 = PAL, 1/3 = dual compatible
local PRG_RAM_avail = (nes_header_10 >> 4) & 1 == 1
local bus_conflicts = (nes_header_10 >> 5) & 1 == 1

local PPUCTRL_addr = 0x2000
local PPUCTRL = 0
local PPUCTRL_flagsBaseNameTableAddr = 0x03
local PPUCTRL_flagAddrInc = 0x04
local PPUCTRL_flagSpritePatTableAddr = 0x08
local PPUCTRL_flagBGPatTableAddr = 0x10
local PPUCTRL_flagSpriteSize = 0x20
local PPUCTRL_flagPPUMasterSlave = 0x40
local PPUCTRL_flagNMIEnable = 0x80

local PPUMASK_addr = 0x2001
local PPUMASK = 0
local PPUMASK_flagGrayscale = 0x01
local PPUMASK_flagShowBGLeft = 0x02
local PPUMASK_flagShowSpritesLeft = 0x04
local PPUMASK_flagEnableBG = 0x08
local PPUMASK_flagEnableSprites = 0x10
local PPUMASK_flagEmphasizeRed = 0x20
local PPUMASK_flagEmphasizeGreen = 0x40
local PPUMASK_flagEmpasizeBlue = 0x80

local PPUSTATUS_addr = 0x2002
local PPUSTATUS = 0 -- 0xA0	-- initial state?
local PPUSTATUS_flagSpriteOverflow = 0x20
local PPUSTATUS_flagSprite0Hit = 0x40
local PPUSTATUS_flagVBlank = 0x80

local OAMADDR_addr = 0x2003
local OAMADDR = 0

local OAMDATA_addr = 0x2004

local OAM_RAM_size = 0x100
local n9_OAM_addr = blobaddr('data', 3)
assert.eq(blobsize('data', 3), OAM_RAM_size)
local OAMDMAaddr = 0x4014

local PPUScrollX = 0
local PPUScrollY = 0
local PPUScrollAddr = 0x2005
local PPUScrollReadX = true	-- x first, y second

local CHR_RAM_ptr = 0	-- uint16 reg accessed via $2006/2007
local CHR_RAM_ptrWriteHi = true	-- "high byte first"
local CHR_RAM_writePtr_addr = 0x2006
local CHR_RAM_RW_addr = 0x2007

-- 0x4000+ is sound registers
local cartRAMAddr = 0x6000 -- 0x6000-0x8000 is cart ram / persistent
local cartRAMSize = 0x2000
local cartRAMEndAddr = cartRAMAddr + cartRAMSize

-- https://www.emulationonline.com/systems/nes/sprite-rendering/
-- this gives an example rom , but that rom isn't even 32kb, so there is no 0xFFF*
-- so I'll just meh it and always use the min(ROM size, 64k) minus 2 4 6 bytes
-- https://forums.nesdev.org/viewtopic.php?t=17413
-- a guy asks directly this question, nobody seems to answer it.
local idk = math.min(0x8000 + nes_PRG_ROM_size, 0x10000)
local NMIAddr = idk - 6
local RESETAddr = idk - 4
local IRQAddr = idk - 2


local nesrom = blobaddr('data', 0)

-- init persistent RAM
local n9PersistAddr = blobaddr'persist'
assert.ge(blobsize'persist', cartRAMSize, "NES needs more save RAM!")
memcpy(nesrom + cartRAMAddr, n9PersistAddr, cartRAMSize)

local n9_CHR_RAM_addr = blobaddr('data', 1)
local CHR_ROM_size = 0x4000
local CHR_ROM_addrMask = 0x3FFF
assert.eq(blobsize('data', 1), CHR_ROM_size)

local n9SheetAddr = blobaddr'sheet'
local n9TilemapAddr = blobaddr'tilemap'
local n9PalAddr = blobaddr'palette'

local nespeek = |i,dontwrite|do
	if i == PPUCTRL_addr then
trace('PPUCTRL read', hex(PPUCTRL))
		return PPUCTRL
	elseif i == PPUMASK_addr then
trace('PPUMASK read', hex(peek(nesrom+i)))
		return PPUMASK
	elseif i == PPUSTATUS_addr then
		local val = PPUSTATUS
trace('PPUSTATUS read', hex(val))
		if not dontwrite then
			PPUSTATUS &= ~PPUSTATUS_flagVBlank -- clear vblank on read
		end
		return val
	elseif i == OAMADDR_addr then
trace('OAMADDR read', hex(peek(nesrom+i)))
		return OAMADDR
	elseif i == OAMDATA_addr then
		local v = peek(n9_OAM_addr + OAMADDR)
trace('OAM read from OAM addr '..hex(OAMADDR)..' = '..v)
		return v
	elseif i == PPUScrollAddr then
trace('PPUScroll read', hex(peek(nesrom+i)))
	elseif i == CHR_RAM_writePtr_addr then
trace('CHR_RAM_ptr read', hex(peek(nesrom+i)))
	elseif i == CHR_RAM_RW_addr then
		local val = peek(n9_CHR_RAM_addr+CHR_RAM_ptr)
trace('CHR_RAM_RW read '..hex(peek(nesrom+i))..' ... as PPU RAM addr '..hex(CHR_RAM_ptr,4)..' val '..hex(val))
		if not dontwrite then
			-- inc upon read/write
			CHR_RAM_ptr += PPUCTRL & PPUCTRL_flagSpriteSize == 0 and 1 or 32
			CHR_RAM_ptr &= CHR_ROM_addrMask
		end
		return val
	end
	-- handle special read sections
	return peek(nesrom+i)
end
local nespeekw = |i,dontwrite| nespeek(i,dontwrite) | (nespeek(i+1,dontwrite)<<8)

-- assumes i is uint16 and v is uint8
local nespoke = |i,v|do
	if i == PPUCTRL_addr then
trace('PPUCTRL write', hex(v))
		PPUCTRL = v
		return
	elseif i == PPUMASK_addr then
trace('PPUMASK write', hex(v))
		PPUMASK = v
		return
	elseif i == PPUSTATUS_addr then
		-- it's a read-only address
trace("PPUSTATUS write but it's read-only...")
	elseif i == OAMADDR_addr then
trace('OAMADDR write', hex(v))
		OAMADDR = v
		return
	elseif i == OAMDATA_addr then
trace('OAMRW write '..hex(v)..' to OAM addr '..hex(OAMADDR))
		poke(n9_OAM_addr + OAMADDR, v)
		OAMADDR = u8(OAMADDR + 1)
		return
	elseif i == PPUScrollAddr then
trace('PPUScroll write', hex(v))
		-- TODO FIXME
		if PPUScrollReadX then
			PPUScrollX = v | ((PPUCTRL & 1) << 8)
			PPUScrollReadX = false
		else
			PPUScrollY = v | ((PPUCTRL & 2) << 7)
			PPUScrollReadX = true
		end
	elseif i == CHR_RAM_writePtr_addr then
trace('CHR_RAM_ptr write', hex(v))
		if CHR_RAM_ptrWriteHi then
			CHR_RAM_ptr &= 0xFF
			CHR_RAM_ptr |= v << 8
			CHR_RAM_ptrWriteHi = false
		else
			CHR_RAM_ptr &= 0xFF00
			CHR_RAM_ptr |= v
			CHR_RAM_ptrWriteHi = true
		end
	elseif i == CHR_RAM_RW_addr then
trace('CHR_RAM_RW write '..hex(v)..' ... to PPU RAM $'..hex(CHR_RAM_ptr,4))
		poke(n9_CHR_RAM_addr+CHR_RAM_ptr, v)
		-- depending on where, update N9 VRAM
		if CHR_RAM_ptr < 0x2000 then
			-- 0000-1000 = pattern table 0
			-- 1000-2000 = pattern table 1
			--[[
			pattern table
			each sprite is 8x8 pixels, 2bpp
			so a sprite is 16 bytes
			so 256 sprites fit in 1 table

			each byte is a row
			first 8 bytes are 1bpp lo-bit
			second 8 bytes are 1bpp hi-bit
			--]]
			--local tableIndex = CHR_RAM_ptr >> 12	-- 0 or 1
			local hi = (CHR_RAM_ptr >> 3) & 1	-- hi bit: 0 or 1
			local y = CHR_RAM_ptr & 0x7
			local sprIndex = CHR_RAM_ptr >> 4
			local sprX = sprIndex & 0x1F
			local sprY = sprIndex >> 5
--trace('...to sprIndex='..sprIndex..' sprX='..sprX..' sprY='..sprY..' hi='..hi)
			for x=0,7 do
				local addr = n9SheetAddr + (x | (sprX << 3) | ((y | (sprY << 3)) << 8))
				local pix = peek(addr)
				pix &= ~(1 << hi)
				pix |= ((v >> (7 - x)) & 1) << hi
				poke(addr, pix)
			end
			-- then the attribute-table is used as bit23 to offset these
		elseif CHR_RAM_ptr < 0x3000 then
			-- 2000-23C0 name table 0
			-- 23C0-2400 attr table 0
			-- 2400-27C0 name table 1
			-- 27C0-2800 attr table 1
			-- 2800-2BC0 name table 2
			-- 2BC0-2C00 attr table 2
			-- 2C00-2FC0 name table 3
			-- 2FC0-3000 attr table 3
			local i = CHR_RAM_ptr
			local tileIndex = (i >> 10) & 3
			local pageaddr = i & 0x3FF
			--if pageaddr < 960 then
				local x = pageaddr & 0x1f
				local y = pageaddr >> 5
trace('...to nametable='..tileIndex..' x='..x..' y='..y)
				-- 32x30 tilemap
				pokew(n9TilemapAddr + (
					(x | ((tileIndex&1)<<5))
					| ((y | ((tileIndex&2)<<4))<<8)
				) << 1, v)
			--else
				-- attribute-flags
			--end
		elseif CHR_RAM_ptr < 0x3F00 then
		elseif CHR_RAM_ptr < 0x3F20 then
			-- 3F00-3F10 image palette
			-- 3F10-3F20 sprite palette

			-- so the index in the sprite tile + attr + extra is a lookup into this table
			-- and this table is then a lookup into the palette
			-- hmmmmmm

			-- how bout I keep the last or so 64 colors (since I have 4 copies in the 256-color palette)
			-- as the defacto NES palette
			-- and then upon these writes, copy the orig palette 192-256 into the used-palette 0-64 or idk sprite vs image or w/e palette 0-128

			-- what should I initialize this to?
			local palindex = (CHR_RAM_ptr - 0x3F00)
			--palindex &= 0xf	-- mirror? or nah?
			pokew(
				n9PalAddr + palindex << 1,
				peekw(n9PalAddr + ((0xC0 | v & 0x3F) << 1))
			)
		end
		-- inc upon read/write
		CHR_RAM_ptr += PPUCTRL & PPUCTRL_flagSpriteSize == 0 and 1 or 32
		CHR_RAM_ptr &= CHR_ROM_addrMask
	elseif i == OAMDMAaddr then
		-- increment?
		memcpy(n9_OAM_addr, nesrom + (v << 8), OAM_RAM_size)
	elseif i >= cartRAMAddr and i < cartRAMEndAddr then
		-- write it to our persistent memory
		poke(n9PersistAddr + i - 0x6000, v)
	end

	-- handle special write sections
	poke(nesrom+i, v)

-- TODO remap addrs 0x200-0x600 to VRAM
end

-- assumes v is uint16
local nespokew = |i,v|do
	nespoke(i, u8(v))
	nespoke(i+1, u8(v>>8))
end

-- used to read state without modifying (i.e. special hardware registers)
local dbg_nespeek=|i| nespeek(i,true)
local dbg_nespeekw=|i| nespeekw(i,true)


local A = 0		-- accumulator
local X = 0		-- x-register
local Y = 0		-- y-register
local P = 0		-- process flags
local S = 0		-- stack
local PC = 0	-- process-counter

local P_flagC = 0x01	-- carry
local P_flagZ = 0x02	-- zero
local P_flagI = 0x04	-- interrupt disable
local P_flagD = 0x08	-- decimal mode
local P_flagB = 0x10	-- break
local P_flag_ = 0x20	-- unused
local P_flagV = 0x40	-- overflow
local P_flagN = 0x80	-- negative
local P_bitC = 0
local P_bitZ = 1
local P_bitI = 2
local P_bitD = 3
local P_bitB = 4
local P_bit_ = 5
local P_bitV = 6
local P_bitN = 7
local P_flagB_ = P_flagB | P_flag_
local P_flagVN = P_flagV | P_flagN

local readPC=||do
	local i = nespeek(PC)
	PC += 1
	return i
end
local readPCw=||do
	local i = nespeekw(PC)
	PC += 2
	return i
end

-- arg : u8
local setZN=|arg|do
	if arg == 0 then
		P |= P_flagZ
	else
		P &= ~P_flagZ
	end
	-- transfer bit 7 from arg to P (this P_flagN)
	P &= ~P_flagN
	P |= arg & P_flagN
	return arg	-- continue to use the arg
end

-- arg : u8
local bit0toC=|arg|do
	-- transfer bit 0 from arg to P (this is P_flagC)
	P &= ~P_flagC
	P |= arg & P_flagC	-- carry is bit 0
	return arg	-- continue to use the arg
end

-- arg : u8
local bit7toC=|arg|do
	P &= ~P_flagC
	P |= (arg >> 7) & P_flagC	-- carry is bit 0
	return arg
end

-- reads offset from PC
local branch=|offset|do PC += s8(readPC()) end

local doCompare=|reg,val|do
	if reg >= val then
		P |= P_flagC
	else
		P &= ~P_flagC
	end
	val = u8(reg - val)
	setZN(val)
end

local SBC=|value|do
	-- transfer NOT `A ~ value`'s bit 7 to P's bit 6 aka P_flagV
	P &= ~P_flagV
	P |= (~(A ~ value) & 0x80) >> 1

	local w
	if P & P_flagD ~= 0 then
		local tmp = 0xF
			+ (A & 0xF)
			- (value & 0xF)
			+ (P & P_flagC)
		if tmp < 0x10 then
			w = 0
			tmp -= 6
		else
			w = 0x10
			tmp -= 0x10
		end
		w += 0xF0 + (A & 0xF0) - (value & 0xF0)
		if w < 0x100 then
			P &= ~P_flagC
			if w < 0x80 then
				P &= ~P_flagV
			end
			w -= 0x60
		else
			P |= P_flagC
			if w >= 0x180 then
				P &= ~P_flagV
			end
		end
		w += tmp
	else
		w = 0xFF + A - value + (P & P_flagC)
		if w < 0x100 then
			P &= ~P_flagC
			if w < 0x80 then
				P &= ~P_flagV
			end
		else
			P |= P_flagC
			if w >= 0x180 then
				P &= ~P_flagV
			end
		end
	end
	A = u8(w)
	setZN(A)
end

local ADC=|value|do
	-- transfer NOT `A ~ value`'s bit 7 to P's bit 6 aka P_flagV
	P &= ~P_flagV
	P |= (~(A ~ value) & 0x80) >> 1

	if P & P_flagD ~= 0 then
		local tmp = (A & 0xF)
			+ (value & 0xF)
			+ (P & P_flagC)
		if tmp >= 10 then
			tmp = 0x10 | ((tmp + 6) & 0xF)
		end
		tmp += (A & 0xF0) + (value & 0xF0)
		if tmp >= 160 then
			P |= P_flagC
			if tmp >= 0x180 then
				P &= ~P_flagV
			end
			tmp += 0x60
		else
			P &= ~P_flagC
			if tmp < 0x80 then
				P &= ~P_flagV
			end
		end
	else
		tmp = A + value + (P & P_flagC)
		if tmp >= 0x100 then
			P |= P_flagC
			if tmp >= 0x180 then
				P &= ~P_flagV
			end
		else
			P &= ~P_flagC
			if tmp < 0x80 then
				P &= ~P_flagV
			end
		end
	end
	A = u8(tmp)
	setZN(A)
end

local BIT=|i|do
	-- transfer bit 6 & 7 of i to bit 6 & 7 of P
	P &= ~P_flagVN
	P |= i & P_flagVN

	-- set P_flagZ based on `A & i`
	if A & i ~= 0 then
		P &= ~P_flagZ
	else
		P |= P_flagZ
	end
end

local DEC=|addr| nespoke(addr, setZN(u8(nespeek(addr) - 1)))
local INC=|addr| nespoke(addr, setZN(u8(nespeek(addr) + 1)))

local incS=||do
	S = u8(S + 1)
end
local decS=||do
	S = u8(S - 1)
end

local stackPush=|b|do
	nespoke(S|0x100, b)
	decS()
end
local stackPop=||do
	incS()
	return nespeek(S|0x100)
end

local stackPushw=|w|do
	-- [[ safe
	stackPush(u8(w >> 8))
	stackPush(u8(w))
	--]]
	--[[ trust overflow won't happen
	decS()
	nespokew(S|0x100, w)
	decS()
	--]]
end
local stackPopw=||do
	-- [[ safe
	-- depends on | evaluating left-to-right
	-- and langfix rewrites this as bit.bor(...)
	-- so this depends on lua arg evaluation being left-to-right
	return stackPop() | (stackPop() << 8)
	--]]
	--[[ trust underflow won't happen
	incS()
	local w = nespeekw(S|0x100)
	incS()
	return w
	--]]
end

RESET=||do
	PC = nespeekw(RESETAddr)
end
IRQ=||do
	stackPushw(PC)
	stackPush(P)
	P |= P_flagI
	PC = nespeekw(IRQAddr)
end
NMI=||do
	stackPushw(PC)
	stackPush(P)
	P |= P_flagI
	PC = nespeekw(NMIAddr)
end
BRK=||do
	stackPushw(PC)
	stackPush(P | P_flagB)
	P |= P_flagI
	PC = nespeekw(IRQAddr)
end



local argStrRel=|skipvalue|
	' $+'..hex(dbg_nespeek(PC))
	..(skipvalue and '' or ' = $'..hex(PC+1+s8(dbg_nespeek(PC)),4))

local argStrImm=||
	' #$'..hex(dbg_nespeek(PC))

local argStrZP=|skipvalue|
	' $'..hex(dbg_nespeek(PC))
	..(skipvalue and '' or ' = '..hex(dbg_nespeek(dbg_nespeek(PC))))

local argStrZPX=|skipvalue|
	' $'..hex(dbg_nespeek(PC))
	..',X = $'..hex(u8(dbg_nespeek(PC) + X))
	..(skipvalue and '' or ' = '..hex(dbg_nespeek(u8(dbg_nespeek(PC) + X))))

local argStrZPY=|skipvalue|
	' $'..hex(dbg_nespeek(PC))
	..',X = $'..hex(u8(dbg_nespeek(PC) + X))
	..(skipvalue and '' or ' = '..hex(dbg_nespeek(u8(dbg_nespeek(PC) + X))))

local argStrAbs=|skipvalue|
	' $'..hex(dbg_nespeekw(PC),4)
	..(skipvalue and '' or ' = '..hex(dbg_nespeek(dbg_nespeekw(PC))))

local argStrAbsX=|skipvalue|
	' $'..hex(dbg_nespeekw(PC),4)
	..',X = $'..hex(u16(dbg_nespeekw(PC) + X),4)
	..(skipvalue and '' or ' = '..hex(dbg_nespeek(u16(dbg_nespeekw(PC) + X))))

local argStrAbsY=|skipvalue|
	' $'..hex(dbg_nespeekw(PC),4)
	..',Y = $'..hex(u16(dbg_nespeekw(PC) + Y),4)
	..(skipvalue and '' or ' = '..hex(dbg_nespeek(u16(dbg_nespeekw(PC) + Y))))

local argStrIZX=|skipvalue|
	' ($'..dbg_nespeek(PC)
	..',X) = ($'..hex(dbg_nespeek(PC) + X,4)
	..') = $'..hex(dbg_nespeekw(dbg_nespeek(PC) + X),4)
	..(skipvalue and '' or ' = '..hex(dbg_nespeek(dbg_nespeekw(dbg_nespeek(PC) + X))))

-- should this be ($arg,Y) or ($arg),Y ? latter in https://www.nesdev.org/obelisk-6502-guide/reference.html
local argStrIZY=|skipvalue|
	' ($'..dbg_nespeek(PC)
	..'),Y = ($'..hex(dbg_nespeek(PC))
	..'+Y) = $'..hex(dbg_nespeekw(dbg_nespeek(PC)),4)..'+'..hex(Y)
	..(skipvalue and '' or ' = '..hex(u16(dbg_nespeekw(dbg_nespeek(PC)) + Y),4))

local argStrFor234=|b234, ...|do
	if b234 == 0x00 then
		return argStrIZX(...)		-- (Indirect,X)
	elseif b234 == 0x04 then
		return argStrZP(...)		-- Zero Page
	elseif b234 == 0x08 then
		return argStrImm(...)		-- Immediate
	elseif b234 == 0x0C then
		return argStrAbs(...)		-- Absolute
	elseif b234 == 0x10 then
		return argStrIZY(...)		-- (Indirect),Y
	elseif b234 == 0x14 then
		return argStrZPX(...)		-- Zero Page,X
	elseif b234 == 0x18 then
		return argStrAbsY(...)		-- Absolute,Y
	elseif b234 == 0x1C then
		return argStrAbsX(...)		-- Absolute,X
	end
end

local writebuf=''
local write=|...| do writebuf ..= table{...}:mapi(tostring):concat'\t' end

RESET()
trace('initPC = $'..hex(PC,4))

local fbAddr = ramaddr'framebuffer'

cycle=0
cpuRun=||do
	cycle+=1
write('A='..hex(A)..' X='..hex(X)..' Y='..hex(Y)..' S='..hex(S)..' P='..hex(P)..' PC='..hex(PC,4)..' op='..hex(nespeek(PC))..' ')
	local op = readPC()

	-- https://www.nesdev.org/wiki/CPU_unofficial_opcodes
	local b01 = bit.band(op, 0x03)	-- 00 01 02 03
	local b567 = bit.band(op, 0xE0)	-- 00 20 40 60 80 A0 C0 E0
	local b234 = bit.band(op, 0x1C)	-- 00 04 08 0C 10 14 08 1C

	-- xxxx:xx00
	-- control
	if b01 == 0x00 then

		-- xxx1:0000
		-- BPL, BMI, BVC, BVS, BCC, BCS, BNE, BEQ
		if op & 0x1F == 0x10 then
			-- branch-on-flag
			-- op & 0x10 <=> branch on flag
			-- op & 0x20: 0 means branch on flag clear, 1 means branch on flag set
			-- missed their chance to swap V and N ... then we'd have ((op >> 6) + 6) & 7 == flag bit

			-- bit 5 specifies whether to test for set vs unset
			local test = (op >> 5) & 1

			local shr
			local b67 = op & 0xC0
			if b67 == 0x00 then
				shr = P_bitN
write(({'BPL', 'BMI'})[test+1]..argStrRel())
			elseif b67 == 0x40 then
				shr = P_bitV
write(({'BVC', 'BVS'})[test+1]..argStrRel())
			elseif b67 == 0x80 then
				shr = P_bitC
write(({'BCC', 'BCS'})[test+1]..argStrRel())
			elseif b67 == 0xC0 then
				shr = P_bitZ
write(({'BNE', 'BEQ'})[test+1]..argStrRel())
			end

			if (P >> shr) & 1 == test then
				PC += s8(readPC())
			end
			PC += 1

		-- xxx1:1000
		elseif op & 0x1F == 0x18 then

		-- 0xx1:1000
			if op == 0x18 then
				P &= ~P_flagC	-- CLC
write'CLC'
			elseif op == 0x38 then
				P |= P_flagC	-- SEC
write'SEC'
			elseif op == 0x58 then
				P &= ~P_flagI	-- CLI
write'CLI'
			elseif op == 0x78 then
				P |= P_flagI 	-- SEI
write'SEI'
			elseif op == 0x98 then
				-- TYA is here for some reason
				-- there's no SEV ...
				-- and CLV goes where SEV should've gone
				A = setZN(Y)	-- TYA
write'TYA'
			elseif op == 0xB8 then
				P &= ~P_flagV 	-- CLV
write'CLV'
			elseif op == 0xD8 then
				P &= ~P_flagD 	-- CLD
write'CLD'
			elseif op == 0xF8 then
				P |= P_flagD 	-- SED
write'SED'
			end

		-- xxxy:yy00 for yyy != 100 and yy != 110
		else
			if op & 0x80 == 0 then	-- bit 7 not set
				if op == 0x00 then	-- BRK
write'BRK'
					BRK()
				elseif op == 0x20 then	-- JSR
write('JSR'..argStrAbs())
					local addr = readPCw()
					stackPushw(u16(PC - 1))
					PC = addr
				elseif op == 0x40 then	-- RTI
write'RTI'
					P = stackPop() | P_flagB_
					PC = stackPopw()
				elseif op == 0x60 then	-- RTS
write'RTS'
					PC = stackPopw() + 1
				elseif op == 0x08 then
write'PHP'
					stackPush(P | P_flagB_) -- PHP
				elseif op == 0x28 then
write'PLP'
					P = stackPop() | P_flagB_  -- PLP
				elseif op == 0x48 then
write'PHA'
					stackPush(A)	-- PHA
				elseif op == 0x68 then
write'PLA'
					A = setZN(stackPop()) 	-- PLA
				elseif op == 0x24 then	-- BIT
write('BIT'..argStrZP())
					BIT(nespeek(readPC()))
				elseif op == 0x2C then	-- BIT
write('BIT'..argStrAbs())
					BIT(nespeek(readPCw()))

				-- JMP
				elseif op == 0x4C then
write('JMP'..argStrAbs(true))		-- Absolute ... but its jump
					PC = readPCw()
				elseif op == 0x6C then
--write('JMP'..argStrAbs()		-- Indirect ... but its jump
write('JMP'..' $'..hex(dbg_nespeekw(PC),4)..(skipvalue and '' or ' = '..hex(dbg_nespeekw(dbg_nespeekw(PC)),4)))
					PC = nespeekw(readPCw())
				else
write'NOP'
				-- else 04 0C 14 1C 34 3C 44 54 5C 64 74 7C is NOP
				end

			else	-- bit 7 set
				-- these match up closely with RMW lower half i.e. op & 0x83 == 0x82

				-- 0x80 NOP
				if op == 0x84 then
write('STY'..argStrZP())
					nespoke(readPC(), Y)	-- STY
				elseif op == 0x88 then
write'DEY'
					Y = setZN(u8(Y - 1)) 	-- DEY
				elseif op == 0x8C then
write('STY'..argStrAbs())
					nespoke(readPCw(), Y)	-- STY
				-- 0x90 handled by the branch set
				elseif op == 0x94 then
write('STY'..argStrZPX())
					nespoke(u8(readPC() + X), Y)	-- STY
				-- 0x98 handled by SE*/CL* set
				elseif op == 0x9C then	-- SHY
write'SHY -- TODO'
				elseif op == 0xA0 then	-- LDY
write('LDY'..argStrImm())
					Y = setZN(readPC())
				elseif op == 0xA4 then
write('LDY'..argStrZP())
					Y = setZN(nespeek(readPC())) 	-- LDY
				elseif op == 0xA8 then
write'TAY'
					Y = setZN(A) 	-- TAY
				elseif op == 0xAC then
write('LDY'..argStrAbs())
					Y = setZN(nespeek(readPCw())) 	-- LDY
				-- 0xB0 = handled by branch set
				elseif op == 0xB4 then
write('LDY'..argStrZPX())
					Y = setZN(nespeek(u8(readPC() + X))) 	-- LDY
				-- 0xB8 handled by SE*/CL* set
				elseif op == 0xBC then
write('LDY'..argStrAbsX())
					Y = setZN(nespeek(readPCw() + X)) 	-- LDY
				elseif op == 0xC0 then
write('CPY'..argStrImm())
					doCompare(Y, readPC())						-- CPY
				elseif op == 0xC4 then
write('CPY'..argStrZP())
					doCompare(Y, nespeek(readPC()))	-- CPY
				elseif op == 0xC8 then
write'INY'
					Y = setZN(u8(Y + 1)) 	-- INY
				elseif op == 0xCC then
write('CPY'..argStrAbs())
					doCompare(Y, nespeek(readPCw()))	-- CPY
				-- 0xD0 handled by branch set
				-- 0xD4 = NOP
				-- 0xD8 handled by SE*/CL* set
				-- 0xDC = NOP
				elseif op == 0xE0 then
write('CPX'..argStrImm())
					doCompare(X, readPC())	-- CPX
				elseif op == 0xE4 then
write('CPX'..argStrZP())
					doCompare(X, nespeek(readPC()))	-- CPX
				elseif op == 0xE8 then
write'INX'
					X = setZN(u8(X + 1)) 	-- INX
				elseif op == 0xEC then
write('CPX'..argStrAbs())
					doCompare(X, nespeek(readPCw()))			-- CPX
				else
write'NOP'
				-- 0xF0 handled by branch set
				-- 0xF4 NOP
				-- 0xF8 handled by SE*/CL*
				-- 0xFC NOP
				end
			end
		end

	else
		-- ALU
		if b01 == 0x01 then
			local arg

			if b567 == 0x00 then
write('ORA'..argStrFor234(b234))
			elseif b567 == 0x20 then
write('AND'..argStrFor234(b234))
			elseif b567 == 0x40 then
write('EOR'..argStrFor234(b234))
			elseif b567 == 0x60 then
write('ADC'..argStrFor234(b234))
			elseif b567 == 0x80 then
				if b234 ~= 0x08 then
write('STA'..argStrFor234(b234, true))
				else
write('NOP'..argStrFor234(b234))
				end
			elseif b567 == 0xA0 then
write('LDA'..argStrFor234(b234))
			elseif b567 == 0xC0 then
write('CMP'..argStrFor234(b234))
			elseif b567 == 0xE0 then
write('SBC'..argStrFor234(b234))
			end


			if b234 == 0x08 then		-- immediate
				arg = readPC()	-- #$00
			else						-- non-immediate, i.e. memory
				if b234 == 0x00 then
					arg = nespeekw(u8(readPC() + X))	-- IZX: (indirect,X)
				elseif b234 == 0x04 then
					arg = readPC()	-- $00
				elseif b234 == 0x0C then
					arg = readPCw()
				elseif b234 == 0x10 then
					arg = u16(nespeekw(readPC()) + Y)	-- IZY: (indirect),Y
				elseif b234 == 0x14 then
					arg = u8(readPC() + X)
				elseif b234 == 0x18 then
					arg = u16(readPCw() + Y)
				elseif b234 == 0x1C then
					arg = u16(readPCw() + X)
				end

				-- wait for STA do we not do the final peek above?
				if b567 ~= 0x80 then
					arg = nespeek(arg)
				end
			end

			if b567 == 0x00 then		-- 01 05 09 0D 11 15 19 1D = ORA
				A = setZN(A | arg)
			elseif b567 == 0x20 then	-- 21 25 29 2D 31 35 39 3D = AND
				A = setZN(A & arg)
			elseif b567 == 0x40 then	-- 41 45 49 4D 51 55 59 5D = EOR
				A = setZN(A ~ arg)
			elseif b567 == 0x60 then	-- 61 65 69 6D 71 75 79 7D = ADC
				ADC(arg)
			elseif b567 == 0x80 then	-- 81 85 8D 91 95 99 9D = STA
				if b234 ~= 0x08 then	-- 89 is NOP
					nespoke(arg, A)
				else
					-- NOP ... does it still read the arg and inc the PC?
				end
			elseif b567 == 0xA0 then	-- A1 A5 A9 AD B1 B5 B9 BD = LDA
				A = setZN(arg)
			elseif b567 == 0xC0 then	-- C1 C5 C9 CD D1 D5 D9 DD = CMP
				doCompare(A, arg)
			elseif b567 == 0xE0 then	-- E1 E5 E9 ED F1 F5 F9 FD = SBC
				SBC(arg)
			end

		-- RMW
		elseif b01 == 0x02 then
			-- xxxx:xx10

			if op & 0x80 == 0 then	-- bit 7 clear

				if b234 == 0x00
				or b234 == 0x10
				then
write'STP'
					-- 0xxx:0010
					-- 02 12 22 32 42 52 62 72 = STP
				elseif b234 == 0x18 then
write'NOP'
					-- 1A 3A 5A 7A = NOP
				else
					local arg, addr
					if b234 == 0x08 then
						-- immediate
						arg = A
					else
						-- not immediate, use address
						-- (should follow same addressing as ALU, maybe merge?)
						if b234 == 0x04 then
							addr = readPC()
						elseif b234 == 0x0C then
							addr = readPCw()
						elseif b234 == 0x14 then
							addr = u8(readPC() + X)
						elseif b234 == 0x1C then
							addr = u16(readPCw() + X)
						end
						arg = nespeek(addr)
					end

					if b567 == 0x00	then -- ASL
						arg = setZN(u8(bit7toC(arg) << 1))
write'ASL'
					elseif b567 == 0x20	then -- ROL
						-- reads C
						-- shifts src left by 1, inserting old C
						-- sets src's old bit7 to the new C as uint8
						-- sets VN flags based on result
						-- returns result
						-- * I'm hoping all operations are evaluated left-to-right, since I'm reading C then writing C in dif bit op args
						-- * NOTICE, no memory evaluation better modify C ...
						arg = setZN((P & P_flagC) | u8(bit7toC(arg) << 1))
write'ROL'
					elseif b567 == 0x40	then -- LSR
						arg = setZN(bit0toC(arg) >> 1)
write'LSR'
					elseif b567 == 0x60	then -- ROR
						arg = setZN((P & P_flagC) | u8(bit7toC(arg) << 1))
write'ROR'
					end

					if b234 == 0x08 then
						-- immediate
						A = arg
					else
						-- not immediate, write back to address
						nespoke(addr, arg)
					end
				end

			else	-- bit 7 set
				-- 1xx0:0010
				if b234 == 0x10 then
write'STP'
					-- 1xx1:0010
					-- 92 B2 D2 F2 = STP
				else

-- TODO TODO maybe things would be better if I separated out bit7==set first ...

					if b567 == 0x80 then
						-- ST*
						-- 0x92 is STP handled above
						if op == 0x86 then
write('STX'..argStrZP())
							nespoke(readPC(), X)	-- STX
						elseif op == 0x8A then
write'TXA'
							A = setZN(X) 	-- TXA
						elseif op == 0x8E then
write('STX'..argStrAbs())
							nespoke(readPCw(), X)	-- STX
						elseif op == 0x96 then
							nespoke(u8(readPC() + Y), X)	-- STX
write('STX'..argStrZPY())
						elseif op == 0x9A then
							S = X 		-- TXS
write'TXS'
						elseif op == 0x9E then
							error'TODO'
							--TODO ?	-- SHX
write'SHX'
						else
write'NOP'
							-- 0x82 = NOP
						end

					elseif b567 == 0xA0 then

						-- LD* / T*
						-- 0xB2 is STP handled above
						if op == 0xA2 then
write('LDX'..argStrImm())
							X = setZN(readPC()) 	-- LDX
						elseif op == 0xA6 then
write('LDX'..argStrZP())
							X = setZN(nespeek(readPC()))	-- LDX
						elseif op == 0xAA then
write'TAX'
							X = setZN(A)	-- TAX
						elseif op == 0xAE then
write('LDX'..argStrAbs())
							X = setZN(nespeek(readPCw()))	-- LDX
						elseif op == 0xB6 then
write('LDY'..argStrZPY())
							X = setZN(nespeek(u8(readPC() + Y)))	-- LDY
						elseif op == 0xBA then
write'TSX'
							X = setZN(S)	-- TSX
						elseif op == 0xBE then
write('LDX'..argStrAbsY())
							X = setZN(nespeek(readPCw() + Y))	-- LDX
						end

					elseif b567 == 0xC0 then

						-- 0xD2 = STP handled above
						if op == 0xC6 then
							DEC(readPC())				-- DEC
write'DEC'
						elseif op == 0xCA then
							X = setZN(u8(X - 1))	-- DEX
write'DEX'
						elseif op == 0xCE then
							DEC(readPCw())
write'DEC'
						elseif op == 0xD6 then
							DEC(u8(readPC() + X))
write'DEC'
						elseif op == 0xDE then
							DEC(readPCw() + X)
write'DEC'
						else
write'NOP'
							-- 0xC2 = NOP
							-- 0xDA = NOP
						end

					elseif b567 == 0xE0 then
						if op == 0xE6 then
							INC(readPC())
write'INC'
						elseif op == 0xEE then
							INC(readPCw())
write'INC'
						elseif op == 0xF6 then
							INC(u8(readPC() + X))
write'INC'
						elseif op == 0xFE then
							INC(readPCw() + X)
write'INC'
						else
write'NOP'
							-- 0xE2 = NOP
							-- 0xEA = NOP (should be INX?)
							-- 0xFA = NOP
						end
					end
				end
			end

		-- unofficial
		elseif b01 == 0x03 then
write'TODO op'
		end
	end
-- [[
trace(writebuf)
--]]
--[[
writebufy ??= 0
text(writebuf, 0, writebufy, 0x30, 0)
writebufy = u8(writebufy + 8)
--]]
writebuf=''

end

update=||do
	--[[
	TODO
	master clock ...
	- every 4 cycles NTSC / 5 cycles PAL, cycle the PPU
	- every 12 cycles NTSC / 16 cycles PAL, cycle the CPU and APU
	ppu ...
	- every 340 cycles, increment a scanline
	- every 261 scanlines for NTSC / 311 for PAL:
	(aka 88740 PPU cycles for NTSC / 105740 PPU cycles for PAL)
	(aka 29580 CPU cycles for NTSC / 33043.75 CPU cycles for PAL)
		- reset scanline to 0
		- flush video to screen
		- frame_finished
	- for scanlines 0-239,
		when cycle # 1-256 of this scanline, do typical scanline stuff:
		- crt x is (cycle-1) % 340
		- crt y is scanline = floor((cycle-1) / 340)
	--]]


trace'vblank'

	-- set at the start of vblank
	PPUSTATUS |= PPUSTATUS_flagVBlank

	-- set before or after?
	if PPUCTRL & PPUCTRL_flagNMIEnable ~= 0 then
trace'NMI'
		NMI()
	end

	-- TODO count cycles and break accordingly? or nah?
	-- 1mil / 60 = 16k or so .. / avg cycles/instr = ?
	--for i=1,1 do
	--for i=1,60 do
	--for i=1,100 do
	for i=1,600 do
		cpuRun()
	end

	-- ... clear at prerender scanline
	PPUSTATUS &= ~(PPUSTATUS_flagSpriteOverflow | PPUSTATUS_flagSprite0Hit | PPUSTATUS_flagVBlank)

	for i=1,600 do
		cpuRun()
	end

	-- rendering OAM ...
	cls()

	-- is background rendering enabled?
	-- (is the "nametable" the same as the "background"? or is it sprites with background bit set?)
	if PPUMASK & PPUMASK_flagEnableBG ~= 0 then
		map(0, 0, 32, 30, -PPUScrollX, -PPUScrollY)
	end

	-- is sprite rendering enabled?
	if PPUMASK & PPUMASK_flagEnableSprites ~= 0 then
		-- draw all sprites ...
		for i=0,255,4 do
			local addr = n9_OAM_addr + i
			local y = peek(addr)
			local sprIndex = peek(addr + 1)
			local attrs = peek(addr + 2)
			local bg = attrs & 0x20 ~= 0
			local pal = attrs & 3
			local x = peek(addr + 3)

			-- check sprite 0 collision
			if i == 0 then
--print('drawing sprite #0 sprIndex '..hex(sprIndex)..' at x='..hex(x)..' y='..hex(y))
				-- TODO test write opacity and set the PPUSTATUS |= PPUSTATUS_flagSprite0Hit
				local sprX = sprIndex & 0x1F
				local sprY = (sprIndex >> 5) & 0x1F
				for dy=0,7 do
					for dx=0,7 do
						local sx = x + dx
						local sy = y + dy
						if sx >= 0 and sx < 255	-- pixel 255 is omitted
						and sy >= 0 and sy < 240
						then
							if sx >= 8
							-- I think this is wrong ...
							--or (bg and PPUMASK & PPUMASK_flagShowBGLeft ~= 0)
							--or (not bg and PPUMASK & PPUMASK_flagShowSpritesLeft ~= 0)
							then
								local sprPix = peek(n9SheetAddr + (
									dx | (sprX << 3)
									| ((dy | (sprY << 3)) << 8)
								))
								if sprPix ~= 0 then
									-- does the framebuffer matter?
									--if fbAddr(sx | (sy << 8)) ~= 0 then
									PPUSTATUS |= PPUSTATUS_flagSprite0Hit
									--end
								end
							end
						end
					end
				end
			end

			local sx, sy = 1, 1
			if attrs & 0x80 ~= 0 then	-- hflip
				sy = -1
				y += 8
			end
			if attrs & 0x40 ~= 0 then	-- vflip
				sx = -1
				x += 8
			end
			spr(sprIndex, x, y, 1, 1,
				pal<<2, -- palette offset
				0, 		-- transparent index
				nil, 	-- sprite bit
				nil, 	-- sprite mask
				sx, sy)
		end
	end

	-- do some instuctions "outside" hislopor idk?
	for i=1,600 do
		cpuRun()
	end

	text('A='..hex(A)..' X='..hex(X)..' Y='..hex(Y)..' S='..hex(S)..' P='..hex(P)..' PC='..hex(PC,4)..' op='..hex(nespeek(PC))..' ', 0, 0, 0x30, 0x0F)
end
