#!/usr/bin/perl

use warnings;
use strict;

use Getopt::Long  qw/GetOptions :config gnu_getopt/;
use POSIX         qw/strftime/;
use Cwd           qw/getcwd/;
use File::Slurp   qw/read_file/;
use List::Util    qw/first/;
use File::Copy    qw/move copy/;
use File::Spec    ();
use Test::Pod;

use OODoc         ();

# In any case, we want modules in this dir to prevail over installed
# versions of the module.
my $here    = getcwd;
unshift @INC, $here, "$here/lib";

#my $format_pod  = 'pod3';
my $format_pod  = 'pod2';
my $format_html = 'html';
my $tmpdir      = $ENV{TMPDIR}         || '/tmp';

my ($verbose, $test) = (0, 0);
my ($rawdir, $distdir, $license);
my ($workdir, $podtailfn, $pmheadfn, $readmefn, $extends);
my ($firstyear, $email, $website, $skip_links);
my ($html_templates, $html_output, $html_docroot, $html_package);
my ($make_pod, $make_dist, $make_raw, $make_html) = (1, 1, 1, undef);

Getopt::Long::Configure 'bundling';
GetOptions
 ( 'dist!'         => \$make_dist
 , 'distdir|d=s'   => \$distdir
 , 'email|e=s'     => \$email
 , 'extents|x=s'   => \$extends
 , 'firstyear|y=i' => \$firstyear
 , 'html!'         => \$make_html
 , 'html-docroot=s'   => \$html_docroot
 , 'html-output=s'    => \$html_output
 , 'html-templates=s' => \$html_templates
 , 'html-package=s'=> \$html_package
 , 'license|l=s'   => \$license
 , 'pod!'          => \$make_pod
 , 'podtail'       => \$podtailfn
 , 'pmhead'        => \$pmheadfn
 , 'readme'        => \$readmefn
 , 'raw!'          => \$make_raw
 , 'rawdir|r=s'    => \$rawdir
 , 'skip_links=s'  => \$skip_links
 , 'test|t!'       => \$test
 , 'verbose|v!'    => \$verbose
 , 'website|u=s'   => \$website
 , 'workdir|w=s'   => \$workdir
 ) or die "ERROR: stopped";

my %licenses = ( artistic => <<__ARTISTIC, gpl => <<__GPL);
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
See F<http://www.perl.com/perl/misc/Artistic.html>
__ARTISTIC
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details: F<http://www.gnu.org/licenses/gpl.html>
__GPL

my $ooversion = $OODoc::VERSION || $ENV{OODOC_VERSION};

sub read_makefile($);
sub parse_additional_dists(@);
sub create_pod($$);
sub create_html($$$$);
sub create_dist($$);
sub publish_dist($$);
sub create_raw_dist($$);
sub publish_raw_dist($$);
sub create_html_dist($$);
sub publish_html_dist($$);
sub create_heads_and_tails($);
sub create_readme($$$);
sub skip_links(@);

#
# collect some project info
#

system "perl Makefile.PL"
   and die "ERROR: Cannot run Makefile.PL for basic information.\n";

my $makefile = read_makefile 'Makefile';

my $project  = $makefile->{DISTNAME};
my $version  = $makefile->{VERSION};
$firstyear ||= $makefile->{FIRST_YEAR};
$rawdir    ||= $makefile->{RAWDIR}  || $ENV{OODIST_RAWDIR}  || '.';
$distdir   ||= $makefile->{DISTDIR} || $ENV{OODIST_DISTDIR} || '.';
$license   ||= $makefile->{LICENSE} || $ENV{OODIST_LICENSE} || 'artistic';
$email     ||= $makefile->{EMAIL};
$website   ||= $makefile->{WEBSITE};
$extends   ||= $makefile->{EXTENDS} || '';;
$podtailfn ||= $makefile->{PODTAIL};
$pmheadfn  ||= $makefile->{PMHEAD};
$html_templates ||= $makefile->{HTML_TEMPLATES} || 'html';
$html_output    ||= $makefile->{HTML_OUTPUT}    || 'public_html';
$html_docroot   ||= $makefile->{HTML_DOCROOT}   || '/';
$html_package   ||= $makefile->{HTML_PACKAGE};

