#!/usr/bin/perl -w

use strict;
use Pod::Usage 1.12;
use Getopt::Long;
use YAML qw( Load );
use Archive::Tar;
use CPAN::Mini::Inject;
use Env;
use Module::Build::ModuleInfo;
use File::Slurp 'write_file';
use File::Temp;

our $VERSION = '0.28_02';
our %options = ();

sub print_version {
  printf( "mcpani v%s, using CPAN::Mini::Inject v%s and Perl v%vd\n",
    $VERSION, $CPAN::Mini::Inject::VERSION, $^V );
}

sub chkactions {
  for my $action ( qw(add update mirror inject) ) {
    return 1 if ( $options{actionname} eq $action );
  }
  return 0;
}

sub setsub {
  $options{actionname} = shift;
  $options{action}     = shift;
}

sub add {
  my $mcpi = shift;

  if ( $options{file} =~ /([^\/]*)-([0-9._]+)\.tar\.gz$/ ) {
    $options{module}  ||= $1;
    $options{version} ||= $2;

    $options{module} =~ s/-/::/g;
  }

  $mcpi->readlist;

  my @modules_to_add;
  if ( $options{'all-in-meta'} ) {
    ## attempt to read the META.yml
    my $meta_data = _load_meta( $options{file} );

    my $provides = $meta_data->{provides} || {};

    if ( !$provides ) {
      warn("Could not find the 'provides' section in META.yml. "
         . "Only adding based on provided command line.\n" );
    }

    PROVIDE_ENTRY:
    while ( my ( $name, $info ) = each( %{$provides} ) ) {
      next PROVIDE_ENTRY if $name eq $options{module};
      my $v
       = defined $info->{version}
       ? $info->{version}
       : 'undef';

      push @modules_to_add,
       {
        version => $v,
        module  => $name
       };
    }

  }
  elsif ( $options{'discover-packages'} ) {
    ## attempt to read the META.yml
    my $meta_data = _load_meta( $options{file} );

    my $provides = _find_provides( $options{file} );

    while ( my ( $module, $version ) = each( %{$provides} ) ) {
      push @modules_to_add,
       {
        version => $version,
        module  => $module
       };
    }
  }

  # always add the module/version provided on the command line
  # as well
  push @modules_to_add,
   {
    version => $options{version},
    module  => $options{module},
   };

  $mcpi->readlist;
  for my $item ( @modules_to_add ) {
    $mcpi->add(
      module   => $item->{module},
      authorid => $options{authorid},
      version  => $item->{version},
      file     => $options{file}
    );

    if ( $options{verbose} ) {
      print "\nAdding module: $item->{module}\n";
      print "Author ID: $options{authorid}\n";
      print "Version: $item->{version}\n";
      print "File: $options{file}\n";
      print "To repository: $mcpi->{config}{repository}\n\n";
    }
  }
  $mcpi->writelist;

}

sub update {
  my $mcpi = shift;

  mirror( $mcpi );
  inject( $mcpi );
}

sub mirror {
  my $mcpi = shift;
  my %mirroropts;

  $mirroropts{remote} = $options{remote}
   if ( defined( $options{remote} ) );
  $mirroropts{local} = $options{local}
   if ( defined( $options{local} ) );
  $mirroropts{trace} = $options{verbose}
   if ( defined( $options{verbose} ) );

  $mcpi->update_mirror( %mirroropts );
}

sub inject {
  my $mcpi = shift;

  print "Injecting modules from $mcpi->{config}{repository}\n"
   if ( $options{verbose} );
  $mcpi->inject( $options{verbose} );
}

sub _load_meta {
  my $filename = shift;

  print "Loading META.yml from $options{file}\n"
   if ( $options{verbose} );
  my $tar = Archive::Tar->new();
  $tar->read( $filename );

  my $meta_filename;

  FILE_LIST:
  for my $name ( $tar->list_files() ) {
    if ( $name =~ /META\.yml$/ ) {
      $meta_filename = $name;
      last FILE_LIST;
    }
  }

  if ( $meta_filename ) {
    return Load( $tar->get_content( $meta_filename ) );
  }

  warn( "Could not load the META.yml from file $options{file}" );
  return {};

}

sub _find_provides {
  my $filename = shift;

  my %provides;

  print "Parsing *.pm from $options{file}\n"
   if ( $options{verbose} );

  my $tar = Archive::Tar->new();
  $tar->read( $filename );

  my $meta_filename;

  # loop through *.pm files in the tarball
  for my $pm_filename ( $tar->list_files() ) {
    next if $pm_filename !~ m/\.pm$/;  # only Perl module files are interesting
    next if $pm_filename =~ m{^t/};    # skip test folder

    # write current .pm file to temp file
    my $tmp_filename = File::Temp->new( UNLINK => 1 )->filename;
    write_file($tmp_filename, $tar->get_content( $pm_filename ));

    # get module info
    my $info = eval { Module::Build::ModuleInfo->new_from_file($tmp_filename) };
    print 'failed to get module info of "'.$pm_filename.'" - "'.$@.'"'
      if $@ and $options{verbose};
    next if not $info;

    foreach my $package (keys %{$info->{'versions'}}) {
      next if $package eq 'main';

      # set version to undef
      my $version = (
        $info->{'versions'}->{$package}
        ? $info->{'versions'}->{$package}->stringify
        : 0
      );

      $provides{$package} = $version;
    }
  }

  return \%provides;
}

# MAIN
Getopt::Long::Configure( 'no_ignore_case' );
Getopt::Long::Configure( 'bundling' );

