-- The Adventures of Batman and Robin
-- 2013-2024, feos and r57shell

-- GLOBALS --
MsgTable    = {}
MsgTime     = 16
MsgOffs     = 24
MsgCutoff   = 60
RNGcount    = 0
SpawnCount  = 0
SpawnDelay  = 0
SpawnX      = 0
SpawnY      = 0
Enemies     = 0
Items       = 0
Hearts      = 0

-- SHORTCUTS --
rb  = memory.read_u8
rbs = memory.read_s8
rw  = memory.read_u16_be
rws = memory.read_s16_be
rl  = memory.read_u32_be
rls = memory.read_s32_be
rex = event.onmemoryexecute
getr= emu.getregister
box = gui.drawBox
text= gui.pixelText
line= gui.drawLine

memory.usememorydomain("M68K BUS")
userdata.set("SpawnCount", 0)
userdata.set("SpawnOpac", 192)

function ToSigned16(num) 
	if num > 32768 then
		num = num - (2 * 32768)
		return num
	else return num
	end
end

function FitX(x, text)
	local length = 0
	if text ~= nil then length = string.len(text)*5 end
	if x <   0 then x = 0
	elseif x+length > 319 then x = 319-length end
	return x
end

function FitY(y)
	    if y <   0 then y = 0
	elseif y > 210 then y = 210 end
	return y
end

function GetCam()
	xcam = rws(0xFFDFC4)
	if rb(0xFFFFF6) == 50 then
		ycam = rws(0xFFDFE0)-20
	else
		ycam = 0
	end
end

function EnemyPos(Base)
	GetCam()
	x1 = rws(Base + 0x12) - xcam
	y1 = rws(Base + 0x14) - ycam
	x2 = rws(Base + 0x16) - xcam
	y2 = rws(Base + 0x18) - ycam
	hp = rws(Base + 0x1E)
end

function PlayerPos()
	local sbase1 = rw(0xFFAD5C) + 0xFF0000
	local sbase2 = rw(0xFFADB6) + 0xFF0000
	p1speedx = rls(sbase1 + 0x18) / 0x10000
	p1speedy = rls(sbase1 + 0x1C) / 0x10000
	p2speedx = rls(sbase2 + 0x18) / 0x10000
	p2speedy = rls(sbase2 + 0x1C) / 0x10000
  	LIFT = rw(0xFF9024)
end

function HandleMsgTable(clear)
	for i = 1, #MsgTable do
		if clear then
			MsgTable[i] = nil
		end		
		if MsgTable[i] then
			GetCam()
			if MsgTable[i].y_ > MsgCutoff then
				MsgY1 = 0
				MsgY2 = 6
			else
				MsgY1 = 203
				MsgY2 = 203				
			end
			line(i * MsgOffs + 3, MsgY2,
				MsgTable[i].x_ - xcam, MsgTable[i].y_,
				0xFF0000 + (MsgTable[i].timer_ << 28))
			text(i * MsgOffs    , MsgY1, MsgTable[i].damage_, "red")
			MsgTable[i].timer_ = MsgTable[i].timer_ - 1
			if MsgTable[i].timer_ <= 0 then
				MsgTable[i] = nil
			end
		end
	end
end

function HandleDamage()
	local damage = getr("M68K D0") &   0xFFFF
	local base   = getr("M68K A2") & 0xFFFFFF
--	print(string.format("%X", getr("M68K PC")))
	EnemyPos(base)
	unit = {
		timer_  = MsgTime,
		damage_ = damage,
		x_      = x1 + xcam,
		y_      = y1
	}
	for i = 1, 200 do
		if MsgTable[i] == nil then
			MsgTable[i] = unit
			break
		end
	end
end

