#!/usr/bin/perl

use warnings;
use strict;
use Getopt::Long;
use Pod::Usage;
use IO::Dir;
use File::Spec::Functions;
use Date::Format;
use File::Which;
use File::Path;
#use File::Copy;

use Cwd;

our $local;
our $branch;
our $tag;
our $rev = 'HEAD';
our $verbose = 0;
our $help = 0;
our $run_ret = 0;

sub logit;

our %conv_files = map { $_ => 1 } qw/
	*.c *.cfg *.cpp *.def *.h *.hin *.in
	*.inc *.m *.m4 *.mft *.s *.rc *.rh
	*.spec *.pl *.txt *._tx makefile*
	.inl CHANGES AUTHORS THANKS/;

our $result = GetOptions ("local=s"      => \$local,   # string
                          "branch=s"   => \$branch,    # string
                          "tag=s"      => \$tag,       # string
                          "revision=i" => \$rev,       # integer
                          "verbose"    => \$verbose,   # flag
                          "help|?"     => \$help    ); # flag
if(!$result) {
	pod2usage(2);
}

if($help) {
	pod2usage(1);
}

if(!(defined $branch || defined $tag || defined $local)) {
	pod2usage(-msg => "Must supply one of --local, --branch or --tag options.", -exitval => 2);
}

if((defined $branch and defined $tag) ||
   (defined $local  and defined $tag) ||
   (defined $branch and defined $local) ||
   (defined $branch and defined $tag and defined $local)) {
	pod2usage(-msg => "Can't declare more than one of the --local, --branch and --tag options.", -exitval => 2);
}

if(defined $local) {
   logit("running in local mode.");
   exit do_local();
}

if(defined $branch) {
   logit("running in branch mode.");
   exit do_branch();
}

if(defined $tag) {
   logit("running in tag mode.");
   exit do_branch();
}

sub do_local {

   my $ret = do_build($local);
   if(!-d $local) {
      logndie("Please privide allegro's base dir via --local and try again.");
   }

   return $ret;
}

sub do_branch {
   my $dir = "allegro-$branch";
   run("svn co -r $rev --non-interactive https://alleg.svn.sourceforge.net/svnroot/alleg/allegro/branches/$branch $dir");

   if(!-d $dir) {
      logndie("$dir does not exist, aborting.");
   }

   return do_build($dir);
}

sub do_tag {
   my $dir = "allegro-$tag";
   run("svn co -r $rev --non-interactive https://alleg.svn.sourceforge.net/svnroot/alleg/allegro/tags/$tag $dir");

   if(!-d $dir) {
      logndie("$dir does not exist, aborting.");
   }

   return do_build($dir);
}

