use strict;
use warnings;

package Boilerplater::File;
use Boilerplater::Util qw( verify_args );
use Boilerplater::Binding::Core::Class;

use Carp;
use Fcntl;
use Scalar::Util qw( blessed );
use File::Path qw( mkpath );
use File::Spec::Functions qw( splitpath catfile );

our %new_PARAMS = (
    blocks       => undef,
    source_class => undef,
    parcel       => undef,
);

sub new {
    my $either = shift;
    verify_args( \%new_PARAMS, @_ ) or confess $@;
    return bless {
        %new_PARAMS,
        blocks       => [],
        source_class => undef,
        modified     => 0,
        @_,
        },
        ref($either) || $either;
}

# Accessors.
sub get_modified     { shift->{modified} }
sub set_modified     { $_[0]->{modified} = $_[1] }
sub get_source_class { shift->{source_class} }

# Obtain all Boilerplater::Class objects defined within this file.
sub get_classes {
    my $self = shift;
    return
        grep { ref $_ and $_->isa('Boilerplater::Class') }
        @{ $self->{blocks} };
}

# Return a string used for an include guard, unique per file.
sub guard_name {
    my $self       = shift;
    my $guard_name = "H_" . uc( $self->{source_class} );
    $guard_name =~ s/\W+/_/g;
    return $guard_name;
}

# Return a string opening the include guard.
sub guard_start {
    my $self       = shift;
    my $guard_name = $self->guard_name;
    return "#ifndef $guard_name\n#define $guard_name 1\n";
}

# Return a string closing the include guard.  Other classes count on being
# able to match this string.
sub guard_close {
    my $self       = shift;
    my $guard_name = $self->guard_name;
    return "#endif /\* $guard_name \*/\n";
}

# Given a base directory, return a path name derived from the File's
# source_class with the specified extension.
sub c_path  { return $_[0]->_some_path( $_[1], '.c' ) }
sub h_path  { return $_[0]->_some_path( $_[1], '.h' ) }
sub bp_path { return $_[0]->_some_path( $_[1], '.bp' ) }

sub _some_path {
    my ( $self, $base_dir, $ext ) = @_;
    my @components = split( '::', $self->{source_class} );
    unshift @components, $base_dir
        if defined $base_dir;
    $components[-1] .= $ext;
    return catfile(@components);
}

my %write_h_PARAMS = (
    dest_dir => undef,
    header   => undef,
    footer   => undef,
);

# Generate a C header file containing all class declarations and literal C
# blocks.
sub write_h {
    my $self = shift;
    verify_args( \%write_h_PARAMS, @_ ) or confess $@;
    my %args   = @_;
    my $h_path = $self->h_path( $args{dest_dir} );
    print "Writing $h_path\n";

    # Unlink then open file.
    my ( undef, $out_dir, undef ) = splitpath($h_path);
    mkpath $out_dir unless -d $out_dir;
    confess("Can't make dir '$out_dir'") unless -d $out_dir;
    unlink $h_path;
    sysopen( my $fh, $h_path, O_CREAT | O_EXCL | O_WRONLY )
        or confess("Can't open '$h_path' for writing");

    my $include_guard_start = $self->guard_start;
    my $include_guard_close = $self->guard_close;

    # Aggregate block content.
    my $content = "";
    for my $block ( @{ $self->{blocks} } ) {
        my $ref = ref $block;
        if ($ref) {
            if ( $block->isa('Boilerplater::Class') ) {
                my $bound = Boilerplater::Binding::Core::Class->new(
                    client => $block, );
                $content .= $bound->to_c_header . "\n";
            }
        }
        else {
            $content .= $block . "\n";
        }
    }

    print $fh <<END_STUFF;
$args{header}

$include_guard_start
$content

$include_guard_close

$args{footer}

END_STUFF
}

my %write_c_PARAMS = (
    dest_dir => undef,
    header   => undef,
    footer   => undef,
);

# Generate a C file containing autogenerated code.
sub write_c {
    my $self = shift;
    verify_args( \%write_h_PARAMS, @_ ) or confess $@;
    my %args   = @_;
    my $c_path = $self->c_path( $args{dest_dir} );
    print "Writing $c_path\n";

    # Unlink then open file.
    my ( undef, $out_dir, undef ) = splitpath($c_path);
    mkpath $out_dir unless -d $out_dir;
    confess("Can't make dir '$out_dir'") unless -d $out_dir;
    unlink $c_path;
    sysopen( my $fh, $c_path, O_CREAT | O_EXCL | O_WRONLY )
        or confess("Can't open '$c_path' for writing");

    # Aggregate content.
    my $content = "";
    for my $block ( @{ $self->{blocks} } ) {
        if ( blessed($block) ) {
            if ( $block->isa('Boilerplater::Class') ) {
                my $bound = Boilerplater::Binding::Core::Class->new(
                    client => $block, );
                $content .= $bound->to_c . "\n";
            }
        }
    }

    print $fh <<END_STUFF;
$args{header}

#include "boil.h"
#include "KinoSearch/Obj/VTable.h"
#include "KinoSearch/Util/CharBuf.h"
#include "KinoSearch/Util/Hash.h"
#include "KinoSearch/Util/VArray.h"
#include "KinoSearch/Util/Err.h"
#include "KinoSearch/Util/Host.h"

$content

$args{footer}

END_STUFF
}

1;

__END__

__POD__

=head1 NAME

Boilerplater::File - A Boilerplater source file.

=head1 DESCRIPTION

An abstraction representing a file which contains Boilerplater code.

=head1 METHODS

=head2 new

    my $file_obj = Boilerplater::File->new(
        blocks       => \@blocks,
        source_class => 'Dog::Dalmation',
    );

=over

=item *

B<blocks> - An arrayref.  Each element must be either a Boilerplater::Class
object or a literal C block.

=item *

B<source_class> - The class name associated with the source file, regardless
of how what classes are defined in the source file. Example: If
source_class is "Foo::Bar", that implies that the source file could be found
at 'Foo/Bar.bp' within the source directory and that the output C header file
should be 'Foo/Bar.h' within the target include directory.

=back

=head1 COPYRIGHT

Copyright 2008-2009 Marvin Humphrey

=head1 LICENSE, DISCLAIMER, BUGS, etc.

See L<KinoSearch> version 0.30.

=cut