$make_html = -d $html_templates
   unless defined $make_html;

$workdir   ||= "$tmpdir/$project";
my @extends = split /\:/, $extends;

# make directories absolute, because we will chdir
for($workdir, $rawdir, $distdir, $html_package, $html_templates, @extends)
{  $_ = File::Spec->rel2abs($_, $here);
}

print "*** Producing $project version $version\n" if $verbose;

my $lictext = $licenses{$license} || '';
if(!$lictext && $license)
{   $lictext = read_file $license
        or die "ERROR: cannot read from licence file $license: $!\n";
}

###
### Start OODoc
###

push @INC, map {"$_/lib"} @extends;

my $doc  = OODoc->new
 ( distribution => $project
 , version      => $version  # version of whole
 , verbose      => ($verbose ? 2 : 0)
 );

#
# Reading all the manual pages
# This could be called more than once to combine different sets of
# manual pages in different formats.
#

print "* Processing files of $project $version\n" if $verbose;

   -d $workdir
or mkdir $workdir
or die "ERROR: cannot create $workdir: $!\n";

my ($pmhead, $podtail) = create_heads_and_tails $makefile;

$doc->processFiles
 ( workdir    => $workdir
 , version    => $version      # version on each file
 , notice     => $pmhead
 , skip_links => skip_links($makefile->{SKIP_LINKS}, $skip_links)
 );

parse_additional_dists @extends
   if $make_pod || $make_html;

create_readme $doc, $readmefn, $workdir
   unless defined $readmefn && ! length $readmefn;

create_pod $doc, $podtail
   if $make_pod;

create_html $doc, $html_templates, $html_output, $html_docroot
   if $make_html;

# Dist package

chdir $workdir
   or die "ERROR: cannot go to my $workdir: $!";

my $temp_distfn = create_dist $distdir, $makefile
   if $make_dist;

if(!$make_dist) {;}
elsif($test) { print "* test mode: dist not published\n" if $verbose }
else         { publish_dist $distdir, $temp_distfn }

# RAW package

chdir $here
   or die "ERROR: cannot go back to $here; $!\n";

my $temp_rawfn = create_raw_dist $rawdir, $makefile
   if $make_raw;

if(!$make_raw) { ; }
elsif($test) { print "* test mode: raw not published\n" if $verbose }
else         { publish_raw_dist $rawdir, $temp_rawfn }

# HTML package

if($make_html && $html_package)
{   chdir $html_output
        or die "ERROR: cannot go to produced html $html_output\n";

    my $temp_htmlfn = create_html_dist $html_package, $makefile;

    if($test) { print "* test mode: html not published\n" if $verbose }
    else      { publish_html_dist $html_package, $temp_htmlfn }
}

print "* Ready\n" if $verbose;

exit 0;

#
# Parse additional distributions
#

sub parse_additional_dists(@)
{   my @extras = @_;

    # Read all files which are in other modules, but contain information
    # which is required to produce the pod.

    foreach my $extra (@extras)
    {   print "* Processing files in $extra\n" if $verbose;
        system "cd $extra && perl Makefile.PL"
           and die "ERROR: failed to collect from $extra\n";

        my $other = read_makefile "$extra/Makefile";
        my $dist  = $other->{DISTNAME};
        my $distv = $other->{VERSION};
        print "Including files from $dist $distv\n";

        $doc->processFiles
         ( workdir      => undef
         , source       => $extra
         , select       => qr[^ lib/ .*? \.(pod|pm) $ ]x
         , version      => $distv
         , distribution => $dist
         , skip_links   => skip_links($other->{SKIP_LINKS}, $skip_links)
         );
    }

    print "* Preparation phase\n" if $verbose;

    $doc->prepare;
}

#
# Create pods
#

