package ExtUtils::XSBuilder::ParseSource;

use strict;
use vars qw{$VERSION $verbose} ;

use Config ();
use Data::Dumper ;
use Carp;
use Parse::RecDescent;
use File::Path qw(mkpath);

use ExtUtils::XSBuilder::C::grammar  ;

$VERSION = '0.03';

$verbose = 1 ;


=pod

=head1 NAME

ExtUtils::XSBuilder::ParseSource - parse C source files

=head2 DESCRIPTION

For more information, see L<ExtUtils::XSBuilder>

=cut

# ============================================================================

sub new {
    my $class = shift;

    my $self = bless {
        @_,
    }, $class;


    $self;
}

# ============================================================================

=pod

=head2 extent_parser (o)

Allows the user to call the Extent or Replace method of the parser to add 
new syntax rules. This is mainly useful to include expansions for 
preprocessor macros.

=cut

sub extent_parser {
}

# ============================================================================
=pod

=head2 preprocess (o)

Allows the user to preprocess the source before it is given to the parser.
You may modify the source, which is given as first argument in place.

=cut

sub preprocess {
}


# ============================================================================

sub parse {
    my $self = shift;

    $self -> find_includes ;
    my $c = $self -> {c} = {} ;
    
    print "Initialize parser\n" if ($verbose) ;
    my $grammar = ExtUtils::XSBuilder::C::grammar::grammar() or croak "Can't find C grammar\n";
    
    $::RD_HINT++;
    
    my $parser = $self -> {parser} = Parse::RecDescent->new($grammar);

    $parser -> {data} = $c ;
    $parser -> {srcobj} = $self ;

    $self -> extent_parser ($parser) ;

    foreach my $inc (@{$self->{includes}})
        {
        print "scan $inc ...\n" if ($verbose) ;
        $self->scan ($inc) ;
        }

}


# ============================================================================

sub scan {

    my ($self, $filename) = @_ ;

    my $txt ;
        {
        local $/ = undef ;
        open FH, $filename or die "Cannot open $filename ($!)" ;
        $txt = <FH> ;
        close FH ;
        }
    local $SIG{__DIE__} = \&Carp::confess;

    $self -> {parser} -> {srcfilename} = $filename ;

    $self -> preprocess ($txt) ;

    return $self -> {parser}->code($txt) or die "Cannot parse $filename" ;

}


# ============================================================================

sub DESTROY {
    my $self = shift;
    unlink $self->{scan_filename}
}


# ============================================================================
=pod

=head2 include_dirs (o)

Returns a reference to the list of directories that should be searched for
include files which contain the functions, structures, etc. to be extracted. 

Default: C<'.'>

=cut

sub include_dirs {
    my $self = shift;
    ['.'],
}


# ============================================================================
=pod

=head2 include_paths (o)

Returns a reference to a list of directories that are given as include
directories to the C compiler. This is mainly used to strip these directories
from filenames to convert absolute paths to relative paths.

Default: empty list (C<[]>)

=cut

sub include_paths {
    my $self = shift;
    [],
}


# ============================================================================
=pod

=head2 unwanted_includes (o)

Returns a reference to a list of include files that should not be processed.

Default: empty list (C<[]>)

=cut

sub unwanted_includes { [] }



# ============================================================================
=pod

=head2 sort_includes (o, include_list)

Passed an array ref of include files, it allows the user to define the sort
order, so includes are processed correctly.

Default: return the passed array reference.

=cut

sub sort_includes {
    
    return $_[1] ;
}



# ============================================================================
=pod

=head2 find_includes (o)

Returns a list of include files to be processed. 

Default: search directories given by C<include_dirs> for all files and build a
list of include files. All files starting with a word matched by 
C<unwanted_includes> are not included in the list.

=cut

sub find_includes {
    my $self = shift;

    return $self->{includes} if $self->{includes};

    require File::Find;

    my(@dirs) = $self->include_dirs;

    unless (-d $dirs[0]) {
        die "could not find include directory";
    }

    print "Will search @dirs for include files...\n" if ($verbose) ;

    my @includes;
    my $unwanted = join '|', @{$self -> unwanted_includes} ;

    for my $dir (@dirs) {
        File::Find::finddepth({
                               wanted => sub {
                                   return unless /\.h$/;
                                   return if ($unwanted && (/^($unwanted)/o));
                                   my $dir = $File::Find::dir;
                                   push @includes, "$dir/$_";
                               },
                               follow => $^O ne 'MSWin32',
                              }, $dir);
    }

    return $self->{includes} = $self -> sort_includes (\@includes) ;
}



