#!perl

use strict;
use warnings;

use Test::More;

use_ok 'CPU::Z80::Disassembler::Instruction';

my $mem;
my $instr;

#------------------------------------------------------------------------------
# test one instruction
sub t_instr (@) {
	my($factory, $addr, $limit, $size, $opcode, $args, 
	   $is_call, $is_branch, $is_break_flow,
	   $string, $dump) = @_;
	
	my $caller_line = (caller)[2];
	ok 1, "[line $caller_line]";

	isa_ok $instr = CPU::Z80::Disassembler::Instruction->$factory(
												$mem, $addr, $limit), 
			'CPU::Z80::Disassembler::Instruction';

	is $instr->memory, 	$mem, 		"memory";
	is $instr->addr, 	$addr, 		"addr = $addr";
	is $instr->size, 	$size, 		"size = $size";
	is $instr->opcode, 	$opcode, 	"opcode = $opcode";
	is !!$instr->is_code, !!($opcode !~ /def|org/), "is_code = ".
									(!!$instr->is_code);
	
	for my $arg (qw( N NN DIS STR )) {
		ok exists($args->{$arg}) ? 
				_list_str($args->{$arg}) eq _list_str($instr->$arg) :
				!defined($instr->$arg),	
			"$arg = ".(exists($args->{$arg}) ? 
							_list_str($args->{$arg}) : 
							'undef');
	}
	
	is $instr->next_addr,	$addr+$size,	"next_addr";

	my @next_code;
	push @next_code, $instr->NN if $instr->opcode =~ /jr|jp .*NN|djnz|call|rst/;
	push @next_code, $instr->next_addr unless $instr->opcode =~ /ret$|reti|retn|jp NN|jr NN|call NN|rst|jp \(|org/;
	is_deeply [$instr->next_code], \@next_code;
	
	my $bytes = [];
	for (0 .. $size-1) {
		push @$bytes, $mem->peek($addr+$_);
	}
	is_deeply $instr->bytes,	$bytes,		"bytes = @$bytes";
	
	is !!$instr->is_call,		!!$is_call,	
									"is_call = $is_call";
	is !!$instr->is_branch,		!!$is_branch,
									"is_branch = $is_branch";
	is !!$instr->is_break_flow,	!!$is_break_flow,
									"is_break_flow = $is_break_flow";
	
	is $instr->as_string,	$string,	"as_string = $string";
	is $instr->dump,		$dump,		"dump = \n$dump";
	
	my $asm = " " x 8 . $string . "\n";
	$asm .= "\n" if $opcode =~ /jp NN|jp \(|jr NN|ret[in]?$|org/;
	is $instr->asm,			$asm,		"asm = \n$asm";
	
}

sub _list_str {
	my($v) = @_;
	ref($v) ? join(",", @$v) : $v;
}


#------------------------------------------------------------------------------
isa_ok $mem = CPU::Z80::Disassembler::Memory->new, 
			'CPU::Z80::Disassembler::Memory';
$mem->load_file('t/data/zx48.rom');

#------------------------------------------------------------------------------
# disassemble - undef 1st byte
is $instr = CPU::Z80::Disassembler::Instruction->disassemble($mem, 0x4000), 
			undef;

#------------------------------------------------------------------------------
# disassemble - undef 2nd byte
isa_ok $mem = CPU::Z80::Disassembler::Memory->new, 
			'CPU::Z80::Disassembler::Memory';
$mem->load_file('t/data/zx48.rom', 0, 0, 3);

is $instr = CPU::Z80::Disassembler::Instruction->disassemble($mem, 2), 
			undef;

isa_ok $mem = CPU::Z80::Disassembler::Memory->new, 
			'CPU::Z80::Disassembler::Memory';
$mem->load_file('t/data/zx48.rom');

#------------------------------------------------------------------------------
# disassemble - invalid opcode
is $instr = CPU::Z80::Disassembler::Instruction->disassemble($mem, 0x1A88), 
			undef;

#------------------------------------------------------------------------------
# disassemble

# no args
t_instr	disassemble => 0x0000,	undef,
				1,	'di',		{}, 				0, 0, 0, 
					"di",
	"0000 F3         di\n";

# N
t_instr	disassemble => 0x107,	undef,
				2,	'ld a, N',	{N => 0xBD}, 		0, 0, 0, 
					"ld a, \$BD",
	"0107 3EBD       ld a, \$BD\n";

# NN
t_instr	disassemble => 0x0002,	undef,
				3,	'ld de, NN',	{NN => 0xFFFF}, 	0, 0, 0, 
					"ld de, \$FFFF",
	"0002 11FFFF     ld de, \$FFFF\n";

# DIS < 0
t_instr	disassemble => 0x0F3E,	undef,
				3,	'ld e, (iy+DIS)', {DIS => -1}, 	0, 0, 0, 
					"ld e, (iy-\$01)",
	"0F3E FD5EFF     ld e, (iy-\$01)\n";

# DIS == 0
t_instr	disassemble => 0x0055,	undef,
				3,	'ld (iy+DIS), l', {DIS => 0}, 	0, 0, 0, 
					"ld (iy), l",
	"0055 FD7500     ld (iy), l\n";

# DIS > 0
t_instr	disassemble => 0x0045,	undef,
				3,	'inc (iy+DIS)', {DIS => 64}, 	0, 0, 0, 
					"inc (iy+\$40)",
	"0045 FD3440     inc (iy+\$40)\n";

# jump relative backwards
t_instr	disassemble => 0x062B,	undef,
				2,	'djnz NN', {NN => 0x0629}, 		0, 1, 0, 
					"djnz \$0629",
	"062B 10FC       djnz \$0629\n";

# jump jump relative forwards
t_instr	disassemble => 0x0043,	undef,
				2,	'jr nz, NN', {NN => 0x0048}, 	0, 1, 0, 
					"jr nz, \$0048",
	"0043 2003       jr nz, \$0048\n";

# inconditional relative jump 
t_instr	disassemble => 0x000E,	undef,
				2,	'jr NN', {NN => 0x0053}, 		0, 1, 1, 
					"jr \$0053",
	"000E 1843       jr \$0053\n";

# inconditional absolute jump 
t_instr	disassemble => 0x0005,	undef,
				3,	'jp NN', {NN => 0x11CB}, 		0, 1, 1, 
					"jp \$11CB",
	"0005 C3CB11     jp \$11CB\n";

# conditional absolute jump 
t_instr	disassemble => 0x00B2,	undef,
				3,	'jp nc, NN', {NN => 0xD441}, 	0, 1, 0, 
					"jp nc, \$D441",
	"00B2 D241D4     jp nc, \$D441\n";

# register jump 
t_instr	disassemble => 0x006F,	undef,
				1,	'jp (hl)', {}, 					0, 0, 1, 
					"jp (hl)",
	"006F E9         jp (hl)\n";

# rst
t_instr	disassemble => 0x0114,	undef,
				1,	'rst N', {N => 8, NN => 8},		1, 1, 1, 
					"rst \$08",
	"0114 CF         rst \$08\n";

# inconditional call 
t_instr	disassemble => 0x001C,	undef,
				3,	'call NN', {NN => 0x007D}, 		1, 1, 1, 
					"call \$007D",
	"001C CD7D00     call \$007D\n";

# conditional call 
t_instr	disassemble => 0x0098,	undef,
				3,	'call nz, NN', {NN => 0x4E49}, 	1, 1, 0, 
					"call nz, \$4E49",
	"0098 C4494E     call nz, \$4E49\n";

# inconditional ret 
t_instr	disassemble => 0x0052,	undef,
				1,	'ret', {},				 		0, 0, 1, 
					"ret",
	"0052 C9         ret\n";

# conditional ret 
t_instr	disassemble => 0x001F,	undef,
				1,	'ret nc', {}, 					0, 0, 0, 
					"ret nc",
	"001F D0         ret nc\n";

# retn
t_instr	disassemble => 0x0072,	undef,
				2,	'retn', {},				 		0, 0, 1, 
					"retn",
	"0072 ED45       retn\n";

# reti
$mem->poke(0x0073, 0x4D);
t_instr	disassemble => 0x0072,	undef,
				2,	'reti', {},				 		0, 0, 1, 
					"reti",
	"0072 ED4D       reti\n";
$mem->poke(0x0073, 0x45);

# composed instr with DIS+1 - decode smallest
for my $limit (undef, 0x702 .. 0x704) {
	ok 1, "limit ".(defined $limit ? sprintf("%04X", $limit) : 'undef');
	
	t_instr	disassemble => 0x06FF,	$limit,
					3,	'ld (ix+DIS), c', {DIS => 11}, 	0, 0, 0, 
						"ld (ix+\$0B), c",
		"06FF DD710B     ld (ix+\$0B), c\n";
}

# composed instr with DIS+1 - decode biggest
for my $limit (-1, 0x6FF .. 0x701, 0x705) {
	ok 1, "limit ".(defined $limit ? sprintf("%04X", $limit) : 'undef');
	
	t_instr	disassemble => 0x06FF,	$limit,
					6,	'ld (ix+DIS), bc', {DIS => 11}, 	0, 0, 0, 
						"ld (ix+\$0B), bc",
		"06FF DD710BDD70 ld (ix+\$0B), bc\n".
		"     0C         \n";
}
	
# defb
is $instr = CPU::Z80::Disassembler::Instruction->defb($mem, 0x4000), 
			undef;
is $instr = CPU::Z80::Disassembler::Instruction->defb($mem, 0x3FFF, 2), 
			undef;
t_instr	defb => 0,		undef,
				1,	'defb N', {N => 0xF3}, 			0, 0, 0, 
					"defb \$F3",
	"0000 F3         defb \$F3\n";

t_instr	defb => 0,		1,
				1,	'defb N', {N => 0xF3}, 			0, 0, 0, 
					"defb \$F3",
	"0000 F3         defb \$F3\n";

t_instr	defb => 0,		2,
				2,	'defb N', {N => [0xF3,0xAF]},	0, 0, 0, 
					"defb \$F3, \$AF",
	"0000 F3AF       defb \$F3, \$AF\n";

t_instr	defb => 0,		8,
				8,	'defb N', {N => [0xF3,0xAF,0x11,0xFF,0xFF,0xC3,0xCB,0x11]},
													0, 0, 0, 
					"defb \$F3, \$AF, \$11, \$FF, \$FF, \$C3, \$CB, \$11", 
	"0000 F3AF11FFFF defb \$F3, \$AF, \$11, \$FF, \$FF, \$C3, \$CB, \$11\n".
	"     C3CB11     \n";

# defw
is $instr = CPU::Z80::Disassembler::Instruction->defw($mem, 0x4000), 
			undef;
is $instr = CPU::Z80::Disassembler::Instruction->defw($mem, 0x3FFF), 
			undef;
is $instr = CPU::Z80::Disassembler::Instruction->defw($mem, 0x4000-3, 2), 
			undef;

t_instr	defw => 0x3FFE,		undef,
				2,	'defw NN', {NN => 0x3C42},		0, 0, 0, 
					"defw \$3C42",
	"3FFE 423C       defw \$3C42\n";

t_instr	defw => 0x3FFE,		1,
				2,	'defw NN', {NN => 0x3C42},		0, 0, 0, 
					"defw \$3C42",
	"3FFE 423C       defw \$3C42\n";

t_instr	defw => 0,		4,
				8,	'defw NN', {NN => [0xAFF3,0xFF11,0xC3FF,0x11CB]},
													0, 0, 0, 
					"defw \$AFF3, \$FF11, \$C3FF, \$11CB",
	"0000 F3AF11FFFF defw \$AFF3, \$FF11, \$C3FF, \$11CB\n".
	"     C3CB11     \n";

# defm
is $instr = CPU::Z80::Disassembler::Instruction->defm($mem, 0x4000, 2), 
			undef;
is $instr = CPU::Z80::Disassembler::Instruction->defm($mem, 0x3FFF, 2), 
			undef;
t_instr	defm => 0x153B, 	22,
				22,	'defm STR', {STR => "1982 Sinclair Research"}, 0, 0, 0, 
					"defm \"1982 Sinclair Research\"",
	"153B 3139383220 defm \"1982 Sinclair Research\"\n".
	"     53696E636C \n".
	"     6169722052 \n".
	"     6573656172 \n".
	"     6368       \n";

# defm7
is $instr = CPU::Z80::Disassembler::Instruction->defm7($mem, 0x4000), 
			undef;
is $instr = CPU::Z80::Disassembler::Instruction->defm7($mem, 0x3FFF), 
			undef;
t_instr	defm7 => 0x153B, 	undef,
				26,	'defm7 STR', {STR => "1982 Sinclair Research Ltd"}, 0, 0, 0, 
					"defm7 \"1982 Sinclair Research Ltd\"",
	"153B 3139383220 defm7 \"1982 Sinclair Research Ltd\"\n".
	"     53696E636C \n".
	"     6169722052 \n".
	"     6573656172 \n".
	"     6368204C74 \n".
	"     E4         \n";

# defmz
is $instr = CPU::Z80::Disassembler::Instruction->defmz($mem, 0x4000), 
			undef;
is $instr = CPU::Z80::Disassembler::Instruction->defmz($mem, 0x3FFF), 
			undef;

$mem->poke(0x153B + 22, 0);		# create a zero-terminated string
t_instr	defmz => 0x153B, 	undef,
				23,	'defmz STR', {STR => "1982 Sinclair Research"}, 0, 0, 0, 
					"defmz \"1982 Sinclair Research\"",
	"153B 3139383220 defmz \"1982 Sinclair Research\"\n".
	"     53696E636C \n".
	"     6169722052 \n".
	"     6573656172 \n".
	"     636800     \n";

# org
t_instr	org => 0x8000,		undef,
				0,	'org NN', {NN => 0x8000}, 			0, 0, 1, 
					"org \$8000",
	"8000            org \$8000\n";

#------------------------------------------------------------------------------
# format

# N
isa_ok	$instr = CPU::Z80::Disassembler::Instruction->disassemble(
												$mem, 0x107), 
			'CPU::Z80::Disassembler::Instruction';
is		$instr->as_string,	"ld a, \$BD";
$instr->format->{N} = sub { shift };
is		$instr->as_string,	"ld a, " . 0xBD;
$instr->format->{N} = sub { 'VAR' };
is		$instr->as_string,	"ld a, VAR";

# NN
isa_ok	$instr = CPU::Z80::Disassembler::Instruction->disassemble(
												$mem, 0x0002), 
			'CPU::Z80::Disassembler::Instruction';
is		$instr->as_string,	"ld de, \$FFFF";
$instr->format->{NN} = sub { shift };
is		$instr->as_string,	"ld de, " . 0xFFFF;
$instr->format->{NN} = sub { 'VAR' };
is		$instr->as_string,	"ld de, VAR";

# DIS < 0
isa_ok	$instr = CPU::Z80::Disassembler::Instruction->disassemble(
												$mem, 0x0F3E), 
			'CPU::Z80::Disassembler::Instruction';
is		$instr->as_string,	"ld e, (iy-\$01)";
$instr->format->{DIS} = sub { sprintf("%+d", shift) };
is		$instr->as_string,	"ld e, (iy-1)";
$instr->format->{DIS} = sub { '+VAR-BASE' };
is		$instr->as_string,	"ld e, (iy+VAR-BASE)";

# DIS == 0
isa_ok	$instr = CPU::Z80::Disassembler::Instruction->disassemble(
												$mem, 0x0055), 
			'CPU::Z80::Disassembler::Instruction';
is		$instr->as_string,	"ld (iy), l";
$instr->format->{DIS} = sub { sprintf("%+d", shift) };
is		$instr->as_string,	"ld (iy+0), l";
$instr->format->{DIS} = sub { '+VAR-BASE' };
is		$instr->as_string,	"ld (iy+VAR-BASE), l";

# DIS > 0
isa_ok	$instr = CPU::Z80::Disassembler::Instruction->disassemble(
												$mem, 0x0045), 
			'CPU::Z80::Disassembler::Instruction';
is		$instr->as_string,	"inc (iy+\$40)";
$instr->format->{DIS} = sub { sprintf("%+d", shift) };
is		$instr->as_string,	"inc (iy+64)";
$instr->format->{DIS} = sub { '+VAR-BASE' };
is		$instr->as_string,	"inc (iy+VAR-BASE)";

# STR
isa_ok	$instr = CPU::Z80::Disassembler::Instruction->defm(
												$mem, 0x153B, 22), 
			'CPU::Z80::Disassembler::Instruction';
is		$instr->as_string,	"defm \"1982 Sinclair Research\"";
$instr->format->{STR} = sub { '"'.(shift).'"' };
is		$instr->as_string,	'defm "1982 Sinclair Research"';
$instr->format->{STR} = sub { 'STR' };
is		$instr->as_string,	"defm STR";


#------------------------------------------------------------------------------
# comment
isa_ok	$instr = CPU::Z80::Disassembler::Instruction->disassemble($mem, 0), 
			'CPU::Z80::Disassembler::Instruction';

is		$instr->as_string,	'di';

$instr->comment("Disable interrupts");
is		$instr->as_string,	'di'. ' ' x 22 . '; Disable interrupts';

$instr->opcode('x' x 24);
is		$instr->as_string,	'x' x 24 . "\n" . ' ' x 32 . '; Disable interrupts';

$instr->opcode('di');
$instr->comment("Disable\ninterrupts");
is		$instr->as_string,	'di'. ' ' x 22 . "; Disable\n" . 
								  ' ' x 32 . "; interrupts";

$instr->opcode('x' x 24);
is		$instr->as_string,	'x' x 24 . "\n" . 
								  ' ' x 32 . "; Disable\n" . 
								  ' ' x 32 . "; interrupts";



done_testing;