sub do_build {
   my $dir = shift;

   if(!-d $dir) {
      logit("$dir does not seem to be a valid directory, are you sure thats where allegro is?");
      return 0;
   }

   my $cwd = getcwd;
   chdir $dir;

   my ($version, $lrev) = grab_version();
   logit("got allegro-$version".(defined $lrev ? "-r$lrev" : ""));

   if(!-e 'misc/fixdll.sh') {
      logit("We don't seem to be running from an existing checkout.");
      logndie("Please rerun from allegro's base dir.");
   }

   if(!-x _) {
      logit("misc/fixdll.sh script is not executable.");
      logndie("Please set the executable bit on misc/fixdll.sh and rerun.");
   }

   run("misc/fixdll.sh");

   if(defined which("cmake") and defined which("pandoc")) {
      logit("Generating documentation from Pandoc source files...");
      if(-e ".pandoc-build") {
         my $err;
         rmtree('.pandoc-build', { keep_root => 1, verbose => $verbose, error => \$err });
         if(defined $err) {
            for(@$err) {
               logit("rmtree error: $_");
            }
            logndie("failed to remove .pandoc-build. aborting.");
         }
      }
      else {
         mkdir '.pandoc-build' or logndie("failed to mkdir .pandoc-build: $!");
      }

      chdir '.pandoc-build' or logndie("failed to chdir to .pandoc-build: $!");

      run('cmake '.updir);
      run('make docs');
      my $changes = catdir(qw/docs txt changes-*.txt/);
      my @changes = glob $changes;
      if(!scalar @changes) {
         logndie("couldn't find changes*.txt file to copy...");
      }

      my ($ver) = ($changes[0] =~ /(\d+\.\d+)/);
      # if there are issues with using 'rename', switch to 'move' from File::Copy
      rename($changes[0], catdir(updir, qw/CHANGES-${ver}.txt/)) or logndie("couldn't move $changes[0]: $!");
      my $docs_html = catdir(qw/docs html/);
      rename($docs_html, catdir(updir, qw/docs html/)) or logndie("couldn't move $docs_html: $!");

      chdir updir or logndie("failed to chdir to parent directory...");
      my $err;
      rmtree('.pandoc-build', { verbose => $verbose, error => \$err });
      if(defined $err) {
         for(@$err) {
            logit("rmtree error: $_");
         }
         logndie("failed to remove .pandoc-build dir. aborting.");
      }
   }
   else {
      logit("Warning: Cmake or Pandoc not found, skipping step.");
   }

   if(defined which('NaturalDocs')) {
      logit("Generating NaturalDocs...");
      chdir catdir(qw/docs naturaldocs/) or logndie("failed to chdir to naturaldocs directory...");
      run("make clean");
      run("make public");
      chdir catdir(updir,updir);
   }
   else {
      logit("Warning: NaturalDocs not found, skipping step.");
   }

   # add tmpfile.txt files
   logit("adding tmpfile.txt files to empty directories.");
   scan_dir('.', \&empty_dir_handler, sub {0});

   # generate manifest
   logit("generating manifest.");
   unlink 'allegro.mft';
   my @manifest = scan_dir('.', sub {0}, \&gen_mft_handler);
   if(!scalar @manifest) {
      logndie("failed to grab file list for manifest...");
   }

   my $mfh = IO::File->new(">allegro.mft");
   if(!defined $mfh) {
      logndie("failed to open allegro.mft for writing...");
   }

   print $mfh join("\n", @manifest);
   undef $mfh;

   $mfh = IO::File->new(">.allegro.mft");
   if(!defined $mfh) {
      logndie("failed to open .allegro.mft for writing...");
   }

   push @manifest, 'allegro.mft';
   print $mfh join("\n", map { catdir($dir,$_) } @manifest);
   undef $mfh;

   logit("done generating manifest.");
   
   logit("starting unix dist build.");
   
   # convert all text files to unix native eol
   logit("converting files to unix line endings.");
   scan_dir('.', sub {0}, \&conv_unix_file_handler);
   
   my $basename = 'allegro-' . $version;

   # append svn revision to basename if we're building a branch, or a local svn checkout
   #  to get rid of this for a tag checkout, export it instead of co'ing it.
   if(defined $branch || (defined $local && (-d ".svn" || -d ".SVN"))) {
      $basename .= '-r' . $lrev;
   }

   my $cd = getcwd;
   chdir updir or logndie("failed to chdir to parent dir");

   # ignore errors for these two
   unlink $basename.'.tar.gz';
   unlink $basename.'.tar.bz2';
   
   if(which('tar')) {
      # create tar.gz archive
      if(which('gzip')) {
         logit("creating tar.gz archive");
         run("tar -T $dir/.allegro.mft -czf $basename.tar.gz");
      }
      
      # create tar.bz2 archive
      if(which('bzip2')) {
         logit("creating tar.bz2 archive");
         run("tar -T $dir/.allegro.mft -cjf $basename.tar.bz2");
      }
   }

   logit("unix distbuild DONE!");

   logit("starting windows dist build.");

   # ignore errors for these two
   unlink $basename.'.zip';
   unlink $basename.'.7z';

   # convert all text files to dos native eol
   logit("converting files to dos line endings.");
   scan_dir($dir, sub {0}, \&conv_dos_file_handler);

   if(defined which('7za')) {
      logit("using p7zip to create archives.");

      logit("creating zip archive.");
      run("7za a -mx0 -tzip $basename.zip \@$dir/.allegro.mft");

      logit("creating 7z archive.");
      run("7za a -mx9 -ms=on $basename.7z \@$dir/.allegro.mft");
   }
   else {
      if(!defined which("zip")) {
         logndie("failed to find 'zip' executable. Can't make zip archive. aborting.");
      }

      logit("creating zip archive.");
      run("cat $dir/.allegro.mft | zip -9 -r -@ $basename.zip $dir");
   }

   # were on a unix compatible system (I hope)
   if($^O ne 'MSWin32' && $^O ne 'dos') {
      # convert all text files to unix native eol
      logit("converting files to unix line endings.");
      scan_dir($dir, sub {0}, \&conv_unix_file_handler);
   }

   #chdir $cd or logndie("failed to chdir back to $cd");
}

