package Language::Zcode::Runtime::Quetzal;

use strict;
use warnings;

=head1 NAME

Language::Zcode::Runtime::Quetzal - Save/restore Z-machine state using the Quetzal standard

=cut

use constant QUETZAL_SUB_ID => "IFZS";
use constant QUETZAL_STACK => "Stks";
use constant QUETZAL_HEADER => "IFhd";
use constant QUETZAL_COMPRESSED_MEMORY => "CMem";
use constant QUETZAL_UNCOMPRESSED_MEMORY => "UMem";

sub parse_quetzal {
    my $st = shift; # string containing entire quetzal file
    my $pos = 0;  # position in $st

    # Stuff we'll be returning
    my ($memory_ref, @call_stack, %header);

    # Read very beginning of save file
    my $t = substr($st, 0, 4); die "Not an IFF file!\n" unless $t eq "FORM";
    $t = substr($st, 4, 4); 
    my $size = unpack "N", $t; print "size $size\n"; 
    die "Not a simple IFF file!\n" unless $size == length($st)-8;
    $t = substr($st, 8, 4); 
    die "Not a Quetzal save!" unless $t eq QUETZAL_SUB_ID;

    # Read a set of chunks and parse them
    $pos = 12;
    my $did_header = 0;
    my ($name, $data);
    while ($pos < $size) {
	# Read the chunk
	my $len;
	#print "Pos $pos\n";
	($name, $len) = unpack "A4 N", substr($st, $pos, 8); $pos += 8;
	my $data = substr($st, $pos, $len); $pos += $len;
    #    print "length $len - ",length $data," - ", length $st,"\n";
	if ($len % 2) { 
	    substr($st, $pos, 1) eq "\0" or die "expecting 0 at $pos!\n"; $pos++
	}
	print "$name, ",length($data),"\n";

	# Do stuff based on chunk
	# Quetzal Spec 5.4: Header info MUST come before Mem/Stacks
	if ($name eq QUETZAL_HEADER) {
	    %header = read_header($data);
	    $did_header = 1;

	} elsif ($name eq QUETZAL_STACK) {
	    die QUETZAL_HEADER . " chunk must come before stack chunk " .
		"in save file\n" unless $did_header;
	    @call_stack = read_stacks($data);

	} elsif ($name eq QUETZAL_COMPRESSED_MEMORY) {
	    die QUETZAL_HEADER . " chunk must come before memory chunk " .
		"in save file\n" unless $did_header;
	    $memory_ref = read_cmem($data);
	} elsif ($name eq QUETZAL_UNCOMPRESSED_MEMORY) {
	    die QUETZAL_HEADER . " chunk must come before memory chunk " .
		"in save file\n" unless $did_header;
	    $memory_ref = read_umem($data);
	} else { print "Ignoring $name chunk\n"; }
    }

    return ($memory_ref, \@call_stack, \%header);
} 

######################

sub read_header {
    my $data = shift;
    my ($release, $serial, $checksum, $PC1, $PC2) = unpack
	"n        A6       n          n     C", 
	substr($data, 0, 13);
    my $PC = 256 * $PC1 + $PC2;
    my %header = (
	release => $release, 
	serial => $serial,
        checksum => $checksum,
       	restore_PC => $PC,
    );
    return %header;
}

sub read_stacks {
    my $data = shift;
    my @call_stack = (); # stack of frames
#    print join " ", unpack "C*", $data;
    my $p = 0;
    while ($p < length $data) {
	my ($PC1, $PC2, $args, $stack_size, $store, $flags);
	($PC1, $PC2, $flags, $store, $args, $stack_size) = unpack
	    "n C     C       C       B8     n", 
	    substr($data, $p, 8); $p += 8;
	my @args = split//,$args;
	my $PC = 256 * $PC1 + $PC2;
	my $discard_result = int($flags >> 4); 
	my $num_locals = $flags & 0xf; # just last four bits
	die "Bad result-discard flag $discard_result" unless $discard_result<2;
	my @locv = unpack "n*", substr($data, $p, $num_locals*2);
	$p += $num_locals*2;
	my @stack = unpack "n*", substr($data, $p, $stack_size*2);
	$p += $stack_size*2;

	my $frame_ref = {
	    next_PC => $PC,
	    discard_bit => $discard_result,
	    store_var => $store,
	    args => join("",@args),
	    locals => \@locv,
	    eval_stack => \@stack,
	};
	push @call_stack, $frame_ref;
    }
    return @call_stack;
}

