The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
use warnings;

use ExtUtils::MakeMaker;
use Config;

use Getopt::Long;
use Data::Dumper;
use Cwd qw(getcwd abs_path);

BEGIN {
    my $min = 5.006;
    if ($] < $min) {
        warn "Perl version $] is below minimum $min required. Upgrade!\n";
        exit 0;
    }
}

my $lmcd_src = "src/libmemcached";
GetOptions(
    'g!'    => \my $opt_g,      # enable debugging
    'pg!'   => \my $opt_pg,     # enable profiling
    'cov!'  => \my $opt_cov,    # enable test coverage analysis
    'bin!'  => \my $opt_bin,    # install bintools from libmemcached
    'src=s' => \$lmcd_src,     # unpacked source code for libmemcached
) or exit 1;

my $lmcd_inst = getcwd()."/src_inst";
my $lmcd_built_lib = "$lmcd_inst/lib/libmemcached$Config{lib_ext}";
my @lmcd_pod = sort <$lmcd_src/docs/memcached_*.pod>;
my $is_developer = (-d ".svn" || -d ".git");

my ($lmcd_h) = eval { build_libmemcached() };
if ($@) {
    warn "Unable to build libmemcached: $@\n";
    warn "Aborted.\n";
    exit 0; # tell cpan testers that this is not a failure
}

my %opts;
if (my $gccversion = $Config{gccversion}) {     # ask gcc to be more pedantic
    print "Your perl was compiled with gcc (version $Config{gccversion}), okay.\n";
    $gccversion =~ s/[^\d\.]//g; # just a number please
    $opts{DEFINE} .= ' -W -Wall -Wpointer-arith -Wbad-function-cast';
    $opts{DEFINE} .= ' -Wno-comment -Wno-sign-compare -Wno-cast-qual';
    $opts{DEFINE} .= ' -Wmissing-noreturn -Wno-unused-parameter' if $gccversion ge "3.0";
    if ($is_developer) {
        #$opts{DEFINE} .= ' -DPERL_GCC_PEDANTIC -ansi -pedantic' if $gccversion ge "3.0";
        $opts{DEFINE} .= ' -Wdisabled-optimization -Wformat'    if $gccversion ge "3.0";
        $opts{DEFINE} .= ' -Wmissing-prototypes';
    }
}
$opts{OPTIMIZE} = "-g"  if $opt_g;
$opts{CCFLAGS}  = "-pg" if $opt_pg;
my $coverage_flags = "";
if ($opt_cov) {
    $opts{OPTIMIZE} = "-O0";
    # http://search.cpan.org/~pjcj/Devel-Cover/gcov2perl
    $coverage_flags = "-fprofile-arcs -ftest-coverage";
    $opts{CCFLAGS}  = $coverage_flags;
}