function Collision()
	GetCam()
	local a0     = getr("M68K A0") & 0xFFFFFF
	local a6     = getr("M68K A6") & 0xFFFFFF
	local wx1    = ToSigned16(getr("M68K D4") & 0xFFFF) - xcam
	local wy1    = ToSigned16(getr("M68K D5") & 0xFFFF) - ycam
	local wx2    = ToSigned16(getr("M68K D6") & 0xFFFF) - xcam
	local wy2    = ToSigned16(getr("M68K D7") & 0xFFFF) - ycam
	local id     = rw(a6 + 2)
	local damage = rw(a6 + 0x12)
--	text(wx2 + 2, wy1 + 1, string.format("%X",a6))
	if damage == 0 then
		damage = rw(a0 + 0x34)
	end
	if DamageHitbox then
		box(wx1, wy1, wx2, wy2, 0xFFFF0000)
		text(wx1 + 2, wy1 + 1, damage)
	else
		box(wx1, wy1, wx2, wy2, 0xFFFFFF00)
		if id == 0x53B4 then Hearts = Hearts + 1 end
	end
end

function InRange(var, num1, num2)
	if var >=  num1 and var <= num2
	then return true
	end
end

function Item()
	GetCam()
	local a6   = getr("M68K A6") & 0xFFFFFF
	local x    = rw(a6 + 0x3E) - xcam
	local y    = rw(a6 + 0x42)
	local code = rb(a6 + 0x19)
	if     InRange(code,  0,  1) then return
	elseif InRange(code,  7, 19) then item = "Amo" -- ammo
	elseif InRange(code, 21, 23) then item = "Cha" -- fast charge
	elseif InRange(code, 24, 26) then item = "Bom" -- bomb
	elseif InRange(code, 27, 29) then item = "Lif" -- life
	elseif InRange(code, 30, 47) then item = "HiP" -- hearts
	else                              item = tostring(code)
	end
	text(x-7, y, string.format("%s"  , item   ), "yellow")
--	text(x-7, y, string.format("\n%X", a6+0x19), "yellow")
end

function Hitbox(address)
	local i = 0
	local base = rw(address)	
	while (base ~= 0) do
		base = base + 0xFF0000
		if rw(base + 2) == 0 then break end
		EnemyPos(base)
		if address == 0xFFDEB2 then
			box(x1, y1, x2, y2, 0xFF00FF00)
		elseif address == 0xFFDEBA then
			box(x1, y1, x2, y2, 0xFF00FFFF)
			text(FitX(x1, hp) + 2, FitY(y1) + 1, hp, 0xFFFF00FF)
		--	if x2 <    0 then text(x1 + 2, y2 - 7, "x:" .. x1      ) end
		--	if x1 >= 320 then text(x1 + 2, y2 - 7, "x:" .. x1 - 320) end
		--	if y2 <    0 then text(x2 + 2, y2 - 7, "y:" .. y2      ) end
			local offtext = ""
			if x2 <    0 then offtext = offtext .. "x:" .. x1       end
			if x1 >= 320 then offtext = offtext .. "x:" .. x1 - 320 end
			if y2 <    0 then offtext = offtext .. "y:" .. y2       end
			if y2 >= 224 then offtext = offtext .. "y:" .. y2 - 224 end
			if offtext ~= "" then
				text(FitX(x1, offtext), FitY(y1) - 7, offtext)
			end
		end
		base = rw(base + 2)
		i = i + 1
		if i > 400 then break end
	end
end

