package Parse::Win32Registry::Win95::File;

use strict;
use warnings;

use base qw(Parse::Win32Registry::File);

use Carp;
use File::Basename;
use Parse::Win32Registry::Base qw(:all);
use Parse::Win32Registry::Win95::Key;

use constant CREG_HEADER_LENGTH => 0x20;
use constant OFFSET_TO_RGKN_BLOCK => 0x20;

sub new {
    my $class = shift;
    my $filename = shift or croak 'No filename specified';

    open my $fh, '<', $filename or croak "Unable to open '$filename': $!";

    # CREG Header
    # 0x00 dword = 'CREG' signature
    # 0x04
    # 0x08 dword = offset to first rgdb block
    # 0x0c
    # 0x10 word  = number of rgdb blocks

    my $bytes_read = sysread($fh, my $creg_header, CREG_HEADER_LENGTH);
    if ($bytes_read != CREG_HEADER_LENGTH) {
        warnf('Could not read registry file header');
        return;
    }

    my ($creg_sig,
        $offset_to_first_rgdb_block,
        $num_rgdb_blocks) = unpack('a4x4Vx4v', $creg_header);

    if ($creg_sig ne 'CREG') {
        warnf('Invalid registry file signature');
        return;
    }

    my $self = {};
    $self->{_filehandle} = $fh;
    $self->{_filename} = $filename;
    $self->{_length} = (stat $fh)[7];
    $self->{_offset_to_first_rgdb_block} = $offset_to_first_rgdb_block;
    $self->{_num_rgdb_blocks} = $num_rgdb_blocks;
    bless $self, $class;

    # get_rgkn will cache the rgkn block for subsequent calls
    my $rgkn_block = $self->get_rgkn;
    return if !defined $rgkn_block; # warning will already have been made

    # Index the rgdb entries by id for faster look up
    $self->_index_rgdb_entries;

    return $self;
}

sub get_timestamp {
    return undef;
}

sub get_timestamp_as_string {
    return iso8601(undef);
}

sub get_embedded_filename {
    return undef;
}

sub get_root_key {
    my $self = shift;

    return $self->get_rgkn->get_root_key;
}

sub get_virtual_root_key {
    my $self = shift;
    my $fake_root = shift;

    my $root_key = $self->get_root_key;
    return if !defined $root_key;

    if (!defined $fake_root) {
        # guess virtual root from filename
        my $filename = basename $self->{_filename};

        if ($filename =~ /USER/i) {
            $fake_root = 'HKEY_USERS';
        }
        elsif ($filename =~ /SYSTEM/i) {
            $fake_root = 'HKEY_LOCAL_MACHINE';
        }
        else {
            $fake_root = 'HKEY_UNKNOWN';
        }
    }

    $root_key->{_name} = $fake_root;
    $root_key->{_key_path} = $fake_root;

    return $root_key;
}

sub _index_rgdb_entries {
    my $self = shift;

    my %index = ();

    # Build index of rgdb key entries
    # Entries are only included if $key_block_num matches $rgdb_block_num
    my $rgdb_block_num = 0;
    my $rgdb_iter = $self->get_rgdb_iterator;
    while (my $rgdb = $rgdb_iter->()) {
        my $rgdb_key_iter = $rgdb->get_key_iterator;
        while (my $rgdb_key = $rgdb_key_iter->()) {
            my $key_id = $rgdb_key->{_id};
            my $key_block_num = $key_id >> 16;
            if ($rgdb_block_num == $key_block_num) {
                $index{$key_id} = $rgdb_key;
            }
        }
        $rgdb_block_num++;
    }

    $self->{_rgdb_index} = \%index;
}

sub _dump_rgdb_index {
    my $self = shift;

    my $rgdb_index = $self->{_rgdb_index};

    foreach my $key_id (sort { $a <=> $b } keys %$rgdb_index) {
        my $rgdb_key = $rgdb_index->{$key_id};
        printf qq{id=0x%x 0x%x,%d/%d "%s" vals=%d\n},
            $key_id,
            $rgdb_key->{_offset},
            $rgdb_key->{_length_used},
            $rgdb_key->{_length},
            $rgdb_key->{_name},
            $rgdb_key->{_num_values};
    }
}

sub get_rgkn {
    my $self = shift;

    # Return cached rgkn block if present
    if (defined $self->{_rgkn}) {
        return $self->{_rgkn};
    }

    my $offset = OFFSET_TO_RGKN_BLOCK;
    my $rgkn_block = Parse::Win32Registry::Win95::RGKN->new($self, $offset);
    $self->{_rgkn} = $rgkn_block;
    return $rgkn_block;
}

sub get_rgdb_iterator {
    my $self = shift;

    my $offset_to_next_rgdb_block = $self->{_offset_to_first_rgdb_block};
    my $num_rgdb_blocks = $self->{_num_rgdb_blocks};

    my $end_of_file = $self->{_length};

    my $rgdb_block_num = 0;

    return Parse::Win32Registry::Iterator->new(sub {
        if ($offset_to_next_rgdb_block > $end_of_file) {
            return; # no more rgdb blocks
        }
        if ($rgdb_block_num >= $num_rgdb_blocks) {
            return; # no more rgdb blocks
        }
        $rgdb_block_num++;
        if (my $rgdb_block = Parse::Win32Registry::Win95::RGDB->new($self,
                                               $offset_to_next_rgdb_block))
        {
            return unless $rgdb_block->get_length > 0;
            $offset_to_next_rgdb_block += $rgdb_block->get_length;
            return $rgdb_block;
        }
    });
}

