#!/usr/bin/perl

use strict;
use warnings;
use 5.014;
use App::af;
use File::chdir;

# PODNAME: af
# ABSTRACT: Command line tool for alienfile
# VERSION


package App::af::download 0.02 {
  use Moose;
  use namespace::autoclean;
  use MooseX::Types::Path::Tiny qw( AbsPath );
  use Path::Tiny qw( path );

  with 'App::af';
  with 'App::af::role::alienfile';


  has local => (
    is       => 'ro',
    isa      => AbsPath,
    traits   => ['App::af::opt'],
    short    => 'l',
    coerce   => 1,
    opt_type => 's',
    default  => '.',
  );

  sub main
  {
    my($self) = @_;
    
    unless(-d $self->local)
    {
      say STDERR "no such directory: @{[ $self->local ]}";
      return 2;
    }

    local $ENV{ALIEN_INSTALL_TYPE} = 'share';    
    my $build = $self->build;
    $build->load_requires('configure');
    $build->load_requires('share');
    eval { $build->download };
    warn $@ if $@;

    unless(defined $build->install_prop->{download} &&
           length $build->install_prop->{download})
    {
      say STDERR "Recipe did not seem to download a file or directory";
      return 2;
    }
    
    my $download = path($build->install_prop->{download});
    
    if(-f $download)
    {
      my $to = $self->local->child($download->basename);
      $download->copy($to) || die "unable to copy $download => $to $!";
      say "Wrote archive to $to";
    }
    elsif(-d $download)
    {
      require File::Copy::Recursive;
      my $to = $self->local->child($download->basename);
      File::Copy::Recursive::dircopy("$download", "$to") || die "unable to copy $download => $to $!";
      say "Wrote directory to $to";
    }
    else
    {
      say STDERR "Recipe did not seem to download a file or directory";
      return 2;
    }
    
    0;
  }

  __PACKAGE__->meta->make_immutable;
}

package App::af::install {
  use Moose;
  use namespace::autoclean;
  use File::Temp qw( tempdir );
  use MooseX::Types::Path::Tiny qw( AbsPath );
  
  with 'App::af';
  with 'App::af::role::alienfile';
  
  
  has stage => (
    is       => 'ro',
    isa      => AbsPath,
    traits   => ['App::af::opt'],
    coerce   => 1,
    lazy     => 1,
    opt_type => 's',
    default  => sub {
      tempdir( CLEANUP => 1);
    },
  );
  
  has prefix => (
    is     => 'rw',
    isa    => AbsPath,
    traits => ['App::af::opt'],
    opt_type => 's',
    coerce => 1,
  );
  
  has type => (
    is       => 'ro',
    isa      => 'Str',
    traits   => ['App::af::opt'],
    opt_type => 's',
  );
  
  has dry_run => (
    is     => 'ro',
    isa    => 'Int',
    traits => ['App::af::opt'],
  );
  
  sub main
  {
    my($self) = @_;
    
    local $ENV{ALIEN_INSTALL_TYPE} = $ENV{ALIEN_INSTALL_TYPE};
    
    if(defined $self->type)
    {
      unless($self->type =~ /^(share|system)$/)
      {
        say STDERR "unknown install type: @{[ $self->type ]}";
        return 2;
      }
      $ENV{ALIEN_INSTALL_TYPE} = $self->type;
    }
    
    if(-d $self->stage && $self->stage->children)
    {
      say "stage directory is not empty";
      exit 2;
    }
    
    my($build, $prefix) = $self->build;
    $self->prefix($prefix) if defined($prefix) && ! $self->prefix;
    
    if($self->dry_run && ! defined $self->prefix)
    {
      $self->prefix(tempdir( CLEANUP => 1 ));
    }
    
    unless(defined $self->prefix)
    {
      say STDERR "You must specify a prefix with --prefix";
      return 2;
    }
    
    if(-d $self->prefix->child('_alien'))
    {
      my $test = $self->prefix->child('_alien/test');
      eval { $test->touch };
      if($@)
      {
        say STDERR "prefix is not writable.  You may need to use su or sudo";
        return 2;
      }
      $test->remove;
    }
    
    $build->set_stage($self->stage->stringify);
    $build->set_prefix($self->prefix->stringify);
    $build->load_requires('configure');
    $build->load_requires($build->install_type);
    $build->download;
    $build->build;
    
    unless($self->dry_run)
    {
      require File::Copy::Recursive;
    
      if(-d $self->prefix)
      {
        # this is slightly dangerous.
        foreach my $child ($self->prefix->children)
        {
          say "rm -rf $child";
          $_->remove_tree({ safe => 0 }) for $self->prefix->children;
        }
      }
    
      File::Copy::Recursive::dircopy($self->stage, $self->prefix) || die "unable to copy @{[ $self->stage ]} => @{[ $self->prefix ]} $!";
      say "copied staged install into @{[ $self->prefix ]}";
    };
    
    0;
  }
  __PACKAGE__->meta->make_immutable;
}