#chdir catdir('allegro')
#run "";

#my @items = scan_dir("4.9", \&scan_file_handler);

sub grab_version {
   my $version;
   # allegro 4.9+
   if(-f 'CMakeLists.txt') {
      # set(ALLEGRO_VERSION .*)
      my $fh = IO::File->new('CMakeLists.txt');
      logndie("failed to open CMakeLists.txt: $!") if !defined $fh;

      my $text = join '', <$fh>;
      undef $fh;

      ($version) = ($text =~ /set\(ALLEGRO_VERSION\s+([\d\.]+)\s*\)/);
   }
   # allegro 4.x
   elsif(-f 'makefile.ver') {
      # shared_version
      my $fh = IO::File->new('makefile.ver');
      logndie("failed to open makefile.ver: $!") if !defined $fh;

      my $text = join '', <$fh>;
      undef $fh;

      ($version) = ($text =~ /shared_version\s*=\s*([\d\.]+)/);
   }

   if(-d '.svn' || -d '.SVN') {
      my $info = run("svn info --non-interactive .");
      my ($rev) = ($info =~ /Last Changed Rev:\s+(\d+)/);
      return ($version, $rev);
   }
   elsif(defined $branch) {
      my $info = run("svn info --non-interactive https://alleg.svn.sourceforge.net/svnroot/alleg/allegro/branches/".$branch);
      my ($rev) = ($info =~ /Last Changed Rev:\s+(\d+)/);

      return ($version, $rev);
   }
   elsif(defined $tag) {
      my $info = run("svn info --non-interactive https://alleg.svn.sourceforge.net/svnroot/alleg/allegro/tags/".$tag);
      my ($rev) = ($info =~ /Last Changed Rev:\s+(\d+)/);

      return ($version, $rev);
   }

   return $version;
}

sub gen_mft_handler {
   my $path = shift;
   my $file = shift;

   return 1;
}

sub create_tmpfiletxt {
   my $dir = shift;
   my $path = catfile($dir, 'tmpfile.txt');
   my $tmpfile = IO::File->new('>'.$path);
   if(!defined $tmpfile) {
      logndie("failed to create $path: $!");
   }

   logit("creating $path");
   print $tmpfile "This file is needed because some unzip programs skip empty directories.";
   close $tmpfile;
}

sub empty_dir_handler {
   my $dir = shift;
   my $items = shift;

   if(!scalar @$items) {
      create_tmpfiletxt($dir);
   }
}

sub conv_eol_file_handler {
   my $path = shift;
   my $file = shift;
   my $type = shift;
   
   my $ret = conv_file_handler($path, $file);

   if($ret) {
      my @stat = lstat($path);
      my $fh = IO::File->new($path);
      my $text = join '', <$fh>;
      undef $fh;

      if($type eq 'unix') {
         logit("dtou: $path");
         $text =~ s/\r//g;
      }
      else {
         logit("utod: $path");
         $text =~ s/([^\r]|^)\n/$1\r\n/g;
      }

      $fh = IO::File->new('>'.$path);
      print $fh $text;
      undef $fh;

      # reset timestamps. not sure why, but mkunixdists.sh does it, so I will too.
      utime $stat[8], $stat[9], $path;
   }

   return $ret;
}

