
package XPT::File;

use strict;
use warnings;

use base qw(XPT);

use Carp;

use constant MAGIC      => "XPCOM\nTypeLib\r\n\032";

sub demarshal {
    my ($r_buffer, $r_offset) = @_;

    my $magic = XPT::ReadBuffer($r_buffer, $r_offset, length(MAGIC));
    die "libxpt: bad magic header in input file; found '",$magic,"', expected '",MAGIC,"'\n"
            unless ($magic eq MAGIC);

    my $major_version = XPT::Read8($r_buffer, $r_offset);
    my $minor_version = XPT::Read8($r_buffer, $r_offset);
    die "libxpt: newer version ",$major_version,".",$minor_version,"\n"
            unless ($major_version == 1);

    my $num_interfaces = XPT::Read16($r_buffer, $r_offset);
    my $file_length = XPT::Read32($r_buffer, $r_offset);

    die "libxpt: File length in header does not match actual length. File may be corrupt\n"
            if ($file_length != length $$r_buffer);

    my $interface_directory_offset = XPT::Read32($r_buffer, $r_offset);
    $XPT::data_pool_offset = XPT::Read32($r_buffer, $r_offset);

    my @annotations = ();
    my %interface_iid = ();
    my %interface_iid_nul = ();
    eval {
        my $annotation = XPT::Annotation::demarshal($r_buffer, $r_offset);
        push @annotations, $annotation;
        while (!$annotation->{is_last}) {
            $annotation = XPT::Annotation::demarshal($r_buffer, $r_offset);
            push @annotations, $annotation;
        }

        my $offset = $interface_directory_offset - 1;
        while ($num_interfaces --) {
            my $entry = XPT::InterfaceDirectoryEntry::demarshal($r_buffer, \$offset);
            if ($entry->{iid}->_is_nul()) {
                my $fullname = ($entry->{name_space} || q{}) . '::' . $entry->{name};
                $interface_iid_nul{$fullname} = $entry;
            }
            else {
                $interface_iid{$entry->{iid}->stringify()} = $entry;
            }
        }
    };
    if ($@) {
        $XPT::demarshal_retcode = 1;
        if ($XPT::demarshal_not_abort) {
            warn $@;
        }
        else {
            die $@;
        }
    }

    return new XPT::File(
            magic                   => $magic,
            major_version           => $major_version,
            minor_version           => $minor_version,
            interface_iid_nul       => \%interface_iid_nul,
            interface_iid           => \%interface_iid,
            annotations             => \@annotations,
            file_length             => $file_length,
            data_pool_offset        => $XPT::data_pool_offset,
    )->_revolve();
}

sub _interface_directory {
    my $self = shift;
    my @list = ();
    foreach (sort keys %{$self->{interface_iid_nul}}) {
        my $entry = $self->{interface_iid_nul}->{$_};
        push @list, $entry;
    }
    foreach (sort keys %{$self->{interface_iid}}) {
        my $entry = $self->{interface_iid}->{$_};
        push @list, $entry;
    }
    return @list;
}

sub _revolve {
    my $self = shift;
    my @interface_directory = $self->_interface_directory();
    foreach my $itf (values %{$self->{interface_iid}}) {
        next unless (defined $itf->{interface_descriptor});     # ISupport
        my $desc = $itf->{interface_descriptor};
        my $idx_parent = $desc->{parent_interface_index};
        if ($idx_parent) {
            if ($idx_parent > scalar(@interface_directory)) {
                warn "parent_interface_index out of range! ($idx_parent)\n";
                $XPT::demarshal_retcode = 1;
            }
            $desc->{parent_interface} = $interface_directory[$idx_parent - 1];
        }
        foreach my $method (@{$desc->{method_descriptors}}) {
            foreach my $param (@{$method->{params}}) {
                my $type = $param->{type};
                if ($type->{tag} == XPT::InterfaceTypeDescriptor) {
                    my $idx = $type->{interface_index};
                    if ($idx > scalar(@interface_directory)) {
                        warn "interface_index out of range! ($idx)\n";
                        next;
                    }
                    $type->{interface} = $interface_directory[$idx - 1];
                }
            }
        }
    }
    return $self;
}

sub marshal {
    my $self = shift;

    my $header_size = length(MAGIC) + 1 + 1 + 2 + 4 + 4 + 4;
    my $annotations = q{};
    foreach (@{$self->{annotations}}) {
        $annotations .= $_->marshal();
    }
#   while ( ($header_size + length($annotations)) % 4) {
#       $annotations .= "\0";
#   }
    $header_size += length($annotations);
    my $interface_directory_offset = $header_size + 1;
    my @interface_directory = $self->_interface_directory();
    $XPT::data_pool = q{};
    my $interface_directory = q{};
    foreach (@interface_directory) {
        $interface_directory .= $_->marshal();
    }

    my $data_pool_offset = $header_size + length($interface_directory);
    my $file_length = $header_size + length($interface_directory) + length($XPT::data_pool);
    my $buffer = $self->{magic};
    $buffer .= XPT::Write8($self->{major_version});
    $buffer .= XPT::Write8($self->{minor_version});
    $buffer .= XPT::Write16(scalar(@interface_directory));
    $buffer .= XPT::Write32($file_length);
    $buffer .= XPT::Write32($interface_directory_offset);
    $buffer .= XPT::Write32($data_pool_offset);
    $buffer .= $annotations;
    $buffer .= $interface_directory;
    $buffer .= $XPT::data_pool;
    return $buffer;
}

