package M6502;

use strict;
use warnings;

use Data::Dump qw/dump/;
use Carp qw/confess/;
use Exporter 'import';
our @EXPORT = qw'dump_R @mem $PC $A $P $X $Y $S $IPeriod $ICount $IRequest $IAutoReset $TrapBadOps $Trap $Trace $debug';
our $VERSION = '0.0.2';
require XSLoader;
XSLoader::load('M6502', $VERSION);

=head1 NAME

M6502 - perl bindings for M6502 CPU emulator

=head1 FUNCTIONS

=cut

our $debug = 0;

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

# program counter
our $PC = 0xbeef;
# CPU registars
our ( $A, $P, $X, $Y, $S ) = (0) x 5;

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 M6502 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("## M6502::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("## M6502::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<M6502.xs>

=head2 _read

Read from memory C callback

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

=cut

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

=head2 _write

Write into memory C callback

  M6502:_write( $address, $byte );

=cut

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

=head2 _update_perl_R

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

=cut

sub _update_perl_R {
	warn "## M6502::update_perl_R(",dump(@_),")\n" if $debug;
	( $A, $P, $X, $Y, $S, $PC, $IPeriod, $ICount, $IRequest, $IAutoReset, $TrapBadOps, $Trap, $Trace ) = @_;
	dump_R();
}

=head1 XS

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

=head2 set_debug

  M6502::set_debug( 0 );

=head2 get_debug

  my $debug = M6502::set_debug();

=head2 reset

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

  M6502::reset();

=head2 update_C_R

Push perl notion of register values to CPU emulator

  M6502::update_C_R();

=head2 update_perl_R

Update perl notion of register values

  M6502::update_perl_R();

=head2 exec

Execute cpu for specified number of cycles

  my $cycles_left = M6502::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(" PC: %04x A:%02x P:%02x X:%02x Y:%02x S:%02x "
		. "IPeriod:%d ICount:%d IRequest:%02x IAutoReset:%02x TrapBadOps:%d Trap:%d Trace:%d"
		. "\n",
		$PC, $A, $P, $X, $Y, $S, $IPeriod, $ICount, $IRequest, $IAutoReset, $TrapBadOps, $Trap, $Trace,
	);
	warn "## M6502::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 = M6502::set_debug($value);
	} else {
		$debug = M6502::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;