sub get_block_iterator {
    my $self = shift;

    my $rgdb_iter;

    return Parse::Win32Registry::Iterator->new(sub {
        if (!defined $rgdb_iter) {
            $rgdb_iter = $self->get_rgdb_iterator;
            return $self->get_rgkn;
        }
        return $rgdb_iter->();
    });
}

*get_hbin_iterator = \&get_block_iterator;


package Parse::Win32Registry::Win95::RGKN;

use strict;
use warnings;

use base qw(Parse::Win32Registry::Entry);

use Carp;
use Parse::Win32Registry::Base qw(:all);

use constant RGKN_HEADER_LENGTH => 0x20;
use constant OFFSET_TO_RGKN_BLOCK => 0x20;

sub new {
    my $class = shift;
    my $regfile = shift;
    my $offset = shift || OFFSET_TO_RGKN_BLOCK;

    croak 'Missing registry file' if !defined $regfile;
    croak 'Missing offset' if !defined $offset;

    my $fh = $regfile->get_filehandle;

    # RGKN Block Header
    # 0x0 dword = 'RGKN' signature
    # 0x4 dword = length of rgkn block
    # 0x8 dword = offset to root key entry (relative to start of rgkn block)

    sysseek($fh, $offset, 0);
    my $bytes_read = sysread($fh, my $rgkn_header, RGKN_HEADER_LENGTH);
    if ($bytes_read != RGKN_HEADER_LENGTH) {
        warnf('Could not read RGKN header at 0x%x', $offset);
        return;
    }

    my ($sig,
        $rgkn_block_length,
        $offset_to_root_key) = unpack('a4VV', $rgkn_header);

    if ($sig ne 'RGKN') {
        warnf('Invalid RGKN block signature at 0x%x', $offset);
        return;
    }

    $offset_to_root_key += $offset;

    my $self = {};
    $self->{_regfile} = $regfile;
    $self->{_offset} = $offset;
    $self->{_length} = $rgkn_block_length;
    $self->{_header_length} = RGKN_HEADER_LENGTH;
    $self->{_allocated} = 1;
    $self->{_tag} = 'rgkn block';
    $self->{_offset_to_root_key} = $offset_to_root_key;
    bless $self, $class;

    return $self;
}

sub get_root_key {
    my $self = shift;

    my $regfile = $self->{_regfile};
    my $offset_to_root_key = $self->{_offset_to_root_key};

    my $root_key = Parse::Win32Registry::Win95::Key->new($regfile,
                                                         $offset_to_root_key);
    return $root_key;
}

sub get_entry_iterator {
    my $self = shift;

    my $root_key = $self->get_root_key;

    # In the unlikely event there is no root key, return an empty iterator
    if (defined $root_key) {
        return $root_key->get_subtree_iterator;
    }
    else {
        return Parse::Win32Registry::Iterator->new(sub {});
    }
}


package Parse::Win32Registry::Win95::RGDB;

use base qw(Parse::Win32Registry::Entry);

use Carp;
use Parse::Win32Registry::Base qw(:all);

use constant RGDB_HEADER_LENGTH => 0x20;

sub new {
    my $class = shift;
    my $regfile = shift;
    my $offset = shift;

    croak 'Missing registry file' if !defined $regfile;
    croak 'Missing offset' if !defined $offset;

    my $fh = $regfile->get_filehandle;

    # RGDB Block Header
    # 0x0 dword = 'RDGB' signature
    # 0x4 dword = length of rgdb block

    sysseek($fh, $offset, 0);
    my $bytes_read = sysread($fh, my $rgdb_header, RGDB_HEADER_LENGTH);
    if ($bytes_read != RGDB_HEADER_LENGTH) {
        return;
    }

    my ($sig,
        $rgdb_block_length) = unpack('a4V', $rgdb_header);

    if ($sig ne 'RGDB') {
        return;
    }

    my $self = {};
    $self->{_regfile} = $regfile;
    $self->{_offset} = $offset;
    $self->{_length} = $rgdb_block_length;
    $self->{_header_length} = RGDB_HEADER_LENGTH;
    $self->{_allocated} = 1;
    $self->{_tag} = 'rgdb block';
    bless $self, $class;

    return $self;
}

sub get_key_iterator {
    my $self = shift;

    my $regfile = $self->{_regfile};
    my $offset = $self->{_offset};
    my $length = $self->{_length};

    my $offset_to_next_rgdb_key = $offset + RGDB_HEADER_LENGTH;
    my $end_of_rgdb_block = $offset + $length;

    return Parse::Win32Registry::Iterator->new(sub {
        if ($offset_to_next_rgdb_key >= $end_of_rgdb_block) {
            return;
        }
        if (my $rgdb_key = Parse::Win32Registry::Win95::RGDBKey->new($regfile,
                                                     $offset_to_next_rgdb_key))
        {
            return unless $rgdb_key->get_length > 0;
            $offset_to_next_rgdb_key += $rgdb_key->get_length;

            # Check rgdb key has not run past end of rgdb block
            if ($offset_to_next_rgdb_key > $end_of_rgdb_block) {
                return;
            }
            return $rgdb_key;
        }
    });
}