sub create_pod($$)
{   my ($doc, $podtail) = @_;

    print "* Creating POD files\n" if $verbose;

    $doc->create
      ( $format_pod
      , workdir => $workdir
      , select  => sub {my $manual = shift; $manual->distribution eq $project}
      , append  => $podtail
      );
}

#
# Create html
#

sub create_html($$$$)
{   my ($doc, $templates, $output, $url) = @_;

    print "* Creating HTML files in $output\n" if $verbose;

    $doc->create
     ( $format_html
     , workdir        => $output
     , format_options => 
        [ html_root      => $url
        , html_meta_data => qq[<link rel="STYLESHEET" href="/oodoc.css">]
        ]
     , manual_format => []
     );
}

sub create_heads_and_tails($)
{   my $makefile = shift;
    my $today    = strftime "%B %d, %Y", localtime;
    my $year     = strftime "%Y", localtime;

    if($firstyear)
    {   $year = $firstyear =~ m/$year$/ ? $firstyear : "$firstyear-$year";
    }

    my $changelog = first { m/^(?:change|contrib)/i } glob '*';

    my $contrib   = $changelog ? <<__CONTRIB : '';
 For other contributors see $changelog.
__CONTRIB

    my $web       = $website ? " Website: F<$website>" : '';
    my $author    = $makefile->{AUTHOR} ? " by $makefile->{AUTHOR}" : '';

    my $pmheadsrc
     = defined $pmheadfn  ? read_file $pmheadfn
     : -f 'PMHEAD.txt'    ? read_file 'PMHEAD.txt'
     :                      <<'__PM_HEAD';
Copyrights $year$author.
${contrib}See the manual pages for details on the licensing terms.
Pod stripped from pm file by OODoc $ooversion.
__PM_HEAD

    my $podtailsrc
     = defined $podtailfn ? read_file $podtailfn
     : -f 'PODTAIL.txt'   ? read_file 'PODTAIL.txt'
     :                      <<'__POD_TAIL';

=head1 SEE ALSO

This module is part of $project distribution version $version,
built on $today.$web

=head1 LICENSE

Copyrights $year$author.$contrib

$lictext

__POD_TAIL

   my $pmhead  = eval qq{"$pmheadsrc"};
   die "ERROR in pmhead: $@" if $@;

   my $podtail = eval qq{"$podtailsrc"};
   die "ERROR in podtail: $@" if $@;

   ($pmhead, $podtail);
}

sub create_readme($$$)
{   my ($doc, $readmefn, $workdir) = @_;
    my @toplevel = glob "$workdir/*";

    my $readme   = first { /\breadme$/i } @toplevel;
    return 1 if $readme;

    print "adding README\n" if $verbose;

    my $manifest = first { /\bmanifest$/i } @toplevel;
    open MANIFEST, '>>', $manifest
        or die "ERROR: cannot append to $manifest: $!\n";
    print MANIFEST "README\n";
    close MANIFEST;

    $readme      = File::Spec->catfile($workdir, 'README');

    if($readmefn)
    {   #
        # user provided README
        #

        print "copying $readmefn as README\n" if $verbose;

        copy $readmefn, $readme
            or die "ERROR: cannot copy $readmefn to $readme: $!\n";

        return 1;
    }

    #
    # Produce a README text
    #

    open README, '>', $readme
        or die "ERROR: cannot write to $readme: $!\n";

    my $date = localtime;

    print README <<__README;
=== README for $project version $version
=   Generated on $date by OODoc $ooversion

There are various ways to install this module:

 (1) if you have a command-line, you can do:
       perl -MCPAN -e 'install <any package from this distribution>'

 (2) if you use Windows, have a look at http://ppm.activestate.com/

 (3) if you have downloaded this module manually (as root/administrator)
       gzip -d $project-$version.tar.gz
       tar -xf $project-$version.tar
       cd $project-$version
       perl Makefile.PL
       make          # optional
       make test     # optional
       make install

For usage, see the included manual-pages or
    http://search.cpan.org/dist/$project-$version/

Please report problems to
    http://rt.cpan.org/Dist/Display.html?Queue=$project

__README

    close README;
    1;
}

