package Z80;

use strict;
use warnings;

use Data::Dump qw/dump/;
use Carp qw/confess/;
use Exporter 'import';
our @EXPORT = qw'dump_R @mem 
 $AF $BC $DE $HL $IX $IY $PC $SP
 $AF1 $BC1 $DE1 $HL1;
 $IFF $I
 $R
 $IPeriod $ICount $IRequest $IAutoReset $TrapBadOps $Trap $Trace
$debug';
our $VERSION = '0.0.1';
require XSLoader;
XSLoader::load('Z80', $VERSION);

=head1 NAME

Z80 - perl bindings for Z80 CPU emulator

=head1 FUNCTIONS

=cut

our $debug = 0;

our @mem = (0xff) x 0x10000;	# 64M

# CPU registars
our (
	$AF, $BC, $DE, $HL, $IX, $IY, $PC, $SP,
	$AF1, $BC1, $DE1, $HL1,
	$IFF, $I,
	$R
) = (0) x 15;

our $IPeriod=1;		# Set IPeriod to number of CPU cycles between calls to Loop6502
our $ICount;
our $IRequest;		# Set to the INT_IRQ when pending IRQ
our $IAutoReset;	# Set to 1 to autom. reset IRequest
our $TrapBadOps=1;	# Set to 1 to warn of illegal opcodes
our $Trap;			# Set Trap to address to trace from
our $Trace;			# Set Trace=1 to start tracing

=head2 init

Setup read and write memory hooks (to implement memory mapped devices)

  $init->(
	read => sub {
		return $mem[$_[0]];
	},
	write => sub {
		$mem[$_[0]] = $_[1];
	},
  );

=cut

our $_rw_hooks = {
	read => sub {
		warn sprintf("# callback read(%04x) not implemented\n", @_) if $debug;
		return $mem[$_[0]];
	},
	write => sub {
		warn sprintf("# callback write(%04x,%02x) not implemented", @_) if $debug;
		$mem[$_[0]] = $_[1];
	},
};

sub init {
	my $self = shift;
	my $args = {@_};
	warn "inside init low-level Z80 from ",ref($self),"\n";

	foreach my $p ( qw/read write/ ) {
		confess "need $p argument as coderef" unless ( $args->{$p} && ref($args->{$p}) eq 'CODE' );
		$_rw_hooks->{$p} = $args->{$p};
	}

};

=head2 poke_code

Write series of bytes into memory passing through MMU (C<read> and C<write>)
functions. If you don't want to trigger MMU, use C<write_chunk>.

  $emu->poke_code( 0xbeef, 0xff, 0x00, 0xff, 0x00, 0xaa );

=cut

sub poke_code {
	my $self = shift;
	my $addr = shift;
	warn sprintf("## Z80::poke_code(%04x,%s)\n", $addr, dump( @_ )) if $self->debug;
	#$mem[$addr++] = $_ foreach @_;
	# call low-level write
	$_rw_hooks->{write}->( $addr++, $_ ) foreach @_;
}

=head2 ram

Read series of bytes into memory without MMU interaction

  my @code = $emu->ram( 0xc000, 0xc1000 );

=cut

sub ram {
	my $self = shift;
	my ( $from, $to ) = @_;
	warn sprintf("## Z80::ram(%04x,%04x)\n", $from, $to) if $self->debug;
	return @mem[ $from .. $to ];
}

=head2 write_chunk

Low-level update of memory, overriding user specified MMU functions C<read> and C<write>

  $emu->write_chunk( $address, $chunk_of_data );

=cut

sub write_chunk {
	my ($self, $addr, $chunk) = @_;
	my $len = length($chunk);
	splice @mem, $addr, $len, unpack('C*', $chunk);
}

=head1 XS Callbacks

This functions are called from C<Z80.xs>

=head2 _read

Read from memory C callback

  $byte = Z80::_read( $address );

=cut

sub _read {
	return $_rw_hooks->{read}->( @_ );
}

=head2 _write

Write into memory C callback

  Z80:_write( $address, $byte );

=cut

sub _write {
	return $_rw_hooks->{write}->( @_ );
}

=head2 _update_perl_R

called by C<Z80.xs> to push changes in registars back to perl variables

=cut

sub _update_perl_R {
	warn "## Z80::update_perl_R(",dump(@_),")\n" if $debug;
	(
		$AF, $BC, $DE, $HL, $IX, $IY, $PC, $SP,
		$AF1, $BC1, $DE1, $HL1,
		$IFF, $I,
		$R,
		$IPeriod, $ICount, $IRequest, $IAutoReset, $TrapBadOps, $Trap, $Trace
	) = @_;

	dump_R();
}

=head1 XS

Following functions are implemented in C<Z80.xs> and exported to perl.

=head2 set_debug

  Z80::set_debug( 0 );

=head2 get_debug

  my $debug = Z80::set_debug();

=head2 reset

Reset 6502 CPU, reading PC from C<0xfffc>

  Z80::reset();

=head2 update_C_R

Push perl notion of register values to CPU emulator

  Z80::update_C_R();

=head2 update_perl_R

Update perl notion of register values

  Z80::update_perl_R();

=head2 exec

Execute cpu for specified number of cycles

  my $cycles_left = Z80::exec( $execute_cpu_cycles );

=head1 Helpers

=head2 dump_R

helper function which dumps registers in humanly readable form

  my $dump = dump_R;

=cut

sub dump_R {
	my $dump = sprintf( " " .
		"AF:%04x BC:%04x DE:%04x HL:%04x IX:%04x IY:%04x PC:%04x SP:%04x | " .
		"AF1:%04x BC1:%04x DE1:%04x HL1:%04x | " .
		"IFF: %02x I: %02x R: %02x | " .
		"IPeriod:%d ICount:%d IRequest:%02x IAutoReset:%02x TrapBadOps:%d Trap:%d Trace:%d" .
		"\n",
		$AF, $BC, $DE, $HL, $IX, $IY, $PC, $SP,
		$AF1, $BC1, $DE1, $HL1,
		$IFF, $I,
		$R,
		$IPeriod, $ICount, $IRequest, $IAutoReset, $TrapBadOps, $Trap, $Trace,
	);
	warn "## Z80::dump_R $dump" if $debug;
	return $dump;
}

=head2 debug

Turn perl and C-level debugging on/off

  $emu->debug( 0 );
  $emu->debug( 1 );
  print $emu->debug;

=cut

sub debug {
	my $self = shift;
	my $value = shift;
	if (defined($value)) {
		$debug = Z80::set_debug($value);
	} else {
		$debug = Z80::get_debug();
	}
	return $debug;
}

=head1 SEE ALSO

L<Orao> is sample implementation using this module

=head1 AUTHOR

Dobrica Pavlinusic, C<< <dpavlin@rot13.org> >>

=head1 COPYRIGHT & LICENSE

Copyright 2007 Dobrica Pavlinusic, All Rights Reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut
1;
