#!/usr/bin/env perl
package App::implode::cli;
use strict;
use warnings;
use Archive::Tar;
use Cwd 'abs_path';
use Carton ();
use Carton::Builder;
use Carton::Environment;
use Carton::Mirror;
use File::Basename 'basename';
use File::Find ();
use File::Path ();
use File::Spec::Functions 'catdir';
use File::Temp 'tempdir';
use IO::Compress::Bzip2;

sub DESTROY {
  my $self = shift;
  return chdir $self->[0] if UNIVERSAL::isa($self, 'ARRAY');
  File::Path::remove_tree($self->{tmpdir}) if $self->{cleanup};
}

sub cached { shift->{cached} }                                                                                    # TODO
sub mirror { shift->{mirror} ||= Carton::Mirror->new($ENV{PERL_CARTON_MIRROR} || $Carton::Mirror::DefaultMirror) }
sub silent { shift->{silent} //= -t STDOUT }
sub tmpdir { shift->{tmpdir} //= tempdir(CLEANUP => $ENV{IMPLODE_NO_CLEANUP} ? 0 : 1) }

sub bundle {
  my $self = shift;
  my $script = do { open my $FH, '<', $self->{script}; local $/; <$FH> };

  open my $OUT, '>', $self->{out} or die "Could not write $self->{out}: $!\n";
  print $OUT $script =~ s/^(#!.+?[\r\n]+)//m ? $1 : "#!/usr/bin/perl\n";
  print $OUT $self->script_header;
  print $OUT $script;
  print $OUT "\n__END__\n";
  $self->tar->write(IO::Compress::Bzip2->new($OUT), COMPRESS_GZIP);
  close $OUT;
  chmod 0755, $self->{out};
}

sub chdir {
  my $self = shift;
  my $guard = bless [abs_path], ref($self);
  chdir $_[0] or die "chdir: $!";
  $guard;
}

sub deps {
  my $self    = shift;
  my $env     = Carton::Environment->build('cpanfile', $self->tmpdir);
  my $builder = Carton::Builder->new(cascade => 1, mirror => $self->mirror, cpanfile => $env->cpanfile);
  my $index_file;

  if (!$self->dir_is_empty($self->tmpdir)) {
    die "Cannot build $self->{script}: @{[$env->install_path]} already exists.\n";
  }

  $self->{cleanup} = 1;
  $index_file = $env->install_path->child("cache/modules/02packages.details.txt");
  $index_file->parent->mkpath;
  $env->snapshot->write_index($index_file);
  $builder->index($index_file);
  $builder->mirror(Carton::Mirror->new($env->vendor_cache)) if $self->cached;
  $builder->install($env->install_path);
  $env->cpanfile->load;
  $env->snapshot->find_installs($env->install_path, $env->cpanfile->requirements);
}

sub dir_is_empty {
  my ($self, $dir) = @_;
  opendir(my $DH, $dir) or return 1;
  not scalar grep {/\w/} readdir $DH;
}

sub script_header {
  my $self = shift;
  return <<'HEADER';
BEGIN {
  require Archive::Tar;
  require File::Spec;
  require File::Temp;
  require IO::Uncompress::Bunzip2;
  $App::implode::explodedir = File::Temp->newdir(CLEANUP => !$ENV{APP_IMPLODE_KEEP_FILES});
  warn "[App::implode] cd $App::implode::explodedir; tar -xfz $0\n" if $ENV{APP_IMPLODE_VERBOSE};
  open my $FH, '<', $0;
  my $d = do{ local $/; <$FH> };
  $d =~ s/^.*\n__END__\r?\n//s;
  my $tar = Archive::Tar->new;
  $tar->read(IO::Uncompress::Bunzip2->new(\$d));
  $tar->setcwd($App::implode::explodedir);
  $tar->extract or die "[App::implode] tar -xfz $0 failed: @{[$tar->error]}";
  unshift @INC, File::Spec->catdir($App::implode::explodedir, 'lib', 'perl5');
  $ENV{PATH} = join ':', grep { defined } File::Spec->catdir($App::implode::explodedir, 'bin'), $ENV{PATH};
  $ENV{PERL5LIB} = join ':', @INC;
}
HEADER
}

sub tar {
  my $self  = shift;
  my $tar   = Archive::Tar->new;
  my $guard = $self->chdir($self->{tmpdir});

  File::Find::find(
    {
      no_chdir => 1,
      wanted   => sub { -f and $tar->add_files($_) }
    },
    qw( bin lib )
  );

  return $tar;
}

sub run {
  my $self = shift;

  $self->{script} = shift or die "Usage: implode myapp.pl [path/to/outfile.pl]\n\n";
  $self->{out} = shift || basename $self->{script};
  -r $self->{script} or die "Cannot read '$self->{script}'.\n";
  -e $self->{out} and die "Outfile '$self->{out}' already exists.\n";

  warn sprintf "Building application in %s\n", $self->tmpdir unless $self->silent;
  $self->deps;
  $self->bundle;

  return 0;
}

exit((bless {})->run(@ARGV)) unless defined wantarray;
no warnings;
'App::implode::cli';