GetOptions(
  'h|help|?' =>
   sub { pod2usage( { -verbose => 1, -input => \*DATA } ); exit },
  'H|man' =>
   sub { pod2usage( { -verbose => 2, -input => \*DATA } ); exit },
  'V|version' => sub { print_version(); exit; },
  'v|verbose' => \$options{verbose},
  'l|local=s' => \$options{local},
  'r|remote=s'   => \$options{remote},
  'p|passive'    => \$ENV{FTP_PASSIVE},
  'add'          => sub { setsub( 'add', \&add ) },
  'update'       => sub { setsub( 'update', \&update ) },
  'mirror'       => sub { setsub( 'mirror', \&mirror ) },
  'inject'       => sub { setsub( 'inject', \&inject ) },
  'module=s'     => \$options{module},
  'authorid=s'   => \$options{authorid},
  'modversion=s' => \$options{version},
  'file=s'       => \$options{file},
  'all-in-meta'  => \$options{'all-in-meta'},
  'signing-key=s' => \$options{'signing_key'},
  'discover-packages' => \$options{'discover-packages'},
) or exit 1;

unless ( defined( $options{action} ) && chkactions() ) {
  pod2usage( { -verbose => 1, -input => \*DATA } );
  exit;
}

my $mcpi = CPAN::Mini::Inject->new->loadcfg( $options{cfg} )->parsecfg;

$CPAN::Checksums::SIGNING_KEY = $options{'signing_key'}
    if ($options{'signing_key'});

&{ $options{action} }( $mcpi );

__END__

=head1 NAME

mcpani -- A command line tool to manage a CPAN Mini Mirror.

=head1 SYNOPSIS

mcpani [options] < --add | --update | --mirror | --inject >

Commands:

    --add               Add a new package to the repository
          --module      Name of the module to add
          --authorid    Author ID of the module
          --modversion  Version number of the module
          --all-in-meta parse all modules in the META.yml
          --discover-packages discover modules in all .pm files
          --file        tar.gz file of the module

    --update            Update local CPAN mirror and inject modules
    --mirror            Update local CPAN mirror from remote
    --inject            Add modules from repository to CPAN mirror

Options:

    -h, --help          This synopsis
    -H, --man           Detailed description

    -l, --local         local location for CPAN::Mini Mirror
    -r, --remote        CPAN mirror to mirror from
    -p, --passive       Enable passive ftp for mirroring.
    -v, --verbose       verbose output
    -V, --version       Version information.
        --signing-key   See CPAN::Checksums $SIGNING_KEY

=head1 COMMAND LINE OPTIONS

=head2 --add

Add a module to the repository for later inclusion in the CPAN Mini
mirror. The add command requires the following parameters:

=over 4

=item --module

This is the name of the module (ie CPAN::Mini::Inject).

=item --authorid

A CPAN 'like' author ID for the module. The author ID does not need to
exist on CPAN.

=item --modversion

Version number of the module. This must match the version number in the
file name.

=item --all-in-meta

This option will add every module listed in the 'provides' section of
the META.yml contained in the tar.gz provided by the --file option.

The options --module and --modversion are still recognized.  If the
same module/version is found in the META.yml it is not duplicated.

If the META.yml file or the 'provides' section is missing, then
a warning is issued and the only module added is the one provided by
--module / --modversion.

=item --file

File name and path of the module. The file name must follow the
standard CPAN naming convention (the resulting file from a
C<make tardist>).

=back

  Example:

  mcpani --add --module CPAN::Mini::Inject --authorid SSORICHE
         --modversion 0.01 file ./CPAN-Mini-Inject-0.01.tar.gz


=head2 --update

Update your local CPAN Mini mirror from a CPAN site. Once completed
add the modules contained in the repository to it. This is the same
as running C<mcpani --mirror> followed by C<mcpani --inject>

=head2 --mirror

Update the local CPAN Mini mirror from CPAN.

=head2 --inject

Add the repository modules into the CPAN Mini mirror.

=head2 -l, --local

A local directory to store the CPAN Mini mirror in. Specifying this
option overrides the value in the config file.

=head2 -r, --remote

A CPAN site to create the local CPAN Mini mirror from.

=head2 -v, --verbose

Display verbose processing information

=head2 -V, --version

Display version information.

=head1 CONFIGURATION FILE

F<mcpani> uses a simple configuration file in the following format:

 local: /www/CPAN
 remote: ftp://ftp.cpan.org/pub/CPAN ftp://ftp.kernel.org/pub/CPAN
 repository: /work/mymodules
 passive: yes
 dirmode: 0755

Description of options:

=over 4

=item * local

location to store local CPAN::Mini mirror (*REQUIRED*)

=item * remote

CPAN site(s) to mirror from. Multiple sites can be listed, with spaces
between them. (*REQUIRED*)

=item * repository

Location to store modules to add to the local CPAN::Mini mirror.

=item * passive

Enable passive FTP.

=item * dirmode

Set the permissions of created directories to the specified mode
(octal value). The default value is based on the umask (if supported).

=back

F<mcpani> will search the following four places in order:

=over 4

=item * file pointed to by the environment variable MCPANI_CONFIG

=item * $HOME/.mcpani/config

=item * /usr/local/etc/mcpani

=item * /etc/mcpani

=back

=head1 CURRENT MAINTAINER

Christian Walde C<< <walde.christian@googlemail.com> >>

=head1 AUTHOR

Shawn Sorichetti C<< <ssoriche@coloredblocks.net> >>

=head1 BUGS

Please report any bugs or feature requests to
C<bug-cpan-mini-inject@rt.cpan.org>, or through the web interface at
L<http://rt.cpan.org>.  I will be notified, and then you'll automatically
be notified of progress on your bug as I make changes.

=head1 Copyright & License

Copyright 2004 Shawn Sorichetti, All Rights Reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut
