#!/opt/local/bin/perl5.28
use strict;
use warnings;

use File::Basename;
use Getopt::Long;
use Parse::Win32Registry 0.51 qw( make_multiple_subtree_iterator
                                  compare_multiple_keys
                                  compare_multiple_values
                                  hexdump );

binmode(STDOUT, ':utf8');

Getopt::Long::Configure('bundling');

GetOptions('values|v'  => \my $show_values,
           'hexdump|x' => \my $show_hexdump,
           'long|l'    => \my $show_long,
           'all|a'     => \my $show_all);

my $show_keys = 1;

my @filenames = ();
my @root_keys = ();
my @start_keys = ();
my $initial_key_path;

if (@ARGV) {
    while (my $filename = shift) {
        if (-r $filename) {
            my $registry = Parse::Win32Registry->new($filename);
            if (defined $registry) {
                my $root_key = $registry->get_root_key;
                if (defined $root_key) {
                    push @root_keys, $root_key;
                    push @filenames, $filename;
                }
            }
        }
        else {
            # If $filename is not a readable file, assume it is a key path:
            $initial_key_path = $filename;
        }
    }
}
else {
    die usage();
}

if (@root_keys < 2) {
    die "Specify two or more filenames\n";
}

@start_keys = @root_keys;
if ($initial_key_path) {
    @start_keys = map { $_->get_subkey($initial_key_path) || undef } @root_keys;
}

my $num_start_keys = grep { defined } @start_keys;
if ($num_start_keys < 1) {
    die "Could not locate the key '$initial_key_path' in any file\n";
}

my $subtree_iter = make_multiple_subtree_iterator(@start_keys);
my $batch_size = @start_keys;

if ($show_long) {
    for (my $num = 0; $num < $batch_size; $num++) {
        print "[$num]:\tFILE\t'$filenames[$num]'\n";
    }
}

my $last_key_shown;

while (my ($keys_ref, $values_ref) = $subtree_iter->get_next) {
    my @keys = @$keys_ref;
    my $any_key = (grep { defined } @keys)[0];
    die "Unexpected error: no keys!" if !defined $any_key;

    if (defined $values_ref) {
        my @values = @$values_ref;
        my $any_value = (grep { defined } @values)[0];
        die "Unexpected error: no values!" if !defined $any_value;

        my @changes = compare_multiple_values(@values);
        my $num_changes = grep { $_ } @changes;
        if ($num_changes > 0 && $show_values) {
            if (!defined $last_key_shown
                      || $last_key_shown ne $any_key->get_path)
            {
                print "-" x $batch_size, "\t", $any_key->get_path, "\n";
                $last_key_shown = $any_key->get_path;
            }
            if (!$show_long) {
                for (my $num = 0; $num < $batch_size; $num++) {
                    my $diff = substr($changes[$num], 0, 1) ||
                                (defined $values[$num] ? '.' : ' ');
                    print $diff;
                }
                print "\t", $any_value->get_name, "\n";
            }
            else {
                for (my $num = 0; $num < $batch_size; $num++) {
                    my $next_change = $changes[$num + 1];
                    if ($changes[$num] || $show_all
                                       || defined $next_change
                                               && $next_change eq 'DELETED')
                    {
                        print "[$num]:\t$changes[$num]\t";
                        if (defined $values[$num]) {
                            if (!$show_hexdump) {
                                print $values[$num]->as_string, "\n";
                            }
                            else {
                                my $value_name = $values[$num]->get_name;
                                $value_name = "(Default)" if $value_name eq "";
                                my $value_type
                                    = $values[$num]->get_type_as_string;
                                print "$value_name ($value_type):\n";
                                print hexdump($values[$num]->get_raw_data);
                            }
                        }
                        else {
                            print "\n";
                        }
                    }
                }
            }
        }
    }
    else {
        my @changes = compare_multiple_keys(@keys);
        my $num_changes = grep { $_ } @changes;
        if ($num_changes > 0 && $show_keys) {
            if (!$show_long) {
                for (my $num = 0; $num < $batch_size; $num++) {
                    my $diff = substr($changes[$num], 0, 1) ||
                                (defined $keys[$num] ? '.' : ' ');
                    print $diff;
                }
                print "\t", $any_key->get_path, "\n";
            }
            else {
                for (my $num = 0; $num < $batch_size; $num++) {
                    my $next_change = $changes[$num+1];
                    if ($changes[$num] || $show_all
                                       || defined $next_change
                                               && $next_change eq 'DELETED')
                    {
                        print "[$num]:\t$changes[$num]\t";
                        if (defined $keys[$num]) {
                            print $keys[$num]->as_string;
                            $last_key_shown = $keys[$num]->get_path;
                        }
                        elsif ($changes[$num] eq 'DELETED') {
                            print $keys[$num-1]->as_string;
                            $last_key_shown = $keys[$num-1]->get_path;
                        }
                        print "\n";
                    }
                }
            }
            $last_key_shown = $any_key->get_path;
        }
    }
}

sub usage {
    my $script_name = basename $0;
    return <<USAGE;
$script_name for Parse::Win32Registry $Parse::Win32Registry::VERSION

Compares two or more registry files.
Defaults to displaying a summary of the changes where each letter
represents a change (N=NEWER, O=OLDER, A=ADDED, D=DELETED, C=CHANGED).
The long output will display full details of each change.

$script_name <file1> <file2> <file3> ... [<subkey>] [-v] [-x] [-l] [-a]
    -v or --values      display values
    -x or --hexdump     display value data as a hex dump
    -l or --long        show each changed key or value instead of a summary
    -a or --all         show all keys and values before and after a change
USAGE
}