function Objects()
	local level = rw(0xFFFFF5)
	if level ~= 818 and level ~= 1026 then return end
	Enemies = 0
	Items   = 0
	GetCam()
	local base = 0xFFAD54
	for i=0,100 do
		local id    = rl (base)
		local link  = rw (base+   6)
		local ptr1  = rw (base+0x0A) + 0xFF0000
		local delay = rl (base+0x2E) >> 8
		local x     = rws(base+0x3E)
		local xsub  = rb (base+0x40)
		local y     = rws(base+0x42)
		local ysub  = rb (base+0x44)
		local hp    = rw (base+0x52)
		local ptr2  = 0
		if id == 0x53B3 then ptr2 = rl(ptr1+0x2A) end
	--	local code = rw (ptr2)
		if base > 0 then
			if ptr2 == 0x27DEE -- helicopter black
			or ptr2 == 0x27F9C -- helicopter red
			or ptr2 == 0x2804E -- plane black
			or ptr2 == 0x28134 -- helicopter green
			or ptr2 == 0x282B8 -- plane red
			or ptr2 == 0x2860A -- missile
			or ptr2 == 0x28DD2 -- helicopter red phase 1
			or ptr2 == 0x28E08 -- helicopter red phase 2
			then
				Enemies = Enemies + 1
			elseif ptr2 == 0x13326
			or     ptr2 == 0x13BDE
			then
				Items = Items + 1
			else
				text(x - xcam, y - ycam, string.format("%d", delay))
			end
			if delay > 0 and delay < 1000 then
				SpawnDelay = delay
			--	print(string.format("%d delay = %d", i, delay))
				local xscr = 100
				local yscr = 30 + i*10
				text(xscr, yscr, string.format("delay %d: %d", i, delay))
			end
		end
		base = link + 0xFF0000
		local a5 = rl(base)
		if a5 == 0x88BE then return end
	end
end

function Spawns()
	local level = rw(0xFFFFF5)
	if level ~= 818 and level ~= 1026 then return end
	local base = getr("M68K A6") & 0xFFFFFF
	local ptr1 = rw(base+0x0A) + 0xFF0000
	local ptr2 = rl(ptr1+0x2A)
	local code = rw(ptr2)
	local text = ""
	SpawnX = rws(base+0x3E) - xcam
	SpawnY = rws(base+0x42) - ycam
	
	if     ptr2 == 0x3A6E2 then text = "HOMING"
	elseif ptr2 == 0x3A5C6 then text = "SCROLLING"
	elseif ptr2 == 0x3A81E then text = "LONG 1"
	elseif ptr2 == 0x3A778 then text = "LONG 2"
	else                        text = "UNKNOWN"
	end
	
	if  code ~= 0xAE6  -- drone
	and code ~= 0xAF2  -- mini-missile
	and code ~= 0x2384 -- item
	then
		userdata.set("SpawnOpac", 192)
		userdata.set("SpawnCount", userdata.get("SpawnCount") + 1)
	--	print(string.format("%02d - %X - %s", userdata.get("SpawnCount"), ptr2, text))
	end
end

function Waves()
	local level = rw(0xFFFFF5)
	if level ~= 1142 then return end
	GetCam()
	local waveCount = rw(0xffdfb0)
	text(120, 8, "Waves: " .. waveCount)
	local base = 0xFFAD54
	for i=0,100 do
		local id     = rl (base)
		local link   = rw (base+   6)
		local x0A    = rw (base+0x0A)
		local x12    = rw (base+0x12)
		local x14    = rw (base+0x14)
		local code   = rl (base+0x1E) & 0xffffff
		local delay  = rl (base+0x2E) >> 8
		local x      = rws(base+0x3E)
		local y      = rws(base+0x42)
		local hp     = rws(base+0x52)
		local nextop = rw (code)
		local wait   = ""
		local color  = 0xffffffff
		if base > 0
		and id  ~= 0x53b4                 -- projectile
		and x0A ~= 0xaf16                 -- batman
		and not (hp <= 0 and id == 0x7D2) -- item
		then
			local xscr = 120
			local yscr = 0 + i*7 + 14
			if base == 0xFFAE62                   then color =   "yellow" end
			if code == 0x411FC                    then color = 0xff00ff00 end
			if code == 0x41206 or code == 0x4130E then color =      "red" end
			if nextop == 0x6ED2 then
				wait = ":: wait enemies == 0"
			elseif nextop >= 0x6EB2 and nextop <= 0x6ED2 then
				wait = string.format(":: wait enemies <= %d", (0x6ED2 - nextop) / 4)
			end
			if x ~= 0 or y ~= 0 then
				line(x - xcam, y - ycam, xscr, yscr+3, 0xAAFFFFFF)
			end
			text(xscr, yscr, string.format(
				"%2d:%4X %4X %4X  Timer:%3d %s",
				i, x0A, x12, x14, delay, wait), color)
			text(x - xcam - 10, y - ycam, i, 0xFF00FF00)
		end
		base = link + 0xFF0000
		if rl(base) == 0x88BE then return end
	end