sub get_entry_iterator {
    my $self = shift;

    my $value_iter;
    my $key_iter = $self->get_key_iterator;

    return Parse::Win32Registry::Iterator->new(sub {
        if (defined $value_iter) {
            my $value = $value_iter->();
            if (defined $value) {
                return $value;
            }
        }

        my $key = $key_iter->();
        if (!defined $key) {
            return; # key iterator finished
        }

        $value_iter = $key->get_value_iterator;
        return $key;
    });
}


package Parse::Win32Registry::Win95::RGDBKey;

use base qw(Parse::Win32Registry::Entry);

use Carp;
use Encode;
use Parse::Win32Registry::Base qw(:all);

use constant RGDB_ENTRY_HEADER_LENGTH => 0x14;

sub new {
    my $class = shift;
    my $regfile = shift;
    my $offset = shift;

    croak 'Missing registry file' if !defined $regfile;
    croak 'Missing offset' if !defined $offset;

    my $fh = $regfile->get_filehandle;

    # RGDB Key Entry
    # 0x00 dword = length of rgdb entry / offset to next rgdb entry
    #              (this length includes any following value entries)
    # 0x04 dword = id (top word = block num, bottom word = id)
    # 0x08 dword = bytes used (unpacked, but not used)
    # 0x0c word  = key name length
    # 0x0e word  = number of values
    # 0x10 dword
    # 0x14       = key name [for key name length bytes]
    # followed immediately by any RGDB Value Entries belonging to this key

    sysseek($fh, $offset, 0);
    my $bytes_read = sysread($fh, my $rgdb_key_entry, RGDB_ENTRY_HEADER_LENGTH);
    if ($bytes_read != RGDB_ENTRY_HEADER_LENGTH) {
        return;
    }

    my ($length,
        $key_id,
        $length_used,
        $name_length,
        $num_values) = unpack('VVVvv', $rgdb_key_entry);

    $bytes_read = sysread($fh, my $name, $name_length);
    if ($bytes_read != $name_length) {
        return;
    }
    $name = decode($Parse::Win32Registry::Base::CODEPAGE, $name);

    # Calculate the length of the entry's key header
    my $header_length = RGDB_ENTRY_HEADER_LENGTH + $name_length;

    # Check for invalid/unused entries
    if ($key_id == 0xffffffff || $length_used == 0xffffffff
                              || $header_length > $length)
    {
        $name = '';
        $header_length = RGDB_ENTRY_HEADER_LENGTH;
    }

    my $self = {};
    $self->{_regfile} = $regfile;
    $self->{_offset} = $offset;
    $self->{_length} = $length;
    $self->{_length_used} = $length_used;
    $self->{_header_length} = $header_length;
    $self->{_allocated} = 1;
    $self->{_tag} = 'rgdb key';
    $self->{_id} = $key_id;
    $self->{_name} = $name;
    $self->{_name_length} = $name_length;
    $self->{_num_values} = $num_values;
    bless $self, $class;

    return $self;
}

sub get_name {
    my $self = shift;

    return $self->{_name};
}

sub parse_info {
    my $self = shift;

    my $info = sprintf '0x%x rgdb key len=0x%x/0x%x "%s" id=0x%x vals=%d',
        $self->{_offset},
        $self->{_length_used},
        $self->{_length},
        $self->{_name},
        $self->{_id},
        $self->{_num_values};

    return $info;
}

sub get_value_iterator {
    my $self = shift;

    my $regfile = $self->{_regfile};

    my $num_values_remaining = $self->{_num_values};

    my $offset = $self->{_offset};

    # offset_to_next_rgdb_value can only be set to a valid offset
    # if num_values_remaining > 0
    my $offset_to_next_rgdb_value = 0xffffffff;
    if ($num_values_remaining > 0) {
        $offset_to_next_rgdb_value = $offset
                                   + $self->{_header_length};
    }

    my $end_of_rgdb_key = $offset + $self->{_length};

    # don't attempt to return values if id is invalid...
    if ($self->{_id} == 0xffffffff) {
        $num_values_remaining = 0;
    }

    return Parse::Win32Registry::Iterator->new(sub {
        if ($num_values_remaining-- <= 0) {
            return;
        }
        if ($offset_to_next_rgdb_value == 0xffffffff) {
            return;
        }
        if ($offset_to_next_rgdb_value > $end_of_rgdb_key) {
            return;
        }
        if (my $value = Parse::Win32Registry::Win95::Value->new($regfile,
                                              $offset_to_next_rgdb_value))
        {
            return unless $value->get_length > 0;
            $offset_to_next_rgdb_value += $value->get_length;
            return $value;
        }
        else {
            return; # no more values
        }
    });
}

1;