#
# Create a distribution
#

sub create_dist($$)
{   my ($dest, $makefile) = @_;

    print "* Preparing test\n" if $verbose;
    system "perl Makefile.PL"
       and die "ERROR: perl Makefile.PL in $workdir failed: $!\n";

    system "make clean >/dev/null"
       and die "ERROR: make clean in $workdir failed: $!\n";

    move 'Makefile.old', 'Makefile'
       or die "ERROR: cannot reinstate Makefile: $!\n";

    print "* Running make\n" if $verbose;
    system "make distdir >/dev/null"
       and die "ERROR: make in $workdir failed: $!\n";

    if(-d 't' || -d 'tests')
    {   print "* Running tests\n" if $verbose;
        system "make disttest"
            and die "ERROR: make test in $workdir failed: $!\n";
    }
    else
    {   print "* No tests to run\n" if $verbose;
    }

    my $distfile = $makefile->{DISTVNAME}. '.tar.gz';
    print "* Building distribution in $distfile\n" if $verbose;
    unlink $distfile;

    system "make dist >/dev/null"
       and die "ERROR: make dist in $workdir failed: $!\n";

    $distfile;
}

sub publish_dist($$)
{   my ($dest, $distfile) = @_;
    return if $dest eq $workdir;

    print "* Distributed package in $dest/$distfile\n"
       if $verbose;

    -f $distfile
       or die "ERROR: cannot find produced $distfile";

       -d $dest
    or mkdir $dest
    or die "ERROR: cannot create $dest: $!\n";

    move $distfile, $dest
       or die "ERROR: cannot move $distfile to $dest: $!\n";
}

#
# RAW
#

sub create_raw_dist($$)
{   my ($dest, $makefile) = @_;

    my $rawname = $makefile->{DISTVNAME}.'-raw';
    my $rawfile = $rawname. '.tar.gz';
    print "* Building raw package $rawfile\n" if $verbose;
    
    unlink $rawfile;
    
    my %manifests;
    foreach my $man (glob "MANIFEST*")
    {   foreach (read_file $man)
        {  s/\s{3,}.*$//;
           next if m/^#/;
           next unless length;
           chomp;
           $manifests{$_}++;
        }
    }
    
    my @manifests = map { "$rawname/$_" } sort keys %manifests;
    symlink('.', $rawname) || readlink $rawname eq '.'
        or die "ERROR: cannot create temp symlink $rawname: $!\n";

    local $" = ' ';
    system "tar czf $rawfile @manifests"
        and die "ERROR: cannot produce $rawfile with tar\n";

    unlink $rawname;
    $rawfile;
}

sub publish_raw_dist($$)
{   my ($dest, $rawfile) = @_;
    return if $dest eq $html_output;

       -d $dest
    or mkdir $dest
    or die "ERROR: cannot create $dest: $!\n";
    print "* Raw package to $dest/$rawfile\n" if $verbose;

    move $rawfile, $dest
        or die "ERROR: cannot move $rawfile to $dest: $!\n";
}

#
# HTMLPKG
#

sub create_html_dist($$)
{   my ($dest, $makefile) = @_;

    my $htmlfile = $makefile->{DISTVNAME}.'-html.tar.gz';
    print "* Building html package $htmlfile\n" if $verbose;

    unlink $htmlfile;

    local $" = ' ';
    system "tar czf $htmlfile *"
        and die "ERROR: cannot produce $htmlfile with tar\n";

    $htmlfile;
}

sub publish_html_dist($$)
{   my ($dest, $htmlfile) = @_;

    return if $dest eq $html_output;

       -d $dest
    or mkdir($dest)
    or die "ERROR: cannot create $dest: $!\n";

    print "* HTML package to $dest/$htmlfile\n"
        if $verbose;

    move $htmlfile, $dest
        or die "ERROR: cannot move $htmlfile to $dest: $!\n";
}