end

function CalculateDelay()
	local d0 = getr("M68K D0") >> 8
	local d1 = getr("M68K D1") >> 8
	print(string.format("Random spawn delay: %d + %d", d1, d0))
end

function PredictItem(rng)
	rng = rng & 0xFFFF
	local a2 = 0x29B78
	local d0 = 0
	local carry = rw(a2) > rng
	a2 = a2 + 2
	if carry then
		a2 = a2 + 2
		while rw(a2) > rng do
			a2 = a2 + 4
		end
		a2 = a2 + 2
	end
	a2 = a2 + rw(a2) + 2
	if a2 == 0x29BBA then a2 = 0 end
	return rw(a2) & 0xFF
end

function RNGroll(seed1, seed2)                     -- subroutine $995C
	local d0 = seed1                                           
	local d1 = seed2                                           
	d1 =  (d1           <<  1) | (d1        >> 31) -- ROL.L   #1,D1
	d1 = ((d1 & 0xFFFF) ~  d0) | (d1 & 0xFFFF0000) -- EOR.W   D0,D1
	d1 = ((d1 & 0xFFFF) << 16) | (d1        >> 16) -- SWAP.W  D1
	d0 =  (d1 ~ d0)  & 0xFFFF                      -- EOR.W   D1,D0
	d0 = ((d0 << 1)  & 0xFFFF) | ((d0 << 1) >> 16) -- ROL.W   #1,D0
	d1 = ((d1 & 0xFFFF) ~  d0) | (d1 & 0xFFFF0000) -- EOR.W   D0,D1
	d1 = ((d1 & 0xFFFF) << 16) | (d1        >> 16) -- SWAP.W  D1
	d0 = ((d1 & 0xFFFF) ~  d0)                     -- EOR.W   D1,D0
	return {(d0 & 0xFFFF), d1}
end

function ItemPrediction()
	local RNG1 = rw (0xFFF5FC)
	local RNG2 = rl (0xFFF5FE)
	local RNG  = RNGroll(RNG1, RNG2)
	local RNG  = RNGroll(RNG[1], RNG[2])
	local item = PredictItem(RNG[1])
	gui.text(0, 170, string.format("%2X", item),"yellow")
end

function Main()
	local color0     = "yellow"
	local color1     = "yellow"
	local color2     = "yellow"
	local base1      = 0xFFAD54
	local base2      = 0xFFADAE
--	local hp1        = rw (0xFFF654)
--	local life1      = rw (0xFFF644)
	local level      = rw (0xFFFFF5)
	local X1         = rw (base1 + 0x3E)
	local X1sub      = rb (base1 + 0x40)
	local Y1         = rws(base1 + 0x42)
	local Y1sub      = rb (base1 + 0x44)
	local X2         = rw (base2 + 0x3E)
	local X2sub      = rb (base2 + 0x40)
	local Y2         = rws(base2 + 0x42)
	local Y2sub      = rb (base2 + 0x44)
	local RNG1       = rw (0xFFF5FC)