# ============================================================================
=pod

=head2 handle_define (o)

Passed a hash ref with the definition of a define, may modify it.
Return false to discard it, return true to keep it.

Default: C<1>

=cut

sub handle_define { 1 } ;


# ============================================================================
=pod

=head2 handle_enum (o)

Passed a hash ref with the definition of a enum value, may modify it.
Return false to discard it, return true to keep it.

Default: C<1>

=cut

sub handle_enum { 1 } ;


# ============================================================================
=pod

=head2 handle_struct (o)

Passed a hash ref with the definition of a struct, may modify it.
Return false to discard it, return true to keep it.

Default: C<1>

=cut

sub handle_struct { 1 } ;



# ============================================================================
=pod

=head2 handle_function (o)

Passed a hash ref with the definition of a function, may modify it.
Return false to discard it, return true to keep it.

Default: C<1>

=cut

sub handle_function { 1 } ;



# ============================================================================
=pod

=head2 handle_callback (o)

Passed a hash ref with the definition of a callback, may modify it.
Return false to discard it, return true to keep it.

Default: C<1>

=cut

sub handle_callback { 1 } ;







# ============================================================================


sub get_constants {
    my($self) = @_;

    my $includes = $self->find_includes;
    my(%constants, %seen);
    my $defines_wanted_re   = $self -> defines_wanted_re ;
    my $defines_wanted      = $self -> defines_wanted ;
    my $defines_unwanted    = $self -> defines_unwanted ;
    my $enums_wanted        = $self -> enums_wanted ;
    my $enums_unwanted      = $self -> enums_unwanted ;

    for my $file (@$includes) {
        open my $fh, $file or die "open $file: $!";
        while (<$fh>) {
            if (s/^\#define\s+(\w+)\s+.*/$1/) {
                chomp;
                next if /_H$/;
                next if $seen{$_}++;
                $self->handle_constant(\%constants, $defines_wanted_re, $defines_wanted, $defines_unwanted);
            }
            elsif (m/enum[^\{]+\{/) {
                $self->handle_enum($fh, \%constants, $enums_wanted, $enums_unwanted);
            }
        }
        close $fh;
    }

    return \%constants;
}

# ============================================================================

sub get_constants {
    my $self = shift;

    my $key = 'parsed_constants';
    return $self->{$key} if $self->{$key};

    my $c = $self->{$key} = $self->{c}{constants}  ||= [] ;


    # sort the constants by the 'name' attribute to ensure a
    # consistent output on different systems.
    $self->{$key} = [sort { $a->{name} cmp $b->{name} } @{$self->{$key}}];
}



# ============================================================================

sub get_functions {
    my $self = shift;

    my $key = 'parsed_fdecls';
    return $self->{$key} if $self->{$key};

    my $c = $self->{c}{functions}  ||= [] ;


    # sort the functions by the 'name' attribute to ensure a
    # consistent output on different systems.
    $self->{$key} = [sort { $a->{name} cmp $b->{name} } @$c];
}

# ============================================================================

sub get_structs {
    my $self = shift;

    my $key = 'typedef_structs';
    return $self->{$key} if $self->{$key};

    my $c = $self->{c}{structures}  ||= [] ;

    # sort the structs by the 'type' attribute to ensure a consistent
    # output on different systems.
    
    $self->{$key} = [sort { $a->{type} cmp $b->{type} } @$c];
}

# ============================================================================

sub get_callbacks {
    my $self = shift;

    my $key = 'typedef_callbacks';
    return $self->{$key} if $self->{$key};

    my $c = $self->{c}{callbacks} ||= [] ;

    # sort the callbacks by the 'type' attribute to ensure a consistent
    # output on different systems.
    $self->{$key} = [sort { $a->{type} cmp $b->{type} } @$c];
}

# ============================================================================
=pod

=head2 package (o)

Return package name for tables

Default: C<'MY'>

=cut

sub package { 'MY' }

# ============================================================================
=pod

=head2 targetdir (o)

Return name of target directory where to write tables

Default: C<'./xsbuilder/tables'>

=cut

sub targetdir { './xsbuilder/tables' }



# ============================================================================

sub write_functions_pm {
    my $self = shift;
    my $file = shift || 'FunctionTable.pm';
    my $name = shift || $self -> package . '::FunctionTable';

    $self->write_pm($file, $name, $self->get_functions);
}

# ============================================================================

sub write_structs_pm {
    my $self = shift;
    my $file = shift || 'StructureTable.pm';
    my $name = shift || $self -> package . '::StructureTable';

    $self->write_pm($file, $name, $self->get_structs);
}

# ============================================================================

sub write_constants_pm {
    my $self = shift;
    my $file = shift || 'ConstantsTable.pm';
    my $name = shift || $self -> package . '::ConstantsTable';

    $self->write_pm($file, $name, $self->get_constants);
}

# ============================================================================

sub write_callbacks_pm {
    my $self = shift;
    my $file = shift || 'CallbackTable.pm';
    my $name = shift || $self -> package . '::CallbackTable';

    $self->write_pm($file, $name, $self->get_callbacks);
}

# ============================================================================

sub pm_path {
    my($self, $file, $name, $create) = @_;

    my @parts = split '::', ($name || $self -> package . '::X') ;
    my($subdir) = join ('/', @parts[0..$#parts-1]) ;

    my $tdir = $self -> targetdir ;
    if (!-d "$tdir/$subdir") {
        if ($create) {
            mkpath ("$tdir/$subdir", 0, 0755) or die "Cannot create directory $tdir/$subdir ($!)" ;
        }
        else {
            die "Missing directory $tdir/$subdir" ;
            }
    }

    return "$tdir/$subdir/$file";
}

# ============================================================================

sub write_pm {
    my($self, $file, $name, $data) = @_;

    require Data::Dumper;
    local $Data::Dumper::Indent = 1;

    $data ||= [] ;

    $file = $self -> pm_path ($file, $name, 1) ;

    # sort the hashes (including nested ones) for a consistent dump
    canonsort(\$data);

    my $dump = Data::Dumper->new([$data],
                                 [$name])->Dump;

    my $package = ref($self) || $self;
    my $version = $self->VERSION;
    my $date = scalar localtime;

    my $new_content = << "EOF";
package $name;

# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# ! WARNING: generated by $package/$version
# !          $date
# !          do NOT edit, any changes will be lost !
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

$dump

1;
EOF

    my $old_content = '';
    if (-e $file) {
        open PM, "<$file" or die "open $file: $!";
        local $/ = undef; # slurp the file
        $old_content = <PM>;
        close PM;
    }

    my $overwrite = 1;
    if ($old_content) {
        # strip the date line, which will never be the same before
        # comparing
        my $table_header = qr{^\#\s!.*};
        (my $old = $old_content) =~ s/$table_header//mg;
        (my $new = $new_content) =~ s/$table_header//mg;
        $overwrite = 0 if $old eq $new;
    }

    if ($overwrite) {
        open PM, ">$file" or die "open $file: $!";
        print PM $new_content;
        close PM;
    }

}

# ============================================================================
#
# canonsort(\$data);
# sort nested hashes in the data structure.
# the data structure itself gets modified
#

sub canonsort {
    my $ref = shift;
    my $type = ref $$ref;

    return unless $type;

    require Tie::IxHash;

    my $data = $$ref;

    if ($type eq 'ARRAY') {
        for my $d (@$data) {
            canonsort(\$d);
        }
    }
    elsif ($type eq 'HASH') {
        for my $d (keys %$data) {
            canonsort(\$data->{$d});
        }

        tie my %ixhash, 'Tie::IxHash';

        # reverse sort so we get the order of:
        # return_type, name, args { type, name } for functions
        # type, elts { type, name } for structures

        for (sort { $b cmp $a } keys %$data) {
            $ixhash{$_} = $data->{$_};
        }

        $$ref = \%ixhash;
    }
}


# ============================================================================
=pod

=head2 run

Call this class method to parse your source. Before you can do so you must
provide a class that overrides the defaults in
L<ExtUtils::XSBuilder::ParseSource>. After that you scan the source files with

    MyClass -> run ;

=cut

sub run

    {
    my ($class) = @_ ;

    my $p = $class -> new() ;

    $p -> parse ; 

    $p -> write_constants_pm ;

    $p -> write_functions_pm ;

    $p -> write_structs_pm ;

    $p -> write_callbacks_pm ;
    }




1;
__END__