#
# read_makefile MAKEFILE
# Collect values of variable defined in the specified MAKEFILE, which was
# produced by "perl Makefile.PL"
#

sub read_makefile($)
{   my $makefile = shift;

    open MAKEFILE, '<', $makefile
       or die "ERROR: cannot open produced Makefile: $makefile";

    my %makefile;
    while( <MAKEFILE> )
    {   $_ .= <MAKEFILE> while !eof MAKEFILE && s/\\$//; # continuations
        s/\n\t*/ /g;

        $makefile{$1} = $2 if m/^([A-Z_][A-Z\d_]+)\s*\=\s*(.*?)\s*$/;

        if(m/^#\s+([A-Z_][A-Z\d_]+)\s*\=>\s*(.*?)\s*$/)
        {   # important information which ended-up in comments ;(
            my ($key, $v) = ($1, $2);
            $v =~ s/q\[([^\]]*)\]/$1/g;  # remove q[]
            $makefile{$key} = $v;
        }
    }

    close MAKEFILE;
    \%makefile;
}

#
# skip_links STRINGS
# Split list of package names to avoid
#

sub skip_links(@)
{   my @links = map { defined $_ ? split(/[\, ]/, $_) : () } @_;
    \@links;
}
    
__END__

=head1 NAME

oodist - create perl distributions with OODoc

=head1 SYNOPSIS

 cd <src>
 oodist [OPTIONS]
   OPTION:                 DEFAULT:
   --pod  or --nopod         produce pod: yes
   --dist or --nodist        produce distribution: yes
   --html or --nohtml        produce html: yes if templates
   --raw  or --noraw         produce package with raw files: yes

  OPTIONS general:
   --distdir  | -d <dir>     makefile:DISTDIR || $ENV{OODOC_DISTDIR} || <src>
   --extends  | -x <path>    empty
   --project  | -p <string>  makefile:DISTNAME
   --rawdir   | -r <dir>     makefile:RAWDIR  || $ENV{OODOC_RAWDIR} || <src>
   --readme        <file>    constructured
   --workdir  | -w <dir>     /tmp/<DISTVNAME>
   --verbose  | -v           true when specified

  OPTIONS for parsers:
   --skip_links <string>     makefile:SKIP_LINKS

  OPTIONS for POD:
   --email    | -e <email>   makefile:EMAIL
   --firstyear| -y <year>    makefile:FIRST_YEAR
   --license  | -l <string>  makefile:LICENSE || $ENV{OODOC_LICENSE} || 'artistic'
   --pmhead   | -h <file>    makefile:PMHEAD  || 'PMHEAD.txt'  || constructed
   --podtail  | -t <file>    makefile:PODTAIL || 'PODTAIL.txt' || constructed
   --website  | -u <url>     makefile:WEBSITE

  OPTIONS for HTML:
   --html-templates <dir>    makefile:HTML_TEMPLATES || 'html'
   --html-output <dir>       makefile:HTML_OUTPUT    || 'public_html'
   --html-docroot <url>      makefile:HTML_DOCROOT   || '/'
   --html-package <dir>      makefile:HTML_PACKAGE

=head1 DESCRIPTION

The C<oodist> script is currently only usable on UNIX in combination
with Makefile.PL.  It is a smart wrapper around the OODoc module,
to avoid start work to produce real POD for modules which use OODoc's
POD extensions.  HTML is not (yet) supported.

=head2 Configuring

All OPTIONS can be specified on the command-line, but you do not want
to specify them explicitly each time you produce a new distribution for
your product.  The options come in two kinds: those which are environment
independent and those which are.  The first group can be set via the
Makefile.PL, the second can be set using environment variables as well.

Example: add to the end of your C<Makefile.PL>

 sub MY::postamble { <<'__POSTAMBLE' }
 FIRST_YEAR   = 2001
 WEBSITE      = http://perl.overmeer.net/oodoc
 EMAIL        = oodoc@overmeer.net
 __POSTAMBLE

=head2 Main options

The process is controlled by four main options.  All options are by
default C<on>.