package App::af::requires {
  use Moose;
  use namespace::autoclean;
  use YAML qw( Dump );


  with 'App::af';
  with 'App::af::role::alienfile';
  with 'App::af::role::phase';
  
  sub main
  {
    my($self) = @_;
    
    $self->check_phase;
    
    my $build = $self->build;
    
    if($self->phase eq 'all')
    {
      print Dump({
        map { $_ => $build->requires($_) } qw( configure any share system )
      });
    }
    else
    {
      print Dump($build->requires($self->phase));
    }
    
    0;
  }
  
  __PACKAGE__->meta->make_immutable;
}

package App::af::missing {
  use Moose;
  use namespace::autoclean;
  use Module::Load qw( load );
  
  with 'App::af';
  with 'App::af::role::alienfile';
  with 'App::af::role::phase';
  
  
  sub main
  {
    my($self) = @_;
    
    $self->check_phase;
    
    my $build = $self->build;
    
    my @reqs;
    my %need;
    
    if($self->phase eq 'all')
    {
      @reqs = map { $build->requires($_) } qw( configure any share system );
    }
    else
    {
      @reqs = ( $build->requires($self->phase) );
    }
        
    foreach my $reqs (@reqs)
    {
      foreach my $module (sort keys %$reqs)
      {
        my $version = $reqs->{$module};
        eval {
          load($module);
          !$version || $module->VERSION($version);
        };
        $need{$module} = 1 if $@
      }
    }
    
    say $_ for sort keys %need;
  
    0;
  }
  
  __PACKAGE__->meta->make_immutable;
}

package App::af::prop {
  use Moose;
  use namespace::autoclean;
  use Module::Load qw( load );

  
  with 'App::af';

  has class => (
    is       => 'ro',
    isa      => 'Str',
    traits   => ['App::af::opt'],
    short    => 'c',
    opt_type => 's',
  );
  
  has $_ => (
    is       => 'ro',
    isa      => 'Int',
    traits   => ['App::af::opt'],
  ) for qw( static cflags libs modversion bin_dir );

  sub main {
    my($self) = @_;
    
    unless($self->class)
    {
      say STDERR "You must specify a class.\n";
      return 2;
    }
    
    my $class = $self->class =~ /::/ ? $self->class : 'Alien::' . $self->class;

    load $class;    
    unless($class->can('runtime_prop'))
    {
      say STDERR "$class was not installed with Alien::Build";
      return 2;
    }
    
    my $prop = $class->runtime_prop;
    
    unless($prop)
    {
      say STDERR "$class was not installed with Alien::Build";
      return 2;
    }
    
    my $found = 0;
    
    if($self->cflags)
    {
      if($self->static)
      {
        say $class->cflags_static;
      }
      else
      {
        say $class->cflags;
      }
      $found = 1;
    }
    
    if($self->libs)
    {
      if($self->static)
      {
        say $class->libs_static;
      }
      else
      {
        say $class->libs;
      }
      $found = 1;
    }
    
    if($self->bin_dir)
    {
      say $_ for $class->bin_dir;
      $found = 1;
    }
    
    if($self->modversion)
    {
      say $class->version // 'undef';
      $found = 1;
    }
    
    unless($found)
    {
      require YAML;
      print YAML::Dump($prop);
    }
    
    0;
  }
  __PACKAGE__->meta->make_immutable;
}