--	local RNG2       = rl (0xFFF5FE)
	local Weapon1    = rb (0xFFF67B)
	local Weapon2    = rb (0xFFF6BB)
	local Charge1    = (rw(0xFFF658) - 0x2800) / -0x80
	local Charge2    = (rw(0xFFF698) - 0x2800) / -0x80
	local ScreenLock = rw (0xFFDFC0)
	if Charge1 <= 0 then Charge1 = 0 color1 = "red" end
	if Charge2 <= 0 then Charge2 = 0 color2 = "red" end
	if RNGcount > 1 then             color0 = "red" end
	HandleMsgTable()
	PlayerPos()
	Objects()
	Waves()
	if level == 818 or level == 1026 then
		line(30, 42, SpawnX, SpawnY, 0x00FF00 + (userdata.get("SpawnOpac") << 24))
		text( 0, 35, string.format("Obj: %d", userdata.get("SpawnCount")), 0xFF00FF00)
		text( 0, 49, string.format("%d %d %s", Enemies, Items, Hearts/2))
		if SpawnDelay > 0 and SpawnDelay < 1000 then
			text(0, 42, string.format("Del: %d", SpawnDelay), "yellow")
		end
	end
	text(  1, 217, string.format("RNG:%X"               , RNG1))
	text( 40, 217, string.format("Lock:%d"              , ScreenLock))
	text( 81, 210, string.format("Pos: %d.%d\nSpd: %.5f", X1, X1sub, p1speedx))
	text(137, 210, string.format("/ %d.%d\n/ %.5f"      , Y1, Y1sub, p1speedy))
	text( 80,  22, string.format("%2.0f"                , Charge1),                 color1)
--	text(235,  20, string.format("%2.0f"                , Charge2),                 color2)
	text( 34, 217, string.format("%d"                   , RNGcount),                color0)
	text(180, 217, string.format("%2d"                  , Weapon1+1),             "yellow")
--	text(300, 217, string.format("%2d"                  , Weapon2+1),             "yellow")
--	text(203, 210, string.format("Pos: %d.%d\nSpd: %.5f", X2, X2sub, p2speedx), 0xFF00FF00)
--	text(260, 210, string.format("/ %d.%d\n/ %.5f"      , Y2, Y2sub, p2speedy), 0xFF00FF00)
	Hitbox(0xFFDEB2)
	Hitbox(0xFFDEBA)
	RNGcount  = 0

	if rb(0xFF4633) == 5 then	
	--	text(143,3,string.format("     %2d", LIFT), 0xFFFF00FF, clear) 
	end
	
	DER =  rw(0xFFAEA0)
--	text(135,3,string.format("     %2d", DER), 0xFFFF00FF, clear)
	
--	emu.frameadvance()
--	gui.clearGraphics()
end

event.onframeend(function()
	local spawnOpac = userdata.get("SpawnOpac") - 4
	if spawnOpac < 0 then spawnOpac = 0 end
	userdata.set("SpawnOpac", spawnOpac)
	Main()
end)

event.onframestart(function()
	SpawnDelay1 = 0
	SpawnDelay2 = 0
	Hearts      = 0
	ItemPrediction()
end)

event.onloadstate(function()
	Enemies    = 0
	Items      = 0
	Hearts     = 0
	SpawnCount = 0
	return HandleMsgTable(1)
end)

rex(function() DamageHitbox = false end   , 0x375A, "DamageHitbox-")
rex(function() DamageHitbox = true  end   , 0x375E, "DamageHitbox+")
rex(function() DamageHitbox = false end   , 0x3768, "DamageHitbox-")
rex(function() DamageHitbox = true  end   , 0x376C, "DamageHitbox+")
rex(function() DamageHitbox = false end   , 0x65C4, "DamageHitbox-")
rex(function() DamageHitbox = true  end   , 0x65C8, "DamageHitbox+")
rex(CalculateDelay                        , 0x6ADE,         "Delay")
rex(function() RNGcount = RNGcount + 1 end, 0x995C,    "RNGcount++")
rex(Item                                  , 0x4738,          "Item")
rex(Item                                  , 0x4534,          "Item")
rex(Spawns                                , 0x8DE6,        "Spawns")
rex(Spawns                                , 0x8DCE,        "Spawns")
rex(Collision                             , 0x8C9A,     "Collision")
rex(HandleDamage                          , 0x1085A, "MeeleeDamage")
rex(HandleDamage                          , 0x10CBA, "WeaponDamage")
rex(HandleDamage                          , 0x10CC4, "WeaponDamage")