### optionally install the command line utilities that come with
### libmemcached as well
if( $opt_bin ) {
    $opts{EXE_FILES} = [ map { s/\.c$//i; $_ } <src/libmemcached/clients/mem*.c> ];
}

WriteMakefile(
    NAME                => 'Memcached::libmemcached',
    AUTHOR              => 'Tim Bunce <Tim.Bunce@pobox.com>',
    VERSION_FROM        => 'libmemcached.pm',
    ABSTRACT_FROM       => 'libmemcached.pm',
    INC                 => "-I$lmcd_inst/include",
    # We want to link to *our* private libmemcached and not one that
    # might already be installed on the system. The LIBS config gets
    # appended to the link command line, so if we used "-L$dir -lmemcached"
    # then the installed lib would get preference over ours.
    # So we explicitly refer to our static library. That also avoids the
    # need to worry about what library might get used at runtime.
    LDFROM           => '$(OBJECT)'." $lmcd_built_lib",
    PREREQ_PM           => {
        'Test::More' => 0,
    },
    dynamic_lib         => {
        OTHERLDFLAGS => ($opt_pg ? "-pg " : "")
                      . ($opt_g  ? "-g "  : "") 
                      . $coverage_flags,
    },
    dist                => {
        COMPRESS => 'gzip -9f', SUFFIX => 'gz',
        DIST_DEFAULT=> 'clean distcheck disttest tardist',
        PREOP => '$(MAKE) -f Makefile.old distdir',
    },
    # see also MY::postamble below
    clean               => {
        FILES => 'Memcached-libmemcached-* lib/Memcached/libmemcached/*_hash.pl *.gcov libmemcached.gcda libmemcached.gcno',
    },
    %opts,
);


{   package MY;

sub postamble {
return qq{
COVER = cover
LMCD_SRC=$lmcd_src
LMCD_INST=$lmcd_inst
LMCD_BUILT_LIB=$lmcd_built_lib
}.q{
coverclean:
	$(COVER) -delete

testcover: coverclean pure_all
	HARNESS_PERL_SWITCHES='-MDevel::Cover=-coverage,branch,-coverage,condition,-coverage,pod,-coverage,statement,-coverage,subroutine' PERL_DL_NONLAZY=1 $(FULLPERLRUN) "-MExtUtils::Command::MM" "-e" "test_harness($(TEST_VERBOSE), '$(INST_LIB)','$(INST_ARCHLIB)')" $(TEST_FILES)
	gcov libmemcached.xs
	gcov2perl *.gcov
	$(COVER) 

clean ::

realclean ::
	$(RM_RF) $(LMCD_INST)
	-cd $(LMCD_SRC) && $(MAKE) distclean


libmemcached.c : $(XSUBPPDIR)/ParseXS.pm typemap

$(OBJECT) : $(LMCD_BUILT_LIB)

svnmanifest::
	svn list -R -r HEAD | sort |grep -v '/$$' > MANIFEST
	svn diff MANIFEST
	svn status

checkkeywords:
	$(RM_RF) blib
	find . -type f \( -name .svn -prune -o -name \*.pm -o -name \*.PL -o -name \*.pl \) \
	    -exec bash -c '[ -z "$$(svn pg svn:keywords {})" ] && echo svn propset svn:keywords \"Id Revision\" {}' \;

checkpod:
	$(RM_RF) blib
	find . -type f \( -name .svn -prune -o -name \*.pm -o -name \*.PL -o -name \*.pl \) \
	    -exec podchecker {} \; 2>&1 | grep -v 'pod syntax OK'

}
}


sub tool_xsubpp {
    #my $string = shift->SUPER::tool_xsubpp(@_);
    # Prepend -It/lib to XSUBPP (we do this instead of editing XSUBPPRUN because
    # XSUBPPRUN didn't exist in perl 5.6)
    return q{
XSUBPPDIR=t/lib/ExtUtils
XSUBPP = -It/lib $(XSUBPPDIR)/xsubpp
XSUBPPRUN = $(PERLRUN) $(XSUBPP)
};
}

} # end of package MY

exit 0;


sub run {
    my ($cmd) = @_;
    warn "$cmd\n";
    system($cmd) == 0
        or die "Error running $cmd\n";
}

sub build_libmemcached {
    sync_libmemcached_pod();
    extract_libmemcached_functions();
    extract_libmemcached_constants();

    return if -d "$lmcd_inst/lib"; # XXX assume it built ok. use 'make realclean' to rm
    mkdir $lmcd_inst, 0775 unless -d $lmcd_inst;
    run("cd $lmcd_src && make distclean") if -f "$lmcd_src/Makefile";
    my $configure_args = '--disable-sasl --with-pic --disable-shared';
    $configure_args .= ' --enable-debug' if $opt_g;
    $configure_args .= ' CFLAGS=-pg LDFLAGS=-pg' if $opt_pg;
    if ($is_developer) {    # XXX make a Makefile.PL argument/option
    }
    run("cd $lmcd_src && ./configure --prefix=$lmcd_inst $configure_args");
    #run("cd $lmcd_src && make test") if $is_developer; # XXX
    run("cd $lmcd_src && make install");
}

sub sync_libmemcached_pod {
    return unless -d ".svn";
    # we duplicate the libmemcached pod in svn so that the docs can be read on search.cpan.org
    my $perl_pod_dir = "lib/Memcached/libmemcached";
    for my $src_pod (@lmcd_pod) {
        (my $dst_pod = $src_pod) =~ s!$lmcd_src/docs!$perl_pod_dir!;
        $dst_pod =~ s/\.pod/\.pm/;
        open my $src, "<$src_pod" or die "Can't open $src_pod: $!";
        open my $dst, ">$dst_pod" or die "Can't open $dst_pod: $!";

        # convert path into package
        (my $dst_pkg = $dst_pod) =~ s{/}{::}g;
        $dst_pkg =~ s{ lib:: (.*?) \.\w+ $ }{$1}x;

        print $dst "package $dst_pkg;\n\n"; # for search.cpan.org
        while (<$src>) {
            print $dst $_;
        }
        print $dst "1;\n";
        close $dst or die "Error closing $dst_pod: $!";
        run("svn add -q $dst_pod");
    }
    # XXX svn rm any $perl_pod_dir/memcached_*.pod that weren't in @lmcd_pod
}

sub extract_libmemcached_functions {
    my %libmemcached_func;

    # find all memcached_* functions
    warn "Reading libmemcached pod docs to find all public functions\n";
    for my $src_pod (@lmcd_pod) {
        open my $fh, "<$src_pod" or die "Can't open $src_pod: $!";
        #warn $src_pod;
        while (<$fh>) {
            next unless /\b(memcached_\w+)\s*\([^3]/;
            $libmemcached_func{$1} = 1
                unless $1 eq 'memcached_return'; # parsing fooled by callback
            #warn "\t$1\t$_";
        }
    }

    # write 
    my $func_pm = "lib/Memcached/libmemcached/func_hash.pl";
    warn "Writing $func_pm\n";
    open my $func_pm_fh, ">$func_pm" or die "Can't open $func_pm: $!";
    local $\ = "\n";
    print $func_pm_fh "# DO NOT EDIT! GENERATED BY $0\n";
    print $func_pm_fh "".Data::Dumper->Dump([\%libmemcached_func], [qw(libmemcached_func)]);
    close $func_pm_fh or die "Error closing $func_pm: $!";

    # sanity check the generated file
    my $loaded = require $func_pm;
    die "$func_pm didn't return a HASH reference ($loaded)"
        unless ref $loaded eq 'HASH';
}

sub extract_libmemcached_constants {
    my %libmemcached_const;

    # find all MEMCACHED_* constants (#define and enum)
    warn "Reading headers to find all constants\n";
    my $in_enum = 0;
    my @const;

    my @headers = ("$lmcd_src/libmemcached/memcached.h", 
                   "$lmcd_src/libmemcached/constants.h");
    for my $h (@headers) {
        open my $fh, "<$h" or die "Can't open $h: $!";
        while (<$fh>) {
            if ($in_enum) {
                if (m/^ \s* } \s* (\w+)/x) { # end of enum
                    $libmemcached_const{$_} = $1 for @const;
                    @const = ();
                    $in_enum = 0;
                }
                elsif (m/^ \s* (MEMCACHED_\w+)/x) {
                    my $symbol = $1;
                    if ( $symbol !~ /MEMCACHED_CALLBACK_(MALLOC|REALLOC|FREE)_FUNCTION/) {
                        push @const, $symbol;
                    }
                }
            }
            elsif (m/^ \s* typedef \s+ enum /x) {
                $in_enum = 1;
            }
            elsif (m/\# \s* define \s+ (MEMCACHED_\w+)/x) {
                my $symbol = $1;
                if ($symbol !~ /_H$/) {
                    $libmemcached_const{$1} = "defines";
                }
            }
        }
    }

    # write raw hash of const names
    my $const_pl = "lib/Memcached/libmemcached/const_hash.pl";
    warn "Writing $const_pl\n";
    open my $const_pl_fh, ">$const_pl" or die "Can't open $const_pl: $!";
    local $\ = "\n";
    print $const_pl_fh "# DO NOT EDIT! GENERATED BY $0\n";
    print $const_pl_fh "".Data::Dumper->Dump([\%libmemcached_const], [qw(libmemcached_const)]);
    close $const_pl_fh or die "Error closing $const_pl: $!";

    # sanity check the generated file
    my $loaded = require $const_pl;
    die "$const_pl didn't return a HASH reference ($loaded)"
        unless ref $loaded eq 'HASH';

    # write raw hash of const names
    my $const_xs = "const-xs.inc";
    warn "Writing $const_xs\n";
    open my $const_xs_fh, ">$const_xs" or die "Can't open $const_xs: $!";
    local $\ = "\n";
    print $const_xs_fh "# DO NOT EDIT! GENERATED BY $0\n";
    print $const_xs_fh "IV\nconstant()";
    print $const_xs_fh "\tALIAS:";
    print $const_xs_fh "\t$_ = $_" for sort keys %libmemcached_const;
    print $const_xs_fh "\tCODE:";
    print $const_xs_fh "\tRETVAL = ix;";
    print $const_xs_fh "\tOUTPUT:";
    print $const_xs_fh "\tRETVAL";
    close $const_xs_fh or die "Error closing $const_xs: $!";

    # now write a pod file to document the constants and tags

    # invert libmemcached_const into hash of tags with arrays of name
    my %libmemcached_tags;
    push @{ $libmemcached_tags{ $libmemcached_const{$_} } }, $_ for keys %libmemcached_const;
    # open file and write prologue
    my $const_pm = "lib/Memcached/libmemcached/constants.pm";
    warn "Writing $const_pm\n";
    open my $const_pm_fh, ">$const_pm" or die "Can't open $const_pm: $!";
    local $\ = "\n";
    print $const_pm_fh "# DO NOT EDIT! GENERATED BY $0\n";
    print $const_pm_fh $_ for (
        "=head1 NAME\n",
        "Memcached::libmemcached::constants - document list of constants defined by libmemcached\n",
        "=head1 DESCRIPTION\n",
        "This file just lists all the constants defined by libmemcached which are available to import via the L</Memcached::libmemcached> module.\n",
        "Each constant can be imported individually by name. "
        ."Groups of related constants, such as the elements of an C<enum> type, "
        ."can be imported as a set using a C<:tag> name. "
        ."See L<Exporter> for more information about tags.\n",
    );
    # write out tags and their constants
    print $const_pm_fh "=head1 TAGS\n";
    for my $tag (sort keys %libmemcached_tags) {
        my $names = $libmemcached_tags{$tag} or die "panic";
        print $const_pm_fh "=head2 :$tag\n";
        print $const_pm_fh "  $_"
            for sort @$names;
        print $const_pm_fh "";
    }
    # close up
    print $const_pm_fh "=cut\n\n1;\n";
    close $const_pm_fh or die "Error closing $const_pm: $!";
#    run("svn add -q $const_pm") if $is_developer;
}