package App::af::list {
  use Moose;
  use namespace::autoclean;
  use Path::Tiny qw( path );
  use Module::Load qw( load );
  use Text::Table;
  
  
  with 'App::af';

  has long => (
    is       => 'ro',
    isa      => 'Int',
    traits   => ['App::af::opt'],
    short    => 'l',
  );
  
  sub main
  {
    my($self) = @_;
    
    my $table;
    $table = Text::Table->new('name', 'alien version', 'package version')
      if $self->long;
    
    foreach my $inc (map { path($_)->absolute } @INC)
    {
      my $dist_root = $inc->child('auto/share/dist');
      next unless -d $dist_root;
      
      foreach my $dist_dir ($dist_root->children)
      {
        next unless -f $dist_dir->child('_alien/alien.json');
        my $dist = $dist_dir->basename;
        my $class = $dist;
        $class =~ s/-/::/g;
        
        unless($table)
        {
          say $class;
          next;
        }
        
        eval { load $class };
        if($@)
        {
          $table->add($class, '---', '---');
        }
        else
        {
          my $perl_version = eval { $class->VERSION } // '---';
          my $pkg_version   = eval { $class->version } // '---';
          $table->add($class, $perl_version, $pkg_version);
        }
      }

    }
    
    print $table if defined $table;
    0;
  }
  
  __PACKAGE__->meta->make_immutable;
}