sub stringify {
    my $self = shift;
    my ($indent) = @_;
    $indent = q{} unless (defined $indent);
    my $new_indent = $indent . q{ } x 3;
    my $more_indent = $new_indent . q{ } x 3;

    my @interface_directory = $self->_interface_directory();
    my $str = $indent . "Header:\n";
    if ($XPT::stringify_verbose) {
        $str .= $new_indent . "Magic beans:           ";
        foreach (split //, $self->{magic}) {
            $str .= sprintf("%02x", ord($_));
        }
        $str .= "\n";
        if ($self->{magic} eq MAGIC) {
            $str .= $new_indent . "                       PASSED\n";
        }
        else {
            $str .= $new_indent . "                       FAILED\n";
        }
    }
    $str .= $new_indent . "Major version:         " . $self->{major_version} . "\n";
    $str .= $new_indent . "Minor version:         " . $self->{minor_version} . "\n";
    $str .= $new_indent . "Number of interfaces:  " . scalar(@interface_directory) . "\n";
    if ($XPT::stringify_verbose) {
        $str .= $new_indent . "File length:           " . $self->{file_length} . "\n"
                if (exists $self->{file_length});
        $str .= $new_indent . "Data pool offset:      " . $self->{data_pool_offset} . "\n"
                if (exists $self->{data_pool_offset});
        $str .= "\n";
    }

    my $nb = -1;
    $str .= $new_indent . "Annotations:\n";
    foreach (@{$self->{annotations}}) {
        $nb ++;
        $str .= $more_indent . "Annotation #" . $nb;
        $str .= $_->stringify($new_indent);
    }
    if ($XPT::stringify_verbose) {
        $str .= $more_indent . "Annotation #" . $nb . " is the last annotation.\n";
    }

    $XPT::param_problems = 0;
    $nb = 0;
    $str .= "\n";
    $str .= $indent . "Interface Directory:\n";
    foreach my $entry (@interface_directory) {
        if ($XPT::stringify_verbose) {
            $str .= $new_indent . "Interface #" . $nb ++ . ":\n";
            $str .= $entry->stringify($new_indent . $new_indent, $self);
        }
        else {
            $str .= $entry->stringify($new_indent, $self);
        }
    }
    if ($XPT::param_problems) {
        $str .= "\nWARNING: ParamDescriptors are present with "
             .  "bad in/out/retval flag information.\n"
             .  "These have been marked with 'XXX'.\n"
             .  "Remember, retval params should always be marked as out!\n";
    }

    return $str;
}

sub add_annotation {
    my $self = shift;
    my ($annotation) = @_;
    $annotation->{is_last} = 0;
    $self->{annotations} = [] unless (exists $self->{annotations});
    push @{$self->{annotations}}, $annotation;
}

sub terminate_annotations {
    my $self = shift;
    if (exists $self->{annotations}) {
        ${$self->{annotations}}[-1]->{is_last} = 1;
    }
    else {
        my $annotation = new XPT::Annotation(
                is_last                 => 1,
                tag                     => 0,
        );
        $self->{annotations} = [ $annotation ];
    }
}

sub add_interface {
    my $self = shift;
    my ($entry) = @_;
    $self->{interface_iid_nul} = {} unless (exists $self->{interface_iid_nul});
    $self->{interface_iid} = {} unless (exists $self->{interface_iid});
    my $fullname = $entry->{name_space} . '::' . $entry->{name};
    if ($entry->{iid}->_is_nul()) {
        return if (exists $self->{interface_iid_nul}->{$fullname});
        foreach (values %{$self->{interface_iid}}) {
            return if ($fullname eq $_->{name_space} . '::' . $_->{name});
        }
        $self->{interface_iid_nul}->{$fullname} = $entry;
    }
    else {
        my $iid = $entry->{iid}->stringify();
        if (exists $self->{interface_iid}->{$iid}) {
            return if (defined $self->{interface_iid}->{$iid}->{interface_descriptor});
        }
        else {
            delete $self->{interface_iid_nul}->{$fullname}
                    if (exists $self->{interface_iid_nul}->{$fullname});
            foreach (values %{$self->{interface_iid}}) {
                croak "ERROR: found duplicate definition of interface $fullname with iids \n"
                        if ($fullname eq $_->{name_space} . '::' . $_->{name});
            }
        }
        $self->{interface_iid}->{$iid} = $entry;
    }
}

sub indexe {
    my $self = shift;
    foreach my $itf (values %{$self->{interface_iid}}) {
        next unless (defined $itf->{interface_descriptor});     # ISupport
        my $desc = $itf->{interface_descriptor};
        $desc->{parent_interface_index} = $self->_find_itf($desc->{parent_interface});
        foreach my $method (@{$desc->{method_descriptors}}) {
            foreach my $param (@{$method->{params}}) {
                my $type = $param->{type};
                if ($type->{tag} == XPT::InterfaceTypeDescriptor) {
                    $type->{interface_index} = $self->_find_itf($type->{interface});
                }
            }
        }
    }
}

sub _find_itf {
    my $self = shift;
    my ($itf) = @_;
    return 0 unless (defined $itf);
    my @interface_directory = $self->_interface_directory();
    my $idx = 1;
    foreach (@interface_directory) {
        if (ref $itf) {
            if        ( $_->{name_space} eq $itf->{name_space}
                    and $_->{name} eq $itf->{name} ) {
                return $idx;
            }
        }
        else {
            if ($itf eq $_->{name_space} . '::' . $_->{name}) {
                return $idx;
            }
        }
        $idx ++;
    }
    if (ref $itf) {
        croak "ERROR: interface $itf->{name_space}::$itf->{name} not found\n";
    }
    else {
        croak "ERROR: interface $itf not found\n";
    }
}

1;