sub conv_unix_file_handler {
   return conv_eol_file_handler(@_, 'unix');
}

sub conv_dos_file_handler {
   return conv_eol_file_handler(@_, 'dos');
}

sub conv_file_handler {
   my $path = shift;
   my $file = shift;
   
   for my $glob ( keys %conv_files ) {
      if(glob_match($glob, $file)) {
         return 1;
      }
   }

   return 0;
}

sub grab_dir_items {
   my $dir = shift;
   my $iodir = IO::Dir->new($dir);

   my @ents;
   while(defined(my $ent = $iodir->read())) {
      next if $ent =~ /^\./;
      push (@ents, $ent);
   }

   return sort {$a cmp $b} @ents;
}

sub scan_dir {
   my $dir = shift;
   my $dir_sub = shift;
   my $file_sub = shift;
   my @stack;

   my @ents = grab_dir_items($dir);

   my @items;
   
   while(1) {
      while (defined(my $ent = shift @ents)) {
         #print "dir: $dir, ent: $ent\n";
         my $full_path = catdir($dir, $ent);

         if(-d $full_path) {
            push @stack, [$dir,[@ents]];
            $dir = $full_path;
            @ents = grab_dir_items($full_path);
            $dir_sub->($full_path, \@ents);
            next;
         }
         elsif(-f _) {
            push @items, $full_path if $file_sub->($full_path, $ent);
         }
      }

      my $ref = pop @stack;
      last unless defined $ref;
      $dir = $ref->[0];
      @ents = @{$ref->[1]};
   }

   return @items;
}


sub glob_match {
   my $glob = shift;
   my $match = shift;

   $glob =~ s/\./\\./g;
   $glob =~ s/\*/.*/g;

   my $re = '^' . $glob . '$';
   
   return $match =~ /$re/;
}

sub run {
   my $command = join ' ', @_;
   logit("run " . $command);

   my $fh;
   my @ret;
   open $fh, $command . " |" or logndie("can't fork: $!");
      
   while(my $line = <$fh>) {
      chomp $line;
      logit('> ' . $line);
      push @ret, $line;
   }

   $run_ret = $? >> 8;
   if($run_ret != 0) {
      logndie("child failed with return: $run_ret");
   }
   
   return @ret if wantarray;
   return join '', @ret;
}

sub logndie {
   logit(@_);
   exit(2);
}

sub logit {
   my $text = join '', @_;
   print $text . "\n" if $verbose;

   my $log = IO::File->new(">> /var/log/build-dist.log");
   if(defined $log) {
      my $time = time2str("%c", time);
      print $log $time.': '.$text."\n";
   }
}

__END__

=head1 NAME

build-dist - Build Allegro distribution archives

=head1 SYNOPSIS

build-dist.pl --branch=4.9 --revision=11001

build-dist.pl --local=allegro-4.9

Options:

   --local           select a local directory (ie: allegro-4.9)
   --branch          select branch (ie: 4.9)
   --tag             select tag (ie: 4.9.8)
   --rev             select a specific revision, defaults to HEAD.
   --verbose         enables verbose output.
   --help            prints this useage info.

=head1 OPTIONS

=over 12

=item B<--branch>

Select which branch to export from subversion. use 'local' to use existing local files.

Only one of --local --branch or --tag may be used at a time.

=item B<--tag>

Select which tag to export from subversion.

Only one of --local --branch or --tag may be used at a time.

=item B<--local>

Select a directory to build archives for.

Only one of --local --branch or --tag may be used at a time.

=item B<--rev>

Select which revision to export from subversion.

=item B<--verbose>

Enable extra output.

=item B<--help>

Print this oh so usefull help info.

=back

=head1 DESCRIPTION

B<build-dist.pl> will build distribution archives for a given tag or branch of Allegro.

Provide the --tag arg to select a specific release version to build.

Provide the --branch arg to select a specific branch to build archives for.

Provide the --local arg to select a local directory to build from.

Optionally provide a revision using --rev[ision] to select any revision of the tag or branch.

=cut