# for testing, allow us to do this file
# without running.
unless(caller)
{
  my $class = App::af->compute_class;
  unless(eval { $class->does('App::af') })
  {
    say STDERR "unknown subcommand";
    exit 2;
  }
  my $app = $class->new(@ARGV);
  exit($app->main // 0);
}

__END__

=pod

=encoding UTF-8

=head1 NAME

af - Command line tool for alienfile

=head1 VERSION

version 0.02

=head1 SYNOPSIS

 af download   --help
 af install    --help
 af requires   --help
 af missing    --help
 af prop       --help
 af list       --help

=head1 DESCRIPTION

The C<af> command is a command line interface to L<alienfile> and
L<Alien::Build>.

=head3 options

These options are available for all subcommands.

=head4 --help

Print the help for either C<af> as a whole, or the specific subcommand.

=head4 --version

Print the version of C<af> and exit.

=head1 SUBCOMMANDS

=head2 download

=head3 Usage

 af download
   [
     ( -f | --file )  alienfile | 
     ( -c | --class ) class
   ]
   [ ( -l | --local ) directory ]

=head3 description

Download the external resource using the usual L<alienfile> logic.  File
will be deposited in the directory indicated by the C<--local> (or C<-l>)
option, or the current working directory if not specified.

=head3 options

=head4 -f | --file

The L<alienfile>.  If neither this option, nor C<-c> is specified, then 
C<alienfile> in the current directory will be assumed.

=head4 -c | --class

Get the L<alienfile> from the already installed Alien module.    You may
omit the C<Alien::> prefix, so for example L<Alien::curl> may be specified
as simply C<curl>.  

=head4 -l | --local

The location to store the downloaded resource.  The current directory
if not specified.

=head2 install

=head3 Usage

 af install
   [
     ( -f | --file )  alienfile | 
     ( -c | --class ) class
   ]
   [ --stage directory ] [ --prefix directory ] [ --type ( share | system ) ]

=head3 description

Install or reinstall using the given L<alienfile> or already installed
L<Alien>.

=head3 options

=head4 -f | --file

The L<alienfile>.  If neither this option, nor C<-c> is specified, then 
C<alienfile> in the current directory will be assumed.

=head4 -c | --class

Get the L<alienfile> from the already installed Alien module.    You may
omit the C<Alien::> prefix, so for example L<Alien::curl> may be specified
as simply C<curl>.  If you do not specify the C<--prefix> option, the
package will replace the already installed one.

=head4 --stage

The stage directory.  By default this is a temporary directory that will
automatically be removed.

=head4 --prefix

The final install location to use.  Required when using the C<-f> option,
but optional when using the C<-c> option.

=head4 --type

Override the install type.  May be either C<share> or C<system>.

=head4 --dry-run

Do not install into the final location.

=head2 requires

=head3 Usage

 af requires
   [
     ( -f | --file )  alienfile | 
     ( -c | --class ) class
   ]
   [ ( -p | --phase ) ( configure | any | share | system ) ]

=head3 description

Print the requirements for the given phase in L<YAML> format.  If the phase
is not provided, then requirements for all phases will be printed separately
in L<YAML> format.

=head3 options

=head4 -f | --file

The L<alienfile>.  If neither this option, nor C<-c> is specified, then 
C<alienfile> in the current directory will be assumed.

=head4 -c | --class

Get the L<alienfile> from the already installed Alien module.    You may
omit the C<Alien::> prefix, so for example L<Alien::curl> may be specified
as simply C<curl>.

=head4 -p | --phase

The phase of the requirement.  Please refer to the L<Alien::Build> documentation
for the meaning of the various phases.

=head2 missing

=head3 Usage

 af missing
   [
     ( -f | --file )  alienfile | 
     ( -c | --class ) class
   ]
   [ ( -p | --phase ) ( configure | any | share | system ) ]

=head3 description

Print the requirements for the given phase in list format that are not
currently fulfilled.  This output can be piped into L<cpanm> in order
to install any missing requirements:

 % af missing | cpanm

If no phase is specified, then the all missing requirements will be
printed.

=head3 options

=head4 -f | --file

The L<alienfile>.  If neither this option, nor C<-c> is specified, then 
C<alienfile> in the current directory will be assumed.

=head4 -c | --class

Get the L<alienfile> from the already installed Alien module.    You may
omit the C<Alien::> prefix, so for example L<Alien::curl> may be specified
as simply C<curl>.

=head4 -p | --phase

The phase of the requirement.  Please refer to the L<Alien::Build> documentation
for the meaning of the various phases.

=head2 prop

=head3 Usage

 af prop ( -c | --class ) class [ --cflags ] [ --libs ] [ --static ] [ --modversion ] [ --bin-dir ]

=head3 prop

Print the runtime properties for the given L<Alien> class.  You may
omit the C<Alien::> prefix, so for example L<Alien::curl> may be queried
as simply C<curl>.  If no specific properties are requested then the
entire runtime property hash will be printed in L<YAML> format.

=head3 options

=head4 -c | --class

The class to query for runtime properties.  This option is required.

=head4 --cflags

Print the compiler flags

=head4 --libs

Print the linker flags

=head4 --static

For either the C<--cflags> or C<--libs> option print the static versions.

=head4 --modversion

Print the version of the Alienized package.  This is not the version of
the L<Alien> module itself.

=head4 --bin-dir

Print the list of directories bundled with a C<share> install.

=head2 list

=head3 Usage

 af list [ -l | --long ]

=head3 prop

Print list of L<Alien> modules already installed that used L<Alien::Build>
as their installer.

=head3 options

=head4 -l | --long

Also print the version number of the L<Alien> module, and the version of the
alienized package.

=head1 AUTHOR

Graham Ollis <plicease@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2017 by Graham Ollis.

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

=cut