sub read_cmem {
    my $data = shift;
    # "0 n" means n PLUS ONE zeros
    (my $diff = $data) =~ s/(\0)(\C)/$1 . $1 x ord $2/ge;
    # bitwise xor with original memory 
    # ("" says they're really strings, not nums, so do char-by-char or'ing)
    my $dynamic_orig = pack "C*", @{&PlotzMemory::get_orig_dynamic_memory};
    my $memory = "$diff" ^ "$dynamic_orig";

#    $diff =~ s/\C/sprintf("%3d", ord $&)/ge;
#    print join "\n", $diff =~ /(.{48})/g,$',"";
    # Note: memory size may be smaller than dynamic memory read in.
    # In that case, Quetzal 3.4, the rest of @mem is all zeros
    my @mem = unpack "C*", $memory;

    return \@mem;
}

sub read_umem {
    my $data = shift;
    my @mem = unpack "C*", $data;
    return \@mem;
}

######################
# Input: header, stack, memory for quetzal
# Output: string that will be a quetzal file
sub build_quetzal {
    my ($href, $sref, $mref) = @_;
    my $collect = QUETZAL_SUB_ID;
    $collect .= write_header(QUETZAL_HEADER, $href);
    $collect .= write_memory(QUETZAL_COMPRESSED_MEMORY, $mref);
    $collect .= write_stacks(QUETZAL_STACK, $sref);

    #print "length collect is ",length $collect,"\n";
    # Add overall header, which includes the size of the rest of the file
    my $IFF = "FORM" . pack("N", length $collect);
    $IFF .= $collect;
    return $IFF;
}

sub write_chunk {
    my ($name, $data) = @_;
    my $len = length $data;
    #print "Pos $pos\n";
    my $str = pack "A4 N", $name, $len;
    $str .= $data;
    $str .= "\0" if $len % 2;
    return $str;
}

sub write_header {
    my ($name, $href) = @_;
    my %header = %$href;
    printf "Rel: %s, Ser#: %s, Check: %s, RestorePC: %s (%x)\n",
	@header{qw(release serial checksum restore_PC restore_PC)} 
	if $main::DEBUG;
    my ($release, $serial, $checksum, $PC) = 
	@header{qw(release serial checksum restore_PC)};
    my $PC1 = $PC >> 8;
    my $PC2 = $PC & 0xFF;
    my $data = pack
	"n        A6       n          n     C", 
        $release, $serial, $checksum, $PC1, $PC2;
    my $str = write_chunk($name, $data);
    return $str;
}

sub write_stacks {
    my ($name, $sref) = @_;
    my $data = "";
    foreach my $frame (@$sref) {
	my %f = %$frame;
	printf "PC %s (%x), call_*n? %s, store %s, args %s, %s locals, stack size %s
	    Locals @{$f{locals}}
	    Stack @{$f{eval_stack}}\n",
	    @f{qw(next_PC next_PC discard_bit store_var args)},
	    $#{$f{locals}} +1, $#{$f{eval_stack}} +1
	    if $main::DEBUG;

	my ($PC, $discard_bit, $store, $args, $locref, $stackref) =
	    @f{qw(next_PC discard_bit store_var args locals eval_stack)};
	die "Bad result-discard flag $discard_bit" unless $discard_bit<2;
	my @split_args = split//,$args; # we don't actually need this
	my @locv = @$locref;
	my @stack = @$stackref;

	my ($PC1, $PC2, $flags, $stack_size, $num_locals);
	$PC1 = $PC >> 8;
	$PC2 = $PC & 0xFF;
	$stack_size = @stack;
	$num_locals = @locv;
	die "Number of locals must be < 16" unless $num_locals < 16;
	$flags = ($discard_bit << 4) | $num_locals;

	my $frame_data = pack
	       "n C     C       C       B8     n", 
	    $PC1, $PC2, $flags, $store, $args, $stack_size;
	$frame_data .= pack "n*", @locv, @stack;
	$data .= $frame_data;
    }
    my $str = write_chunk($name, $data);
    return $str;
}

# write COMPRESSED memory
sub write_memory {
    my ($name, $mem_ref) = @_;
    # memory at time of save
    my $memory = pack "C*", @$mem_ref;
    # bitwise xor with original memory 
    # ("" to specify they're really strings, not nums)
    my $dynamic_orig = pack "C*", @{&PlotzMemory::get_orig_dynamic_memory};
    my $diff = "$memory" ^ "$dynamic_orig";
    # Remove trailing zeros (which show no change from orig memory 
    # beyond a certain point)
    $diff =~ s/\0+$//;

    # Compress
    (my $data = $diff) =~ s/\0{1,256}/"\0" . chr(length($&)-1)/ge;

#    $memory =~ s/\C/sprintf("%3d", ord $&)/ge;
#    print join "\n", $memory =~ /(.{48})/g,$',"";
#    for (my $i=0; $i < @mem; $i+=16) {
#	my $m = $i>@mem-16 ? $#mem : $i+15;
#	printf "%3d"x($m-$i +1) ."\n", @mem[$i..$m];
#    }
#    print "@mem\n";

    my $str = write_chunk($name, $data);
    return $str;
}

1;
