The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
use warnings;

use Test::More 'no_plan';

BEGIN {
  use_ok( 'Acme::6502' );
}

my %test_lut = (
  m => sub {
    return shift->read_8( hex shift );
  },
  ps => sub {
    return shift->get_p;
  },
  pc => sub {
    return shift->get_pc;
  },
  sp => sub {
    return shift->get_s;
  },
  acc => sub {
    return shift->get_a;
  },
  ix => sub {
    return shift->get_x;
  },
  iy => sub {
    return shift->get_y;
  },
  s => sub {
    return $_[0]->get_p & $_[0]->N ? 1 : 0;
  },
  v => sub {
    return $_[0]->get_p & $_[0]->V ? 1 : 0;
  },
  b => sub {
    return $_[0]->get_p & $_[0]->B ? 1 : 0;
  },
  d => sub {
    return $_[0]->get_p & $_[0]->D ? 1 : 0;
  },
  i => sub {
    return $_[0]->get_p & $_[0]->I ? 1 : 0;
  },
  z => sub {
    return $_[0]->get_p & $_[0]->Z ? 1 : 0;
  },
  c => sub {
    return $_[0]->get_p & $_[0]->C ? 1 : 0;
  },
);

my %regset_lut = (
  ps => sub {
    shift->set_p( shift );
  },
  pc => sub {
    shift->set_pc( shift );
  },
  sp => sub {
    shift->set_s( shift );
  },
  acc => sub {
    shift->set_a( shift );
  },
  ix => sub {
    shift->set_x( shift );
  },
  iy => sub {
    shift->set_y( shift );
  },
  s => sub {
    $_[0]->set_p( $_[0]->get_p & ~$_[0]->N );
    $_[0]->set_p( $_[0]->get_p | $_[0]->N ) if $_[1];
  },
  v => sub {
    $_[0]->set_p( $_[0]->get_p & ~$_[0]->V );
    $_[0]->set_p( $_[0]->get_p | $_[0]->V ) if $_[1];
  },
  b => sub {
    $_[0]->set_p( $_[0]->get_p & ~$_[0]->B );
    $_[0]->set_p( $_[0]->get_p | $_[0]->B ) if $_[1];
  },
  d => sub {
    $_[0]->set_p( $_[0]->get_p & ~$_[0]->D );
    $_[0]->set_p( $_[0]->get_p | $_[0]->D ) if $_[1];
  },
  i => sub {
    $_[0]->set_p( $_[0]->get_p & ~$_[0]->I );
    $_[0]->set_p( $_[0]->get_p | $_[0]->I ) if $_[1];
  },
  z => sub {
    $_[0]->set_p( $_[0]->get_p & ~$_[0]->Z );
    $_[0]->set_p( $_[0]->get_p | $_[0]->Z ) if $_[1];
  },
  c => sub {
    $_[0]->set_p( $_[0]->get_p & ~$_[0]->C );
    $_[0]->set_p( $_[0]->get_p | $_[0]->C ) if $_[1];
  },
);

my $glob = $ENV{TEST_OP} || '*';
my @files = glob( "t/monkeynes/script_${glob}.txt" );

for my $file ( @files ) {
  open( my $script, $file ) || die qq(cannot load test script "$file");
  _diag( qq(Running script "$file") );
  my @lines = <$script>;
  chomp( @lines );
  run_script( @lines );
  close( $script );
}

sub run_script {
  my $cpu;
  for ( @_ ) {
    chomp;
    next if m{^\s*$};
    next if m{^save};
    if ( m{^# (.+)} ) {
      _diag( $1 );
    }
    elsif ( $_ eq 'clear' ) {
      next;
    }
    elsif ( $_ eq 'power on' ) {
      $cpu = Acme::6502->new();
      $cpu->set_s( 255 );
      $cpu->set_p( $cpu->get_p | $cpu->R );
      isa_ok( $cpu, 'Acme::6502' );
    }
    elsif ( $_ eq 'memclear' ) {
      $cpu->poke_code( 0, ( 0 ) x 65536 );
      _diag( 'Mem cleared' );
    }
    elsif ( $_ eq 'step' ) {
      _diag( 'Running next instruction...' );
      $cpu->run( 1 );
    }
    elsif ( m{^regset (.+) (.+)} ) {
      $regset_lut{ lc $1 }->( $cpu, hex $2 );
      _diag( "$1 set to $2" );
    }
    elsif ( m{^regs(?: (.+))?} ) {
      diag_regs( $cpu, $1 );
    }
    elsif ( m{^memset (.+) (.+)} ) {
      $cpu->write_8( hex $1, hex $2 );
      is( $cpu->read_8( hex $1 ), hex $2, "Mem[$1] set to $2" );
    }
    elsif ( m{^test (.+) (.+) (.+)} ) {
      my ( $op, @args ) = split( /:/, $1 );
      my $cmp = $2;
      $cmp = '==' if $cmp eq '=';
      cmp_ok( $test_lut{ lc $op }->( $cpu, @args ),
        $cmp, hex $3, "$1 $2 $3" );
    }
    elsif ( m{^op (.+)} ) {
      my ( $op, $args_hex ) = split( ' ', $1 );
      _diag( "OP: $1" );
      $args_hex = '' unless defined $args_hex;
      my @args = ( $args_hex =~ m{(..)}g );
      my $pc = hex( 8000 );
      $cpu->poke_code(
        $pc,
        map { hex( $_ || 0 ) } $op,
        @args[ 0 .. 1 ]
      );
      $cpu->set_pc( $pc );
      $cpu->run( 1 );
    }
    else {
      use Data::Dumper;
      warn Dumper $_;
    }
  }
}

sub diag_regs {
  my $cpu = shift;
  my $reg = uc shift;

  _diag( 'CPU Registers' ) if !$reg;
  _diag( sprintf '  PC:    $%X', $cpu->get_pc )
   if !$reg || $reg eq 'PC';
  _diag( sprintf '  SP:    $%X', $cpu->get_s ) if !$reg || $reg eq 'SP';
  _diag( sprintf '  ACC:   $%X', $cpu->get_a )
   if !$reg || $reg eq 'ACC';
  _diag( sprintf '  IX:    $%X', $cpu->get_x ) if !$reg || $reg eq 'IX';
  _diag( sprintf '  IY:    $%X', $cpu->get_y ) if !$reg || $reg eq 'IY';
  # this should be fixed to handle just one flag at a time
  _diag( '  Flags  S V - B D I Z C' )
   if !$reg || $reg =~ m{^(PS|[SVBDIZC])$};
  _diag(
    sprintf '  PS:    %d %d %d %d %d %d %d %d',
    split( //, sprintf( '%08b', $cpu->get_p ) )
  ) if !$reg || $reg =~ m{^(PS|[SVBDIZC])$};
}

sub _diag {
  return unless $ENV{DIAG_6502};
  diag( @_ );
}