=over 4

=item . --pod or --nopod

Produce pod files in the working directory and in the distribution.

=item . --dist or --nodist

Create a distribution, containing all files from the MANIFEST plus
produced files.

=item . --html or --nohtml

Create html manual pages.  The C<--html-templates> options must
point to an existing directory (defaults to the C<html/> sub-directory).

=item . --raw  or --noraw

Create a package which contains the files which are needed to produce
the distribution: the pm files still including the oodoc markup.

=back

=head2 General options

The other OPTIONS in detail

=over 4

=item . --readme <filename>

Copy the specified README file into the distribution, if there is no
README yet.  The name will be added to the MANIFEST.  If the option
is not specified, a simple file will be created.  If this option is
specified, but the filename is empty, then the README will not be
produced.

=item . --distdir  | -d <dir>

The location where the final file to be distributed is placed, by default
in the source directory.  Ignored when C<--test> is used.

=item . --extends  | -x <path>

A colon seperated list of directories which contains "raw oodoc" pm and
pod files which are (in some cases) used to provide data for base-classes
of this module.

=item . --rawdir   | --r <dir>

The location where a I<raw> version of the distribution is made.  The
normal distribution contains expanded POD and stripped PM files.  For that
reason, you can not use it a backup for your files.  If you have co-workers
on the module, you may wish to give them the originals.
gnored when C<--test> is used.

=item . --test     | -t

Run in test mode: the raw and real distributions are produced, but not
moved to their final location.  You will end-up with these archives
in the work-directory (see C<--workdir>).

=item . --verbose  | --v

Shows what happens during the process.

=item . --workdir  | -w <dir>

The processing will take place in a seperate directory: the stripped pm's
and produced pod files will end-up there.

If not provided, that directory will be named after the project, and
located in C<$ENV{TMPDIR}>, which defaults to C</tmp>.  For instance
C</tmp/OODoc/>

=back

=head2 Options for parsers

=over 4

=item . --skip_links <strings>

A blank or comma separated list of packages which will have a manual
page, but cannot be loaded under their name.  Sub-packages are excluded
as well.  For instance, XML::LibXML has many manual-pages without
a package with the same name.

=back

=head2 Options to produce POD

=over 4

=item . --email    | -e <email>

The email address which can be used to contact the authors.  Used in the
automatic podtail.

=item . --firstyear| -y <string>

The first year that a release for this software was made.  Used in the
automatic podtail.  The specified string can be more complex than a
simple year, for instance C<1998-2000,2003>.  The last year will be
automatically added like C<-2006>, which results in C<1998-2000,2003-2006>.
When the current year is detected at the end of the string, the year will
not be added again.

=item . --license  | -l ['gpl'|'artistic'|filename]

The lisense option is a special case: it can contain either the
strings C<gpl> or C<artistic>, or a filename which is used to
read the actual license text from.  The default is C<artistic>

=item . --pmhead

Each of the stripped C<pm> files will have content of the file
added at the top.  Each line will get a comment '# ' marker before
it.  If not specified, a short notice will be produced automatically.

Implicitly, if a file C<PMHEAD.txt> exists, that will be used.
variables found in the text will be filled-in.

=item . --podtail

Normally, a trailing text is automatically produced, based on all kinds
of facts.  However, you can specify your own file. If exists, the content
is read from a file named C<PODTAIL.txt>.

After reading the file, variables will be filled in: scalars like
C<$version>, C<$project>, C<$website> etc are to your disposal.

=item . --website  | -u <url>

Where the HTML documentation related to this module is publicly visible.

=back

=head2 Options to produce HTML

=over 4

=item . --html-docroot <url>

This (usually relative) URL is prepended to all absolute links in the
HTML output.

=item . --html-output <dir>

The directory where the produced HTML pages are written to.

=item . --html-templates <dir>

The directory which contains the templates for HTML pages which are used
to construct the html version of the manual-pages.

=item . --html-package <dir>

When specified, the html-output directory will get packaged into a
a tar achive in this specified directory.

=back
